diff --git a/src/components/inline-tools/inline-tool-bold.ts b/src/components/inline-tools/inline-tool-bold.ts index c3a4d9d2..38fe1125 100644 --- a/src/components/inline-tools/inline-tool-bold.ts +++ b/src/components/inline-tools/inline-tool-bold.ts @@ -34,11 +34,6 @@ export default class BoldInlineTool implements InlineTool { } as SanitizerConfig; } - /** - * Native Document's command that uses for Bold - */ - private readonly commandName: string = 'bold'; - /** * Create button for Inline Toolbar */ @@ -47,12 +42,174 @@ export default class BoldInlineTool implements InlineTool { icon: IconBold, name: 'bold', onActivate: () => { - document.execCommand(this.commandName); + this.toggleBold(); + }, + isActive: () => { + const selection = window.getSelection(); + + return selection ? this.isSelectionBold(selection) : false; }, - isActive: () => document.queryCommandState(this.commandName), }; } + /** + * Apply or remove bold formatting using modern Selection API + */ + private toggleBold(): void { + const selection = window.getSelection(); + + if (!selection || selection.rangeCount === 0) { + return; + } + + const range = selection.getRangeAt(0); + + if (range.collapsed) { + return; + } + + // Check if selection is already wrapped in tag + const isBold = this.isSelectionBold(selection); + + if (isBold) { + // Unwrap: remove tags while preserving content + this.unwrapBoldTags(range); + } else { + // Wrap: surround selection with tag + this.wrapWithBold(range); + } + } + + /** + * Check if current selection is within a tag + * + * @param selection - The Selection object to check + */ + private isSelectionBold(selection: Selection): boolean { + if (!selection || selection.rangeCount === 0) { + return false; + } + + const range = selection.getRangeAt(0); + const container = range.commonAncestorContainer; + + // Check if container itself is a tag + if (container.nodeType === Node.ELEMENT_NODE && (container as Element).tagName === 'B') { + return true; + } + + // Check if container is inside a tag + let parent: Node | null = container.nodeType === Node.TEXT_NODE ? container.parentElement : container as Element; + + while (parent && parent.nodeType === Node.ELEMENT_NODE) { + if ((parent as Element).tagName === 'B') { + return true; + } + parent = parent.parentElement; + } + + return false; + } + + /** + * Wrap selection with tag + * + * @param range - The Range object containing the selection to wrap + */ + private wrapWithBold(range: Range): void { + const bElement = document.createElement('b'); + + try { + range.surroundContents(bElement); + } catch (error) { + // If surroundContents fails (e.g., range spans multiple elements), + // extract content and wrap it + const contents = range.extractContents(); + + bElement.appendChild(contents); + range.insertNode(bElement); + } + + // Restore selection + const selection = window.getSelection(); + + if (selection) { + selection.removeAllRanges(); + const newRange = document.createRange(); + + newRange.selectNodeContents(bElement); + selection.addRange(newRange); + } + } + + /** + * Remove tags while preserving content + * + * @param range - The Range object containing the selection to unwrap + */ + private unwrapBoldTags(range: Range): void { + const container = range.commonAncestorContainer; + let boldElement: HTMLElement | null = null; + + // Find the element + if (container.nodeType === Node.ELEMENT_NODE && (container as Element).tagName === 'B') { + boldElement = container as HTMLElement; + } else { + let parent: Node | null = container.nodeType === Node.TEXT_NODE ? container.parentElement : container as Element; + + while (parent && parent.nodeType === Node.ELEMENT_NODE) { + if ((parent as Element).tagName === 'B') { + boldElement = parent as HTMLElement; + break; + } + parent = parent.parentElement; + } + } + + if (boldElement) { + const selection = window.getSelection(); + + if (!selection || selection.rangeCount === 0) { + return; + } + + // Save references to first and last child before unwrapping + const firstChild = boldElement.firstChild; + const lastChild = boldElement.lastChild; + + // Replace with its contents + const parent = boldElement.parentNode; + + if (parent && firstChild && lastChild) { + // Insert all children before the bold element + while (boldElement.firstChild) { + parent.insertBefore(boldElement.firstChild, boldElement); + } + parent.removeChild(boldElement); + + // Restore selection to the unwrapped content + selection.removeAllRanges(); + const newRange = document.createRange(); + + if (firstChild === lastChild && firstChild.nodeType === Node.TEXT_NODE) { + // Single text node: try to preserve offsets + const textLength = firstChild.textContent?.length ?? 0; + const start = Math.min(range.startOffset, textLength); + const end = Math.min(range.endOffset, textLength); + + newRange.setStart(firstChild, start); + newRange.setEnd(firstChild, end); + } else { + // Multiple nodes: select from first to last + newRange.setStartBefore(firstChild); + newRange.setEndAfter(lastChild); + } + + selection.addRange(newRange); + } + } + } + /** * Set a shortcut *