import { action, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, returnTrue, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { GetEffectiveAcl, inheritParentAcls, SetPropSetterCb } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import * as GoldenLayout from '../../goldenLayout'; import { DragManager } from '../../util/DragManager'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; import { DocumentView } from '../nodes/DocumentView'; import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere'; import { OverlayView } from '../OverlayView'; import { ScriptingRepl } from '../ScriptingRepl'; import { UndoStack } from '../UndoStack'; import './CollectionDockingView.scss'; import { CollectionSubView } from './CollectionSubView'; const _global = (window /* browser */ || global) /* node */ as any; @observer export class CollectionDockingView extends CollectionSubView() { static tabClass: JSX.Element | null = null; /** * Initialize by assigning the add split method to DocumentView and by * configuring golden layout to render its documents using the specified React component * @param ele - typically would be set to TabDocView */ public static Init(ele: any) { this.tabClass = ele; DocumentView.addSplit = CollectionDockingView.AddSplit; } // eslint-disable-next-line no-use-before-define @observable public static Instance: CollectionDockingView | undefined = undefined; private _reactionDisposer?: IReactionDisposer; private _lightboxReactionDisposer?: IReactionDisposer; private _containerRef = React.createRef(); private _flush: UndoManager.Batch | undefined; private _unmounting = false; private _ignoreStateChange = ''; public tabMap: Set = new Set(); public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; } private _goldenLayout: any = null; static _highlightStyleSheet: any = addStyleSheet(); constructor(props: any) { super(props); makeObservable(this); if (this._props.renderDepth < 0) CollectionDockingView.Instance = this; // Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; DragManager.StartWindowDrag = this.StartOtherDrag; this.Document.myTrails; // this is equivalent to having a prefetchProxy for myTrails which is needed for the My Trails button in the UI which assumes that Doc.ActiveDashboard.myTrails is legit... } /** * Switches from dragging a document around a freeform canvas to dragging it as a tab to be docked. * * @param e fake mouse down event position data containing pageX and pageY coordinates * @param dragDocs the documents to be dragged * @param batch optionally an undo batch that has been started to use instead of starting a new batch */ public StartOtherDrag = (e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => { this._flush = this._flush ?? UndoManager.StartBatch('golden layout drag'); const config = dragDocs.length === 1 ? DashboardView.makeDocumentConfig(dragDocs[0]) : { type: 'row', content: dragDocs.map(doc => DashboardView.makeDocumentConfig(doc)) }; const dragSource = CollectionDockingView.Instance?._goldenLayout.createDragSource(document.createElement('div'), config); this.tabDragStart(dragSource, finishDrag); dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }); return true; }; tabItemDropped = () => DragManager.CompleteWindowDrag?.(false); tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => { this._flush = this._flush ?? UndoManager.StartBatch('tab move'); const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc; dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc])); DragManager.CompleteWindowDrag = (aborted: boolean) => { if (aborted) { proxy._dragListener.AbortDrag(); if (this._flush) { this._flush.cancel(); // cancel the undo change being logged this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout) } DragManager.CompleteWindowDrag = undefined; } finishDrag?.(aborted); setTimeout(this.endUndoBatch, 100); }; }; @undoBatch public CloseFullScreen = () => { this._goldenLayout._maximisedItem?.toggleMaximise(); this.stateChanged(); }; @undoBatch public static CloseSplit(document: Opt, panelName?: string): boolean { if (CollectionDockingView.Instance) { const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find(tabView => (panelName ? tabView.contentItem.config.props.panelName === panelName : tabView.DashDoc === document)); if (tab) { const j = tab.header.parent.contentItems.indexOf(tab.contentItem); if (j !== -1) { tab.header.parent.contentItems[j].remove(); CollectionDockingView.Instance.endUndoBatch(); return CollectionDockingView.Instance.layoutChanged(); } } } return false; } @undoBatch public static ReplaceTab(document: Doc, mods: OpenWhereMod, stack: any, panelName: string, addToSplit?: boolean, keyValue?: boolean): boolean { const instance = CollectionDockingView.Instance; if (!instance) return false; const newConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue); if (!panelName && stack) { const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config); const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); stack.contentItems[activeContentItemIndex].remove(); return instance.layoutChanged(); } const tab = Array.from(instance.tabMap.keys()).find(tabView => tabView.contentItem.config.props.panelName === panelName); if (tab) { const j = tab.header.parent.contentItems.indexOf(tab.contentItem); if (newConfig.props.documentId !== tab.header.parent.contentItems[j].config.props.documentId) { tab.header.parent.addChild(newConfig, undefined); !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove(); return instance.layoutChanged(); } return false; } return CollectionDockingView.AddSplit(document, mods, stack, panelName); } @undoBatch public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { return Array.from(CollectionDockingView.Instance?.tabMap.keys() ?? []).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue); } // // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // @action public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { if (document?._type_collection === CollectionViewType.Docking && !keyValue) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tabView => tabView.DashDoc === document && !tabView.contentItem.config.props.keyValue && !keyValue); if (tab) { tab.header.parent.setActiveContentItem(tab.contentItem); return true; } const instance = CollectionDockingView.Instance; const glayRoot = instance._goldenLayout.root; if (!instance) return false; const docContentConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue); CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split'); setTimeout(CollectionDockingView.Instance.endUndoBatch, 100); if (!pullSide && stack) { stack.addChild(docContentConfig, undefined); setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1])); } else { const newContentItem = () => { const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout); newItem.callDownwards('_$init'); return newItem; }; if (glayRoot.contentItems.length === 0) { // if no rows / columns glayRoot.addChild(newContentItem()); } else if (glayRoot.contentItems[0].isStack) { glayRoot.contentItems[0].addChild(docContentConfig); } else if (glayRoot.contentItems.length === 1 && glayRoot.contentItems[0].contentItems.length === 1 && glayRoot.contentItems[0].contentItems[0].contentItems.length === 0) { glayRoot.contentItems[0].contentItems[0].addChild(docContentConfig); } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row switch (pullSide) { // eslint-disable-next-line default-case-last default: case OpenWhereMod.none: case OpenWhereMod.right: glayRoot.contentItems[0].addChild(newContentItem()); break; case OpenWhereMod.left: glayRoot.contentItems[0].addChild(newContentItem(), 0); break; case OpenWhereMod.top: case OpenWhereMod.bottom: { // if not going in a row layout, must add already existing content into column const rowlayout = glayRoot.contentItems[0]; const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout); const newItem = newContentItem(); instance._goldenLayout.saveScrollTops(rowlayout.element); rowlayout.parent.replaceChild(rowlayout, newColumn); if (pullSide === 'top') { newColumn.addChild(rowlayout, undefined, true); newColumn.addChild(newItem, 0, true); } else if (pullSide === 'bottom') { newColumn.addChild(newItem, undefined, true); newColumn.addChild(rowlayout, 0, true); } instance._goldenLayout.restoreScrollTops(rowlayout.element); rowlayout.config.height = 50; newItem.config.height = 50; } } } else { // if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column switch (pullSide) { case 'top': glayRoot.contentItems[0].addChild(newContentItem(), 0); break; case 'bottom': glayRoot.contentItems[0].addChild(newContentItem()); break; case 'left': case 'right': default: { // if not going in a row layout, must add already existing content into column const collayout = glayRoot.contentItems[0]; const newRow = collayout.layoutManager.createContentItem({ type: 'row' }, instance._goldenLayout); const newItem = newContentItem(); instance._goldenLayout.saveScrollTops(collayout.element); collayout.parent.replaceChild(collayout, newRow); if (pullSide === 'left') { newRow.addChild(collayout, undefined, true); newRow.addChild(newItem, 0, true); } else { newRow.addChild(newItem, undefined, true); newRow.addChild(collayout, 0, true); } instance._goldenLayout.restoreScrollTops(collayout.element); collayout.config.width = 50; newItem.config.width = 50; } } } instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); } return instance.layoutChanged(); } @undoBatch @action layoutChanged() { this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); this._goldenLayout.emit('stateChanged'); this.stateChanged(); return true; } setupGoldenLayout = async () => { if (this._unmounting) return; // const config = StrCast(this.Document.dockingConfig, JSON.stringify(DashboardView.resetDashboard(this.Document))); const config = StrCast(this.Document.dockingConfig); if (config) { const matches = config.match(/"documentId":"[a-z0-9-]+"/g); const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? []; await Promise.all(docids.map(id => DocServer.GetRefField(id))); if (this._goldenLayout) { if (config === JSON.stringify(this._goldenLayout.toConfig())) { return; } try { this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); this._goldenLayout.unbind('stackCreated', this.stackCreated); } catch (e) { /* empty */ } this.tabMap.clear(); this._goldenLayout.destroy(); } const glay = (this._goldenLayout = new GoldenLayout(JSON.parse(config))); glay.on('tabCreated', this.tabCreated); glay.on('tabDestroyed', this.tabDestroyed); glay.on('stackCreated', this.stackCreated); glay.registerComponent('DocumentFrameRenderer', CollectionDockingView.tabClass); glay.container = this._containerRef.current; glay.init(); glay.root.layoutManager.on('itemDropped', this.tabItemDropped); glay.root.layoutManager.on('dragStart', this.tabDragStart); glay.root.layoutManager.on('activeContentItemChanged', this.stateChanged); } else { console.log('ERROR: no config for dashboard!!'); } }; /** * This publishes Docs having titles starting with '@' to Doc.myPublishedDocs * Once published, any text that uses the 'title' in its body will automatically * be linked to this published document. * @param target * @param title */ titleChanged = (target: any, value: any) => { const title = Field.toString(value); if (title.startsWith('@') && !title.substring(1).match(/[()[\]@]/) && title.length > 1) { const embedding = DocListCast(target.proto_embeddings).lastElement(); embedding && Doc.AddToMyPublished(embedding); } else if (!title.startsWith('@')) { DocListCast(target.proto_embeddings).forEach(doc => Doc.RemFromMyPublished(doc)); } }; componentDidMount: () => void = async () => { this._props.setContentViewBox?.(this); this._unmounting = false; SetPropSetterCb('title', this.titleChanged); // this overrides any previously assigned callback for the property if (this._containerRef.current) { this._lightboxReactionDisposer = reaction( () => DocumentView.LightboxDoc(), doc => setTimeout(() => !doc && this.onResize()) ); new _global.ResizeObserver(this.onResize).observe(this._containerRef.current); this._reactionDisposer = reaction( () => StrCast(this.Document.dockingConfig), config => { if (!this._goldenLayout || this._ignoreStateChange !== config) { // bcz: TODO! really need to diff config with ignoreStateChange and modify the current goldenLayout instead of building a new one. this.setupGoldenLayout(); } this._ignoreStateChange = ''; } ); reaction( () => this._props.PanelWidth(), width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows { fireImmediately: true } ); reaction( () => [SnappingManager.userBackgroundColor, SnappingManager.userBackgroundColor], () => { clearStyleSheetRules(CollectionDockingView._highlightStyleSheet); addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { background: `${SnappingManager.userBackgroundColor} !important` }); addStyleSheetRule(CollectionDockingView._highlightStyleSheet, 'lm_controls', { color: `${SnappingManager.userColor} !important` }); addStyleSheetRule(SnappingManager.SettingsStyle, 'lm_header', { background: `${SnappingManager.userBackgroundColor} !important` }); }, { fireImmediately: true } ); } }; componentWillUnmount: () => void = () => { this._unmounting = true; try { this._goldenLayout.unbind('stackCreated', this.stackCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); } catch (e) { /* empty */ } this._goldenLayout?.destroy(); window.removeEventListener('resize', this.onResize); window.removeEventListener('mouseup', this.onPointerUp); this._reactionDisposer?.(); this._lightboxReactionDisposer?.(); }; // ViewBoxInterface overrides override isUnstyledView = returnTrue; @action onResize = () => { const cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed !DocumentView.LightboxDoc() && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); }; endUndoBatch = () => { const json = JSON.stringify(this._goldenLayout.toConfig()); const matches = json.match(/"documentId":"[a-z0-9-]+"/g); const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')); const docs = !docids ? [] : docids .map(id => DocServer.GetCachedRefField(id)) .filter(f => f) .map(f => f as Doc); const changesMade = this.Document.dockingConfig !== json; if (changesMade) { if (![AclAdmin, AclEdit].includes(GetEffectiveAcl(this.dataDoc))) { this.layoutDoc.dockingConfig = json; this.layoutDoc.data = new List(docs); } else { Doc.SetInPlace(this.Document, 'dockingConfig', json, true); Doc.SetInPlace(this.Document, 'data', new List(docs), true); } } this._flush?.end(); this._flush = undefined; }; @action onPointerUp = (): void => { window.removeEventListener('pointerup', this.onPointerUp); DragManager.CompleteWindowDrag = undefined; setTimeout(this.endUndoBatch, 100); }; @action onPointerDown = (e: React.PointerEvent): void => { let hitFlyout = false; for (let par = e.target as any; !hitFlyout && par; par = par.parentElement) { hitFlyout = par.className === 'dockingViewButtonSelector'; } if (!hitFlyout) { const htmlTarget = e.target as HTMLElement; window.addEventListener('mouseup', this.onPointerUp); if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) { const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : ''; if (className.includes('lm_maximise')) { // this._flush = UndoManager.StartBatch('tab maximize'); } else { const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement); const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget); if (map?.DashDoc && DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.())) { DocumentView.SelectView(DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.()), false); } } } } if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { e.stopPropagation(); } }; public CaptureThumbnail() { const content = this.DocumentView?.()?.ContentDiv; if (content) { const _width = DivWidth(content); const _height = DivHeight(content); return UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', iconFile => { const proto = this.dataDoc; // Cast(img.proto, Doc, null)!; proto.thumb_nativeWidth = _width; proto.thumb_nativeHeight = _height; proto.thumb = new ImageField(iconFile); }); } return undefined; } public static async TakeSnapshot(doc: Doc | undefined, clone = false) { if (!doc) return undefined; let json = StrCast(doc.dockingConfig); if (clone) { const cloned = await Doc.MakeClone(doc); Array.from(cloned.map.entries()).forEach(entry => { json = json.replace(entry[0], entry[1][Id]); }); cloned.clone[DocData].dockingConfig = json; return DashboardView.openDashboard(cloned.clone); } const matches = json.match(/"documentId":"[a-z0-9-]+"/g); const origtabids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) || []; const origtabs = origtabids .map(id => DocServer.GetCachedRefField(id)) .filter(f => f) .map(f => f as Doc); const newtabs = origtabs.map(origtab => { const origtabdocs = DocListCast(origtab.data); const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeEmbedding(origtab); const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc)); if (newtabdocs.length) { newtab[DocData].data = new List(newtabdocs); newtabdocs.forEach(ntab => Doc.SetContainer(ntab, newtab)); } json = json.replace(origtab[Id], newtab[Id]); return newtab; }); const dashboardDoc = Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) }); dashboardDoc.pane_count = 1; dashboardDoc.myOverlayDocs = new List(); dashboardDoc.myPublishedDocs = new List(); DashboardView.SetupDashboardTrails(dashboardDoc); DashboardView.SetupDashboardCalendars(dashboardDoc); // Zaul TODO: needed? return DashboardView.openDashboard(dashboardDoc); } @action stateChanged = () => { this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); const json = JSON.stringify(this._goldenLayout.toConfig()); const changesMade = this.Document.dockingConfig !== json; return changesMade; }; tabDestroyed = (tab: any) => { this._flush = this._flush ?? UndoManager.StartBatch('tab movement'); const dashDoc = tab.DashDoc; if (dashDoc && ![DocumentType.PRES].includes(dashDoc.type) && !tab.contentItem.config.props.keyValue) { Doc.AddDocToList(Doc.MyHeaderBar, 'data', dashDoc, undefined, undefined, true); // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed if (dashDoc.embedContainer === this.Document) dashDoc.embedContainer = undefined; if (!dashDoc.embedContainer) { Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashDoc, undefined, true, true); Doc.RemoveEmbedding(dashDoc, dashDoc); } } if (CollectionDockingView.Instance) { const dview = CollectionDockingView.Instance.Document; const { fieldKey } = CollectionDockingView.Instance.props; Doc.RemoveDocFromList(dview, fieldKey, dashDoc); this.tabMap.delete(tab); tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); this.stateChanged(); } }; tabCreated = (tab: any) => { this.tabMap.add(tab); tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content) }; stackCreated = (stackIn: any) => { const stack = stackIn.header ? stackIn : stackIn.origin; stack.header?.element.on('mousedown', (e: any) => { const dashboard = Doc.ActiveDashboard; if (dashboard && e.target === stack.header?.element[0] && e.button === 2) { dashboard.pane_count = NumCast(dashboard.pane_count) + 1; const docToAdd = Docs.Create.FreeformDocument([], { _width: this._props.PanelWidth(), _height: this._props.PanelHeight(), _freeform_backgroundGrid: true, _layout_fitWidth: true, title: `Untitled Tab ${NumCast(dashboard.pane_count)}`, }); Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true); inheritParentAcls(this.Document, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }); const addNewDoc = undoable(() => { const dashboard = Doc.ActiveDashboard; if (dashboard) { dashboard.pane_count = NumCast(dashboard.pane_count) + 1; const docToAdd = Docs.Create.FreeformDocument([], { _width: this._props.PanelWidth(), _height: this._props.PanelHeight(), _layout_fitWidth: true, _freeform_backgroundGrid: true, title: `Untitled Tab ${NumCast(dashboard.pane_count)}`, }); Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd, undefined, undefined, true); inheritParentAcls(this.dataDoc, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }, 'add new tab'); stack.header?.controlsContainer .find('.lm_close') // get the close icon .off('click') // unbind the current click handler .click( action(() => { // if (confirm('really close this?')) { if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) { const batch = UndoManager.StartBatch('close stack'); stack.remove(); setTimeout(() => { this.stateChanged(); batch.end(); }); } else { alert('cant delete the last stack'); } }) ); stack.element.click((e: any) => { if (stack.contentItems.length === 0 && Array.from(document.elementsFromPoint(e.originalEvent.x, e.originalEvent.y)).some(ele => ele?.className === 'empty-tabs-message')) { addNewDoc(); } }); stack.header?.controlsContainer .find('.lm_maximise') // get the close icon .click(() => setTimeout(this.stateChanged)); stack.header?.controlsContainer .find('.lm_popout') // get the popout icon .off('click') // unbind the current click handler .click(addNewDoc); }; render() { const href = ImageCast(this.Document.thumb)?.url?.href; return this._props.renderDepth > -1 ? (
{href ? ( thumbnail of nested dashboard': return OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); case "": return OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200, title: 'Undo stack' }); default: } Doc.AddToMyOverlay(doc); return true; } }, 'opens up document in location specified', '(doc: any)' ); ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback function openRepl() { return 'openRepl'; }, 'opens up document in screen overlay layer', '(doc: any)' ); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(async function snapshotDashboard() { const batch = UndoManager.StartBatch('snapshot'); await CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard); batch.end(); }, 'creates a snapshot copy of a dashboard');