From 0fc127867e08bf89e0f7868b3aa973cd6babf950 Mon Sep 17 00:00:00 2001 From: Rias Date: Fri, 19 Jun 2026 09:26:57 +0200 Subject: [PATCH] Cleanup Overtype overrides --- package-lock.json | 6 +- package.json | 2 +- .../modules/markdown-field/MarkdownField.ts | 9 +- .../markdown-field/behaviors/shortcuts.ts | 74 +-------- .../behaviors/toolbar/active-states.ts | 107 ------------- .../markdown-field/behaviors/toolbar/index.ts | 2 - .../markdown-field/behaviors/toolbar/items.ts | 60 +++++++- .../behaviors/toolbar/keyboard.ts | 143 ------------------ 8 files changed, 60 insertions(+), 343 deletions(-) delete mode 100644 resources/js/modules/markdown-field/behaviors/toolbar/active-states.ts delete mode 100644 resources/js/modules/markdown-field/behaviors/toolbar/keyboard.ts diff --git a/package-lock.json b/package-lock.json index f64ab473299..eb1d1efd9af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "axios": "^1.17.0", "laravel-vite-plugin": "^3.1.0", "lit": "^3.3.2", - "overtype": "^2.3.10", + "overtype": "^2.4.0", "tailwindcss": "^4.2.4", "vue": "^3.5.33" }, @@ -16463,7 +16463,9 @@ } }, "node_modules/overtype": { - "version": "2.3.10", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/overtype/-/overtype-2.4.0.tgz", + "integrity": "sha512-V5HJcdwyN/Lye1vvU0d47y1VaEqSGsWRdCa3ZCZB/DZHdR840i1z0oBGYAmbLe/I2H2cxLx9Bv3tuVl4dTgUpA==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.4", diff --git a/package.json b/package.json index 28cc3a0ade3..7188c034762 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "axios": "^1.17.0", "laravel-vite-plugin": "^3.1.0", "lit": "^3.3.2", - "overtype": "^2.3.10", + "overtype": "^2.4.0", "tailwindcss": "^4.2.4", "vue": "^3.5.33" } diff --git a/resources/js/modules/markdown-field/MarkdownField.ts b/resources/js/modules/markdown-field/MarkdownField.ts index 7fd99d057e4..0ab68e7c5b0 100644 --- a/resources/js/modules/markdown-field/MarkdownField.ts +++ b/resources/js/modules/markdown-field/MarkdownField.ts @@ -17,12 +17,7 @@ import { } from './behaviors/preview'; import {registerShortcutBehavior} from './behaviors/shortcuts'; import {themeOptions} from './behaviors/theme'; -import { - replaceMarkdownGuideButton, - syncToolbarKeyboardNavigation, - syncToolbarButtonStates, - toolbarItems, -} from './behaviors/toolbar'; +import {replaceMarkdownGuideButton, toolbarItems} from './behaviors/toolbar'; import {fileUploadOptions} from './behaviors/uploads'; import markdownIcon from '@icons/brands/markdown.svg?raw'; import './MarkdownField.css'; @@ -186,8 +181,6 @@ class MarkdownField extends LitElement { () => this.linkPopoverController?.destroy(), ...(charCounterCleanup ? [charCounterCleanup] : []), replaceMarkdownGuideButton(editor), - syncToolbarKeyboardNavigation(editor), - syncToolbarButtonStates(editor, previewController), registerLinkPasteBehavior(editor, previewController), registerShortcutBehavior(editor, previewController), ]; diff --git a/resources/js/modules/markdown-field/behaviors/shortcuts.ts b/resources/js/modules/markdown-field/behaviors/shortcuts.ts index b23f3dc42af..21135b4a3ea 100644 --- a/resources/js/modules/markdown-field/behaviors/shortcuts.ts +++ b/resources/js/modules/markdown-field/behaviors/shortcuts.ts @@ -7,13 +7,7 @@ export function registerShortcutBehavior( preview: PreviewController ): () => void { async function handleKeydown(event: KeyboardEvent): Promise { - if (event.key === 'Tab') { - event.stopPropagation(); - - return; - } - - if (!isModifierKeyPressed(event)) { + if (event.defaultPrevented || !isModifierKeyPressed(event)) { return; } @@ -25,18 +19,6 @@ export function registerShortcutBehavior( event.preventDefault(); - if (action === 'indent') { - indentSelection(editor.textarea); - - return; - } - - if (action === 'outdent') { - outdentSelection(editor.textarea); - - return; - } - if (action === 'togglePreview') { await preview.toggle(); @@ -55,24 +37,11 @@ export function registerShortcutBehavior( }; } -type ShortcutAction = - | 'indent' - | 'outdent' - | 'toggleCode' - | 'togglePreview' - | 'toggleQuote'; +type ShortcutAction = 'toggleCode' | 'togglePreview' | 'toggleQuote'; function shortcutAction(event: KeyboardEvent): ShortcutAction | null { const key = event.key.toLowerCase(); - if (key === ']') { - return 'indent'; - } - - if (key === '[') { - return 'outdent'; - } - if (key === 'e' && !event.shiftKey) { return 'toggleCode'; } @@ -87,42 +56,3 @@ function shortcutAction(event: KeyboardEvent): ShortcutAction | null { return null; } - -function indentSelection(textarea: HTMLTextAreaElement): void { - replaceSelectedLines(textarea, (line) => ` ${line}`); -} - -function outdentSelection(textarea: HTMLTextAreaElement): void { - replaceSelectedLines(textarea, (line) => line.replace(/^( {1,2}|\t)/, '')); -} - -function replaceSelectedLines( - textarea: HTMLTextAreaElement, - transformLine: (line: string) => string -): void { - const {selectionEnd, selectionStart, value} = textarea; - const lineStart = value.lastIndexOf('\n', selectionStart - 1) + 1; - const lineEndOffset = value.indexOf( - '\n', - effectiveSelectionEnd(value, selectionStart, selectionEnd) - ); - const lineEnd = lineEndOffset === -1 ? value.length : lineEndOffset; - const replacement = value - .slice(lineStart, lineEnd) - .split('\n') - .map(transformLine) - .join('\n'); - - textarea.setRangeText(replacement, lineStart, lineEnd, 'preserve'); - textarea.dispatchEvent(new Event('input', {bubbles: true})); -} - -function effectiveSelectionEnd( - value: string, - selectionStart: number, - selectionEnd: number -): number { - return selectionEnd > selectionStart && value[selectionEnd - 1] === '\n' - ? selectionEnd - 1 - : selectionEnd; -} diff --git a/resources/js/modules/markdown-field/behaviors/toolbar/active-states.ts b/resources/js/modules/markdown-field/behaviors/toolbar/active-states.ts deleted file mode 100644 index 5d40bc3f1c1..00000000000 --- a/resources/js/modules/markdown-field/behaviors/toolbar/active-states.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {markdownActions, type OverType as OverTypeInstance} from 'overtype'; -import type {PreviewController} from '../preview'; - -const headingLevelByButton = {h4: 4, h5: 5, h6: 6}; -const nonToggleButtonNames = ['asset', 'guide', 'link', 'upload']; -const stateAttributes = ['aria-controls', 'aria-expanded', 'aria-pressed']; - -export function syncToolbarButtonStates( - editor: OverTypeInstance, - preview: PreviewController -): () => void { - const sync = () => { - for (const [buttonName, button] of Object.entries( - editor.toolbar?.buttons ?? {} - )) { - if ( - button instanceof HTMLElement && - !nonToggleButtonNames.includes(buttonName) && - !button.hasAttribute('aria-pressed') - ) { - button.setAttribute('aria-pressed', 'false'); - } - } - - const activeFormats = markdownActions.getActiveFormats(editor.textarea); - syncPressedButton(editor, 'code', activeFormats.includes('code')); - syncPressedButton( - editor, - 'strikethrough', - hasSurroundingMarker(editor.textarea, '~~') - ); - - const activeHeadingLevel = headingLevel(editor.textarea); - for (const [buttonName, level] of Object.entries(headingLevelByButton)) { - syncPressedButton(editor, buttonName, activeHeadingLevel === level); - } - - syncPressedButton(editor, 'preview', preview.isActive()); - toolbarButton(editor, 'link')?.removeAttribute('aria-pressed'); - - clearButtonState(editor, 'asset'); - clearButtonState(editor, 'upload'); - }; - - editor.textarea.addEventListener('input', sync); - editor.textarea.addEventListener('selectionchange', sync); - sync(); - - return () => { - editor.textarea.removeEventListener('input', sync); - editor.textarea.removeEventListener('selectionchange', sync); - }; -} - -function syncPressedButton( - editor: OverTypeInstance, - buttonName: string, - active: boolean -): void { - const button = toolbarButton(editor, buttonName); - - button?.classList.toggle('active', active); - button?.setAttribute('aria-pressed', active.toString()); -} - -function toolbarButton( - editor: OverTypeInstance, - buttonName: string -): HTMLElement | null { - const button = editor.toolbar?.buttons?.[buttonName]; - - return button instanceof HTMLElement ? button : null; -} - -function headingLevel(textarea: HTMLTextAreaElement): number { - const {selectionStart, value} = textarea; - const lineStart = - value.lastIndexOf('\n', Math.max(0, selectionStart - 1)) + 1; - const nextLineBreak = value.indexOf('\n', selectionStart); - const lineEnd = nextLineBreak === -1 ? value.length : nextLineBreak; - - return value.slice(lineStart, lineEnd).match(/^(#{1,6})\s/)?.[1]?.length ?? 0; -} - -function hasSurroundingMarker( - textarea: HTMLTextAreaElement, - marker: string -): boolean { - const {selectionEnd, selectionStart, value} = textarea; - const beforeSelection = value.slice(0, selectionStart); - const afterSelection = value.slice(selectionEnd); - - return ( - (beforeSelection.split(marker).length - 1) % 2 === 1 && - afterSelection.includes(marker) - ); -} - -function clearButtonState(editor: OverTypeInstance, buttonName: string): void { - const button = toolbarButton(editor, buttonName); - - button?.classList.remove('active'); - - for (const attribute of stateAttributes) { - button?.removeAttribute(attribute); - } -} diff --git a/resources/js/modules/markdown-field/behaviors/toolbar/index.ts b/resources/js/modules/markdown-field/behaviors/toolbar/index.ts index 568dca386d8..0f97ed8f200 100644 --- a/resources/js/modules/markdown-field/behaviors/toolbar/index.ts +++ b/resources/js/modules/markdown-field/behaviors/toolbar/index.ts @@ -1,4 +1,2 @@ -export {syncToolbarButtonStates} from './active-states'; export {replaceMarkdownGuideButton} from './guide-link'; export {toolbarItems} from './items'; -export {syncToolbarKeyboardNavigation} from './keyboard'; diff --git a/resources/js/modules/markdown-field/behaviors/toolbar/items.ts b/resources/js/modules/markdown-field/behaviors/toolbar/items.ts index 5146fb0febb..1aab01b1a5a 100644 --- a/resources/js/modules/markdown-field/behaviors/toolbar/items.ts +++ b/resources/js/modules/markdown-field/behaviors/toolbar/items.ts @@ -20,6 +20,7 @@ type ToolbarCallbacks = { }; type ToolbarAction = NonNullable; +type ToolbarActiveState = NonNullable; const strikethroughFormat = { prefix: '~~', @@ -78,9 +79,17 @@ function toolbarButtonGroups( strikethroughFormat ); editor.textarea.dispatchEvent(new Event('input', {bubbles: true})); - } + }, + undefined, + ({editor}) => hasSurroundingMarker(editor.textarea, '~~') + ), + customizeToolbarButton( + toolbarButtons.code, + t('Code'), + 'code', + undefined, + ({activeFormats}) => activeFormats.includes('code') ), - customizeToolbarButton(toolbarButtons.code, t('Code'), 'code'), ], [ customizeToolbarButton(toolbarButtons.h1, t('Heading 1'), 'h1'), @@ -145,21 +154,30 @@ function toolbarButtonGroups( } function headingButton(level: 4 | 5 | 6, title: string): CraftToolbarButton { - return customToolbarButton(`h${level}`, `h${level}`, title, ({editor}) => { - markdownActions.insertHeader(editor.textarea, level, true); - editor.textarea.dispatchEvent(new Event('input', {bubbles: true})); - }); + return customToolbarButton( + `h${level}`, + `h${level}`, + title, + ({editor}) => { + markdownActions.insertHeader(editor.textarea, level, true); + editor.textarea.dispatchEvent(new Event('input', {bubbles: true})); + }, + undefined, + ({editor}) => headingLevel(editor.textarea) === level + ); } function customizeToolbarButton( button: ToolbarButton, title: string, icon: string, - optionName = button.name + optionName = button.name, + isActive = button.isActive ): CraftToolbarButton { return { ...button, icon: customIcons[icon] ?? button.icon, + isActive, optionName, title, }; @@ -170,13 +188,39 @@ function customToolbarButton( icon: string, title: string, action: ToolbarAction, - actionId?: string + actionId?: string, + isActive?: ToolbarActiveState ): CraftToolbarButton { return { actionId, action, icon: customIcons[icon] ?? '', + isActive, name, title, }; } + +function headingLevel(textarea: HTMLTextAreaElement): number { + const {selectionStart, value} = textarea; + const lineStart = + value.lastIndexOf('\n', Math.max(0, selectionStart - 1)) + 1; + const nextLineBreak = value.indexOf('\n', selectionStart); + const lineEnd = nextLineBreak === -1 ? value.length : nextLineBreak; + + return value.slice(lineStart, lineEnd).match(/^(#{1,6})\s/)?.[1]?.length ?? 0; +} + +function hasSurroundingMarker( + textarea: HTMLTextAreaElement, + marker: string +): boolean { + const {selectionEnd, selectionStart, value} = textarea; + const beforeSelection = value.slice(0, selectionStart); + const afterSelection = value.slice(selectionEnd); + + return ( + (beforeSelection.split(marker).length - 1) % 2 === 1 && + afterSelection.includes(marker) + ); +} diff --git a/resources/js/modules/markdown-field/behaviors/toolbar/keyboard.ts b/resources/js/modules/markdown-field/behaviors/toolbar/keyboard.ts deleted file mode 100644 index 7189bfcca94..00000000000 --- a/resources/js/modules/markdown-field/behaviors/toolbar/keyboard.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type {OverType as OverTypeInstance} from 'overtype'; - -export function syncToolbarKeyboardNavigation( - editor: OverTypeInstance -): () => void { - const toolbar = editor.toolbar?.container; - - if (!(toolbar instanceof HTMLElement)) { - return () => {}; - } - - if (editor.textarea.id) { - toolbar.setAttribute('aria-controls', editor.textarea.id); - } - - let activeItem = toolbarItemsForKeyboard(toolbar)[0] ?? null; - - const sync = () => { - const items = toolbarItemsForKeyboard(toolbar); - - if (!items.length) { - return; - } - - if (!activeItem || !items.includes(activeItem)) { - const firstItem = items[0]; - - if (!firstItem) { - return; - } - - activeItem = firstItem; - } - - for (const item of items) { - item.tabIndex = item === activeItem ? 0 : -1; - } - }; - - const focusItem = (item: HTMLElement) => { - activeItem = item; - sync(); - item.focus(); - }; - - const moveFocus = (direction: 1 | -1) => { - const items = toolbarItemsForKeyboard(toolbar); - - if (!items.length) { - return; - } - - const currentIndex = Math.max( - 0, - items.indexOf(document.activeElement as HTMLElement) - ); - const nextIndex = (currentIndex + direction + items.length) % items.length; - const nextItem = items[nextIndex]; - - if (nextItem) { - focusItem(nextItem); - } - }; - - const handleFocus = (event: FocusEvent) => { - const item = event.target; - - if (!(item instanceof HTMLElement) || !isToolbarKeyboardItem(item)) { - return; - } - - activeItem = item; - sync(); - }; - - const handleKeydown = (event: KeyboardEvent) => { - if (!isToolbarKeyboardItem(event.target)) { - return; - } - - switch (event.key) { - case 'ArrowLeft': - event.preventDefault(); - moveFocus(-1); - break; - case 'ArrowRight': - event.preventDefault(); - moveFocus(1); - break; - case 'Home': - event.preventDefault(); - focusFirstItem(toolbar); - break; - case 'End': - event.preventDefault(); - focusLastItem(toolbar); - break; - } - }; - - toolbar.addEventListener('focusin', handleFocus); - toolbar.addEventListener('keydown', handleKeydown); - sync(); - - return () => { - toolbar.removeEventListener('focusin', handleFocus); - toolbar.removeEventListener('keydown', handleKeydown); - toolbar.removeAttribute('aria-controls'); - - for (const item of toolbarItemsForKeyboard(toolbar)) { - item.removeAttribute('tabindex'); - } - }; -} - -function focusFirstItem(toolbar: HTMLElement): void { - const item = toolbarItemsForKeyboard(toolbar)[0]; - - if (item) { - item.focus(); - } -} - -function focusLastItem(toolbar: HTMLElement): void { - const items = toolbarItemsForKeyboard(toolbar); - const item = items[items.length - 1]; - - if (item) { - item.focus(); - } -} - -function toolbarItemsForKeyboard(toolbar: HTMLElement): HTMLElement[] { - return Array.from(toolbar.querySelectorAll('[data-button]')) - .filter(isToolbarKeyboardItem) - .filter((item) => !item.matches(':disabled')); -} - -function isToolbarKeyboardItem(item: EventTarget | null): item is HTMLElement { - return ( - item instanceof HTMLElement && item.closest('[role="toolbar"]') !== null - ); -}