UNPKG

dockview-core

Version:

Zero dependency layout manager supporting tabs, grids and splitviews

317 lines (316 loc) 10.8 kB
import { Emitter, addDisposableListener, } from './events'; import { CompositeDisposable } from './lifecycle'; export class OverflowObserver extends CompositeDisposable { constructor(el) { super(); this._onDidChange = new Emitter(); this.onDidChange = this._onDidChange.event; this._value = null; this.addDisposables(this._onDidChange, watchElementResize(el, (entry) => { const hasScrollX = entry.target.scrollWidth > entry.target.clientWidth; const hasScrollY = entry.target.scrollHeight > entry.target.clientHeight; this._value = { hasScrollX, hasScrollY }; this._onDidChange.fire(this._value); })); } } export function watchElementResize(element, cb) { const observer = new ResizeObserver((entires) => { /** * Fast browser window resize produces Error: ResizeObserver loop limit exceeded. * The error isn't visible in browser console, doesn't affect functionality, but degrades performance. * See https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded/58701523#58701523 */ requestAnimationFrame(() => { const firstEntry = entires[0]; cb(firstEntry); }); }); observer.observe(element); return { dispose: () => { observer.unobserve(element); observer.disconnect(); }, }; } export const removeClasses = (element, ...classes) => { for (const classname of classes) { if (element.classList.contains(classname)) { element.classList.remove(classname); } } }; export const addClasses = (element, ...classes) => { for (const classname of classes) { if (!element.classList.contains(classname)) { element.classList.add(classname); } } }; export const toggleClass = (element, className, isToggled) => { const hasClass = element.classList.contains(className); if (isToggled && !hasClass) { element.classList.add(className); } if (!isToggled && hasClass) { element.classList.remove(className); } }; export function isAncestor(testChild, testAncestor) { while (testChild) { if (testChild === testAncestor) { return true; } testChild = testChild.parentNode; } return false; } export function getElementsByTagName(tag, document) { return Array.prototype.slice.call(document.querySelectorAll(tag), 0); } export function trackFocus(element) { return new FocusTracker(element); } /** * Track focus on an element. Ensure tabIndex is set when an HTMLElement is not focusable by default */ class FocusTracker extends CompositeDisposable { constructor(element) { super(); this._onDidFocus = new Emitter(); this.onDidFocus = this._onDidFocus.event; this._onDidBlur = new Emitter(); this.onDidBlur = this._onDidBlur.event; this.addDisposables(this._onDidFocus, this._onDidBlur); let hasFocus = isAncestor(document.activeElement, element); let loosingFocus = false; const onFocus = () => { loosingFocus = false; if (!hasFocus) { hasFocus = true; this._onDidFocus.fire(); } }; const onBlur = () => { if (hasFocus) { loosingFocus = true; window.setTimeout(() => { if (loosingFocus) { loosingFocus = false; hasFocus = false; this._onDidBlur.fire(); } }, 0); } }; this._refreshStateHandler = () => { const currentNodeHasFocus = isAncestor(document.activeElement, element); if (currentNodeHasFocus !== hasFocus) { if (hasFocus) { onBlur(); } else { onFocus(); } } }; this.addDisposables(addDisposableListener(element, 'focus', onFocus, true)); this.addDisposables(addDisposableListener(element, 'blur', onBlur, true)); } refreshState() { this._refreshStateHandler(); } } // quasi: apparently, but not really; seemingly const QUASI_PREVENT_DEFAULT_KEY = 'dv-quasiPreventDefault'; // mark an event directly for other listeners to check export function quasiPreventDefault(event) { event[QUASI_PREVENT_DEFAULT_KEY] = true; } // check if this event has been marked export function quasiDefaultPrevented(event) { return event[QUASI_PREVENT_DEFAULT_KEY]; } export function addStyles(document, styleSheetList) { const styleSheets = Array.from(styleSheetList); for (const styleSheet of styleSheets) { if (styleSheet.href) { const link = document.createElement('link'); link.href = styleSheet.href; link.type = styleSheet.type; link.rel = 'stylesheet'; document.head.appendChild(link); } let cssTexts = []; try { if (styleSheet.cssRules) { cssTexts = Array.from(styleSheet.cssRules).map((rule) => rule.cssText); } } catch (err) { // security errors (lack of permissions), ignore } for (const rule of cssTexts) { const style = document.createElement('style'); style.appendChild(document.createTextNode(rule)); document.head.appendChild(style); } } } export function getDomNodePagePosition(domNode) { const { left, top, width, height } = domNode.getBoundingClientRect(); return { left: left + window.scrollX, top: top + window.scrollY, width: width, height: height, }; } /** * Check whether an element is in the DOM (including the Shadow DOM) * @see https://terodox.tech/how-to-tell-if-an-element-is-in-the-dom-including-the-shadow-dom/ */ export function isInDocument(element) { let currentElement = element; while (currentElement === null || currentElement === void 0 ? void 0 : currentElement.parentNode) { if (currentElement.parentNode === document) { return true; } else if (currentElement.parentNode instanceof DocumentFragment) { // handle shadow DOMs currentElement = currentElement.parentNode.host; } else { currentElement = currentElement.parentNode; } } return false; } export function addTestId(element, id) { element.setAttribute('data-testid', id); } /** * Should be more efficient than element.querySelectorAll("*") since there * is no need to store every element in-memory using this approach */ function allTagsNamesInclusiveOfShadowDoms(tagNames) { const iframes = []; function findIframesInNode(node) { if (node.nodeType === Node.ELEMENT_NODE) { if (tagNames.includes(node.tagName)) { iframes.push(node); } if (node.shadowRoot) { findIframesInNode(node.shadowRoot); } for (const child of node.children) { findIframesInNode(child); } } } findIframesInNode(document.documentElement); return iframes; } export function disableIframePointEvents(rootNode = document) { const iframes = allTagsNamesInclusiveOfShadowDoms(['IFRAME', 'WEBVIEW']); const original = new WeakMap(); // don't hold onto HTMLElement references longer than required for (const iframe of iframes) { original.set(iframe, iframe.style.pointerEvents); iframe.style.pointerEvents = 'none'; } return { release: () => { var _a; for (const iframe of iframes) { iframe.style.pointerEvents = (_a = original.get(iframe)) !== null && _a !== void 0 ? _a : 'auto'; } iframes.splice(0, iframes.length); // don't hold onto HTMLElement references longer than required }, }; } export function getDockviewTheme(element) { function toClassList(element) { const list = []; for (let i = 0; i < element.classList.length; i++) { list.push(element.classList.item(i)); } return list; } let theme = undefined; let parent = element; while (parent !== null) { theme = toClassList(parent).find((cls) => cls.startsWith('dockview-theme-')); if (typeof theme === 'string') { break; } parent = parent.parentElement; } return theme; } export class Classnames { constructor(element) { this.element = element; this._classNames = []; } setClassNames(classNames) { for (const className of this._classNames) { toggleClass(this.element, className, false); } this._classNames = classNames .split(' ') .filter((v) => v.trim().length > 0); for (const className of this._classNames) { toggleClass(this.element, className, true); } } } const DEBOUCE_DELAY = 100; export function isChildEntirelyVisibleWithinParent(child, parent) { // const childPosition = getDomNodePagePosition(child); const parentPosition = getDomNodePagePosition(parent); if (childPosition.left < parentPosition.left) { return false; } if (childPosition.left + childPosition.width > parentPosition.left + parentPosition.width) { return false; } return true; } export function onDidWindowMoveEnd(window) { const emitter = new Emitter(); let previousScreenX = window.screenX; let previousScreenY = window.screenY; let timeout; const checkMovement = () => { if (window.closed) { return; } const currentScreenX = window.screenX; const currentScreenY = window.screenY; if (currentScreenX !== previousScreenX || currentScreenY !== previousScreenY) { clearTimeout(timeout); timeout = setTimeout(() => { emitter.fire(); }, DEBOUCE_DELAY); previousScreenX = currentScreenX; previousScreenY = currentScreenY; } requestAnimationFrame(checkMovement); }; checkMovement(); return emitter; } export function onDidWindowResizeEnd(element, cb) { let resizeTimeout; const disposable = new CompositeDisposable(addDisposableListener(element, 'resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { cb(); }, DEBOUCE_DELAY); })); return disposable; }