@fluidframework/react 2.90.0 → 2.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/react.alpha.api.md +8 -8
  3. package/lib/reactSharedTreeView.d.ts +6 -6
  4. package/lib/reactSharedTreeView.d.ts.map +1 -1
  5. package/lib/reactSharedTreeView.js +16 -18
  6. package/lib/reactSharedTreeView.js.map +1 -1
  7. package/lib/test/reactSharedTreeView.spec.js +3 -3
  8. package/lib/test/reactSharedTreeView.spec.js.map +1 -1
  9. package/lib/test/text/textEditor.test.js +100 -44
  10. package/lib/test/text/textEditor.test.js.map +1 -1
  11. package/lib/test/useObservation.spec.js +8 -8
  12. package/lib/test/useObservation.spec.js.map +1 -1
  13. package/lib/test/useTree.spec.js +15 -15
  14. package/lib/test/useTree.spec.js.map +1 -1
  15. package/lib/text/formatted/quillFormattedView.d.ts +14 -2
  16. package/lib/text/formatted/quillFormattedView.d.ts.map +1 -1
  17. package/lib/text/formatted/quillFormattedView.js +165 -71
  18. package/lib/text/formatted/quillFormattedView.js.map +1 -1
  19. package/lib/text/plain/plainTextView.d.ts +2 -2
  20. package/lib/text/plain/plainTextView.d.ts.map +1 -1
  21. package/lib/text/plain/plainTextView.js +16 -21
  22. package/lib/text/plain/plainTextView.js.map +1 -1
  23. package/lib/text/plain/quillView.d.ts +2 -2
  24. package/lib/text/plain/quillView.d.ts.map +1 -1
  25. package/lib/text/plain/quillView.js +15 -21
  26. package/lib/text/plain/quillView.js.map +1 -1
  27. package/lib/useObservation.js +6 -6
  28. package/lib/useObservation.js.map +1 -1
  29. package/lib/useTree.d.ts +7 -7
  30. package/lib/useTree.d.ts.map +1 -1
  31. package/lib/useTree.js +6 -6
  32. package/lib/useTree.js.map +1 -1
  33. package/package.json +14 -13
  34. package/react.test-files.tar +0 -0
  35. package/src/reactSharedTreeView.tsx +11 -13
  36. package/src/text/formatted/quillFormattedView.tsx +176 -58
  37. package/src/text/plain/plainTextView.tsx +6 -6
  38. package/src/text/plain/quillView.tsx +6 -6
  39. package/src/useObservation.ts +6 -6
  40. package/src/useTree.ts +19 -12
