UNPKG

dockview-core

Version:

Zero dependency layout manager supporting tabs, grids and splitviews

399 lines (398 loc) 17.5 kB
import { disableIframePointEvents, quasiDefaultPrevented, toggleClass, } from '../dom'; import { Emitter, addDisposableListener, } from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; class AriaLevelTracker { constructor() { this._orderedList = []; } push(element) { this._orderedList = [ ...this._orderedList.filter((item) => item !== element), element, ]; this.update(); } destroy(element) { this._orderedList = this._orderedList.filter((item) => item !== element); this.update(); } update() { for (let i = 0; i < this._orderedList.length; i++) { this._orderedList[i].setAttribute('aria-level', `${i}`); this._orderedList[i].style.zIndex = `calc(var(--dv-overlay-z-index, 999) + ${i * 2})`; } } } const arialLevelTracker = new AriaLevelTracker(); export class Overlay extends CompositeDisposable { set minimumInViewportWidth(value) { this.options.minimumInViewportWidth = value; } set minimumInViewportHeight(value) { this.options.minimumInViewportHeight = value; } get element() { return this._element; } get isVisible() { return this._isVisible; } constructor(options) { super(); this.options = options; this._element = document.createElement('div'); this._onDidChange = new Emitter(); this.onDidChange = this._onDidChange.event; this._onDidChangeEnd = new Emitter(); this.onDidChangeEnd = this._onDidChangeEnd.event; this.addDisposables(this._onDidChange, this._onDidChangeEnd); this._element.className = 'dv-resize-container'; this._isVisible = true; this.setupResize('top'); this.setupResize('bottom'); this.setupResize('left'); this.setupResize('right'); this.setupResize('topleft'); this.setupResize('topright'); this.setupResize('bottomleft'); this.setupResize('bottomright'); this._element.appendChild(this.options.content); this.options.container.appendChild(this._element); // if input bad resize within acceptable boundaries this.setBounds(Object.assign(Object.assign(Object.assign(Object.assign({ height: this.options.height, width: this.options.width }, ('top' in this.options && { top: this.options.top })), ('bottom' in this.options && { bottom: this.options.bottom })), ('left' in this.options && { left: this.options.left })), ('right' in this.options && { right: this.options.right }))); arialLevelTracker.push(this._element); } setVisible(isVisible) { if (isVisible === this.isVisible) { return; } this._isVisible = isVisible; toggleClass(this.element, 'dv-hidden', !this.isVisible); } bringToFront() { arialLevelTracker.push(this._element); } setBounds(bounds = {}) { if (typeof bounds.height === 'number') { this._element.style.height = `${bounds.height}px`; } if (typeof bounds.width === 'number') { this._element.style.width = `${bounds.width}px`; } if ('top' in bounds && typeof bounds.top === 'number') { this._element.style.top = `${bounds.top}px`; this._element.style.bottom = 'auto'; this.verticalAlignment = 'top'; } if ('bottom' in bounds && typeof bounds.bottom === 'number') { this._element.style.bottom = `${bounds.bottom}px`; this._element.style.top = 'auto'; this.verticalAlignment = 'bottom'; } if ('left' in bounds && typeof bounds.left === 'number') { this._element.style.left = `${bounds.left}px`; this._element.style.right = 'auto'; this.horiziontalAlignment = 'left'; } if ('right' in bounds && typeof bounds.right === 'number') { this._element.style.right = `${bounds.right}px`; this._element.style.left = 'auto'; this.horiziontalAlignment = 'right'; } const containerRect = this.options.container.getBoundingClientRect(); const overlayRect = this._element.getBoundingClientRect(); // region: ensure bounds within allowable limits // a minimum width of minimumViewportWidth must be inside the viewport const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width)); // a minimum height of minimumViewportHeight must be inside the viewport const yOffset = Math.max(0, this.getMinimumHeight(overlayRect.height)); if (this.verticalAlignment === 'top') { const top = clamp(overlayRect.top - containerRect.top, -yOffset, Math.max(0, containerRect.height - overlayRect.height + yOffset)); this._element.style.top = `${top}px`; this._element.style.bottom = 'auto'; } if (this.verticalAlignment === 'bottom') { const bottom = clamp(containerRect.bottom - overlayRect.bottom, -yOffset, Math.max(0, containerRect.height - overlayRect.height + yOffset)); this._element.style.bottom = `${bottom}px`; this._element.style.top = 'auto'; } if (this.horiziontalAlignment === 'left') { const left = clamp(overlayRect.left - containerRect.left, -xOffset, Math.max(0, containerRect.width - overlayRect.width + xOffset)); this._element.style.left = `${left}px`; this._element.style.right = 'auto'; } if (this.horiziontalAlignment === 'right') { const right = clamp(containerRect.right - overlayRect.right, -xOffset, Math.max(0, containerRect.width - overlayRect.width + xOffset)); this._element.style.right = `${right}px`; this._element.style.left = 'auto'; } this._onDidChange.fire(); } toJSON() { const container = this.options.container.getBoundingClientRect(); const element = this._element.getBoundingClientRect(); const result = {}; if (this.verticalAlignment === 'top') { result.top = parseFloat(this._element.style.top); } else if (this.verticalAlignment === 'bottom') { result.bottom = parseFloat(this._element.style.bottom); } else { result.top = element.top - container.top; } if (this.horiziontalAlignment === 'left') { result.left = parseFloat(this._element.style.left); } else if (this.horiziontalAlignment === 'right') { result.right = parseFloat(this._element.style.right); } else { result.left = element.left - container.left; } result.width = element.width; result.height = element.height; return result; } setupDrag(dragTarget, options = { inDragMode: false }) { const move = new MutableDisposable(); const track = () => { let offset = null; const iframes = disableIframePointEvents(); move.value = new CompositeDisposable({ dispose: () => { iframes.release(); }, }, addDisposableListener(window, 'pointermove', (e) => { const containerRect = this.options.container.getBoundingClientRect(); const x = e.clientX - containerRect.left; const y = e.clientY - containerRect.top; toggleClass(this._element, 'dv-resize-container-dragging', true); const overlayRect = this._element.getBoundingClientRect(); if (offset === null) { offset = { x: e.clientX - overlayRect.left, y: e.clientY - overlayRect.top, }; } const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width)); const yOffset = Math.max(0, this.getMinimumHeight(overlayRect.height)); const top = clamp(y - offset.y, -yOffset, Math.max(0, containerRect.height - overlayRect.height + yOffset)); const bottom = clamp(offset.y - y + containerRect.height - overlayRect.height, -yOffset, Math.max(0, containerRect.height - overlayRect.height + yOffset)); const left = clamp(x - offset.x, -xOffset, Math.max(0, containerRect.width - overlayRect.width + xOffset)); const right = clamp(offset.x - x + containerRect.width - overlayRect.width, -xOffset, Math.max(0, containerRect.width - overlayRect.width + xOffset)); const bounds = {}; // Anchor to top or to bottom depending on which one is closer if (top <= bottom) { bounds.top = top; } else { bounds.bottom = bottom; } // Anchor to left or to right depending on which one is closer if (left <= right) { bounds.left = left; } else { bounds.right = right; } this.setBounds(bounds); }), addDisposableListener(window, 'pointerup', () => { toggleClass(this._element, 'dv-resize-container-dragging', false); move.dispose(); this._onDidChangeEnd.fire(); })); }; this.addDisposables(move, addDisposableListener(dragTarget, 'pointerdown', (event) => { if (event.defaultPrevented) { event.preventDefault(); return; } // if somebody has marked this event then treat as a defaultPrevented // without actually calling event.preventDefault() if (quasiDefaultPrevented(event)) { return; } track(); }), addDisposableListener(this.options.content, 'pointerdown', (event) => { if (event.defaultPrevented) { return; } // if somebody has marked this event then treat as a defaultPrevented // without actually calling event.preventDefault() if (quasiDefaultPrevented(event)) { return; } if (event.shiftKey) { track(); } }), addDisposableListener(this.options.content, 'pointerdown', () => { arialLevelTracker.push(this._element); }, true)); if (options.inDragMode) { track(); } } setupResize(direction) { const resizeHandleElement = document.createElement('div'); resizeHandleElement.className = `dv-resize-handle-${direction}`; this._element.appendChild(resizeHandleElement); const move = new MutableDisposable(); this.addDisposables(move, addDisposableListener(resizeHandleElement, 'pointerdown', (e) => { e.preventDefault(); let startPosition = null; const iframes = disableIframePointEvents(); move.value = new CompositeDisposable(addDisposableListener(window, 'pointermove', (e) => { const containerRect = this.options.container.getBoundingClientRect(); const overlayRect = this._element.getBoundingClientRect(); const y = e.clientY - containerRect.top; const x = e.clientX - containerRect.left; if (startPosition === null) { // record the initial dimensions since as all subsequence moves are relative to this startPosition = { originalY: y, originalHeight: overlayRect.height, originalX: x, originalWidth: overlayRect.width, }; } let top = undefined; let bottom = undefined; let height = undefined; let left = undefined; let right = undefined; let width = undefined; const moveTop = () => { top = clamp(y, -Number.MAX_VALUE, startPosition.originalY + startPosition.originalHeight > containerRect.height ? this.getMinimumHeight(containerRect.height) : Math.max(0, startPosition.originalY + startPosition.originalHeight - Overlay.MINIMUM_HEIGHT)); height = startPosition.originalY + startPosition.originalHeight - top; bottom = containerRect.height - top - height; }; const moveBottom = () => { top = startPosition.originalY - startPosition.originalHeight; height = clamp(y - top, top < 0 && typeof this.options .minimumInViewportHeight === 'number' ? -top + this.options.minimumInViewportHeight : Overlay.MINIMUM_HEIGHT, Number.MAX_VALUE); bottom = containerRect.height - top - height; }; const moveLeft = () => { left = clamp(x, -Number.MAX_VALUE, startPosition.originalX + startPosition.originalWidth > containerRect.width ? this.getMinimumWidth(containerRect.width) : Math.max(0, startPosition.originalX + startPosition.originalWidth - Overlay.MINIMUM_WIDTH)); width = startPosition.originalX + startPosition.originalWidth - left; right = containerRect.width - left - width; }; const moveRight = () => { left = startPosition.originalX - startPosition.originalWidth; width = clamp(x - left, left < 0 && typeof this.options .minimumInViewportWidth === 'number' ? -left + this.options.minimumInViewportWidth : Overlay.MINIMUM_WIDTH, Number.MAX_VALUE); right = containerRect.width - left - width; }; switch (direction) { case 'top': moveTop(); break; case 'bottom': moveBottom(); break; case 'left': moveLeft(); break; case 'right': moveRight(); break; case 'topleft': moveTop(); moveLeft(); break; case 'topright': moveTop(); moveRight(); break; case 'bottomleft': moveBottom(); moveLeft(); break; case 'bottomright': moveBottom(); moveRight(); break; } const bounds = {}; // Anchor to top or to bottom depending on which one is closer if (top <= bottom) { bounds.top = top; } else { bounds.bottom = bottom; } // Anchor to left or to right depending on which one is closer if (left <= right) { bounds.left = left; } else { bounds.right = right; } bounds.height = height; bounds.width = width; this.setBounds(bounds); }), { dispose: () => { iframes.release(); }, }, addDisposableListener(window, 'pointerup', () => { move.dispose(); this._onDidChangeEnd.fire(); })); })); } getMinimumWidth(width) { if (typeof this.options.minimumInViewportWidth === 'number') { return width - this.options.minimumInViewportWidth; } return 0; } getMinimumHeight(height) { if (typeof this.options.minimumInViewportHeight === 'number') { return height - this.options.minimumInViewportHeight; } return 0; } dispose() { arialLevelTracker.destroy(this._element); this._element.remove(); super.dispose(); } } Overlay.MINIMUM_HEIGHT = 20; Overlay.MINIMUM_WIDTH = 20;