@onodocs/canvas 0.2.1 → 0.3.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.
- package/README.md +63 -1
- package/SDK.md +63 -1
- package/index.js +65 -13
- package/package.json +3 -3
- package/types/canvas/presentation.d.ts +5 -1
- package/types/canvas/text-selection.d.ts +4 -2
- package/types/foundation/page-interaction.d.ts +9 -0
- package/types/render-api/index.d.ts +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,69 @@ Batch range offsets refer to the original paragraph text. Empty ranges insert te
|
|
|
60
60
|
|
|
61
61
|
Browser updates rebuild layout and refresh mounted views. Query, semantic, layout, page and geometry values obtained earlier remain snapshots; read them again after updating. Application attachments are removed during refresh and can be reattached using fresh geometry. Updates run in submission order and accept `{ signal }` for cancellation. The original `source` is unchanged.
|
|
62
62
|
|
|
63
|
-
Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before `renderPage` or `
|
|
63
|
+
Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before `renderPage`, `pdf`, or `save`. Exports reflect the updated text.
|
|
64
|
+
|
|
65
|
+
## Save an edited Word document
|
|
66
|
+
|
|
67
|
+
`save()` returns DOCX bytes in Node, the browser, and server rendering. Saving without edits returns the original file bytes. Text edits preserve surrounding run formatting and unedited package content. Saving and updating run in submission order. Both accept `{ signal }`; cancellation leaves the committed document available. Call `dispose()` when finished, including for Node documents, to release the preserved source package.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
71
|
+
import { openDocument } from "@onodocs/sdk";
|
|
72
|
+
|
|
73
|
+
const doc = await openDocument(await readFile("template.docx"));
|
|
74
|
+
try {
|
|
75
|
+
await doc.update({
|
|
76
|
+
target: doc.query.contentControls().where({ tag: "customer" }).one(),
|
|
77
|
+
text: "Willow Design"
|
|
78
|
+
});
|
|
79
|
+
await writeFile("completed.docx", await doc.save());
|
|
80
|
+
} finally {
|
|
81
|
+
doc.dispose();
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
In a browser, create a Blob from the returned bytes with type `application/vnd.openxmlformats-officedocument.wordprocessingml.document`. In a combined deployment, serve `await doc.save()` from an application-owned download endpoint with that content type and a `.docx` filename. The backend processing document owns saving; a Canvas-only frontend does not contain the original Word package.
|
|
86
|
+
|
|
87
|
+
The SDK package includes a complete command-line example. After installing it, run `node node_modules/@onodocs/sdk/examples/fill-template.mjs template.docx completed.docx customer "Willow Design"`. Set `ONODOCS_LICENSE_KEY` for trial or commercial use. The source workspace keeps the same example at `examples/fill-template.mjs`.
|
|
88
|
+
|
|
89
|
+
The initial editing profile covers ordinary text and unbound text or rich-text content controls within one paragraph, including supported tables, headers, footers and notes. Edits inside bound or content-locked controls, temporary or specialized controls, fields, revisions, alternate-content branches, and source runs with generated or unsupported content are rejected before publication. Plain-text controls that disallow multiple lines reject newlines. Filling an unbound text control clears its placeholder-display flag. Edits elsewhere preserve these constructs. `DocumentSaveError.reason` identifies source-writing restrictions; existing semantic target errors remain `RangeError`.
|
|
90
|
+
|
|
91
|
+
Signed packages, enforced document protection, enabled change tracking, macro-enabled documents and templates can be saved unchanged, but editing them is not supported. Field recalculation and revision authoring are not included. Field caches remain as imported, and saved bookmark/comment markers retain their original run boundaries. Original authored formatting is preserved independently of rendering compatibility decisions.
|
|
92
|
+
|
|
93
|
+
Evaluation exports contain the full Word document without adding a Word watermark. The existing non-production evaluation terms apply; rendered evaluation output continues to include its watermark.
|
|
94
|
+
|
|
95
|
+
## Edit formatting and paragraphs
|
|
96
|
+
|
|
97
|
+
`edit()` accepts a logical selection with paragraph IDs and UTF-16 offsets. It returns the selection in the resulting document. Use fresh queries after editing because structural operations rebuild the document and its IDs. The `source` getter then describes the new committed package.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const paragraph = doc.query.paragraphs().first();
|
|
101
|
+
let selection = {
|
|
102
|
+
start: { paragraphId: paragraph.id, offset: 0 },
|
|
103
|
+
end: { paragraphId: paragraph.id, offset: paragraph.text.length }
|
|
104
|
+
};
|
|
105
|
+
selection = await doc.edit({ kind: "format", selection, formatting: { bold: true, fontSize: 18 } });
|
|
106
|
+
selection = await doc.edit({ kind: "replace", selection, text: "First paragraph\nSecond paragraph" });
|
|
107
|
+
await doc.edit({ kind: "align", selection, alignment: "center" });
|
|
108
|
+
const bytes = await doc.save();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Character formatting supports `bold`, `italic`, `underline`, `strike`, `fontSize` in points, `fontFamily`, and `color` as `#RRGGBB`. Values change direct formatting on the selected text while preserving the other authored properties. `run.formatting` exposes the common effective character properties for controls; `paragraph.alignment` exposes left, center, right or justified alignment. These convenience properties describe ordinary Latin text; they do not replace the full semantic typography model for script-specific inspection.
|
|
112
|
+
|
|
113
|
+
A replacement with a collapsed selection inserts text. Newlines split paragraphs; set `paragraphBreaks: false` for line breaks within a paragraph. Replacing a selection across adjacent paragraphs joins their surviving prefix and suffix. Empty text deletes the selection. New text inherits the starting run's formatting unless the operation includes `formatting`. Split paragraphs inherit the starting paragraph's properties. The first paragraph's properties win when joining.
|
|
114
|
+
|
|
115
|
+
Rich editing supports ordinary text paragraphs, including empty paragraphs and paragraphs in table cells and secondary stories. Operations affecting paragraphs with fields, links, controls, drawings, comments, bookmarks, tracked revisions or unsupported children reject atomically. Splitting or joining section boundaries and joining across cells or stories also reject. Such content remains preserved elsewhere in the document. The narrower `update()` operation continues to support safe text replacement within unbound controls.
|
|
116
|
+
|
|
117
|
+
Node and browser documents expose `edit`; server documents forward the same operation. Parsing, semantic construction and browser layout finish before publication, so cancellation or failure leaves the previous document usable. This first implementation rebuilds the document for structural edits. Browser resources stay with the document lifetime, and the sample keeps undo history as document snapshots. Large documents need further editing-performance work before this can serve as a general-purpose editor.
|
|
118
|
+
|
|
119
|
+
## Run the inline browser editor sample
|
|
120
|
+
|
|
121
|
+
From the source workspace, run `npm run build` and `npm run example:editor`, then open `http://127.0.0.1:5190`. The sample opens its included Word document and accepts local DOCX files. Click directly on a rendered page to type, drag to select text, and use the toolbar to change formatting. Enter splits paragraphs; Backspace at a paragraph's start joins it to the previous paragraph. Shift+Enter inserts a line break. The sample also supports keyboard selection, copy/cut, plain-text paste, undo/redo and Word download. Files remain in the browser.
|
|
122
|
+
|
|
123
|
+
The sample is in `examples/browser-editor/`. It consumes only exported SDK and Canvas APIs. Canvas `createDocument` accepts a native textarea through `viewOptions.input`, plus an `onSelectionChange` callback in the same options. The view positions this input at the published caret, handles selection using layout geometry, and exposes `selection` and `setSelection`. The application handles input events and submits `doc.edit()` operations. Text, line wrapping and pagination remain rendered by OnoDocs.
|
|
124
|
+
|
|
125
|
+
Run `npm run validate:editor` for the browser editing journey. The sample currently uses the workspace build; it requires the new editing APIs and does not work against the earlier published package.
|
|
64
126
|
|
|
65
127
|
## Render and attach ordinary DOM
|
|
66
128
|
|
package/SDK.md
CHANGED
|
@@ -60,7 +60,69 @@ Batch range offsets refer to the original paragraph text. Empty ranges insert te
|
|
|
60
60
|
|
|
61
61
|
Browser updates rebuild layout and refresh mounted views. Query, semantic, layout, page and geometry values obtained earlier remain snapshots; read them again after updating. Application attachments are removed during refresh and can be reattached using fresh geometry. Updates run in submission order and accept `{ signal }` for cancellation. The original `source` is unchanged.
|
|
62
62
|
|
|
63
|
-
Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before `renderPage` or `
|
|
63
|
+
Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before `renderPage`, `pdf`, or `save`. Exports reflect the updated text.
|
|
64
|
+
|
|
65
|
+
## Save an edited Word document
|
|
66
|
+
|
|
67
|
+
`save()` returns DOCX bytes in Node, the browser, and server rendering. Saving without edits returns the original file bytes. Text edits preserve surrounding run formatting and unedited package content. Saving and updating run in submission order. Both accept `{ signal }`; cancellation leaves the committed document available. Call `dispose()` when finished, including for Node documents, to release the preserved source package.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
71
|
+
import { openDocument } from "@onodocs/sdk";
|
|
72
|
+
|
|
73
|
+
const doc = await openDocument(await readFile("template.docx"));
|
|
74
|
+
try {
|
|
75
|
+
await doc.update({
|
|
76
|
+
target: doc.query.contentControls().where({ tag: "customer" }).one(),
|
|
77
|
+
text: "Willow Design"
|
|
78
|
+
});
|
|
79
|
+
await writeFile("completed.docx", await doc.save());
|
|
80
|
+
} finally {
|
|
81
|
+
doc.dispose();
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
In a browser, create a Blob from the returned bytes with type `application/vnd.openxmlformats-officedocument.wordprocessingml.document`. In a combined deployment, serve `await doc.save()` from an application-owned download endpoint with that content type and a `.docx` filename. The backend processing document owns saving; a Canvas-only frontend does not contain the original Word package.
|
|
86
|
+
|
|
87
|
+
The SDK package includes a complete command-line example. After installing it, run `node node_modules/@onodocs/sdk/examples/fill-template.mjs template.docx completed.docx customer "Willow Design"`. Set `ONODOCS_LICENSE_KEY` for trial or commercial use. The source workspace keeps the same example at `examples/fill-template.mjs`.
|
|
88
|
+
|
|
89
|
+
The initial editing profile covers ordinary text and unbound text or rich-text content controls within one paragraph, including supported tables, headers, footers and notes. Edits inside bound or content-locked controls, temporary or specialized controls, fields, revisions, alternate-content branches, and source runs with generated or unsupported content are rejected before publication. Plain-text controls that disallow multiple lines reject newlines. Filling an unbound text control clears its placeholder-display flag. Edits elsewhere preserve these constructs. `DocumentSaveError.reason` identifies source-writing restrictions; existing semantic target errors remain `RangeError`.
|
|
90
|
+
|
|
91
|
+
Signed packages, enforced document protection, enabled change tracking, macro-enabled documents and templates can be saved unchanged, but editing them is not supported. Field recalculation and revision authoring are not included. Field caches remain as imported, and saved bookmark/comment markers retain their original run boundaries. Original authored formatting is preserved independently of rendering compatibility decisions.
|
|
92
|
+
|
|
93
|
+
Evaluation exports contain the full Word document without adding a Word watermark. The existing non-production evaluation terms apply; rendered evaluation output continues to include its watermark.
|
|
94
|
+
|
|
95
|
+
## Edit formatting and paragraphs
|
|
96
|
+
|
|
97
|
+
`edit()` accepts a logical selection with paragraph IDs and UTF-16 offsets. It returns the selection in the resulting document. Use fresh queries after editing because structural operations rebuild the document and its IDs. The `source` getter then describes the new committed package.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const paragraph = doc.query.paragraphs().first();
|
|
101
|
+
let selection = {
|
|
102
|
+
start: { paragraphId: paragraph.id, offset: 0 },
|
|
103
|
+
end: { paragraphId: paragraph.id, offset: paragraph.text.length }
|
|
104
|
+
};
|
|
105
|
+
selection = await doc.edit({ kind: "format", selection, formatting: { bold: true, fontSize: 18 } });
|
|
106
|
+
selection = await doc.edit({ kind: "replace", selection, text: "First paragraph\nSecond paragraph" });
|
|
107
|
+
await doc.edit({ kind: "align", selection, alignment: "center" });
|
|
108
|
+
const bytes = await doc.save();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Character formatting supports `bold`, `italic`, `underline`, `strike`, `fontSize` in points, `fontFamily`, and `color` as `#RRGGBB`. Values change direct formatting on the selected text while preserving the other authored properties. `run.formatting` exposes the common effective character properties for controls; `paragraph.alignment` exposes left, center, right or justified alignment. These convenience properties describe ordinary Latin text; they do not replace the full semantic typography model for script-specific inspection.
|
|
112
|
+
|
|
113
|
+
A replacement with a collapsed selection inserts text. Newlines split paragraphs; set `paragraphBreaks: false` for line breaks within a paragraph. Replacing a selection across adjacent paragraphs joins their surviving prefix and suffix. Empty text deletes the selection. New text inherits the starting run's formatting unless the operation includes `formatting`. Split paragraphs inherit the starting paragraph's properties. The first paragraph's properties win when joining.
|
|
114
|
+
|
|
115
|
+
Rich editing supports ordinary text paragraphs, including empty paragraphs and paragraphs in table cells and secondary stories. Operations affecting paragraphs with fields, links, controls, drawings, comments, bookmarks, tracked revisions or unsupported children reject atomically. Splitting or joining section boundaries and joining across cells or stories also reject. Such content remains preserved elsewhere in the document. The narrower `update()` operation continues to support safe text replacement within unbound controls.
|
|
116
|
+
|
|
117
|
+
Node and browser documents expose `edit`; server documents forward the same operation. Parsing, semantic construction and browser layout finish before publication, so cancellation or failure leaves the previous document usable. This first implementation rebuilds the document for structural edits. Browser resources stay with the document lifetime, and the sample keeps undo history as document snapshots. Large documents need further editing-performance work before this can serve as a general-purpose editor.
|
|
118
|
+
|
|
119
|
+
## Run the inline browser editor sample
|
|
120
|
+
|
|
121
|
+
From the source workspace, run `npm run build` and `npm run example:editor`, then open `http://127.0.0.1:5190`. The sample opens its included Word document and accepts local DOCX files. Click directly on a rendered page to type, drag to select text, and use the toolbar to change formatting. Enter splits paragraphs; Backspace at a paragraph's start joins it to the previous paragraph. Shift+Enter inserts a line break. The sample also supports keyboard selection, copy/cut, plain-text paste, undo/redo and Word download. Files remain in the browser.
|
|
122
|
+
|
|
123
|
+
The sample is in `examples/browser-editor/`. It consumes only exported SDK and Canvas APIs. Canvas `createDocument` accepts a native textarea through `viewOptions.input`, plus an `onSelectionChange` callback in the same options. The view positions this input at the published caret, handles selection using layout geometry, and exposes `selection` and `setSelection`. The application handles input events and submits `doc.edit()` operations. Text, line wrapping and pagination remain rendered by OnoDocs.
|
|
124
|
+
|
|
125
|
+
Run `npm run validate:editor` for the browser editing journey. The sample currently uses the workspace build; it requires the new editing APIs and does not work against the earlier published package.
|
|
64
126
|
|
|
65
127
|
## Render and attach ordinary DOM
|
|
66
128
|
|
package/index.js
CHANGED
|
@@ -1578,7 +1578,7 @@ __name(fillExactSectionColumnSeparator, "fillExactSectionColumnSeparator");
|
|
|
1578
1578
|
|
|
1579
1579
|
// packages/canvas/src/text-selection.ts
|
|
1580
1580
|
var nextView = 0;
|
|
1581
|
-
function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, signal) {
|
|
1581
|
+
function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, signal, input, onSelectionChange) {
|
|
1582
1582
|
const document2 = viewport.ownerDocument;
|
|
1583
1583
|
const window = document2.defaultView;
|
|
1584
1584
|
const svg = "http://www.w3.org/2000/svg";
|
|
@@ -1607,7 +1607,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1607
1607
|
for (const caret of value.carets) {
|
|
1608
1608
|
const offset = offsets[caret.scalarOffset - value.scalarStart];
|
|
1609
1609
|
if (offset === void 0) continue;
|
|
1610
|
-
const stop = { line, offset: length + offset, x: caret.x, index: stops.length };
|
|
1610
|
+
const stop = { line, offset: length + offset, x: caret.x, index: stops.length, ...caret.paragraphOffset === void 0 ? {} : { position: { paragraphId: value.paragraphId, offset: caret.paragraphOffset } } };
|
|
1611
1611
|
line.stops.push(stop);
|
|
1612
1612
|
stops.push(stop);
|
|
1613
1613
|
}
|
|
@@ -1639,6 +1639,10 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1639
1639
|
let menu;
|
|
1640
1640
|
viewport.tabIndex = 0;
|
|
1641
1641
|
viewport.style.userSelect = "none";
|
|
1642
|
+
if (input) {
|
|
1643
|
+
viewport.style.position = "relative";
|
|
1644
|
+
viewport.append(input);
|
|
1645
|
+
}
|
|
1642
1646
|
for (const shell of shells) shell.style.cursor = "text";
|
|
1643
1647
|
function paint() {
|
|
1644
1648
|
for (const [index, layer] of layers) {
|
|
@@ -1646,7 +1650,13 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1646
1650
|
layer.replaceChildren();
|
|
1647
1651
|
if (!nearby.has(index)) layers.delete(index);
|
|
1648
1652
|
}
|
|
1649
|
-
if (!anchor || !focus || document2.activeElement !== viewport && !menu?.contains(document2.activeElement)) return;
|
|
1653
|
+
if (!anchor || !focus || !input && document2.activeElement !== viewport && !menu?.contains(document2.activeElement)) return;
|
|
1654
|
+
if (input) {
|
|
1655
|
+
const shell = shells[focus.line.item.pageIndex], bounds = shell.getBoundingClientRect(), origin = viewport.getBoundingClientRect();
|
|
1656
|
+
const point = focus.line.transform.transformPoint({ x: focus.x, y: focus.line.item.selection.top });
|
|
1657
|
+
const scale = bounds.width / pageBounds[focus.line.item.pageIndex].width;
|
|
1658
|
+
Object.assign(input.style, { position: "absolute", left: `${bounds.left - origin.left + point.x * scale}px`, top: `${bounds.top - origin.top + point.y * scale}px`, height: `${focus.line.item.selection.height * scale}px` });
|
|
1659
|
+
}
|
|
1650
1660
|
const start = Math.min(anchor.offset, focus.offset), end = Math.max(anchor.offset, focus.offset);
|
|
1651
1661
|
for (const index of nearby) for (const line of pages[index] ?? []) {
|
|
1652
1662
|
let rectangle2 = function(x, width, caret = false) {
|
|
@@ -1707,7 +1717,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1707
1717
|
__name(paint, "paint");
|
|
1708
1718
|
function nearest(clientX, clientY) {
|
|
1709
1719
|
if (!logical.length) return;
|
|
1710
|
-
let result,
|
|
1720
|
+
let result, bestY = Infinity, bestX = Infinity;
|
|
1711
1721
|
let first = 0, last = shells.length - 1;
|
|
1712
1722
|
while (first < last) {
|
|
1713
1723
|
const middle = first + last >>> 1;
|
|
@@ -1732,9 +1742,9 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1732
1742
|
const dy = Math.max(value.top - y, 0, y - value.top - value.height) * box.height / height;
|
|
1733
1743
|
for (const stop of line.stops) {
|
|
1734
1744
|
const dx = Math.abs(stop.x - x) * box.width / width;
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1745
|
+
if (dy < bestY || dy === bestY && dx < bestX) {
|
|
1746
|
+
bestY = dy;
|
|
1747
|
+
bestX = dx;
|
|
1738
1748
|
result = stop;
|
|
1739
1749
|
}
|
|
1740
1750
|
}
|
|
@@ -1748,6 +1758,8 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1748
1758
|
focus = stop;
|
|
1749
1759
|
if (!extend || !anchor) anchor = stop;
|
|
1750
1760
|
paint();
|
|
1761
|
+
onSelectionChange?.(currentSelection());
|
|
1762
|
+
if (input && document2.activeElement === viewport) input.focus({ preventScroll: true });
|
|
1751
1763
|
if (scroll) {
|
|
1752
1764
|
const shell = shells[stop.line.item.pageIndex];
|
|
1753
1765
|
const point = stop.line.transform.transformPoint({ x: stop.x, y: stop.line.item.selection.top });
|
|
@@ -1788,13 +1800,23 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1788
1800
|
pointerPosition = { x: event.clientX, y: event.clientY };
|
|
1789
1801
|
select(nearest(event.clientX, event.clientY), true);
|
|
1790
1802
|
}, { signal });
|
|
1803
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
1804
|
+
if (event.target.closest("button,input,textarea,select,a,[data-onodocs-attachment]")) return;
|
|
1805
|
+
const stop = nearest(event.clientX, event.clientY);
|
|
1806
|
+
if (!stop) return;
|
|
1807
|
+
const segment = [...new Intl.Segmenter(void 0, { granularity: "word" }).segment(text)].find((value) => stop.offset >= value.index && stop.offset < value.index + value.segment.length);
|
|
1808
|
+
if (!segment) return;
|
|
1809
|
+
event.preventDefault();
|
|
1810
|
+
anchor = logical.find((value) => value.offset >= segment.index);
|
|
1811
|
+
select(logical.find((value) => value.offset >= segment.index + segment.segment.length), true);
|
|
1812
|
+
}, { signal });
|
|
1791
1813
|
for (const type of ["pointerup", "pointercancel", "lostpointercapture"]) viewport.addEventListener(type, (event) => {
|
|
1792
1814
|
if (event.pointerId !== pointer) return;
|
|
1793
1815
|
pointer = void 0;
|
|
1794
1816
|
window.cancelAnimationFrame(frame);
|
|
1795
1817
|
}, { signal });
|
|
1796
1818
|
viewport.addEventListener("keydown", (event) => {
|
|
1797
|
-
if (event.target !== viewport || !logical.length) return;
|
|
1819
|
+
if (event.target !== viewport && event.target !== input || !logical.length || event.isComposing) return;
|
|
1798
1820
|
const command = event.ctrlKey || event.metaKey;
|
|
1799
1821
|
if (command && event.key.toLowerCase() === "a") {
|
|
1800
1822
|
event.preventDefault();
|
|
@@ -1813,7 +1835,11 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1813
1835
|
else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
1814
1836
|
const delta = event.key === "ArrowLeft" ? -1 : 1;
|
|
1815
1837
|
if (!event.shiftKey && anchor.offset !== focus.offset) next = delta < 0 ? anchor.offset < focus.offset ? anchor : focus : anchor.offset > focus.offset ? anchor : focus;
|
|
1816
|
-
else {
|
|
1838
|
+
else if (command) {
|
|
1839
|
+
const words = [...new Intl.Segmenter(void 0, { granularity: "word" }).segment(text)].filter((value) => value.isWordLike);
|
|
1840
|
+
const offset = delta < 0 ? words.filter((value) => value.index < focus.offset).at(-1)?.index ?? 0 : words.find((value) => value.index > focus.offset)?.index ?? text.length;
|
|
1841
|
+
next = logical.find((value) => value.offset >= offset) ?? logical.at(-1);
|
|
1842
|
+
} else {
|
|
1817
1843
|
let index = logicalIndex.get(focus) + delta;
|
|
1818
1844
|
while (logical[index]?.offset === focus.offset) index += delta;
|
|
1819
1845
|
next = logical[Math.max(0, Math.min(logical.length - 1, index))];
|
|
@@ -1830,7 +1856,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1830
1856
|
select(next, event.shiftKey, true);
|
|
1831
1857
|
}, { signal });
|
|
1832
1858
|
document2.addEventListener("copy", (event) => {
|
|
1833
|
-
if (document2.activeElement !== viewport && !menu?.contains(document2.activeElement) || !focus || anchor.offset === focus.offset) return;
|
|
1859
|
+
if (document2.activeElement !== viewport && document2.activeElement !== input && !menu?.contains(document2.activeElement) || !focus || anchor.offset === focus.offset) return;
|
|
1834
1860
|
event.clipboardData?.setData("text/plain", selectedText());
|
|
1835
1861
|
event.preventDefault();
|
|
1836
1862
|
}, { signal });
|
|
@@ -1918,7 +1944,25 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1918
1944
|
window.cancelAnimationFrame(frame);
|
|
1919
1945
|
for (const layer of layers.values()) layer.remove();
|
|
1920
1946
|
}, { once: true });
|
|
1921
|
-
|
|
1947
|
+
function currentSelection() {
|
|
1948
|
+
if (!anchor?.position || !focus?.position) return;
|
|
1949
|
+
return anchor.offset <= focus.offset ? { start: anchor.position, end: focus.position } : { start: focus.position, end: anchor.position };
|
|
1950
|
+
}
|
|
1951
|
+
__name(currentSelection, "currentSelection");
|
|
1952
|
+
const byParagraph = /* @__PURE__ */ new Map();
|
|
1953
|
+
for (const stop of logical) if (stop.position) {
|
|
1954
|
+
let offsets = byParagraph.get(stop.position.paragraphId);
|
|
1955
|
+
if (!offsets) byParagraph.set(stop.position.paragraphId, offsets = /* @__PURE__ */ new Map());
|
|
1956
|
+
offsets.set(stop.position.offset, stop);
|
|
1957
|
+
}
|
|
1958
|
+
return { paint, get selection() {
|
|
1959
|
+
return currentSelection();
|
|
1960
|
+
}, setSelection(value) {
|
|
1961
|
+
const start = byParagraph.get(value.start.paragraphId)?.get(value.start.offset), end = byParagraph.get(value.end.paragraphId)?.get(value.end.offset);
|
|
1962
|
+
if (!start || !end) return;
|
|
1963
|
+
anchor = start;
|
|
1964
|
+
select(end, true, true);
|
|
1965
|
+
} };
|
|
1922
1966
|
}
|
|
1923
1967
|
__name(mountTextSelection, "mountTextSelection");
|
|
1924
1968
|
|
|
@@ -1965,10 +2009,10 @@ function mountDocument(document2, container, options = {}, onDispose) {
|
|
|
1965
2009
|
let selection;
|
|
1966
2010
|
let selectionLifetime;
|
|
1967
2011
|
function prepareSelection(event) {
|
|
1968
|
-
if (event
|
|
2012
|
+
if (event?.target?.closest("[data-onodocs-attachment]")) return;
|
|
1969
2013
|
if (selection) return;
|
|
1970
2014
|
selectionLifetime = new AbortController();
|
|
1971
|
-
selection = mountTextSelection(document2.pages.flatMap((page) => page.interaction.text), element, shells.map((entry) => entry.shell), document2.pages.map((page) => page.size), nearby, AbortSignal.any([lifetime.signal, selectionLifetime.signal]));
|
|
2015
|
+
selection = mountTextSelection(document2.pages.flatMap((page) => page.interaction.text), element, shells.map((entry) => entry.shell), document2.pages.map((page) => page.size), nearby, AbortSignal.any([lifetime.signal, selectionLifetime.signal]), options.input, options.onSelectionChange);
|
|
1972
2016
|
}
|
|
1973
2017
|
__name(prepareSelection, "prepareSelection");
|
|
1974
2018
|
for (const type of ["pointerdown", "keydown", "contextmenu", "focus"]) element.addEventListener(type, prepareSelection, { capture: true, signal: lifetime.signal });
|
|
@@ -2041,6 +2085,14 @@ function mountDocument(document2, container, options = {}, onDispose) {
|
|
|
2041
2085
|
}
|
|
2042
2086
|
const view = {
|
|
2043
2087
|
element,
|
|
2088
|
+
get selection() {
|
|
2089
|
+
return selection?.selection;
|
|
2090
|
+
},
|
|
2091
|
+
setSelection(value) {
|
|
2092
|
+
checkOpen();
|
|
2093
|
+
prepareSelection();
|
|
2094
|
+
selection.setSelection(value);
|
|
2095
|
+
},
|
|
2044
2096
|
get disposed() {
|
|
2045
2097
|
return disposed;
|
|
2046
2098
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onodocs/canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OnoDocs canvas viewer, selection and HTML attachments for prepared Word document pages.",
|
|
6
6
|
"homepage": "https://onodocs.com/developers/",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
|
-
"onodocsReleaseDate": "2026-10-
|
|
18
|
+
"onodocsReleaseDate": "2026-10-08",
|
|
19
19
|
"onodocsEnvironment": "live",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"sideEffects": false,
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"type": "git",
|
|
41
41
|
"url": "https://github.com/onodocs/onodocs.git"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "cf082bcfda5fc7581332b4251fa27c39a9a66ec7"
|
|
44
44
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** Spec: pipeline.semantic.document-update */
|
|
2
2
|
/** Spec: pipeline.sdk.presentation */
|
|
3
|
-
import type { DocumentFragment, DocumentHit, DocumentPoint } from "../render-api/index.js";
|
|
3
|
+
import type { DocumentFragment, DocumentHit, DocumentPoint, DocumentSelection } from "../render-api/index.js";
|
|
4
4
|
import type { DocumentPage, PageSize } from "./index.js";
|
|
5
5
|
export interface ViewOptions {
|
|
6
6
|
readonly zoom?: number | "fit-width";
|
|
7
7
|
readonly gap?: number;
|
|
8
8
|
readonly title?: string;
|
|
9
|
+
readonly input?: HTMLTextAreaElement;
|
|
10
|
+
readonly onSelectionChange?: (selection: DocumentSelection | undefined) => void;
|
|
9
11
|
}
|
|
10
12
|
export interface DocumentAttachment {
|
|
11
13
|
readonly element: HTMLElement;
|
|
@@ -42,6 +44,8 @@ export interface DocumentView {
|
|
|
42
44
|
point: DocumentPoint;
|
|
43
45
|
}> | undefined;
|
|
44
46
|
hitTest(point: DocumentPoint): DocumentHit | undefined;
|
|
47
|
+
readonly selection: DocumentSelection | undefined;
|
|
48
|
+
setSelection(selection: DocumentSelection): void;
|
|
45
49
|
scrollTo(anchor: ViewAnchor, options?: ScrollIntoViewOptions): boolean;
|
|
46
50
|
attach(element: HTMLElement, options: AttachmentOptions): DocumentAttachment;
|
|
47
51
|
dispose(): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { PageTextLine } from "../render-api/index.js";
|
|
1
|
+
import type { DocumentSelection, PageTextLine } from "../render-api/index.js";
|
|
2
2
|
import type { DocumentPage } from "./index.js";
|
|
3
3
|
/** Spec: pipeline.sdk.presentation */
|
|
4
|
-
export declare function mountTextSelection(pageText: readonly PageTextLine[], viewport: HTMLElement, shells: readonly HTMLElement[], pageBounds: readonly DocumentPage["size"][], nearby: ReadonlySet<number>, signal: AbortSignal): {
|
|
4
|
+
export declare function mountTextSelection(pageText: readonly PageTextLine[], viewport: HTMLElement, shells: readonly HTMLElement[], pageBounds: readonly DocumentPage["size"][], nearby: ReadonlySet<number>, signal: AbortSignal, input?: HTMLTextAreaElement, onSelectionChange?: (selection: DocumentSelection | undefined) => void): {
|
|
5
5
|
paint: () => void;
|
|
6
|
+
readonly selection: DocumentSelection | undefined;
|
|
7
|
+
setSelection(value: DocumentSelection): void;
|
|
6
8
|
};
|
|
7
9
|
//# sourceMappingURL=text-selection.d.ts.map
|
|
@@ -3,6 +3,14 @@ export interface DocumentPoint {
|
|
|
3
3
|
readonly x: number;
|
|
4
4
|
readonly y: number;
|
|
5
5
|
}
|
|
6
|
+
export interface ParagraphPosition {
|
|
7
|
+
readonly paragraphId: string;
|
|
8
|
+
readonly offset: number;
|
|
9
|
+
}
|
|
10
|
+
export interface DocumentSelection {
|
|
11
|
+
readonly start: ParagraphPosition;
|
|
12
|
+
readonly end: ParagraphPosition;
|
|
13
|
+
}
|
|
6
14
|
export interface PageRectangle extends DocumentPoint {
|
|
7
15
|
readonly width: number;
|
|
8
16
|
readonly height: number;
|
|
@@ -54,6 +62,7 @@ export interface PageTextLine {
|
|
|
54
62
|
carets: readonly Readonly<{
|
|
55
63
|
scalarOffset: number;
|
|
56
64
|
x: number;
|
|
65
|
+
paragraphOffset?: number;
|
|
57
66
|
}>[];
|
|
58
67
|
}>;
|
|
59
68
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from "./page-display-list.js";
|
|
2
2
|
export * from "./page-source.js";
|
|
3
|
-
export type { DocumentFragment, DocumentHit, DocumentPoint, PageTextLine, PageInteraction, FontFaceResource, ImageResourceData } from "../foundation/index.js";
|
|
3
|
+
export type { DocumentFragment, DocumentHit, DocumentPoint, DocumentSelection, ParagraphPosition, PageTextLine, PageInteraction, FontFaceResource, ImageResourceData } from "../foundation/index.js";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|