@@ -1 +1 @@
1
- {"version":3,"file":"useTree.spec.js","sourceRoot":"","sources":["../../src/test/useTree.spec.tsx"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\n\nimport { SchemaFactory } from \"@fluidframework/tree\";\nimport { render } from \"@testing-library/react\";\nimport globalJsdom from \"global-jsdom\";\nimport * as React from \"react\";\n\nimport { toPropTreeNode, type PropTreeNode } from \"../propNode.js\";\nimport { objectIdNumber } from \"../simpleIdentifier.js\";\nimport {\n\tusePropTreeNode,\n\twithMemoizedTreeObservations,\n\twithTreeObservations,\n} from \"../useTree.js\";\n\ndescribe(\"useTree\", () => {\n\tdescribe(\"dom tests\", () => {\n\t\tlet cleanup: () => void;\n\n\t\tbefore(() => {\n\t\t\tcleanup = globalJsdom();\n\t\t});\n\n\t\tafter(() => {\n\t\t\tcleanup();\n\t\t});\n\n\t\tit(\"withTreeObservations example\", () => {\n\t\t\tconst builder = new SchemaFactory(\"example\");\n\t\t\tclass Item extends builder.object(\"Item\", { text: SchemaFactory.string }) {}\n\t\t\tconst ItemComponentBug = ({ item }: { item: Item }): JSX.Element => (\n\t\t\t\t<span>{item.text}</span> // Reading `text`, a mutable value from a React prop, causes an invalidation bug.\n\t\t\t);\n\n\t\t\tconst ItemComponent = withTreeObservations(\n\t\t\t\t({ item }: { item: Item }): JSX.Element => <span>{item.text}</span>,\n\t\t\t);\n\n\t\t\tconst ItemParentComponent = ({ item }: { item: PropTreeNode<Item> }): JSX.Element => (\n\t\t\t\t<ItemComponent item={item} />\n\t\t\t);\n\n\t\t\tconst InvalidItemParentComponent = ({\n\t\t\t\titem,\n\t\t\t}: {\n\t\t\t\titem: PropTreeNode<Item>;\n\t\t\t}): JSX.Element => (\n\t\t\t\t// @ts-expect-error PropTreeNode turns this invalidation bug into a compile error\n\t\t\t\t<span>{item.text}</span>\n\t\t\t);\n\t\t});\n\n\t\tfor (const reactStrictMode of [false, true]) {\n\t\t\t/**\n\t\t\t * Check then clear, the contents of `log`.\n\t\t\t *\n\t\t\t * When in StrictMode, React may double render, so that case is not checked for an exact match.\n\t\t\t */\n\t\t\tfunction checkRenderLog(log: string[], expected: readonly string[]): void {\n\t\t\t\tif (reactStrictMode) {\n\t\t\t\t\tassert.deepEqual(new Set(log), new Set(expected));\n\t\t\t\t} else {\n\t\t\t\t\tassert.deepEqual(log, expected);\n\t\t\t\t}\n\t\t\t\tlog.length = 0;\n\t\t\t}\n\n\t\t\tdescribe(`StrictMode: ${reactStrictMode}`, () => {\n\t\t\t\tit(\"usePropTreeNode\", () => {\n\t\t\t\t\tconst builder = new SchemaFactory(\"tree-react-api\");\n\n\t\t\t\t\tclass Point extends builder.object(\"Point\", {\n\t\t\t\t\t\tx: builder.number,\n\t\t\t\t\t\ty: builder.number,\n\t\t\t\t\t}) {}\n\n\t\t\t\t\tconst log: string[] = [];\n\n\t\t\t\t\tfunction PointComponent(props: { node: PropTreeNode<Point> }): JSX.Element {\n\t\t\t\t\t\tlog.push(\"render\");\n\t\t\t\t\t\tconst { x, y } = usePropTreeNode(props.node, (node) => {\n\t\t\t\t\t\t\tlog.push(`usePropTreeNode`);\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tx: node.x,\n\t\t\t\t\t\t\t\ty: node.y,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn <span>{`x: ${x}, y: ${y}`}</span>;\n\t\t\t\t\t}\n\n\t\t\t\t\tfunction ParentComponent(props: { node: PropTreeNode<Point> }): JSX.Element {\n\t\t\t\t\t\tlog.push(\"parent\");\n\t\t\t\t\t\treturn <PointComponent node={props.node} />;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst point = new Point({ x: 1, y: 1 });\n\t\t\t\t\tconst propPoint = toPropTreeNode(point);\n\n\t\t\t\t\tconst content = <ParentComponent node={propPoint} />;\n\n\t\t\t\t\tconst rendered = render(content, { reactStrictMode });\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"x: 1, y: 1\");\n\t\t\t\t\tcheckRenderLog(log, [\"parent\", \"render\", \"usePropTreeNode\"]);\n\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tassertLogEmpty(log);\n\n\t\t\t\t\tpoint.x = 2;\n\t\t\t\t\tassertLogEmpty(log);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\t// Parent which passed node down did not rerender, but PointComponent which read from it did:\n\t\t\t\t\tcheckRenderLog(log, [\"render\", \"usePropTreeNode\"]);\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"x: 2, y: 1\");\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tdescribe(\"withTreeObservations and array\", () => {\n\t\t\t\tconst builder = new SchemaFactory(\"tree-react-api\");\n\n\t\t\t\tclass Item extends builder.object(\"Item\", {\n\t\t\t\t\tx: SchemaFactory.number,\n\t\t\t\t}) {}\n\n\t\t\t\tclass Collection extends builder.array(\"Collection\", Item) {}\n\n\t\t\t\tbeforeEach(() => {\n\t\t\t\t\t// Ensure the log starts empty for each test.\n\t\t\t\t\tlog.length = 0;\n\t\t\t\t});\n\n\t\t\t\tconst log: string[] = [];\n\n\t\t\t\tconst ItemComponent = withMemoizedTreeObservations(\n\t\t\t\t\t(props: { item: Item }): JSX.Element => {\n\t\t\t\t\t\tlog.push(`Item: ${props.item.x}`);\n\t\t\t\t\t\treturn <span>{`${props.item.x}`}</span>;\n\t\t\t\t\t},\n\t\t\t\t\t{ onInvalidation: () => log.push(\"Item invalidated\") },\n\t\t\t\t);\n\n\t\t\t\tconst CollectionComponent = withTreeObservations(\n\t\t\t\t\t(props: { collection: Collection }): JSX.Element => {\n\t\t\t\t\t\tlog.push(\"Collection\");\n\n\t\t\t\t\t\tconst items = props.collection.map((item) => (\n\t\t\t\t\t\t\t<ItemComponent key={objectIdNumber(item)} item={item} />\n\t\t\t\t\t\t));\n\n\t\t\t\t\t\treturn <div>{items}</div>;\n\t\t\t\t\t},\n\t\t\t\t\t{ onInvalidation: () => log.push(\"Collection invalidated\") },\n\t\t\t\t);\n\n\t\t\t\tconst ParentComponent = withTreeObservations(\n\t\t\t\t\t(props: { node: Collection }): JSX.Element => {\n\t\t\t\t\t\tlog.push(\"Parent\");\n\t\t\t\t\t\treturn <CollectionComponent collection={props.node} />;\n\t\t\t\t\t},\n\t\t\t\t\t{ onInvalidation: () => log.push(\"Parent invalidated\") },\n\t\t\t\t);\n\n\t\t\t\tit(\"empty\", () => {\n\t\t\t\t\tconst collection = new Collection([]);\n\t\t\t\t\tconst content = <ParentComponent node={collection} />;\n\t\t\t\t\trender(content, { reactStrictMode });\n\t\t\t\t\tcheckRenderLog(log, [\"Parent\", \"Collection\"]);\n\t\t\t\t});\n\n\t\t\t\t// This confirms that modifying an array does not needlessly invalid parents and reuses children (if they use memo)\n\t\t\t\tit(\"array editing: insertion\", () => {\n\t\t\t\t\tconst collection = new Collection([{ x: 1 }, { x: 2 }, { x: 3 }]);\n\t\t\t\t\tconst content = <ParentComponent node={collection} />;\n\t\t\t\t\tconst rendered = render(content, { reactStrictMode });\n\t\t\t\t\tcheckRenderLog(log, [\"Parent\", \"Collection\", \"Item: 1\", \"Item: 2\", \"Item: 3\"]);\n\t\t\t\t\tcollection.insertAtEnd(new Item({ x: 4 }));\n\t\t\t\t\tcheckRenderLog(log, [\"Collection invalidated\"]);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection\", \"Item: 4\"]);\n\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"1234\");\n\t\t\t\t});\n\n\t\t\t\t// This confirms the same as the above, but testes some harder cases.\n\t\t\t\t// For example this one depends on stable keys to reusing children due to indexes changing.\n\t\t\t\tit(\"array editing: general\", () => {\n\t\t\t\t\tconst collection = new Collection([{ x: 1 }, { x: 2 }, { x: 3 }]);\n\t\t\t\t\tconst content = <ParentComponent node={collection} />;\n\t\t\t\t\tconst rendered = render(content, { reactStrictMode });\n\t\t\t\t\tcheckRenderLog(log, [\"Parent\", \"Collection\", \"Item: 1\", \"Item: 2\", \"Item: 3\"]);\n\t\t\t\t\tcollection.moveToEnd(0);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection invalidated\"]);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection\"]);\n\t\t\t\t\tcollection.removeAt(1);\n\t\t\t\t\tcollection.insertAtStart(new Item({ x: 4 }));\n\t\t\t\t\tcheckRenderLog(log, [\"Collection invalidated\"]);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection\", \"Item: 4\"]);\n\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"421\");\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t});\n});\n\n/**\n * Assert that an array is empty.\n *\n * Not inlined because doing so causes TypeScript to infer the array type as never[] afterwards and breaks push.\n * Better than asserting length is 0 as this gets a better error message on failure.\n */\nfunction assertLogEmpty(log: string[]): void {\n\tassert.deepEqual(log, []);\n}\n"]}
1
+ {"version":3,"file":"useTree.spec.js","sourceRoot":"","sources":["../../src/test/useTree.spec.tsx"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"node:assert\";\n\nimport { SchemaFactory } from \"@fluidframework/tree\";\nimport { render } from \"@testing-library/react\";\nimport globalJsdom from \"global-jsdom\";\n\nimport { toPropTreeNode, type PropTreeNode } from \"../propNode.js\";\nimport { objectIdNumber } from \"../simpleIdentifier.js\";\nimport {\n\tusePropTreeNode,\n\twithMemoizedTreeObservations,\n\twithTreeObservations,\n} from \"../useTree.js\";\n\ndescribe(\"useTree\", () => {\n\tdescribe(\"dom tests\", () => {\n\t\tlet cleanup: () => void;\n\n\t\tbefore(() => {\n\t\t\tcleanup = globalJsdom();\n\t\t});\n\n\t\tafter(() => {\n\t\t\tcleanup();\n\t\t});\n\n\t\tit(\"withTreeObservations example\", () => {\n\t\t\tconst builder = new SchemaFactory(\"example\");\n\t\t\tclass Item extends builder.object(\"Item\", { text: SchemaFactory.string }) {}\n\t\t\tconst ItemComponentBug = ({ item }: { item: Item }): JSX.Element => (\n\t\t\t\t<span>{item.text}</span> // Reading `text`, a mutable value from a React prop, causes an invalidation bug.\n\t\t\t);\n\n\t\t\tconst ItemComponent = withTreeObservations(\n\t\t\t\t({ item }: { item: Item }): JSX.Element => <span>{item.text}</span>,\n\t\t\t);\n\n\t\t\tconst ItemParentComponent = ({ item }: { item: PropTreeNode<Item> }): JSX.Element => (\n\t\t\t\t<ItemComponent item={item} />\n\t\t\t);\n\n\t\t\tconst InvalidItemParentComponent = ({\n\t\t\t\titem,\n\t\t\t}: {\n\t\t\t\titem: PropTreeNode<Item>;\n\t\t\t}): JSX.Element => (\n\t\t\t\t// @ts-expect-error PropTreeNode turns this invalidation bug into a compile error\n\t\t\t\t<span>{item.text}</span>\n\t\t\t);\n\t\t});\n\n\t\tfor (const reactStrictMode of [false, true]) {\n\t\t\t/**\n\t\t\t * Check then clear, the contents of `log`.\n\t\t\t *\n\t\t\t * When in StrictMode, React may double render, so that case is not checked for an exact match.\n\t\t\t */\n\t\t\tfunction checkRenderLog(log: string[], expected: readonly string[]): void {\n\t\t\t\tif (reactStrictMode) {\n\t\t\t\t\tassert.deepEqual(new Set(log), new Set(expected));\n\t\t\t\t} else {\n\t\t\t\t\tassert.deepEqual(log, expected);\n\t\t\t\t}\n\t\t\t\tlog.length = 0;\n\t\t\t}\n\n\t\t\tdescribe(`StrictMode: ${reactStrictMode}`, () => {\n\t\t\t\tit(\"usePropTreeNode\", () => {\n\t\t\t\t\tconst builder = new SchemaFactory(\"tree-react-api\");\n\n\t\t\t\t\tclass Point extends builder.object(\"Point\", {\n\t\t\t\t\t\tx: builder.number,\n\t\t\t\t\t\ty: builder.number,\n\t\t\t\t\t}) {}\n\n\t\t\t\t\tconst log: string[] = [];\n\n\t\t\t\t\tfunction PointComponent(props: { node: PropTreeNode<Point> }): JSX.Element {\n\t\t\t\t\t\tlog.push(\"render\");\n\t\t\t\t\t\tconst { x, y } = usePropTreeNode(props.node, (node) => {\n\t\t\t\t\t\t\tlog.push(`usePropTreeNode`);\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tx: node.x,\n\t\t\t\t\t\t\t\ty: node.y,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn <span>{`x: ${x}, y: ${y}`}</span>;\n\t\t\t\t\t}\n\n\t\t\t\t\tfunction ParentComponent(props: { node: PropTreeNode<Point> }): JSX.Element {\n\t\t\t\t\t\tlog.push(\"parent\");\n\t\t\t\t\t\treturn <PointComponent node={props.node} />;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst point = new Point({ x: 1, y: 1 });\n\t\t\t\t\tconst propPoint = toPropTreeNode(point);\n\n\t\t\t\t\tconst content = <ParentComponent node={propPoint} />;\n\n\t\t\t\t\tconst rendered = render(content, { reactStrictMode });\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"x: 1, y: 1\");\n\t\t\t\t\tcheckRenderLog(log, [\"parent\", \"render\", \"usePropTreeNode\"]);\n\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tassertLogEmpty(log);\n\n\t\t\t\t\tpoint.x = 2;\n\t\t\t\t\tassertLogEmpty(log);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\t// Parent which passed node down did not rerender, but PointComponent which read from it did:\n\t\t\t\t\tcheckRenderLog(log, [\"render\", \"usePropTreeNode\"]);\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"x: 2, y: 1\");\n\t\t\t\t});\n\t\t\t});\n\n\t\t\tdescribe(\"withTreeObservations and array\", () => {\n\t\t\t\tconst builder = new SchemaFactory(\"tree-react-api\");\n\n\t\t\t\tclass Item extends builder.object(\"Item\", {\n\t\t\t\t\tx: SchemaFactory.number,\n\t\t\t\t}) {}\n\n\t\t\t\tclass Collection extends builder.array(\"Collection\", Item) {}\n\n\t\t\t\tbeforeEach(() => {\n\t\t\t\t\t// Ensure the log starts empty for each test.\n\t\t\t\t\tlog.length = 0;\n\t\t\t\t});\n\n\t\t\t\tconst log: string[] = [];\n\n\t\t\t\tconst ItemComponent = withMemoizedTreeObservations(\n\t\t\t\t\t(props: { item: Item }): JSX.Element => {\n\t\t\t\t\t\tlog.push(`Item: ${props.item.x}`);\n\t\t\t\t\t\treturn <span>{`${props.item.x}`}</span>;\n\t\t\t\t\t},\n\t\t\t\t\t{ onInvalidation: () => log.push(\"Item invalidated\") },\n\t\t\t\t);\n\n\t\t\t\tconst CollectionComponent = withTreeObservations(\n\t\t\t\t\t(props: { collection: Collection }): JSX.Element => {\n\t\t\t\t\t\tlog.push(\"Collection\");\n\n\t\t\t\t\t\tconst items = props.collection.map((item) => (\n\t\t\t\t\t\t\t<ItemComponent key={objectIdNumber(item)} item={item} />\n\t\t\t\t\t\t));\n\n\t\t\t\t\t\treturn <div>{items}</div>;\n\t\t\t\t\t},\n\t\t\t\t\t{ onInvalidation: () => log.push(\"Collection invalidated\") },\n\t\t\t\t);\n\n\t\t\t\tconst ParentComponent = withTreeObservations(\n\t\t\t\t\t(props: { node: Collection }): JSX.Element => {\n\t\t\t\t\t\tlog.push(\"Parent\");\n\t\t\t\t\t\treturn <CollectionComponent collection={props.node} />;\n\t\t\t\t\t},\n\t\t\t\t\t{ onInvalidation: () => log.push(\"Parent invalidated\") },\n\t\t\t\t);\n\n\t\t\t\tit(\"empty\", () => {\n\t\t\t\t\tconst collection = new Collection([]);\n\t\t\t\t\tconst content = <ParentComponent node={collection} />;\n\t\t\t\t\trender(content, { reactStrictMode });\n\t\t\t\t\tcheckRenderLog(log, [\"Parent\", \"Collection\"]);\n\t\t\t\t});\n\n\t\t\t\t// This confirms that modifying an array does not needlessly invalid parents and reuses children (if they use memo)\n\t\t\t\tit(\"array editing: insertion\", () => {\n\t\t\t\t\tconst collection = new Collection([{ x: 1 }, { x: 2 }, { x: 3 }]);\n\t\t\t\t\tconst content = <ParentComponent node={collection} />;\n\t\t\t\t\tconst rendered = render(content, { reactStrictMode });\n\t\t\t\t\tcheckRenderLog(log, [\"Parent\", \"Collection\", \"Item: 1\", \"Item: 2\", \"Item: 3\"]);\n\t\t\t\t\tcollection.insertAtEnd(new Item({ x: 4 }));\n\t\t\t\t\tcheckRenderLog(log, [\"Collection invalidated\"]);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection\", \"Item: 4\"]);\n\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"1234\");\n\t\t\t\t});\n\n\t\t\t\t// This confirms the same as the above, but testes some harder cases.\n\t\t\t\t// For example this one depends on stable keys to reusing children due to indexes changing.\n\t\t\t\tit(\"array editing: general\", () => {\n\t\t\t\t\tconst collection = new Collection([{ x: 1 }, { x: 2 }, { x: 3 }]);\n\t\t\t\t\tconst content = <ParentComponent node={collection} />;\n\t\t\t\t\tconst rendered = render(content, { reactStrictMode });\n\t\t\t\t\tcheckRenderLog(log, [\"Parent\", \"Collection\", \"Item: 1\", \"Item: 2\", \"Item: 3\"]);\n\t\t\t\t\tcollection.moveToEnd(0);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection invalidated\"]);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection\"]);\n\t\t\t\t\tcollection.removeAt(1);\n\t\t\t\t\tcollection.insertAtStart(new Item({ x: 4 }));\n\t\t\t\t\tcheckRenderLog(log, [\"Collection invalidated\"]);\n\t\t\t\t\trendered.rerender(content);\n\t\t\t\t\tcheckRenderLog(log, [\"Collection\", \"Item: 4\"]);\n\n\t\t\t\t\tassert.equal(rendered.baseElement.textContent, \"421\");\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t});\n});\n\n/**\n * Assert that an array is empty.\n *\n * Not inlined because doing so causes TypeScript to infer the array type as never[] afterwards and breaks push.\n * Better than asserting length is 0 as this gets a better error message on failure.\n */\nfunction assertLogEmpty(log: string[]): void {\n\tassert.deepEqual(log, []);\n}\n"]}
@@ -5,10 +5,10 @@
5
5
  import { FormattedTextAsTree } from "@fluidframework/tree/internal";
