import { html, GemElement, customElement, connectStore, refobject, RefObject } from '@mantou/gem'; import { PanEventDetail } from '@mantou/gem/elements/gesture'; import '@mantou/gem/elements/gesture'; import { Window } from '../lib/layout'; import { GetPanelContent } from '../lib/panel'; import { cancelHandleWindow, dropHandleWindow, setWindowPanTimeout, store, updateCurrentPanel, updatePanelSort, updateWindowPosition, updateWindowType, updateWindowZIndex, independentPanel, loadContentInPanel, } from '../lib/store'; import { GemPanelTitleElement } from './panel-title'; import { distance } from '../lib/utils'; import { theme } from '../lib/theme'; import { CANCEL_WINDOW_DRAGOVER_DISTANCE, ENTWE_PANEL_SORT_DISTANCE, ENTWE_WINDOW_DRAGOVER_DISTANCE, NEWWINDOW_FROM_PANEL_Y_OFFSET, WINDOW_TITLEBAR_HEIGHT, } from '../lib/const'; import './window-mask'; import './window-handle'; import './panel-title'; import './panel-placeholder'; const getContentWeakMap = new WeakMap>(); function execGetContent(fn: GetPanelContent | undefined, panelName: string) { if (!fn) return; if (!getContentWeakMap.has(fn)) { getContentWeakMap.set( fn, fn(panelName).then((result) => loadContentInPanel(panelName, result)), ); } } export const windowTagName = 'gem-panel-window'; type State = { independentWindow: Window | null; // store? panelName: string | null; move: boolean; offsetX: number; offsetY: number; parentOffsetX: number; parentOffsetY: number; clientX: number; clientY: number; scrollX: number; }; @customElement(windowTagName) @connectStore(store) export class GemPanelWindowElement extends GemElement { @refobject windowRef: RefObject; window: Window; state: State = { independentWindow: null, panelName: null, move: false, offsetX: 0, offsetY: 0, parentOffsetX: 0, parentOffsetY: 0, clientX: 0, clientY: 0, scrollX: 0, }; #onMoveTitleStart = (panelName: string, evt: PointerEvent) => { evt.stopPropagation(); // prevent const target = evt.currentTarget as HTMLElement; target.setPointerCapture(evt.pointerId); const { x, y } = target.getBoundingClientRect(); const parentRect = target.offsetParent?.getBoundingClientRect(); this.setState({ panelName, offsetX: evt.clientX - x, offsetY: evt.clientY - y, parentOffsetX: parentRect?.x, parentOffsetY: parentRect?.y, clientX: evt.clientX, clientY: evt.clientY, }); }; #onMoveTitle = (evt: PointerEvent) => { const { panelName, move, offsetY, parentOffsetY, clientX, clientY } = this.state; if (!panelName) return; // first move if (!move && distance(evt.clientX - clientX, evt.clientY - clientY) < ENTWE_PANEL_SORT_DISTANCE) return; const target = evt.currentTarget as HTMLElement; if (Math.abs(evt.clientY - (parentOffsetY + offsetY)) > NEWWINDOW_FROM_PANEL_Y_OFFSET) { this.#createIndependentWindow(evt); } else { this.setState({ move: true, clientX: evt.clientX, clientY: evt.clientY, scrollX: target.offsetParent?.scrollLeft, }); const ele = this.shadowRoot?.elementFromPoint(evt.clientX, parentOffsetY + offsetY); if (ele instanceof GemPanelTitleElement && ele.panelName !== panelName) { updatePanelSort(this.window, panelName, ele.panelName); } } }; #onMoveTitleEnd = () => { // pointerup -> click setTimeout(() => { this.setState({ panelName: null, move: false }); }); }; #createIndependentWindow = (evt: PointerEvent) => { const { panelName, offsetX, offsetY } = this.state; if (!panelName) return; // Transfer event target this.setPointerCapture(evt.pointerId); const { width, height } = this.getBoundingClientRect(); const independentWindow = independentPanel(this.window, panelName, [ evt.clientX - offsetX, evt.clientY - offsetY - WINDOW_TITLEBAR_HEIGHT, width, height, ]); this.setState({ independentWindow, panelName: null, move: false, clientX: evt.clientX, clientY: evt.clientY, }); }; #onMove = (evt: PointerEvent) => { const { clientX, clientY, independentWindow } = this.state; if (independentWindow) { clearTimeout(store.windowPanTimer); this.setState({ clientX: evt.clientX, clientY: evt.clientY }); const [x, y] = [evt.clientX - clientX, evt.clientY - clientY]; updateWindowPosition(independentWindow, [x, y]); setWindowPanTimeout(this, independentWindow, [evt.clientX, evt.clientY]); if (distance(x, y) > CANCEL_WINDOW_DRAGOVER_DISTANCE) { cancelHandleWindow(); } } }; #onMoveEnd = () => { const { independentWindow } = this.state; if (independentWindow) { dropHandleWindow(independentWindow); setTimeout(() => { this.setState({ independentWindow: null }); }); } }; #onActivePanel = (panelName: string) => { const { move, independentWindow } = this.state; if (!move && !independentWindow) { updateCurrentPanel(this.window, panelName); } }; #onHeaderPan = ({ detail }: CustomEvent) => { clearTimeout(store.windowPanTimer); if (this.window.isGridWindow()) { if (distance(detail.x, detail.y) > ENTWE_WINDOW_DRAGOVER_DISTANCE) { updateWindowType(this.window, this.getBoundingClientRect()); } } else { setWindowPanTimeout(this, this.window, [detail.clientX, detail.clientY]); if (distance(detail.x, detail.y) > CANCEL_WINDOW_DRAGOVER_DISTANCE) { cancelHandleWindow(); } updateWindowPosition(this.window, [detail.x, detail.y]); } }; #onHeaderEnd = () => { dropHandleWindow(this.window); }; #onHeaderWheel = (evt: WheelEvent) => { const target = evt.currentTarget as HTMLElement; target.scrollBy(evt.deltaY, 0); }; #onFocusWindow = () => { updateWindowZIndex(this.window); }; mounted = () => { this.addEventListener('focus', this.#onFocusWindow); this.addEventListener('pointermove', this.#onMove); this.addEventListener('pointerup', this.#onMoveEnd); this.addEventListener('pointercancel', this.#onMoveEnd); this.effect( ([currentPanel]) => { if (currentPanel && !currentPanel.content) { execGetContent(currentPanel.getContent, currentPanel.name); } }, () => { const { panels, current } = this.window; return [store.panels[panels[current]]]; }, ); }; render = () => { const isGrid = this.window.isGridWindow(); const { panels, gridArea, current, position, dimension, zIndex, engross } = this.window; const { panelName, move, offsetX, clientX, scrollX, parentOffsetX } = this.state; const currentPanel = store.panels[panels[current]]; return html`
${isGrid ? '' : html` `}
${panels.map( (p, index) => html` this.#onActivePanel(p)} @pointerdown=${(evt: PointerEvent) => this.#onMoveTitleStart(p, evt)} @pointermove=${this.#onMoveTitle} @pointerup=${this.#onMoveTitleEnd} @pointercancel=${this.#onMoveTitleEnd} > `, )} ${panelName && move ? html` ` : ''}
${currentPanel?.content || currentPanel?.placeholder || html``}
${store.hoverWindow === this.window ? html`` : ''}
`; }; focus = () => { this.windowRef.element?.focus(); }; }