From 6685295188dc4d0aed6d9fc9c585cb3b08a348dd Mon Sep 17 00:00:00 2001 From: Juck Date: Thu, 12 Oct 2023 18:32:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=201.=E7=AE=80=E5=8C=96=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - pnpm-lock.yaml | 7 - src/main.ts | 144 +++----------- src/types/obsidian.d.ts | 8 +- src/ui/popover.ts | 415 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 451 insertions(+), 124 deletions(-) create mode 100644 src/ui/popover.ts diff --git a/package.json b/package.json index a1a5383..4179998 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,6 @@ "husky": "^8.0.3", "jsdom": "^22.1.0", "lint-staged": "^14.0.1", - "monkey-around": "^2.3.0", "npm-run-all": "^4.1.5", "obsidian": "^1.4.11", "obsidian-dataview": "^0.5.56", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c4c18b..b2ddaff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,9 +199,6 @@ devDependencies: lint-staged: specifier: ^14.0.1 version: 14.0.1 - monkey-around: - specifier: ^2.3.0 - version: 2.3.0 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -5992,10 +5989,6 @@ packages: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} dev: true - /monkey-around@2.3.0: - resolution: {integrity: sha512-QWcCUWjqE/MCk9cXlSKZ1Qc486LD439xw/Ak8Nt6l2PuL9+yrc9TJakt7OHDuOqPRYY4nTWBAEFKn32PE/SfXA==} - dev: true - /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} diff --git a/src/main.ts b/src/main.ts index 932e562..45992c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import 'virtual:uno.css'; -import { around } from 'monkey-around'; import { App, Editor, - type EphemeralState, type MarkdownFileInfo, MarkdownPreviewRenderer, MarkdownView, @@ -14,9 +12,6 @@ import { TAbstractFile, TFile, Tasks, - type ViewState, - WorkspaceContainer, - WorkspaceItem, WorkspaceLeaf, WorkspaceWindow, debounce, @@ -24,6 +19,7 @@ import { } from 'obsidian'; import { ref } from 'vue'; import type { Database } from 'sql.js'; +import { HoverEditor, type HoverEditorParent } from '@/ui/popover'; import { expandEmmetAbbreviation } from '@/utils/emmet'; import { usePomodoroStore, useSystemStore } from '@/stores'; import Replacer from '@/Replacer'; @@ -52,7 +48,6 @@ import { NotifyUtil } from '@/utils/ntfy/notify'; import { EditorUtil, EditorUtils } from '@/utils/editor'; import t from '@/i18n'; import { UpdateModal } from '@/ui/modal/UpdateModal'; -import { HoverEditor, type HoverEditorParent } from '@/popover'; // import { initWorker } from '@/web-worker'; @@ -66,7 +61,6 @@ export default class AwesomeBrainManagerPlugin extends Plugin { private pomodoroHistoryView: PomodoroHistoryView | null; resizeFunction: () => any; clickFunction: (evt: MouseEvent) => any; - activeLeafChangeFunction: (leaf: WorkspaceLeaf) => any; fileOpenFunction: (file: TFile | null) => any; layoutChangeFunction: () => any; windowOpenFunction: (win: WorkspaceWindow, window: Window) => any; @@ -75,9 +69,9 @@ export default class AwesomeBrainManagerPlugin extends Plugin { fileMenuFunction: (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => any; editorMenuFunction: (menu: Menu, editor: Editor, info: MarkdownView | MarkdownFileInfo) => any; editorChangeFunction: (editor: Editor, info: MarkdownView | MarkdownFileInfo) => any; - editorPasteFunction: (evt: ClipboardEvent, editor: Editor, info: MarkdownView) => any; + editorPasteFunction: (evt: ClipboardEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo) => any; editorDropFunction: (evt: DragEvent, editor: Editor, info: MarkdownView | MarkdownFileInfo) => any; - codemirrorFunction: (cm: CodeMirror.Editor, info: MarkdownView) => any; + codemirrorFunction: (cm: CodeMirror.Editor) => any; quitFunction: (tasks: Tasks) => any; vaultCreateFunction: (file: TAbstractFile) => any; @@ -111,7 +105,6 @@ export default class AwesomeBrainManagerPlugin extends Plugin { this.editorChangeFunction = this.customizeEditorChange.bind(this); this.editorPasteFunction = this.customizeEditorPaste.bind(this); this.fileMenuFunction = this.customizeFileMenu.bind(this); - this.activeLeafChangeFunction = this.customizeActiveLeafChange.bind(this); this.codemirrorFunction = this.customizeCodeMirror.bind(this); this.vaultCreateFunction = this.customizeVaultCreate.bind(this); this.vaultModifyFunction = this.customizeVaultModify.bind(this); @@ -243,7 +236,11 @@ export default class AwesomeBrainManagerPlugin extends Plugin { onCodeMirrorChange(editor); } - async customizeEditorPaste(evt: ClipboardEvent, editor: Editor, markdownView: MarkdownView): Promise { + async customizeEditorPaste( + evt: ClipboardEvent, + editor: Editor, + info: MarkdownView | MarkdownFileInfo, + ): Promise { // LoggerUtil.log(''); } @@ -257,26 +254,7 @@ export default class AwesomeBrainManagerPlugin extends Plugin { }); } - async customizeActiveLeafChange(leaf: WorkspaceLeaf): Promise { - HoverEditor.activePopover?.hoverEl.removeClass('is-active'); - const hoverEditor = (HoverEditor.activePopover = leaf ? HoverEditor.forLeaf(leaf) : undefined); - if (hoverEditor && leaf) { - hoverEditor.hoverEl.addClass('is-active'); - const titleEl = hoverEditor.hoverEl.querySelector('.popover-title'); - if (!titleEl) return; - titleEl.textContent = leaf.view?.getDisplayText(); - if (leaf?.view?.getViewType()) { - hoverEditor.hoverEl.setAttribute('data-active-view-type', leaf.view.getViewType()); - } - if (leaf.view?.file?.path) { - titleEl.setAttribute('data-path', leaf.view.file.path); - } else { - titleEl.removeAttribute('data-path'); - } - } - } - - async customizeCodeMirror(cm: CodeMirror.Editor, view: MarkdownView): Promise { + async customizeCodeMirror(cm: CodeMirror.Editor): Promise { // LoggerUtil.log(''); } @@ -299,7 +277,6 @@ export default class AwesomeBrainManagerPlugin extends Plugin { override async onload(): Promise { await this.pluginDataIO.load(); LoggerUtil.init(SETTINGS.debugEnable); - this.patchWorkspaceLeaf(); DBUtil.init(this, () => { usePomodoroStore().loadPomodoroData(); this.startPomodoroTask(); @@ -330,72 +307,6 @@ export default class AwesomeBrainManagerPlugin extends Plugin { this.announceUpdate(); } - patchWorkspaceLeaf() { - this.register( - around(WorkspaceLeaf.prototype, { - getRoot(old) { - return function () { - const top = old.call(this); - top.getRoot === this.getRoot ? top : top.getRoot(); - // bugfix make.md冲突,不能使用ctrl+o打开文件 #bug - return top; - }; - }, - onResize(old) { - return function () { - this.view?.onResize(); - }; - }, - setViewState(old) { - return async function (viewState: ViewState, eState?: unknown) { - const result = await old.call(this, viewState, eState); - // try and catch files that are opened from outside of the - // HoverEditor class so that we can update the popover title bar - try { - const he = HoverEditor.forLeaf(this); - if (he) { - if (viewState.type) he.hoverEl.setAttribute('data-active-view-type', viewState.type); - const titleEl = he.hoverEl.querySelector('.popover-title'); - if (titleEl) { - titleEl.textContent = this.view?.getDisplayText(); - if (this.view?.file?.path) { - titleEl.setAttribute('data-path', this.view.file.path); - } else { - titleEl.removeAttribute('data-path'); - } - } - } - } catch { - // ignore - } - return result; - }; - }, - setEphemeralState(old) { - return function (state: EphemeralState) { - old.call(this, state); - if (state.focus && this.view?.getViewType() === 'empty') { - // Force empty (no-file) view to have focus so dialogs don't reset active pane - this.view.contentEl.tabIndex = -1; - this.view.contentEl.focus(); - } - }; - }, - }), - ); - this.register( - around(WorkspaceItem.prototype, { - getContainer(old) { - return function () { - if (!old) return; // 0.14.x doesn't have this - if (!this.parentSplit || this instanceof WorkspaceContainer) return old.call(this); - return this.parentSplit.getContainer(); - }; - }, - }), - ); - } - private startPomodoroTask() { // 进来就找到ing任务,如果有,则开始interval任务,倒计时准备弹窗提醒 // 监听数据库变化事件,若变化,则刷新监听的任务 @@ -517,13 +428,6 @@ export default class AwesomeBrainManagerPlugin extends Plugin { }); } - spawnPopover(initiatingEl?: HTMLElement, onShowCallback?: () => unknown): WorkspaceLeaf { - const parent = this.app.workspace.activeLeaf as unknown as HoverEditorParent; - if (!initiatingEl) initiatingEl = parent.containerEl; - const hoverPopover = new HoverEditor(parent, initiatingEl!, this, undefined, onShowCallback); - return hoverPopover.attachLeaf(); - } - private setupCommands() { this.addCommand({ id: 'cut-line', @@ -729,9 +633,10 @@ export default class AwesomeBrainManagerPlugin extends Plugin { // window.addEventListener('languagechange', () => { // console.log('languagechange event detected!'); // }); - this.registerDomEvent(activeDocument, 'selectionchange', async (e: MouseEvent) => { - EditorUtil.changeToolbarPopover(e, SETTINGS.toolbar); - }); + const selectionChangeCallback = async (e: Event) => { + EditorUtil.changeToolbarPopover(e as MouseEvent, SETTINGS.toolbar); + }; + this.registerDomEvent(activeDocument, 'selectionchange', selectionChangeCallback); this.registerDomEvent(activeDocument, 'click', async (e: MouseEvent) => { toggleMouseClickEffects(e, SETTINGS.clickString); }); @@ -742,16 +647,26 @@ export default class AwesomeBrainManagerPlugin extends Plugin { }); }; const previewCursorCallback = (e: CustomEvent) => { - const newLeaf = this.spawnPopover(undefined, () => this.app.workspace.setActiveLeaf(newLeaf, false, true)); - newLeaf.openLinkText(e.detail.cursorTarget.title, e.detail.cursorTarget.path); + const newLeaf = new HoverEditor( + this.app.workspace.activeLeaf as unknown as HoverEditorParent, + this.app.workspace.activeLeaf!.containerEl, + this, + 300, + () => { + this.app.workspace.setActiveLeaf(newLeaf, false, true); + }, + ).attachLeaf(); + newLeaf!.openLinkText(e.detail.cursorTarget.title, e.detail.cursorTarget.path); }; window.addEventListener('mousemove', mouseMoveCallback); this.register(() => window.removeEventListener('mousemove', mouseMoveCallback)); - window.addEventListener(eventTypes.previewCursor, previewCursorCallback); - this.register(() => window.removeEventListener(eventTypes.previewCursor, previewCursorCallback)); + window.addEventListener(eventTypes.previewCursor, previewCursorCallback as EventListener); + this.register(() => + window.removeEventListener(eventTypes.previewCursor, previewCursorCallback as EventListener), + ); const openBrowserCallback = this.openBrowserHandle.bind(this); - window.addEventListener(eventTypes.openBrowser, openBrowserCallback); - this.register(() => window.removeEventListener(eventTypes.openBrowser, openBrowserCallback)); + window.addEventListener(eventTypes.openBrowser, openBrowserCallback as EventListener); + this.register(() => window.removeEventListener(eventTypes.openBrowser, openBrowserCallback as EventListener)); [ this.app.workspace.on('dataview:refresh-views', this.maybeRefresh), this.app.workspace.on('codemirror', this.codemirrorFunction), @@ -761,7 +676,6 @@ export default class AwesomeBrainManagerPlugin extends Plugin { this.app.workspace.on('editor-paste', this.editorPasteFunction), this.app.workspace.on('file-menu', this.fileMenuFunction), this.app.workspace.on('editor-menu', this.editorMenuFunction), - this.app.workspace.on('active-leaf-change', this.activeLeafChangeFunction), this.app.vault.on('create', this.vaultCreateFunction), this.app.vault.on('modify', this.vaultModifyFunction), this.app.vault.on('delete', this.vaultDeleteFunction), diff --git a/src/types/obsidian.d.ts b/src/types/obsidian.d.ts index 9c5694e..63465e9 100644 --- a/src/types/obsidian.d.ts +++ b/src/types/obsidian.d.ts @@ -79,6 +79,7 @@ declare module 'obsidian' { viewRegistry: ViewRegistry; openWithDefaultApp(path: string): void; } + interface Workspace { activeLeaf: WorkspaceLeaf; floatingSplit: any; @@ -89,6 +90,7 @@ declare module 'obsidian' { interface WorkspaceTabs { children: any; } + interface ViewRegistry { typeByExtension: Record; // file extensions to view types viewByType: Record View>; // file extensions to view types @@ -103,7 +105,10 @@ declare module 'obsidian' { containerEl: HTMLElement; } interface MarkdownView { - editMode: { cm: EditorView }; + editMode: { + reinit(): unknown; + cm: EditorView; + }; } interface MarkdownEditView { editorEl: HTMLElement; @@ -122,6 +127,7 @@ declare module 'obsidian' { } interface Workspace { /** Sent to rendered dataview components to tell them to possibly refresh */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any on(name: 'dataview:refresh-views', callback: () => void, ctx?: any): EventRef; recordHistory(leaf: WorkspaceLeaf, pushHistory: boolean): void; iterateLeaves( diff --git a/src/ui/popover.ts b/src/ui/popover.ts new file mode 100644 index 0000000..d674b48 --- /dev/null +++ b/src/ui/popover.ts @@ -0,0 +1,415 @@ +import { + Component, + HoverPopover, + MarkdownView, + type MousePos, + PopoverState, + View, + Workspace, + WorkspaceLeaf, + WorkspaceSplit, + WorkspaceTabs, + requireApiVersion, + setIcon, +} from 'obsidian'; +import HoverEditorPlugin from '@/main'; +import { useSystemStore } from '@/stores'; + +import { genId } from '@/utils/common'; +import { SETTINGS } from '@/settings'; + +const popovers = new WeakMap(); +export interface HoverEditorParent { + hoverPopover: HoverEditor | null; + containerEl?: HTMLElement; + view?: View; + dom?: HTMLElement; + parent: HoverEditorParent | null; +} +type ConstructableWorkspaceSplit = new (ws: Workspace, dir: 'horizontal' | 'vertical') => WorkspaceSplit; + +function nosuper(base: new (...args: unknown[]) => T): new () => T { + const derived = function () { + return Object.setPrototypeOf(new Component(), new.target.prototype); + }; + derived.prototype = base.prototype; + return Object.setPrototypeOf(derived, base); +} + +export class HoverEditor extends nosuper(HoverPopover) { + onTarget: boolean; + + onHover: boolean; + + shownPos: MousePos | null; + + detaching = false; + + opening = false; + + rootSplit: WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(window.app.workspace, 'vertical'); + + targetRect = this.targetEl?.getBoundingClientRect(); + + titleEl: HTMLElement; + + containerEl: HTMLElement; + + parent: HoverEditorParent | null; + + oldPopover: HoverEditor | null; + + document: Document = this.targetEl?.ownerDocument ?? window.activeDocument ?? window.document; + + id = genId(8); + + originalPath: string; // these are kept to avoid adopting targets w/a different link + originalLinkText: string; + + static activePopover?: HoverEditor; + + static activeWindows() { + const windows: Window[] = [window]; + const { floatingSplit } = app.workspace; + if (floatingSplit) { + for (const split of floatingSplit.children) { + if (split.win) windows.push(split.win); + } + } + return windows; + } + + static forLeaf(leaf: WorkspaceLeaf | undefined) { + // leaf can be null such as when right clicking on an internal link + const el = leaf && document.body.matchParent.call(leaf.containerEl, '.hover-popover'); // work around matchParent race condition + return el ? popovers.get(el) : undefined; + } + + hoverEl: HTMLElement = this.document.defaultView!.createDiv({ + cls: 'popover hover-popover', + attr: { id: 'he' + this.id }, + }); + + constructor( + parent: HoverEditorParent, + public targetEl: HTMLElement, + public plugin: HoverEditorPlugin, + waitTime?: number, + public onShowCallback?: () => unknown, + ) { + // + super(); + + this.oldPopover = parent.hoverPopover ?? null; + if (waitTime === undefined) { + waitTime = 300; + } + this.onTarget = true; + this.onHover = false; + this.shownPos = null; + this.parent = parent; + this.waitTime = waitTime; + this.state = PopoverState.Showing; + + this.timer = window.setTimeout(this.show.bind(this), waitTime); + + // custom logic begin + popovers.set(this.hoverEl, this); + this.hoverEl.addClass('hover-editor'); + this.containerEl = this.hoverEl.createDiv('popover-content'); + this.buildWindowControls(); + this.setInitialDimensions(); + } + + updateLeaves() { + if (this.onTarget && this.targetEl && !this.document.contains(this.targetEl)) { + this.onTarget = false; + this.transition(); + } + let leafCount = 0; + this.plugin.app.workspace.iterateLeaves(leaf => { + leafCount++; + }, this.rootSplit); + if (leafCount === 0) { + this.hide(); // close if we have no leaves + } + this.hoverEl.setAttribute('data-leaf-count', leafCount.toString()); + } + + attachLeaf(): WorkspaceLeaf { + this.rootSplit.getRoot = () => app.workspace[this.document === document ? 'rootSplit' : 'floatingSplit']!; + this.titleEl.insertAdjacentElement('afterend', this.rootSplit.containerEl); + const leaf = this.plugin.app.workspace.createLeafInParent(this.rootSplit, 0); + this.updateLeaves(); + return leaf; + } + + onload(): void { + super.onload(); + this.registerEvent(this.plugin.app.workspace.on('layout-change', this.updateLeaves, this)); + this.registerEvent( + app.workspace.on('layout-change', () => { + // Ensure that top-level items in a popover are not tabbed + this.rootSplit.children.forEach((item, index) => { + if (item instanceof WorkspaceTabs) { + this.rootSplit.replaceChild(index, item.children[0]); + } + }); + }), + ); + } + + leaves() { + const leaves: WorkspaceLeaf[] = []; + this.plugin.app.workspace.iterateLeaves(leaf => { + leaves.push(leaf); + }, this.rootSplit); + return leaves; + } + + setInitialDimensions() { + this.hoverEl.style.height = SETTINGS.initialHeight.value; + this.hoverEl.style.width = SETTINGS.initialWidth.value; + } + + buildWindowControls() { + this.titleEl = this.document.defaultView!.createDiv('popover-titlebar'); + this.titleEl.createDiv('popover-title'); + const popoverActions = this.titleEl.createDiv('popover-actions'); + + const closeEl = popoverActions.createEl('a', 'popover-action mod-close'); + setIcon(closeEl, 'x'); + closeEl.addEventListener('click', event => { + this.hide(); + }); + this.containerEl.prepend(this.titleEl); + } + + onShow() { + // Once we've been open for closeDelay, use the closeDelay as a hiding timeout + const closeDelay = SETTINGS.closeDelay.value; + setTimeout(() => (this.waitTime = closeDelay), closeDelay); + + this.oldPopover?.hide(); + this.oldPopover = null; + + this.hoverEl.toggleClass('is-new', true); + + this.document.body.addEventListener( + 'click', + () => { + this.hoverEl.toggleClass('is-new', false); + }, + { once: true, capture: true }, + ); + + if (this.parent) { + this.parent.hoverPopover = this; + } + + // Workaround until 0.15.7 + if (requireApiVersion('0.15.1') && !requireApiVersion('0.15.7')) + app.workspace.iterateLeaves(leaf => { + if (leaf.view instanceof MarkdownView) (leaf.view.editMode as any).reinit?.(); + }, this.rootSplit); + + this.onShowCallback?.(); + this.onShowCallback = undefined; // only call it once + } + + transition() { + if (this.state === PopoverState.Hiding) { + this.state = PopoverState.Shown; + clearTimeout(this.timer); + } + } + + position(pos?: MousePos | null): void { + // without this adjustment, the x dimension keeps sliding over to the left as you progressively mouse over files + // disabling this for now since messing with pos.x like this breaks the detect() logic + // if (pos && pos.x !== undefined) { + // pos.x = pos.x + 20; + // } + + // native obsidian logic + if (pos === undefined) { + pos = this.shownPos; + } + + let rect; + + if (pos) { + rect = { + top: pos.y - 10, + bottom: pos.y + 10, + left: pos.x, + right: pos.x, + }; + } + + this.document.body.appendChild(this.hoverEl); + positionEl(rect, this.hoverEl, { gap: 10 }, this.document); + } + + show() { + // native obsidian logic start + if (!this.targetEl || this.document.body.contains(this.targetEl)) { + this.state = PopoverState.Shown; + this.timer = 0; + const mouseCoords: MousePos = useSystemStore().systemState.mouseCoords; + this.shownPos = mouseCoords; + this.position(mouseCoords); + this.onShow(); + app.workspace.onLayoutChange(); + this.load(); + } else { + this.hide(); + } + // native obsidian logic end + + // if this is an image view, set the dimensions to the natural dimensions of the image + // an interactjs reflow will be triggered to constrain the image to the viewport if it's + // too large + if (this.hoverEl.dataset.imgHeight && this.hoverEl.dataset.imgWidth) { + this.hoverEl.style.height = parseFloat(this.hoverEl.dataset.imgHeight) + this.titleEl.offsetHeight + 'px'; + this.hoverEl.style.width = parseFloat(this.hoverEl.dataset.imgWidth) + 'px'; + } + } + + onHide() { + this.oldPopover = null; + if (this.parent?.hoverPopover === this) { + this.parent.hoverPopover = null; + } + } + + hide() { + this.onTarget = this.onHover = false; + this.detaching = true; + + // A timer might be pending to call show() for the first time, make sure + // it doesn't bring us back up after we close + if (this.timer) { + clearTimeout(this.timer); + this.timer = 0; + } + + // Hide our HTML element immediately, even if our leaves might not be + // detachable yet. This makes things more responsive and improves the + // odds of not showing an empty popup that's just going to disappear + // momentarily. + this.hoverEl.hide(); + + // If a file load is in progress, we need to wait until it's finished before + // detaching leaves. Because we set .detaching, The in-progress openFile() + // will call us again when it finishes. + if (this.opening) return; + + const leaves = this.leaves(); + if (leaves.length) { + // Detach all leaves before we unload the popover and remove it from the DOM. + // Each leaf.detach() will trigger layout-changed and the updateLeaves() + // method will then call hide() again when the last one is gone. + leaves.forEach(leaf => { + leaf.detach(); + // Newer obsidians don't switch the active leaf until layout processing :( + if (leaf === app.workspace.activeLeaf) app.workspace.activeLeaf = null; + }); + } else { + this.parent = null; + return this.nativeHide(); + } + } + + nativeHide() { + const { hoverEl, targetEl } = this; + + this.state = PopoverState.Hidden; + + hoverEl.detach(); + + if (targetEl) { + const parent = targetEl.matchParent('.hover-popover'); + if (parent) popovers.get(parent)?.transition(); + } + + this.onHide(); + this.unload(); + } +} + +/** + * It positions an element relative to a rectangle, taking into account the boundaries of the element's + * offset parent + * @param rect - The rectangle of the element you want to position the popup relative to. + * @param {HTMLElement} el - The element to position + * @param [options] - { + * @returns An object with the top, left, and vresult properties. + */ +export function positionEl( + rect: { top: number; bottom: number; left: number; right: number }, + el: HTMLElement, + options: { gap?: number; preference?: string; offsetParent?: HTMLElement; horizontalAlignment?: string }, + document: Document, +) { + options = options || {}; + el.show(); + const gap = options.gap || 0; + const verticalPref = options.preference || 'bottom'; + const parentEl = options.offsetParent || el.offsetParent || document.documentElement; + const horizontalAlignment = options.horizontalAlignment || 'left'; + const parentTop = parentEl.scrollTop + 10; + const parentBottom = parentEl.scrollTop + parentEl.clientHeight - 10; + const top = Math.min(rect.top, parentBottom); + const bottom = Math.max(rect.bottom, parentTop); + const elHeight = el.offsetHeight; + const fitsAbove = rect.top - parentTop >= elHeight + gap; + const fitsBelow = parentBottom - rect.bottom >= elHeight + gap; + let topResult = 0; + let vresult = ''; // vertical result + + if (!fitsAbove || ('top' !== verticalPref && fitsBelow)) { + if (!fitsBelow || ('bottom' !== verticalPref && fitsAbove)) { + if (parentEl.clientHeight < elHeight + gap) { + topResult = parentTop; + vresult = 'overlap'; + } else { + if ('top' === verticalPref) { + topResult = parentTop + gap; + vresult = 'overlap'; + } else { + topResult = parentBottom - elHeight; + vresult = 'overlap'; + } + } + } else { + topResult = bottom + gap; + vresult = 'bottom'; + } + } else { + topResult = top - gap - elHeight; + vresult = 'top'; + } + + const leftBoundary = parentEl.scrollLeft + 10; + const rightBoundary = parentEl.scrollLeft + parentEl.clientWidth - 10; + const elWidth = el.offsetWidth; + let leftResult = 'left' === horizontalAlignment ? rect.left : rect.right - elWidth; + + if (leftResult < leftBoundary) { + leftResult = leftBoundary; + } else { + if (leftResult > rightBoundary - elWidth) { + leftResult = rightBoundary - elWidth; + } + } + + el.style.top = ''.concat(topResult.toString(), 'px'); + el.style.left = ''.concat(leftResult.toString(), 'px'); + + return { + top: topResult, + left: leftResult, + vresult: vresult, + }; +}