6
6
  export { FormattedTextAsTree } from "@fluidframework/tree/internal";
7
7
  import DeltaPackage from "quill-delta";
8
- import * as React from "react";
9
8
  import { type PropTreeNode } from "../../propNode.js";
10
9
  import type { UndoRedo } from "../../undoRedo.js";
11
10
  type Delta = DeltaPackage.default;
11
+ type QuillDeltaOp = DeltaPackage.Op;
12
12
  declare const Delta: typeof DeltaPackage.default;
13
13
  /**
14
14
  * Props for the FormattedMainView component.
@@ -30,7 +30,7 @@ export type FormattedEditorHandle = Pick<UndoRedo, "undo" | "redo">;
30
30
  * Uses {@link @fluidframework/tree#FormattedTextAsTree.Tree} for the data-model and Quill for the rich text editor UI.
31
31
  * @internal
32
32
  */
33
- export declare const FormattedMainView: React.ForwardRefExoticComponent<FormattedMainViewProps & React.RefAttributes<FormattedEditorHandle>>;
33
+ export declare const FormattedMainView: import("react").ForwardRefExoticComponent<FormattedMainViewProps & import("react").RefAttributes<FormattedEditorHandle>>;
34
34
  /**
35
35
  * Parse CSS font-size from a pasted HTML element's inline style.
36
36
  * Returns a Quill size name if the pixel value matches a supported size, undefined otherwise.
@@ -51,4 +51,16 @@ export declare function parseCssFontFamily(node: HTMLElement): string | undefine
51
51
  * @see https://quilljs.com/docs/modules/clipboard#addmatcher
52
52
  */
