diff --git a/packages/core/package.json b/packages/core/package.json index 46d79d81..616dfb7e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@fileverse-dev/fortune-core", - "version": "1.3.11", + "version": "1.3.12", "main": "lib/index.js", "module": "es/index.js", "typings": "lib/index.d.ts", diff --git a/packages/core/src/modules/inline-string.ts b/packages/core/src/modules/inline-string.ts index 30b40bb4..6d2b8737 100644 --- a/packages/core/src/modules/inline-string.ts +++ b/packages/core/src/modules/inline-string.ts @@ -366,6 +366,15 @@ function extendCssText(origin: string, cover: string, isLimit = true) { return newCss; } +function escapeHtmlAttr(s: string): string { + return s + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(//g, ">"); +} + export function updateInlineStringFormat( ctx: Context, attr: keyof Cell, @@ -381,6 +390,46 @@ export function updateInlineStringFormat( const $textEditor = cellInput; + // Firefox (and sometimes other browsers) can produce element-based ranges + // (e.g. Ctrl+A selects node contents: startContainer/endContainer are the editor DIV + // and offsets are child-node indexes, not character indexes). The legacy string-slicing + // logic below treats offsets as character positions and can accidentally inject the + // editor's own outerHTML into itself. Handle element-based ranges safely first. + if ( + range.startContainer === $textEditor && + range.endContainer === $textEditor && + range.collapsed === false + ) { + const start = range.startOffset; + const end = range.endOffset; + + const children = Array.from($textEditor.childNodes).slice(start, end); + children.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as HTMLElement; + if (el.tagName === "SPAN") { + const cssText = getCssText(el.style.cssText, attr, value); + el.setAttribute("style", cssText); + } + } else if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent ?? ""; + if (text.length === 0) return; + const wrapper = document.createElement("span"); + const cssText = getCssText("", attr, value); + wrapper.setAttribute("style", cssText); + wrapper.textContent = text; + node.parentNode?.replaceChild(wrapper, node); + } + }); + + // Restore selection across the whole editor contents. + const newRange = document.createRange(); + newRange.selectNodeContents($textEditor); + w.removeAllRanges(); + w.addRange(newRange); + return; + } + if (range.collapsed === true) { return; } @@ -426,7 +475,7 @@ export function updateInlineStringFormat( cssText = extendCssText(box.style.cssText, cssText); } } - cont += `${left}`; + cont += `${left}`; } if (mid !== "") { @@ -441,7 +490,7 @@ export function updateInlineStringFormat( } } - cont += `${mid}`; + cont += `${mid}`; } if (right !== "") { @@ -454,7 +503,7 @@ export function updateInlineStringFormat( cssText = extendCssText(box.style.cssText, cssText); } } - cont += `${right}`; + cont += `${right}`; } if (startContainer.parentElement?.tagName === "SPAN") { @@ -510,38 +559,48 @@ export function updateInlineStringFormat( for (let i = 0; i < startSpanIndex; i += 1) { const span = spans[i]; const content = span.innerHTML; - cont += `${content}`; + cont += `${content}`; } if (sleft !== "") { - cont += `${sleft}`; + cont += `${sleft}`; } if (sright !== "") { const cssText = getCssText(startSpan!.style.cssText, attr, value); - cont += `${sright}`; + cont += `${sright}`; } if (startSpanIndex < endSpanIndex) { for (let i = startSpanIndex + 1; i < endSpanIndex; i += 1) { const span = spans[i]; const content = span.innerHTML; - cont += `${content}`; + cont += `${content}`; } } if (eleft !== "") { const cssText = getCssText(endSpan!.style.cssText, attr, value); - cont += `${eleft}`; + cont += `${eleft}`; } if (eright !== "") { - cont += `${eright}`; + cont += `${eright}`; } for (let i = endSpanIndex + 1; i < spans.length; i += 1) { const span = spans[i]; const content = span.innerHTML; - cont += `${content}`; + cont += `${content}`; } $textEditor.innerHTML = cont; @@ -570,19 +629,11 @@ export function updateInlineStringFormat( } } -function escapeHtmlAttr(s: string): string { - return s - .replace(/&/g, "&") - .replace(/"/g, """) - .replace(//g, ">"); -} - function getLinkDataAttrs(span: HTMLElement): string { if (span.dataset?.linkType && span.dataset?.linkAddress) { - return ` data-link-type='${escapeHtmlAttr( + return ` data-link-type="${escapeHtmlAttr( span.dataset.linkType - )}' data-link-address='${escapeHtmlAttr(span.dataset.linkAddress)}'`; + )}" data-link-address="${escapeHtmlAttr(span.dataset.linkAddress)}"`; } return ""; } @@ -628,7 +679,9 @@ export function applyLinkToSelection( ) as HTMLElement | null; if (box != null) cssText = extendCssText(box.style.cssText, cssText); } - cont += `${left}`; + cont += `${left}`; } if (mid !== "") { let cssText = getLinkStyleCssText(span!.style.cssText); @@ -638,9 +691,11 @@ export function applyLinkToSelection( ) as HTMLElement | null; if (box != null) cssText = extendCssText(box.style.cssText, cssText); } - cont += `${mid}`; + )}" data-link-address="${escapeHtmlAttr(linkAddress)}">${mid}`; } if (right !== "") { let { cssText } = span!.style; @@ -650,7 +705,9 @@ export function applyLinkToSelection( ) as HTMLElement | null; if (box != null) cssText = extendCssText(box.style.cssText, cssText); } - cont += `${right}`; + cont += `${right}`; } if (startContainer.parentElement?.tagName === "SPAN") { (span as HTMLElement).outerHTML = cont; @@ -683,48 +740,54 @@ export function applyLinkToSelection( let cont = ""; for (let i = 0; i < startSpanIndex; i += 1) { const sp = spans[i] as HTMLElement; - cont += `${ - sp.innerHTML - }`; + cont += `${sp.innerHTML}`; } if (sleft !== "") { - cont += `${sleft}`; + cont += `${sleft}`; } if (sright !== "") { const cssText = getLinkStyleCssText(startSpan!.style.cssText); - cont += `${sright}`; + )}" data-link-address="${escapeHtmlAttr(linkAddress)}">${sright}`; } if (startSpanIndex < endSpanIndex) { for (let i = startSpanIndex + 1; i < endSpanIndex; i += 1) { const sp = spans[i]; const cssText = getLinkStyleCssText(sp.style.cssText); - cont += `${ + )}" data-link-address="${escapeHtmlAttr(linkAddress)}">${ sp.innerHTML }`; } } if (eleft !== "") { const cssText = getLinkStyleCssText(endSpan!.style.cssText); - cont += `${eleft}`; + )}" data-link-address="${escapeHtmlAttr(linkAddress)}">${eleft}`; } if (eright !== "") { - cont += `${eright}`; + cont += `${eright}`; } for (let i = endSpanIndex + 1; i < spans.length; i += 1) { const sp = spans[i] as HTMLElement; - cont += `${ - sp.innerHTML - }`; + cont += `${sp.innerHTML}`; } $textEditor.innerHTML = cont; spans = $textEditor.querySelectorAll("span"); diff --git a/packages/react/package.json b/packages/react/package.json index fec1b087..a0a00316 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@fileverse-dev/fortune-react", - "version": "1.3.11", + "version": "1.3.12", "main": "lib/index.js", "types": "lib/index.d.ts", "module": "es/index.js", @@ -16,7 +16,7 @@ "tsc": "tsc" }, "dependencies": { - "@fileverse-dev/fortune-core": "1.3.11", + "@fileverse-dev/fortune-core": "1.3.12", "@fileverse/ui": "5.0.0", "@tippyjs/react": "^4.2.6", "@types/regenerator-runtime": "^0.13.6",