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",