53
53
  export declare function clipboardFormatMatcher(node: Node, delta: Delta): Delta;
54
+ /** Extract a LineTag from Quill attributes, or undefined if none present. Quill only supports one LineTag at a time. */
55
+ export declare function parseLineTag(attributes?: Record<string, unknown>): FormattedTextAsTree.LineTag | undefined;
56
+ /**
57
+ * Build a Quill Delta representing the full tree content.
58
+ * Iterates through formatted characters and groups consecutive characters
59
+ * with identical formatting into single insert operations for efficiency.
60
+ *
61
+ * @remarks
62
+ * This is used to sync Quill's display when the tree changes externally
63
+ * (e.g., from a remote collaborator's edit).
64
+ */
65
+ export declare function buildDeltaFromTree(root: FormattedTextAsTree.Tree): QuillDeltaOp[];
54
66
  //# sourceMappingURL=quillFormattedView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quillFormattedView.d.ts","sourceRoot":"","sources":["../../../src/text/formatted/quillFormattedView.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAmB,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,YAAY,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGlD,KAAK,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC;AAElC,QAAA,MAAM,KAAK,6BAAuB,CAAC;AAEnC;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,sGAK5B,CAAC;AAaH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAiBtE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAkBxE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,CActE"}
1
+ {"version":3,"file":"quillFormattedView.d.ts","sourceRoot":"","sources":["../../../src/text/formatted/quillFormattedView.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAmB,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,YAAY,MAAM,aAAa,CAAC;AAWvC,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGlD,KAAK,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC;AAClC,KAAK,YAAY,GAAG,YAAY,CAAC,EAAE,CAAC;AACpC,QAAA,MAAM,KAAK,6BAAuB,CAAC;AAEnC;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,0HAI7B,CAAC;AAkCF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAiBtE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAkBxE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,CActE;AAoBD,wHAAwH;AACxH,wBAAgB,YAAY,CAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,mBAAmB,CAAC,OAAO,GAAG,SAAS,CAgBzC;AA8ED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,CAAC,IAAI,GAAG,YAAY,EAAE,CA4DjF"}
@@ -1,3 +1,4 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
2
  /*!
2
3
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
4
  * Licensed under the MIT License.
@@ -7,7 +8,7 @@ import { Tree, TreeAlpha, FormattedTextAsTree } from "@fluidframework/tree/inter
7
8
  export { FormattedTextAsTree } from "@fluidframework/tree/internal";
8
9
  import Quill from "quill";
9
10
  import DeltaPackage from "quill-delta";
10
- import * as React from "react";
11
+ import { forwardRef, useEffect, useImperativeHandle, useReducer, useRef, useState, } from "react";
11
12
  import * as ReactDOM from "react-dom";
12
13
  import { unwrapPropTreeNode } from "../../propNode.js";
13
14
  const Delta = DeltaPackage.default;
@@ -17,8 +18,8 @@ const Delta = DeltaPackage.default;
17
18
  * Uses {@link @fluidframework/tree#FormattedTextAsTree.Tree} for the data-model and Quill for the rich text editor UI.
18
19
  * @internal
19
20
  */
20
- export const FormattedMainView = React.forwardRef(({ root, undoRedo }, ref) => {
21
- return React.createElement(FormattedTextEditorView, { root: root, undoRedo: undoRedo, ref: ref });
21
+ export const FormattedMainView = forwardRef(({ root, undoRedo }, ref) => {
22
+ return _jsx(FormattedTextEditorView, { root: root, undoRedo: undoRedo, ref: ref });
22
23
  });
23
24
  FormattedMainView.displayName = "FormattedMainView";
24
25
  /** Quill size names mapped to pixel values for tree storage. */
@@ -31,6 +32,25 @@ const fontSet = new Set(["monospace", "serif", "sans-serif", "Arial"]);
31
32
  const defaultSize = 12;
32
33
  /** Default font when no explicit font is specified. */
33
34
  const defaultFont = "Arial";
35
+ /** default heading for when an unsupported header is supplied */
36
+ const defaultHeading = "h5";
37
+ /** Quill header numbers → LineTag values. */
38
+ const headerToLineTag = {
39
+ 1: "h1",
40
+ 2: "h2",
41
+ 3: "h3",
42
+ 4: "h4",
43
+ 5: "h5",
44
+ };
45
+ /** LineTag values → Quill attributes. Used by buildDeltaFromTree (tree → Quill). */
46
+ const lineTagToQuillAttributes = {
47
+ h1: { header: 1 },
48
+ h2: { header: 2 },
49
+ h3: { header: 3 },
50
+ h4: { header: 4 },
51
+ h5: { header: 5 },
52
+ li: { list: "bullet" },
53
+ };
34
54
  /**
35
55
  * Parse CSS font-size from a pasted HTML element's inline style.
36
56
  * Returns a Quill size name if the pixel value matches a supported size, undefined otherwise.
@@ -117,18 +137,42 @@ function parseSize(size) {
117
137
  }
118
138
  return defaultSize;
119
139
  }
140
+ /** Extract a LineTag from Quill attributes, or undefined if none present. Quill only supports one LineTag at a time. */
141
+ export function parseLineTag(attributes) {
142
+ if (!attributes)
143
+ return undefined;
144
+ // Quill should never send both header and list attributes simultaneously.
145
+ assert(!(typeof attributes.header === "number" && typeof attributes.list === "string"), 0xce2 /* expected at most one line tag (header or list), but received both */);
146
+ if (typeof attributes.header === "number") {
147
+ const tag = headerToLineTag[attributes.header] ?? defaultHeading;
148
+ return FormattedTextAsTree.LineTag(tag);
149
+ }
150
+ if (attributes.list === "bullet") {
151
+ return FormattedTextAsTree.LineTag("li");
152
+ }
153
+ return undefined;
154
+ }
155
+ /** Create a StringAtom containing a StringLineAtom with the given line tag. */
156
+ function createLineAtom(lineTag) {
157
+ return new FormattedTextAsTree.StringAtom({
158
+ content: new FormattedTextAsTree.StringLineAtom({
159
+ tag: lineTag,
160
+ }),
161
+ format: new FormattedTextAsTree.CharacterFormat(quillAttributesToFormat()),
162
+ });
163
+ }
120
164
  /**
121
165
  * Convert Quill attributes to a complete CharacterFormat object.
122
166
  * Used when inserting new characters - all format properties must have values.
123
167
  * Missing attributes default to false/default values.
124
168
  */
125
- function quillAttrsToFormat(attrs) {
169
+ function quillAttributesToFormat(attributes) {
126
170
  return {
127
- bold: attrs?.bold === true,
128
- italic: attrs?.italic === true,
129
- underline: attrs?.underline === true,
130
- size: parseSize(attrs?.size),
131
- font: typeof attrs?.font === "string" ? attrs.font : defaultFont,
171
+ bold: attributes?.bold === true,
172
+ italic: attributes?.italic === true,
173
+ underline: attributes?.underline === true,
174
+ size: parseSize(attributes?.size),
175
+ font: typeof attributes?.font === "string" ? attributes.font : defaultFont,
132
176
  };
133
177
  }
134
178
  /**
@@ -137,21 +181,21 @@ function quillAttrsToFormat(attrs) {
137
181
  * Only includes properties that were explicitly set in the Quill attributes,
138
182
  * allowing selective format updates without overwriting unrelated properties.
139
183
  */
140
- function quillAttrsToPartial(attrs) {
141
- if (!attrs)
184
+ function quillAttributesToPartial(attributes) {
185
+ if (!attributes)
142
186
  return {};
143
187
  const format = {};
144
188
  // Only include attributes that are explicitly present in the Quill delta
145
- if ("bold" in attrs)
146
- format.bold = attrs.bold === true;
147
- if ("italic" in attrs)
148
- format.italic = attrs.italic === true;
149
- if ("underline" in attrs)
150
- format.underline = attrs.underline === true;
151
- if ("size" in attrs)
152
- format.size = parseSize(attrs.size);
153
- if ("font" in attrs)
154
- format.font = typeof attrs.font === "string" ? attrs.font : defaultFont;
189
+ if ("bold" in attributes)
190
+ format.bold = attributes.bold === true;
191
+ if ("italic" in attributes)
192
+ format.italic = attributes.italic === true;
193
+ if ("underline" in attributes)
194
+ format.underline = attributes.underline === true;
195
+ if ("size" in attributes)
196
+ format.size = parseSize(attributes.size);
197
+ if ("font" in attributes)
198
+ format.font = typeof attributes.font === "string" ? attributes.font : defaultFont;
155
199
  return format;
156
200
  }
157
201
  /**
@@ -159,25 +203,25 @@ function quillAttrsToPartial(attrs) {
159
203
  * Used when building Quill deltas from tree content to sync external changes.
160
204
  * Only includes non-default values to keep deltas minimal.
161
205
  */
162
- function formatToQuillAttrs(format) {
163
- const attrs = {};
206
+ function formatToQuillAttributes(format) {
207
+ const attributes = {};
164
208
  // Only include non-default formatting to keep Quill deltas minimal
165
209
  if (format.bold)
166
- attrs.bold = true;
210
+ attributes.bold = true;
167
211
  if (format.italic)
168
- attrs.italic = true;
212
+ attributes.italic = true;
169
213
  if (format.underline)
170
- attrs.underline = true;
214
+ attributes.underline = true;
171
215
  if (format.size !== defaultSize) {
172
216
  // Convert pixel value back to Quill size name if possible
173
- attrs.size =
217
+ attributes.size =
174
218
  format.size in sizeReverse
175
219
  ? sizeReverse[format.size]
176
220
  : `${format.size}px`;
177
221
  }
178
222
  if (format.font !== defaultFont)
179
- attrs.font = format.font;
180
- return attrs;
223
+ attributes.font = format.font;
224
+ return attributes;
181
225
  }
182
226
  /**
183
227
  * Build a Quill Delta representing the full tree content.
@@ -188,11 +232,11 @@ function formatToQuillAttrs(format) {
188
232
  * This is used to sync Quill's display when the tree changes externally
189
233
  * (e.g., from a remote collaborator's edit).
190
234
  */
191
- function buildDeltaFromTree(root) {
235
+ export function buildDeltaFromTree(root) {
192
236
  const ops = [];
193
237
  // Accumulator for current run of identically-formatted text
194
238
  let text = "";
195
- let attrs = {};
239
+ let previousAttributes = {};
196
240
  // JSON key for current attributes, used for equality comparison
197
241
  // TODO:Performance: implement faster equality check.
198
242
  let key = "";
@@ -201,26 +245,41 @@ function buildDeltaFromTree(root) {
201
245
  if (!text)
202
246
  return;
203
247
  const op = { insert: text };
204
- if (Object.keys(attrs).length > 0)
205
- op.attributes = attrs;
248
+ if (Object.keys(previousAttributes).length > 0)
249
+ op.attributes = previousAttributes;
206
250
  ops.push(op);
207
251
  };
208
252
  // Iterate through each formatted character in the tree
209
253
  // TODO:Performance: Optimize this loop by adding an API to get runs to FormattedTextAsTree.Tree, and implementing that using cursors.
210
254
  // Something like `getUniformRun(startIndex, maxLength): number` and `substring(startIndex, length): string`.
211
255
  for (const atom of root.charactersWithFormatting()) {
212
- const a = formatToQuillAttrs(atom.format);
213
- const k = JSON.stringify(a);
214
- if (k === key) {
215
- // Same formatting as previous character - extend current run
216
- text += atom.content.content;
256
+ const currentAttributes = formatToQuillAttributes(atom.format);
257
+ if (atom.content instanceof FormattedTextAsTree.StringLineAtom) {
258
+ // Merge line-specific attributes (header/list) into the format
259
+ const lineTag = atom.content.tag.value;
260
+ Object.assign(currentAttributes, lineTagToQuillAttributes[lineTag]);
261
+ // Line atoms always break the current run and emit a newline
262
+ pushRun();
263
+ text = "";
264
+ key = "";
265
+ const op = { insert: "\n" };
266
+ if (Object.keys(currentAttributes).length > 0)
267
+ op.attributes = currentAttributes;
268
+ ops.push(op);
217
269
  }
218
270
  else {
219
- // Different formatting - push current run and start new one
220
- pushRun();
221
- text = atom.content.content;
222
- attrs = a;
223
- key = k;
271
+ const stringifiedAttributes = JSON.stringify(currentAttributes);
272
+ if (stringifiedAttributes === key) {
273
+ // Same formatting as previous character - extend run
274
+ text += atom.content.content;
275
+ }
276
+ else {
277
+ // Different formatting - push previous run and start a new one
278
+ pushRun();
279
+ text = atom.content.content;
280
+ previousAttributes = currentAttributes;
281
+ key = stringifiedAttributes;
282
+ }
224
283
  }
225
284
  }
226
285
  // Push any remaining accumulated text
@@ -244,26 +303,26 @@ function buildDeltaFromTree(root) {
244
303
  * to make targeted edits (insert at index, delete range, format range) rather
245
304
  * than replacing all content on each change.
246
305
  */
247
- const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo }, ref) => {
306
+ const FormattedTextEditorView = forwardRef(({ root: propRoot, undoRedo }, ref) => {
248
307
  // Unwrap the PropTreeNode to get the actual tree node
249
308
  const root = unwrapPropTreeNode(propRoot);
250
309
  // DOM element where Quill will mount its editor
251
- const editorRef = React.useRef(null);
310
+ const editorRef = useRef(null);
252
311
  // Quill instance, persisted across renders to avoid re-initialization
253
- const quillRef = React.useRef(null);
312
+ const quillRef = useRef(null);
254
313
  // Guards against update loops between Quill and the tree
255
- const isUpdating = React.useRef(false);
314
+ const isUpdating = useRef(false);
256
315
  // Container element for undo/redo button portal
257
- const [undoRedoContainer, setUndoRedoContainer] = React.useState(undefined);
316
+ const [undoRedoContainer, setUndoRedoContainer] = useState(undefined);
258
317
  // Force re-render when undo/redo state changes
259
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
318
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
260
319
  // Expose undo/redo methods via ref
261
- React.useImperativeHandle(ref, () => ({
320
+ useImperativeHandle(ref, () => ({
262
321
  undo: () => undoRedo?.undo(),
263
322
  redo: () => undoRedo?.redo(),
264
323
  }));
265
324
  // Initialize Quill editor with formatting toolbar using Quill provided CSS
266
- React.useEffect(() => {
325
+ useEffect(() => {
267
326
  if (!editorRef.current || quillRef.current)
268
327
  return;
269
328
  const quill = new Quill(editorRef.current, {
@@ -275,6 +334,8 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
275
334
  ["bold", "italic", "underline"],
276
335
  [{ size: ["small", false, "large", "huge"] }],
277
336
  [{ font: [] }],
337
+ [{ header: [1, 2, 3, 4, 5, false] }],
338
+ [{ list: "bullet" }],
278
339
  ["clean"],
279
340
  ],
280
341
  clipboard: [Node.ELEMENT_NODE, clipboardFormatMatcher],
@@ -316,7 +377,36 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
316
377
  const retainedStr = content.slice(utf16Pos, utf16Pos + op.retain);
317
378
  const cpCount = codepointCount(retainedStr);
318
379
  if (op.attributes) {
319
- root.formatRange(cpPos, cpPos + cpCount, quillAttrsToPartial(op.attributes));
380
+ const lineTag = parseLineTag(op.attributes);
381
+ // Case 1: Applying line formatting (header/list) to an existing newline in the document.
382
+ if (lineTag !== undefined && content[utf16Pos] === "\n") {
383
+ // Swap existing newline atom to StringLineAtom
384
+ root.removeRange(cpPos, cpPos + 1);
385
+ root.insertWithFormattingAt(cpPos, [createLineAtom(lineTag)]);
386
+ // Case 2: Applying line formatting past the end of content. Quill's implicit trailing newline.
387
+ }
388
+ else if (lineTag !== undefined && utf16Pos >= content.length) {
389
+ // Quill's implicit trailing newline — insert a new line atom
390
+ root.insertWithFormattingAt(cpPos, [createLineAtom(lineTag)]);
391
+ content += "\n";
392
+ // Case 3: clearing line formatting. Deletes StringLineAtom and inserts a plain
393
+ // StringTextAtom("\n") in its place.
394
+ }
395
+ else if (lineTag === undefined &&
396
+ content[utf16Pos] === "\n" &&
397
+ root.charactersWithFormatting()[cpPos]?.content instanceof
398
+ FormattedTextAsTree.StringLineAtom) {
399
+ // Quill is clearing line formatting (e.g. { retain: 1, attributes: { header: null } }).
400
+ // StringLineAtom and StringTextAtom are distinct schema types in the tree,
401
+ // so we can't convert between them via formatRange — we must delete the
402
+ // StringLineAtom and insert a plain StringTextAtom("\n") in its place.
403
+ root.removeRange(cpPos, cpPos + 1);
404
+ root.insertAt(cpPos, "\n");
405
+ // Case 4: Normal character formatting (bold, italic, size, etc...)
406
+ }
407
+ else {
408
+ root.formatRange(cpPos, cpPos + cpCount, quillAttributesToPartial(op.attributes));
409
+ }
320
410
  }
321
411
  utf16Pos += op.retain;
322
412
  cpPos += cpCount;
@@ -331,9 +421,15 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
331
421
  // Don't advance positions - next op starts at same position
332
422
  }
333
423
  else if (typeof op.insert === "string") {
334
- // Insert: add new text with formatting at current position
335
- root.defaultFormat = new FormattedTextAsTree.CharacterFormat(quillAttrsToFormat(op.attributes));
336
- root.insertAt(cpPos, op.insert);
424
+ const lineTag = parseLineTag(op.attributes);
425
+ if (lineTag !== undefined && op.insert === "\n") {
426
+ root.insertWithFormattingAt(cpPos, [createLineAtom(lineTag)]);
427
+ }
428
+ else {
429
+ // Insert: add new text with formatting at current position
430
+ root.defaultFormat = new FormattedTextAsTree.CharacterFormat(quillAttributesToFormat(op.attributes));
431
+ root.insertAt(cpPos, op.insert);
432
+ }
337
433
  // Update content to reflect insertion
338
434
  content = content.slice(0, utf16Pos) + op.insert + content.slice(utf16Pos);
339
435
  // Advance by inserted content length
@@ -363,7 +459,7 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
363
459
  }, []);
364
460
  // Sync Quill when tree changes externally (e.g., from remote collaborators).
365
461
  // Uses event subscription instead of render-time observation for efficiency.
366
- React.useEffect(() => {
462
+ useEffect(() => {
367
463
  return Tree.on(root, "treeChanged", () => {
368
464
  // Skip if we caused the tree change ourselves via the text-change handler
369
465
  if (!quillRef.current || isUpdating.current)
@@ -388,7 +484,7 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
388
484
  });
389
485
  }, [root]);
390
486
  // Subscribe to undo/redo state changes to update button disabled state
391
- React.useEffect(() => {
487
+ useEffect(() => {
392
488
  if (!undoRedo)
393
489
  return;
394
490
  return undoRedo.onStateChange(() => {
@@ -397,12 +493,9 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
397
493
  }, [undoRedo]);
398
494
  // Render undo/redo buttons via portal into Quill toolbar
399
495
  const undoRedoButtons = undoRedoContainer
400
- ? ReactDOM.createPortal(React.createElement(React.Fragment, null,
401
- React.createElement("button", { type: "button", className: "ql-undo", disabled: undoRedo?.canUndo() !== true, onClick: () => undoRedo?.undo() }),
402
- React.createElement("button", { type: "button", className: "ql-redo", disabled: undoRedo?.canRedo() !== true, onClick: () => undoRedo?.redo() })), undoRedoContainer)
496
+ ? ReactDOM.createPortal(_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "ql-undo", disabled: undoRedo?.canUndo() !== true, onClick: () => undoRedo?.undo() }), _jsx("button", { type: "button", className: "ql-redo", disabled: undoRedo?.canRedo() !== true, onClick: () => undoRedo?.redo() })] }), undoRedoContainer)
403
497
  : undefined;
404
- return (React.createElement("div", { style: { height: "100%", display: "flex", flexDirection: "column" } },
405
- React.createElement("style", null, `
498
+ return (_jsxs("div", { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [_jsx("style", { children: `
406
499
  .ql-container { height: 100%; font-size: 14px; }
407
500
  .ql-editor { height: 100%; outline: none; }
408
501
  .ql-editor.ql-blank::before { color: #999; font-style: italic; }
@@ -412,15 +505,16 @@ const FormattedTextEditorView = React.forwardRef(({ root: propRoot, undoRedo },
412
505
  .ql-undo::after { content: "↶"; font-size: 18px; }
413
506
  .ql-redo::after { content: "↷"; font-size: 18px; }
414
507
  .ql-undo:disabled, .ql-redo:disabled { opacity: 0.3; cursor: not-allowed; }
415
- `),
416
- React.createElement("h2", { style: { margin: "10px 0" } }, "Collaborative Formatted Text Editor"),
417
- React.createElement("div", { ref: editorRef, style: {
418
- flex: 1,
419
- minHeight: "300px",
420
- border: "1px solid #ccc",
421
- borderRadius: "4px",
422
- } }),
423
- undoRedoButtons));
508
+ /* custom css altering Quill's default bullet point alignment */
509
+ /* vertically center bullets in list items, since Quill's bullet has no inherent height */
510
+ li[data-list="bullet"] { display: flex; align-items: center; }
511
+ li[data-list="bullet"] .ql-ui { align-self: center; }
512
+ ` }), _jsx("h2", { style: { margin: "10px 0" }, children: "Collaborative Formatted Text Editor" }), _jsx("div", { ref: editorRef, style: {
513
+ flex: 1,
514
+ minHeight: "300px",
515
+ border: "1px solid #ccc",
516
+ borderRadius: "4px",
517
+ } }), undoRedoButtons] }));
424
518
  });
425
519
  FormattedTextEditorView.displayName = "FormattedTextEditorView";
426
520
  //# sourceMappingURL=quillFormattedView.js.map