UNPKG

sussudio

Version:

An unofficial VS Code Internal API

474 lines (473 loc) 20.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { isFirefox } from "../../browser.mjs"; import { DataTransfers } from "../../dnd.mjs"; import { $, addDisposableListener, append, clearNode, EventHelper, EventType, trackFocus } from "../../dom.mjs"; import { DomEmitter } from "../../event.mjs"; import { StandardKeyboardEvent } from "../../keyboardEvent.mjs"; import { Gesture, EventType as TouchEventType } from "../../touch.mjs"; import { Color, RGBA } from "../../../common/color.mjs"; import { Emitter, Event } from "../../../common/event.mjs"; import { Disposable, DisposableStore } from "../../../common/lifecycle.mjs"; import "../../../../css!./paneview.mjs"; import { localize } from "../../../../nls.mjs"; import { Sizing, SplitView } from './splitview'; /** * A Pane is a structured SplitView view. * * WARNING: You must call `render()` after you construct it. * It can't be done automatically at the end of the ctor * because of the order of property initialization in TypeScript. * Subclasses wouldn't be able to set own properties * before the `render()` call, thus forbidding their use. */ export class Pane extends Disposable { static HEADER_SIZE = 22; element; header; body; _expanded; _orientation; expandedSize = undefined; _headerVisible = true; _bodyRendered = false; _minimumBodySize; _maximumBodySize; _ariaHeaderLabel; styles = {}; animationTimer = undefined; _onDidChange = this._register(new Emitter()); onDidChange = this._onDidChange.event; _onDidChangeExpansionState = this._register(new Emitter()); onDidChangeExpansionState = this._onDidChangeExpansionState.event; get ariaHeaderLabel() { return this._ariaHeaderLabel; } set ariaHeaderLabel(newLabel) { this._ariaHeaderLabel = newLabel; this.header.setAttribute('aria-label', this.ariaHeaderLabel); } get draggableElement() { return this.header; } get dropTargetElement() { return this.element; } _dropBackground; get dropBackground() { return this._dropBackground; } get minimumBodySize() { return this._minimumBodySize; } set minimumBodySize(size) { this._minimumBodySize = size; this._onDidChange.fire(undefined); } get maximumBodySize() { return this._maximumBodySize; } set maximumBodySize(size) { this._maximumBodySize = size; this._onDidChange.fire(undefined); } get headerSize() { return this.headerVisible ? Pane.HEADER_SIZE : 0; } get minimumSize() { const headerSize = this.headerSize; const expanded = !this.headerVisible || this.isExpanded(); const minimumBodySize = expanded ? this.minimumBodySize : 0; return headerSize + minimumBodySize; } get maximumSize() { const headerSize = this.headerSize; const expanded = !this.headerVisible || this.isExpanded(); const maximumBodySize = expanded ? this.maximumBodySize : 0; return headerSize + maximumBodySize; } orthogonalSize = 0; constructor(options) { super(); this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; this._orientation = typeof options.orientation === 'undefined' ? 0 /* Orientation.VERTICAL */ : options.orientation; this._ariaHeaderLabel = localize('viewSection', "{0} Section", options.title); this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : this._orientation === 1 /* Orientation.HORIZONTAL */ ? 200 : 120; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; this.element = $('.pane'); } isExpanded() { return this._expanded; } setExpanded(expanded) { if (this._expanded === !!expanded) { return false; } this.element?.classList.toggle('expanded', expanded); this._expanded = !!expanded; this.updateHeader(); if (expanded) { if (!this._bodyRendered) { this.renderBody(this.body); this._bodyRendered = true; } if (typeof this.animationTimer === 'number') { clearTimeout(this.animationTimer); } append(this.element, this.body); } else { this.animationTimer = window.setTimeout(() => { this.body.remove(); }, 200); } this._onDidChangeExpansionState.fire(expanded); this._onDidChange.fire(expanded ? this.expandedSize : undefined); return true; } get headerVisible() { return this._headerVisible; } set headerVisible(visible) { if (this._headerVisible === !!visible) { return; } this._headerVisible = !!visible; this.updateHeader(); this._onDidChange.fire(undefined); } get orientation() { return this._orientation; } set orientation(orientation) { if (this._orientation === orientation) { return; } this._orientation = orientation; if (this.element) { this.element.classList.toggle('horizontal', this.orientation === 1 /* Orientation.HORIZONTAL */); this.element.classList.toggle('vertical', this.orientation === 0 /* Orientation.VERTICAL */); } if (this.header) { this.updateHeader(); } } render() { this.element.classList.toggle('expanded', this.isExpanded()); this.element.classList.toggle('horizontal', this.orientation === 1 /* Orientation.HORIZONTAL */); this.element.classList.toggle('vertical', this.orientation === 0 /* Orientation.VERTICAL */); this.header = $('.pane-header'); append(this.element, this.header); this.header.setAttribute('tabindex', '0'); // Use role button so the aria-expanded state gets read https://github.com/microsoft/vscode/issues/95996 this.header.setAttribute('role', 'button'); this.header.setAttribute('aria-label', this.ariaHeaderLabel); this.renderHeader(this.header); const focusTracker = trackFocus(this.header); this._register(focusTracker); this._register(focusTracker.onDidFocus(() => this.header.classList.add('focused'), null)); this._register(focusTracker.onDidBlur(() => this.header.classList.remove('focused'), null)); this.updateHeader(); const eventDisposables = this._register(new DisposableStore()); const onKeyDown = this._register(new DomEmitter(this.header, 'keydown')); const onHeaderKeyDown = Event.map(onKeyDown.event, e => new StandardKeyboardEvent(e), eventDisposables); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === 3 /* KeyCode.Enter */ || e.keyCode === 10 /* KeyCode.Space */, eventDisposables)(() => this.setExpanded(!this.isExpanded()), null)); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === 15 /* KeyCode.LeftArrow */, eventDisposables)(() => this.setExpanded(false), null)); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === 17 /* KeyCode.RightArrow */, eventDisposables)(() => this.setExpanded(true), null)); this._register(Gesture.addTarget(this.header)); [EventType.CLICK, TouchEventType.Tap].forEach(eventType => { this._register(addDisposableListener(this.header, eventType, e => { if (!e.defaultPrevented) { this.setExpanded(!this.isExpanded()); } })); }); this.body = append(this.element, $('.pane-body')); // Only render the body if it will be visible // Otherwise, render it when the pane is expanded if (!this._bodyRendered && this.isExpanded()) { this.renderBody(this.body); this._bodyRendered = true; } if (!this.isExpanded()) { this.body.remove(); } } layout(size) { const headerSize = this.headerVisible ? Pane.HEADER_SIZE : 0; const width = this._orientation === 0 /* Orientation.VERTICAL */ ? this.orthogonalSize : size; const height = this._orientation === 0 /* Orientation.VERTICAL */ ? size - headerSize : this.orthogonalSize - headerSize; if (this.isExpanded()) { this.body.classList.toggle('wide', width >= 600); this.layoutBody(height, width); this.expandedSize = size; } } style(styles) { this.styles = styles; if (!this.header) { return; } this.updateHeader(); } updateHeader() { const expanded = !this.headerVisible || this.isExpanded(); this.header.style.lineHeight = `${this.headerSize}px`; this.header.classList.toggle('hidden', !this.headerVisible); this.header.classList.toggle('expanded', expanded); this.header.setAttribute('aria-expanded', String(expanded)); this.header.style.color = this.styles.headerForeground ? this.styles.headerForeground.toString() : ''; this.header.style.backgroundColor = this.styles.headerBackground ? this.styles.headerBackground.toString() : ''; this.header.style.borderTop = this.styles.headerBorder && this.orientation === 0 /* Orientation.VERTICAL */ ? `1px solid ${this.styles.headerBorder}` : ''; this._dropBackground = this.styles.dropBackground; this.element.style.borderLeft = this.styles.leftBorder && this.orientation === 1 /* Orientation.HORIZONTAL */ ? `1px solid ${this.styles.leftBorder}` : ''; } } class PaneDraggable extends Disposable { pane; dnd; context; static DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5)); dragOverCounter = 0; // see https://github.com/microsoft/vscode/issues/14470 _onDidDrop = this._register(new Emitter()); onDidDrop = this._onDidDrop.event; constructor(pane, dnd, context) { super(); this.pane = pane; this.dnd = dnd; this.context = context; pane.draggableElement.draggable = true; this._register(addDisposableListener(pane.draggableElement, 'dragstart', e => this.onDragStart(e))); this._register(addDisposableListener(pane.dropTargetElement, 'dragenter', e => this.onDragEnter(e))); this._register(addDisposableListener(pane.dropTargetElement, 'dragleave', e => this.onDragLeave(e))); this._register(addDisposableListener(pane.dropTargetElement, 'dragend', e => this.onDragEnd(e))); this._register(addDisposableListener(pane.dropTargetElement, 'drop', e => this.onDrop(e))); } onDragStart(e) { if (!this.dnd.canDrag(this.pane) || !e.dataTransfer) { e.preventDefault(); e.stopPropagation(); return; } e.dataTransfer.effectAllowed = 'move'; if (isFirefox) { // Firefox: requires to set a text data transfer to get going e.dataTransfer?.setData(DataTransfers.TEXT, this.pane.draggableElement.textContent || ''); } const dragImage = append(document.body, $('.monaco-drag-image', {}, this.pane.draggableElement.textContent || '')); e.dataTransfer.setDragImage(dragImage, -10, -10); setTimeout(() => document.body.removeChild(dragImage), 0); this.context.draggable = this; } onDragEnter(e) { if (!this.context.draggable || this.context.draggable === this) { return; } if (!this.dnd.canDrop(this.context.draggable.pane, this.pane)) { return; } this.dragOverCounter++; this.render(); } onDragLeave(e) { if (!this.context.draggable || this.context.draggable === this) { return; } if (!this.dnd.canDrop(this.context.draggable.pane, this.pane)) { return; } this.dragOverCounter--; if (this.dragOverCounter === 0) { this.render(); } } onDragEnd(e) { if (!this.context.draggable) { return; } this.dragOverCounter = 0; this.render(); this.context.draggable = null; } onDrop(e) { if (!this.context.draggable) { return; } EventHelper.stop(e); this.dragOverCounter = 0; this.render(); if (this.dnd.canDrop(this.context.draggable.pane, this.pane) && this.context.draggable !== this) { this._onDidDrop.fire({ from: this.context.draggable.pane, to: this.pane }); } this.context.draggable = null; } render() { let backgroundColor = null; if (this.dragOverCounter > 0) { backgroundColor = (this.pane.dropBackground || PaneDraggable.DefaultDragOverBackgroundColor).toString(); } this.pane.dropTargetElement.style.backgroundColor = backgroundColor || ''; } } export class DefaultPaneDndController { canDrag(pane) { return true; } canDrop(pane, overPane) { return true; } } export class PaneView extends Disposable { dnd; dndContext = { draggable: null }; element; paneItems = []; orthogonalSize = 0; size = 0; splitview; animationTimer = undefined; _onDidDrop = this._register(new Emitter()); onDidDrop = this._onDidDrop.event; orientation; boundarySashes; onDidSashChange; onDidSashReset; onDidScroll; constructor(container, options = {}) { super(); this.dnd = options.dnd; this.orientation = options.orientation ?? 0 /* Orientation.VERTICAL */; this.element = append(container, $('.monaco-pane-view')); this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation })); this.onDidSashReset = this.splitview.onDidSashReset; this.onDidSashChange = this.splitview.onDidSashChange; this.onDidScroll = this.splitview.onDidScroll; const eventDisposables = this._register(new DisposableStore()); const onKeyDown = this._register(new DomEmitter(this.element, 'keydown')); const onHeaderKeyDown = Event.map(Event.filter(onKeyDown.event, e => e.target instanceof HTMLElement && e.target.classList.contains('pane-header'), eventDisposables), e => new StandardKeyboardEvent(e), eventDisposables); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === 16 /* KeyCode.UpArrow */, eventDisposables)(() => this.focusPrevious())); this._register(Event.filter(onHeaderKeyDown, e => e.keyCode === 18 /* KeyCode.DownArrow */, eventDisposables)(() => this.focusNext())); } addPane(pane, size, index = this.splitview.length) { const disposables = new DisposableStore(); pane.onDidChangeExpansionState(this.setupAnimation, this, disposables); const paneItem = { pane: pane, disposable: disposables }; this.paneItems.splice(index, 0, paneItem); pane.orientation = this.orientation; pane.orthogonalSize = this.orthogonalSize; this.splitview.addView(pane, size, index); if (this.dnd) { const draggable = new PaneDraggable(pane, this.dnd, this.dndContext); disposables.add(draggable); disposables.add(draggable.onDidDrop(this._onDidDrop.fire, this._onDidDrop)); } } removePane(pane) { const index = this.paneItems.findIndex(item => item.pane === pane); if (index === -1) { return; } this.splitview.removeView(index, pane.isExpanded() ? Sizing.Distribute : undefined); const paneItem = this.paneItems.splice(index, 1)[0]; paneItem.disposable.dispose(); } movePane(from, to) { const fromIndex = this.paneItems.findIndex(item => item.pane === from); const toIndex = this.paneItems.findIndex(item => item.pane === to); if (fromIndex === -1 || toIndex === -1) { return; } const [paneItem] = this.paneItems.splice(fromIndex, 1); this.paneItems.splice(toIndex, 0, paneItem); this.splitview.moveView(fromIndex, toIndex); } resizePane(pane, size) { const index = this.paneItems.findIndex(item => item.pane === pane); if (index === -1) { return; } this.splitview.resizeView(index, size); } getPaneSize(pane) { const index = this.paneItems.findIndex(item => item.pane === pane); if (index === -1) { return -1; } return this.splitview.getViewSize(index); } layout(height, width) { this.orthogonalSize = this.orientation === 0 /* Orientation.VERTICAL */ ? width : height; this.size = this.orientation === 1 /* Orientation.HORIZONTAL */ ? width : height; for (const paneItem of this.paneItems) { paneItem.pane.orthogonalSize = this.orthogonalSize; } this.splitview.layout(this.size); } setBoundarySashes(sashes) { this.boundarySashes = sashes; this.updateSplitviewOrthogonalSashes(sashes); } updateSplitviewOrthogonalSashes(sashes) { if (this.orientation === 0 /* Orientation.VERTICAL */) { this.splitview.orthogonalStartSash = sashes?.left; this.splitview.orthogonalEndSash = sashes?.right; } else { this.splitview.orthogonalEndSash = sashes?.bottom; } } flipOrientation(height, width) { this.orientation = this.orientation === 0 /* Orientation.VERTICAL */ ? 1 /* Orientation.HORIZONTAL */ : 0 /* Orientation.VERTICAL */; const paneSizes = this.paneItems.map(pane => this.getPaneSize(pane.pane)); this.splitview.dispose(); clearNode(this.element); this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation })); this.updateSplitviewOrthogonalSashes(this.boundarySashes); const newOrthogonalSize = this.orientation === 0 /* Orientation.VERTICAL */ ? width : height; const newSize = this.orientation === 1 /* Orientation.HORIZONTAL */ ? width : height; this.paneItems.forEach((pane, index) => { pane.pane.orthogonalSize = newOrthogonalSize; pane.pane.orientation = this.orientation; const viewSize = this.size === 0 ? 0 : (newSize * paneSizes[index]) / this.size; this.splitview.addView(pane.pane, viewSize, index); }); this.size = newSize; this.orthogonalSize = newOrthogonalSize; this.splitview.layout(this.size); } setupAnimation() { if (typeof this.animationTimer === 'number') { window.clearTimeout(this.animationTimer); } this.element.classList.add('animated'); this.animationTimer = window.setTimeout(() => { this.animationTimer = undefined; this.element.classList.remove('animated'); }, 200); } getPaneHeaderElements() { return [...this.element.querySelectorAll('.pane-header')]; } focusPrevious() { const headers = this.getPaneHeaderElements(); const index = headers.indexOf(document.activeElement); if (index === -1) { return; } headers[Math.max(index - 1, 0)].focus(); } focusNext() { const headers = this.getPaneHeaderElements(); const index = headers.indexOf(document.activeElement); if (index === -1) { return; } headers[Math.min(index + 1, headers.length - 1)].focus(); } dispose() { super.dispose(); this.paneItems.forEach(i => i.disposable.dispose()); } }