/
githubmirror
/
marktext
Обзор
Документация
Войти
/
githubmirror
/
marktext
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/muya/src/ui/inlineFormatToolbar/index.ts
291 строка
9 KB
Ran Luo
fix(muya): let Enter pass through to the block when only the format toolbar is shown (#3196) (#4736)
26 июн 2026, 10:19
Не верифицирован
26 июн 2026, 10:19
ca9c27e
Код
Авторство
О чём код?
import type { VNode } from 'snabbdom'; import type { Muya } from '../../index'; import type { Token } from '../../inlineRenderer/types'; import type { IBaseOptions } from '../types'; import type { FormatToolIcon } from './config'; import Format from '../../block/base/format'; import { isKeyboardEvent } from '../../utils'; import { h, patch } from '../../utils/snabbdom'; import BaseFloat from '../baseFloat'; import icons from './config'; import './index.css'; /** Default float options for inline format toolbar */ const defaultOptions = { placement: 'top' as const, offsetOptions: { mainAxis: 5, crossAxis: 0, alignmentAxis: 0, }, showArrow: false, }; /** Format keyboard shortcuts without shift modifier */ const FORMAT_SHORTCUTS = { b: 'strong', i: 'em', u: 'u', d: 'del', e: 'inline_code', l: 'link', } as const; /** Format keyboard shortcuts with shift modifier */ const FORMAT_SHORTCUTS_SHIFT = { h: 'mark', e: 'inline_math', i: 'image', r: 'clear', } as const; /** Keys that should not trigger toolbar hiding */ const NON_EDITING_KEYS = new Set([ 'Shift', 'Control', 'Meta', 'Alt', 'Tab', ]); /** * Inline format toolbar for text formatting * Provides quick access to text formatting options like bold, italic, etc. * Appears when text is selected */ export class InlineFormatToolbar extends BaseFloat { static pluginName = 'formatPicker'; // Passive float: must not capture nav keys, or Enter over a selection is // swallowed while it's shown (#3196). public override capturesContentKeydown = false; /** Previous virtual node for patching */ private _oldVNode: VNode | null = null; /** The block containing the selected text */ private _block: Format | null = null; /** Currently applied formats in the selection */ private _formats: Token[] = []; /** Toolbar configuration options */ public override options: IBaseOptions; /** Format tool icons configuration */ private _icons: FormatToolIcon[] = icons; /** Container element for the format toolbar */ private _formatContainer: HTMLDivElement = document.createElement('div'); /** * Create inline format toolbar instance * @param muya - Muya editor instance * @param options - Toolbar options */ constructor(muya: Muya, options = {}) { const name = 'mu-format-picker'; const opts = Object.assign({}, defaultOptions, options); super(muya, name, opts); this.options = opts; this.container!.appendChild(this._formatContainer); this.floatBox!.classList.add('mu-format-picker-container'); this.listen(); } /** * Listen to format picker events and keyboard shortcuts */ override listen() { const { eventCenter, domNode, editor } = this.muya; super.listen(); eventCenter.subscribe('muya-format-picker', ({ reference, block }) => { if (reference) { this._block = block; this._formats = block.getFormatsInRange().formats; requestAnimationFrame(() => { this.show(reference); this._render(); }); } else { this.hide(); } }); // While open, re-sync the highlight from the selection's current // formats — this is how formats applied outside the toolbar (menu / // command / shortcut) light up their buttons. Single-block tool, so // ignore collapsed / cross-block selections. eventCenter.subscribe('selection-change', ({ formats, isCollapsed, isSelectionInSameBlock }) => { if (!this.status || isCollapsed || !isSelectionInSameBlock) return; this._formats = formats; this._render(); }); eventCenter.attachDOMEvent(domNode, 'keydown', (event) => { this._handleKeydown(event, editor); }); } /** * Handle keyboard events for format shortcuts and toolbar hiding * @param event - Keyboard event * @param editor - Editor instance */ private _handleKeydown(event: Event, editor: typeof this.muya.editor) { if (!isKeyboardEvent(event)) return; const { key, shiftKey, metaKey, ctrlKey } = event; const selection = editor.selection.getSelection(); if (!selection) return; const { anchor, isSelectionInSameBlock } = selection; const anchorBlock = anchor.block; if (!isSelectionInSameBlock) return; // Hide toolbar on editing operations if (!(anchorBlock instanceof Format) || (!metaKey && !ctrlKey)) { this._hideOnEditingKey(key, metaKey, ctrlKey); return; } // Handle format shortcuts this._handleFormatShortcut(event, key, shiftKey, anchorBlock); } /** * Hide toolbar when an editing key is pressed * @param key - Key name * @param metaKey - Meta key state * @param ctrlKey - Control key state */ private _hideOnEditingKey(key: string, metaKey: boolean, ctrlKey: boolean) { // Don't hide if it's a modifier/navigation key or if format shortcut is pressed if (NON_EDITING_KEYS.has(key) || metaKey || ctrlKey) return; if (this.status) { this.hide(); } } /** * Handle format keyboard shortcuts * @param event - Keyboard event * @param key - Key name * @param shiftKey - Shift key state * @param anchorBlock - Anchor block */ private _handleFormatShortcut( event: KeyboardEvent, key: string, shiftKey: boolean, anchorBlock: Format, ) { const shortcuts = shiftKey ? FORMAT_SHORTCUTS_SHIFT : FORMAT_SHORTCUTS; const formatType = shortcuts[key as keyof typeof shortcuts]; if (formatType) { event.preventDefault(); anchorBlock.format(formatType); } } /** * Render the format toolbar UI */ private _render() { const { _icons: icons, _oldVNode: oldVNode, _formatContainer: formatContainer, _formats: formats } = this; const { i18n } = this.muya; const children = icons.map(icon => this._createIconItem(icon, formats, i18n)); const vnode = h('ul', children); patch(oldVNode || formatContainer, vnode); this._oldVNode = vnode; } /** * Create a format icon item * @param icon - Icon configuration * @param formats - Currently applied formats * @param i18n - Internationalization instance */ private _createIconItem(icon: FormatToolIcon, formats: Token[], i18n: typeof this.muya.i18n) { const iconElement = h( 'i.icon', h( 'i.icon-inner', { style: { 'background': `url(${icon.icon}) no-repeat`, 'background-size': '100%', }, }, '', ), ); const iconWrapper = h('div.icon-wrapper', iconElement); const isActive = formats.some( f => f.type === icon.type || (f.type === 'html_tag' && f.tag === icon.type), ); const itemSelector = `li.item.${icon.type}${isActive ? '.active' : ''}`; return h( itemSelector, { attrs: { title: `${i18n.t(icon.tooltip)}\n${icon.shortcut}`, }, on: { click: event => this._selectItem(event, icon), }, }, [iconWrapper], ); } /** * Handle format item selection * @param event - Click event * @param item - Selected format tool icon */ private _selectItem(event: Event, item: FormatToolIcon) { event.preventDefault(); event.stopPropagation(); const { selection } = this.muya.editor; const { anchor, focus, anchorBlock, anchorPath, focusBlock, focusPath } = selection; if (!anchor || !focus || !anchorBlock || !focusBlock) return; // Restore selection before formatting selection.setSelection( { offset: anchor.offset, block: anchorBlock, path: anchorPath }, { offset: focus.offset, block: focusBlock, path: focusPath }, ); this._block!.format(item.type); // Hide toolbar for link and image, re-render for other formats if (/link|image/.test(item.type)) { this.hide(); } else { this._formats = this._block!.getFormatsInRange().formats; this._render(); } } }