components_selection_context_menu.js
import { ContextMenu } from "./context_menu.js"
/**
* A context menu for word-selection operations.
*
* Shown when the user right-clicks on a word that belongs to an active word
* selection. Provides "Add link" and "Search text" actions.
*
* @example
* const menu = new SelectionContextMenu(event.clientX, event.clientY, {
* onAddLink: () => { handleHyperlink(); },
* onSearchText: () => { handleSearch(); },
* onGenerateLiveQuote: () => { handleEmbed(); }, // null to hide
* onMute: () => { handleMute(); }, // null to hide
* onApplyEffect: () => { handleEffect(); }, // null to hide
* onDismiss: () => { menu.close(); },
* });
*
* // Later:
* menu.close();
*/
export class SelectionContextMenu extends ContextMenu {
/**
* @param {number} x - Preferred left position in viewport pixels.
* @param {number} y - Preferred top position in viewport pixels.
* @param {object} callbacks - Callback functions for menu actions.
* @param {function(): void} [callbacks.onAddLink] - Called when "Add link" is clicked.
* @param {function(): void} [callbacks.onSearchText] - Called when "Search text" is clicked.
* @param {function(): void|null} [callbacks.onGenerateLiveQuote] - Called when "Generate Live Quote" is clicked. Pass null to hide the item.
* @param {function(): void|null} [callbacks.onMute] - Called when "Mute" is clicked. Pass null to hide the item.
* @param {function(): void|null} [callbacks.onApplyEffect] - Called when "Apply effect" is clicked. Pass null to hide the item.
* @param {string|null} [callbacks.applyEffectDisabledReason] - Tooltip/reason shown when apply effect is disabled. Pass null if not disabled.
* @param {function(): void|null} [callbacks.onAddNote] - Called when "Add note" is clicked. Pass null to hide the item.
* @param {function(): void} [callbacks.onDismiss] - Called when the menu is dismissed.
* @param {{message: string, href: (string|undefined)}|null} [callbacks.info] - Info widget config passed to the base ContextMenu.
*/
constructor(x, y, { onAddLink, onSearchText, onGenerateLiveQuote, onMute, onApplyEffect, applyEffectDisabledReason, onAddNote, onDismiss, info = { message: 'Perform actions on the currently selected words.', href: '/docs' } } = {}) {
super('Selection', onDismiss, info);
this._onAddLink = onAddLink ?? (() => {});
this._onSearchText = onSearchText ?? (() => {});
this._onGenerateLiveQuote = onGenerateLiveQuote ?? null;
this._onMute = onMute ?? null;
this._onApplyEffect = onApplyEffect ?? null;
this._applyEffectDisabledReason = applyEffectDisabledReason ?? null;
this._onAddNote = onAddNote ?? null;
this.#buildControls();
// Prevent mousedown from clearing the native text selection before
// click callbacks have a chance to fire.
this.root.addEventListener('mousedown', (e) => e.preventDefault());
this._mount(x, y);
}
/** Builds and appends the menu items to the root element. */
#buildControls() {
const controls = document.createElement('div');
controls.className = 'ctx-controls';
controls.appendChild(this.makeItem(
'<span class="icon icon-plus" style="width:14px;height:14px;"></span>',
'Add link',
'⇧K',
() => { this._onDismiss(); this._onAddLink(); },
));
controls.appendChild(this.makeItem(
'<span class="icon icon-crosshair" style="width:14px;height:14px;"></span>',
'Search text',
'⇧F',
() => { this._onDismiss(); this._onSearchText(); },
));
if (this._onGenerateLiveQuote !== null) {
controls.appendChild(this.makeItem(
'<span style="font-size:0.85rem;line-height:1;letter-spacing:-0.05em">«»</span>',
'Generate Live Quote',
'⇧Q',
() => { this._onDismiss(); this._onGenerateLiveQuote(); },
));
}
if (this._onMute !== null) {
controls.appendChild(this.makeItem(
'<span style="font-size:0.85rem;line-height:1;">🔇</span>',
'Mute',
'⇧M',
() => { this._onDismiss(); this._onMute(); },
));
}
if (this._onApplyEffect !== null || this._applyEffectDisabledReason) {
controls.appendChild(this.makeItem(
'<span style="font-size:0.85rem;line-height:1;">✦</span>',
'Apply effect',
'⇧E',
this._applyEffectDisabledReason ? null : () => { this._onDismiss(); this._onApplyEffect(); },
this._applyEffectDisabledReason,
));
}
if (this._onAddNote !== null) {
controls.appendChild(this.makeItem(
'✎',
'Add note',
'⇧N',
() => { this._onDismiss(); this._onAddNote(); },
));
}
this.root.appendChild(controls);
}
}