{ "version": 3, "sources": ["../../../../src/lib/shapes/shared/TextHelpers.ts"], "sourcesContent": ["// Adapted (mostly copied) the work of https://github.com/fregante\n// Copyright (c) Federico Brigante (bfred.it)\n\n// TODO: Most of this file can be moved into a DOM utils library.\n\n/** @internal */\nexport type ReplacerCallback = (substring: string, ...args: unknown[]) => string\n\n/**\t@public */\nexport const INDENT = ' '\n\n/** @internal */\nexport class TextHelpers {\n\tstatic insertTextFirefox(field: HTMLTextAreaElement | HTMLInputElement, text: string): void {\n\t\t// Found on https://www.everythingfrontend.com/blog/insert-text-into-textarea-at-cursor-position.html \uD83C\uDF88\n\t\tfield.setRangeText(\n\t\t\ttext,\n\t\t\tfield.selectionStart || 0,\n\t\t\tfield.selectionEnd || 0,\n\t\t\t'end' // Without this, the cursor is either at the beginning or text remains selected\n\t\t)\n\n\t\tfield.dispatchEvent(\n\t\t\tnew InputEvent('input', {\n\t\t\t\tdata: text,\n\t\t\t\tinputType: 'insertText',\n\t\t\t\tisComposing: false, // TODO: fix @types/jsdom, this shouldn't be required\n\t\t\t})\n\t\t)\n\t}\n\n\t/**\n\t * Inserts text at the cursor\u2019s position, replacing any selection, with **undo** support and by\n\t * firing the input event.\n\t */\n\tstatic insert(field: HTMLTextAreaElement | HTMLInputElement, text: string): void {\n\t\tconst document = field.ownerDocument\n\t\tconst initialFocus = document.activeElement\n\t\tif (initialFocus !== field) {\n\t\t\tfield.focus()\n\t\t}\n\n\t\tif (!document.execCommand('insertText', false, text)) {\n\t\t\tTextHelpers.insertTextFirefox(field, text)\n\t\t}\n\n\t\tif (initialFocus === document.body) {\n\t\t\tfield.blur()\n\t\t} else if (initialFocus instanceof HTMLElement && initialFocus !== field) {\n\t\t\tinitialFocus.focus()\n\t\t}\n\t}\n\n\t/**\n\t * Replaces the entire content, equivalent to field.value = text but with **undo** support and by\n\t * firing the input event.\n\t */\n\tstatic set(field: HTMLTextAreaElement | HTMLInputElement, text: string): void {\n\t\tfield.select()\n\t\tTextHelpers.insert(field, text)\n\t}\n\n\t/** Get the selected text in a field or an empty string if nothing is selected. */\n\tstatic getSelection(field: HTMLTextAreaElement | HTMLInputElement): string {\n\t\tconst { selectionStart, selectionEnd } = field\n\t\treturn field.value.slice(\n\t\t\tselectionStart ? selectionStart : undefined,\n\t\t\tselectionEnd ? selectionEnd : undefined\n\t\t)\n\t}\n\n\t/**\n\t * Adds the wrappingText before and after field\u2019s selection (or cursor). If endWrappingText is\n\t * provided, it will be used instead of wrappingText at on the right.\n\t */\n\tstatic wrapSelection(\n\t\tfield: HTMLTextAreaElement | HTMLInputElement,\n\t\twrap: string,\n\t\twrapEnd?: string\n\t): void {\n\t\tconst { selectionStart, selectionEnd } = field\n\t\tconst selection = TextHelpers.getSelection(field)\n\t\tTextHelpers.insert(field, wrap + selection + (wrapEnd ?? wrap))\n\n\t\t// Restore the selection around the previously-selected text\n\t\tfield.selectionStart = (selectionStart || 0) + wrap.length\n\t\tfield.selectionEnd = (selectionEnd || 0) + wrap.length\n\t}\n\n\t/** Finds and replaces strings and regex in the field\u2019s value. */\n\tstatic replace(\n\t\tfield: HTMLTextAreaElement | HTMLInputElement,\n\t\tsearchValue: string | RegExp,\n\t\treplacer: string | ReplacerCallback\n\t): void {\n\t\t/** Remembers how much each match offset should be adjusted */\n\t\tlet drift = 0\n\t\tfield.value.replace(searchValue, (...args): string => {\n\t\t\t// Select current match to replace it later\n\t\t\tconst matchStart = drift + (args[args.length - 2] as number)\n\t\t\tconst matchLength = args[0].length\n\t\t\tfield.selectionStart = matchStart\n\t\t\tfield.selectionEnd = matchStart + matchLength\n\t\t\tconst replacement = typeof replacer === 'string' ? replacer : replacer(...args)\n\t\t\tTextHelpers.insert(field, replacement)\n\t\t\t// Select replacement. Without this, the cursor would be after the replacement\n\t\t\tfield.selectionStart = matchStart\n\t\t\tdrift += replacement.length - matchLength\n\t\t\treturn replacement\n\t\t})\n\t}\n\n\tstatic findLineEnd(value: string, currentEnd: number): number {\n\t\t// Go to the beginning of the last line\n\t\tconst lastLineStart = value.lastIndexOf('\\n', currentEnd - 1) + 1\n\t\t// There's nothing to unindent after the last cursor, so leave it as is\n\t\tif (value.charAt(lastLineStart) !== '\\t') {\n\t\t\treturn currentEnd\n\t\t}\n\t\treturn lastLineStart + 1 // Include the first character, which will be a tab\n\t}\n\n\tstatic indent(element: HTMLTextAreaElement): void {\n\t\tconst { selectionStart, selectionEnd, value } = element\n\t\tconst selectedContrast = value.slice(selectionStart, selectionEnd)\n\t\t// The first line should be indented, even if it starts with \\n\n\t\t// The last line should only be indented if includes any character after \\n\n\t\tconst lineBreakCount = /\\n/g.exec(selectedContrast)?.length\n\n\t\tif (lineBreakCount && lineBreakCount > 0) {\n\t\t\t// Select full first line to replace everything at once\n\t\t\tconst firstLineStart = value.lastIndexOf('\\n', selectionStart - 1) + 1\n\n\t\t\tconst newSelection = element.value.slice(firstLineStart, selectionEnd - 1)\n\t\t\tconst indentedText = newSelection.replace(\n\t\t\t\t/^|\\n/g, // Match all line starts\n\t\t\t\t`$&${INDENT}`\n\t\t\t)\n\t\t\tconst replacementsCount = indentedText.length - newSelection.length\n\n\t\t\t// Replace newSelection with indentedText\n\t\t\telement.setSelectionRange(firstLineStart, selectionEnd - 1)\n\t\t\tTextHelpers.insert(element, indentedText)\n\n\t\t\t// Restore selection position, including the indentation\n\t\t\telement.setSelectionRange(selectionStart + 1, selectionEnd + replacementsCount)\n\t\t} else {\n\t\t\tTextHelpers.insert(element, INDENT)\n\t\t}\n\t}\n\n\t// The first line should always be unindented\n\t// The last line should only be unindented if the selection includes any characters after \\n\n\tstatic unindent(element: HTMLTextAreaElement): void {\n\t\tconst { selectionStart, selectionEnd, value } = element\n\n\t\t// Select the whole first line because it might contain \\t\n\t\tconst firstLineStart = value.lastIndexOf('\\n', selectionStart - 1) + 1\n\t\tconst minimumSelectionEnd = TextHelpers.findLineEnd(value, selectionEnd)\n\n\t\tconst newSelection = element.value.slice(firstLineStart, minimumSelectionEnd)\n\t\tconst indentedText = newSelection.replace(/(^|\\n)(\\t| {1,2})/g, '$1')\n\t\tconst replacementsCount = newSelection.length - indentedText.length\n\n\t\t// Replace newSelection with indentedText\n\t\telement.setSelectionRange(firstLineStart, minimumSelectionEnd)\n\t\tTextHelpers.insert(element, indentedText)\n\n\t\t// Restore selection position, including the indentation\n\t\tconst firstLineIndentation = /\\t| {1,2}/.exec(value.slice(firstLineStart, selectionStart))\n\n\t\tconst difference = firstLineIndentation ? firstLineIndentation[0].length : 0\n\n\t\tconst newSelectionStart = selectionStart - difference\n\t\telement.setSelectionRange(\n\t\t\tselectionStart - difference,\n\t\t\tMath.max(newSelectionStart, selectionEnd - replacementsCount)\n\t\t)\n\t}\n\n\tstatic indentCE(element: HTMLElement): void {\n\t\tconst selection = window.getSelection()\n\t\tconst value = element.innerText\n\t\tconst selectionStart = getCaretIndex(element) ?? 0\n\t\tconst selectionEnd = getCaretIndex(element) ?? 0\n\t\tconst selectedContrast = value.slice(selectionStart, selectionEnd)\n\t\t// The first line should be indented, even if it starts with \\n\n\t\t// The last line should only be indented if includes any character after \\n\n\t\tconst lineBreakCount = /\\n/g.exec(selectedContrast)?.length\n\n\t\tif (lineBreakCount && lineBreakCount > 0) {\n\t\t\t// Select full first line to replace everything at once\n\t\t\tconst firstLineStart = value.lastIndexOf('\\n', selectionStart - 1) + 1\n\n\t\t\tconst newSelection = value.slice(firstLineStart, selectionEnd - 1)\n\t\t\tconst indentedText = newSelection.replace(\n\t\t\t\t/^|\\n/g, // Match all line starts\n\t\t\t\t`$&${INDENT}`\n\t\t\t)\n\t\t\tconst replacementsCount = indentedText.length - newSelection.length\n\n\t\t\t// Replace newSelection with indentedText\n\n\t\t\tif (selection) {\n\t\t\t\tselection.setBaseAndExtent(\n\t\t\t\t\telement,\n\t\t\t\t\tselectionStart + 1,\n\t\t\t\t\telement,\n\t\t\t\t\tselectionEnd + replacementsCount\n\t\t\t\t)\n\t\t\t\t// element.setSelectionRange(firstLineStart, selectionEnd - 1)\n\t\t\t\t// TextHelpers.insert(element, indentedText)\n\n\t\t\t\t// Restore selection position, including the indentation\n\t\t\t\t// element.setSelectionRange(selectionStart + 1, selectionEnd + replacementsCount)\n\t\t\t}\n\t\t} else {\n\t\t\tconst selection = window.getSelection()\n\t\t\telement.innerText = value.slice(0, selectionStart) + INDENT + value.slice(selectionStart)\n\t\t\tselection?.setBaseAndExtent(element, selectionStart + 1, element, selectionStart + 2)\n\t\t\t// TextHelpers.insert(element, INDENT)\n\t\t}\n\t}\n\n\tstatic unindentCE(element: HTMLElement): void {\n\t\tconst selection = window.getSelection()\n\t\tconst value = element.innerText\n\t\t// const { selectionStart, selectionEnd } = element\n\t\tconst selectionStart = getCaretIndex(element) ?? 0\n\t\tconst selectionEnd = getCaretIndex(element) ?? 0\n\n\t\t// Select the whole first line because it might contain \\t\n\t\tconst firstLineStart = value.lastIndexOf('\\n', selectionStart - 1) + 1\n\t\tconst minimumSelectionEnd = TextHelpers.findLineEnd(value, selectionEnd)\n\n\t\tconst newSelection = value.slice(firstLineStart, minimumSelectionEnd)\n\t\tconst indentedText = newSelection.replace(/(^|\\n)(\\t| {1,2})/g, '$1')\n\t\tconst replacementsCount = newSelection.length - indentedText.length\n\n\t\tif (selection) {\n\t\t\t// Replace newSelection with indentedText\n\t\t\tselection.setBaseAndExtent(element, firstLineStart, element, minimumSelectionEnd)\n\t\t\t// TextHelpers.insert(element, indentedText)\n\n\t\t\t// Restore selection position, including the indentation\n\t\t\tconst firstLineIndentation = /\\t| {1,2}/.exec(value.slice(firstLineStart, selectionStart))\n\n\t\t\tconst difference = firstLineIndentation ? firstLineIndentation[0].length : 0\n\n\t\t\tconst newSelectionStart = selectionStart - difference\n\t\t\tselection.setBaseAndExtent(\n\t\t\t\telement,\n\t\t\t\tselectionStart - difference,\n\t\t\t\telement,\n\t\t\t\tMath.max(newSelectionStart, selectionEnd - replacementsCount)\n\t\t\t)\n\t\t}\n\t}\n\n\tstatic fixNewLines = /\\r?\\n|\\r/g\n\n\tstatic normalizeText(text: string) {\n\t\treturn text.replace(TextHelpers.fixNewLines, '\\n')\n\t}\n\n\tstatic normalizeTextForDom(text: string) {\n\t\treturn text\n\t\t\t.replace(TextHelpers.fixNewLines, '\\n')\n\t\t\t.split('\\n')\n\t\t\t.map((x) => x || ' ')\n\t\t\t.join('\\n')\n\t}\n}\n\nfunction getCaretIndex(element: HTMLElement) {\n\tif (typeof window.getSelection === 'undefined') return\n\tconst selection = window.getSelection()\n\tif (!selection) return\n\tlet position = 0\n\tif (selection.rangeCount !== 0) {\n\t\tconst range = selection.getRangeAt(0)\n\t\tconst preCaretRange = range.cloneRange()\n\t\tpreCaretRange.selectNodeContents(element)\n\t\tpreCaretRange.setEnd(range.endContainer, range.endOffset)\n\t\tposition = preCaretRange.toString().length\n\t}\n\treturn position\n}\n"], "mappings": "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", "names": ["selection"] }