93 lines
3.4 KiB
JavaScript
93 lines
3.4 KiB
JavaScript
/**
|
|
* Shared lifecycle helpers for the toolbar dropdown action panels
|
|
* (size, font, link, color): DOM bootstrapping, positioning relative to the
|
|
* toolbar, and outside-interaction dismissal.
|
|
*/
|
|
import { PANEL_GAP, PANEL_MARGIN, PARENT_TOOLBAR_HEIGHT } from '../../constants/layout.js';
|
|
import { getToolbarEl } from './toolbar/toolbar.js';
|
|
|
|
/**
|
|
* Injects the panel stylesheet (once) and mounts the panel root element.
|
|
* @param {{ styleId: string, styles: string, html: string }} config
|
|
* @returns {HTMLElement} The mounted panel root.
|
|
*/
|
|
export function createPanelElement({ styleId, styles, html }) {
|
|
if (!document.getElementById(styleId)) {
|
|
const style = document.createElement('style');
|
|
style.id = styleId;
|
|
style.textContent = styles;
|
|
document.head.appendChild(style);
|
|
}
|
|
const wrapper = document.createElement('div');
|
|
wrapper.innerHTML = html;
|
|
const element = wrapper.firstElementChild;
|
|
document.body.appendChild(element);
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Positions a dropdown panel below (or above) the toolbar, left-aligned to its
|
|
* trigger button and clamped to the viewport.
|
|
* @param {HTMLElement} element - Panel root.
|
|
* @param {{ buttonAction?: string|null, matchToolbarWidth?: boolean }} [options]
|
|
*/
|
|
export function positionDropdownPanel(element, { buttonAction = null, matchToolbarWidth = false } = {}) {
|
|
const toolbarElement = getToolbarEl();
|
|
if (!toolbarElement || !element) return;
|
|
|
|
const toolbarBounds = toolbarElement.getBoundingClientRect();
|
|
if (matchToolbarWidth) element.style.width = `${toolbarBounds.width}px`;
|
|
|
|
const button = buttonAction ? toolbarElement.querySelector(`[data-action="${buttonAction}"]`) : null;
|
|
const anchorBounds = button ? button.getBoundingClientRect() : toolbarBounds;
|
|
const panelWidth = matchToolbarWidth ? toolbarBounds.width : element.offsetWidth;
|
|
const panelHeight = element.offsetHeight;
|
|
|
|
const spaceBelow = window.innerHeight - PARENT_TOOLBAR_HEIGHT - toolbarBounds.bottom;
|
|
const top = Math.max(PANEL_MARGIN, spaceBelow >= panelHeight + PANEL_GAP
|
|
? toolbarBounds.bottom + PANEL_GAP
|
|
: toolbarBounds.top - panelHeight - PANEL_GAP);
|
|
|
|
const left = Math.max(PANEL_MARGIN, Math.min(anchorBounds.left, window.innerWidth - panelWidth - PANEL_MARGIN));
|
|
|
|
element.style.left = `${left}px`;
|
|
element.style.top = `${top}px`;
|
|
}
|
|
|
|
/**
|
|
* Builds attach/detach-able listeners that dismiss a panel on outside
|
|
* mousedown.
|
|
* @param {{
|
|
* getElement: () => HTMLElement|null,
|
|
* dismiss: () => void,
|
|
* ignoreToolbar?: boolean,
|
|
* ignoreMouseDown?: ((event: MouseEvent) => boolean)|null,
|
|
* captureMouseDown?: boolean,
|
|
* }} config
|
|
* @returns {{ attach: () => void, detach: () => void }}
|
|
*/
|
|
export function createOutsideDismiss({ getElement, dismiss, ignoreToolbar = true, ignoreMouseDown = null, captureMouseDown = false }) {
|
|
function onMouseDown(event) {
|
|
if (getElement()?.contains(event.target)) return;
|
|
if (ignoreToolbar && getToolbarEl()?.contains(event.target)) return;
|
|
if (ignoreMouseDown?.(event)) return;
|
|
dismiss();
|
|
}
|
|
return {
|
|
attach() {
|
|
document.addEventListener('mousedown', onMouseDown, captureMouseDown);
|
|
},
|
|
detach() {
|
|
document.removeEventListener('mousedown', onMouseDown, captureMouseDown);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @returns {Range|null} Clone of the current selection range, if any.
|
|
*/
|
|
export function captureSelectionRange() {
|
|
const selection = window.getSelection();
|
|
return selection && selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null;
|
|
}
|