@onodocs/canvas 0.2.1 → 0.4.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 +407 -219
- package/SDK.md +407 -219
- package/agreement-review.md +13 -0
- package/ai-report-demo.md +40 -0
- package/application-integration.md +87 -0
- package/document-collaboration.md +50 -0
- package/document-forms.md +84 -0
- package/document-modes.md +44 -0
- package/index.js +264 -39
- package/package.json +11 -3
- package/proposal.md +36 -0
- package/templates.md +71 -0
- package/types/canvas/index.d.ts +3 -0
- package/types/canvas/presentation.d.ts +7 -1
- package/types/canvas/text-selection.d.ts +10 -2
- package/types/foundation/page-interaction.d.ts +26 -0
- package/types/render-api/index.d.ts +1 -1
package/index.js
CHANGED
|
@@ -80,6 +80,15 @@ function geometryPoint(point, transform) {
|
|
|
80
80
|
}
|
|
81
81
|
__name(geometryPoint, "geometryPoint");
|
|
82
82
|
function hitTestPage(interaction, point) {
|
|
83
|
+
for (let index = interaction.areas.length - 1; index >= 0; index--) {
|
|
84
|
+
const area = interaction.areas[index];
|
|
85
|
+
if (area.kind !== "image") continue;
|
|
86
|
+
const local = inversePoint(point, area.fragment.transform);
|
|
87
|
+
if (local && insideRectangle(local, area.fragment.rectangle) && area.fragment.clips.every((clip) => {
|
|
88
|
+
const p = inversePoint(point, clip.transform);
|
|
89
|
+
return p !== void 0 && insideRectangle(p, clip.rectangle);
|
|
90
|
+
})) return area;
|
|
91
|
+
}
|
|
83
92
|
for (let index = interaction.lines.length - 1; index >= 0; index--) {
|
|
84
93
|
const line = interaction.lines[index], fragment = line.fragment;
|
|
85
94
|
const local = inversePoint(point, fragment.transform);
|
|
@@ -1578,8 +1587,9 @@ __name(fillExactSectionColumnSeparator, "fillExactSectionColumnSeparator");
|
|
|
1578
1587
|
|
|
1579
1588
|
// packages/canvas/src/text-selection.ts
|
|
1580
1589
|
var nextView = 0;
|
|
1581
|
-
function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, signal) {
|
|
1590
|
+
function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, signal, input, onSelectionChange) {
|
|
1582
1591
|
const document2 = viewport.ownerDocument;
|
|
1592
|
+
const activeElement = /* @__PURE__ */ __name(() => viewport.getRootNode().activeElement, "activeElement");
|
|
1583
1593
|
const window = document2.defaultView;
|
|
1584
1594
|
const svg = "http://www.w3.org/2000/svg";
|
|
1585
1595
|
const viewId = nextView++;
|
|
@@ -1592,6 +1602,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1592
1602
|
const pages = shells.map(() => []);
|
|
1593
1603
|
const stops = [];
|
|
1594
1604
|
const chunks = [];
|
|
1605
|
+
const paragraphRanges = /* @__PURE__ */ new Map();
|
|
1595
1606
|
let length = 0;
|
|
1596
1607
|
let previous;
|
|
1597
1608
|
for (const item of lines) {
|
|
@@ -1607,7 +1618,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1607
1618
|
for (const caret of value.carets) {
|
|
1608
1619
|
const offset = offsets[caret.scalarOffset - value.scalarStart];
|
|
1609
1620
|
if (offset === void 0) continue;
|
|
1610
|
-
const stop = { line, offset: length + offset, x: caret.x, index: stops.length };
|
|
1621
|
+
const stop = { line, offset: length + offset, x: caret.x, index: stops.length, ...caret.paragraphOffset === void 0 ? {} : { position: { paragraphId: value.paragraphId, offset: caret.paragraphOffset } } };
|
|
1611
1622
|
line.stops.push(stop);
|
|
1612
1623
|
stops.push(stop);
|
|
1613
1624
|
}
|
|
@@ -1615,11 +1626,32 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1615
1626
|
pages[item.pageIndex].push(line);
|
|
1616
1627
|
chunks.push(value.scalars.join(""));
|
|
1617
1628
|
length = line.end;
|
|
1629
|
+
const range = paragraphRanges.get(value.paragraphId);
|
|
1630
|
+
if (range) range.end = length;
|
|
1631
|
+
else paragraphRanges.set(value.paragraphId, { start: line.start, end: length });
|
|
1618
1632
|
previous = value;
|
|
1619
1633
|
}
|
|
1620
1634
|
const text = chunks.join("");
|
|
1621
1635
|
const logical = [...stops].sort((a, b) => a.offset - b.offset || a.index - b.index);
|
|
1622
1636
|
const logicalIndex = new Map(logical.map((stop, index) => [stop, index]));
|
|
1637
|
+
const editableLogical = logical.filter((stop) => stop.position);
|
|
1638
|
+
const editableStories = /* @__PURE__ */ new Map(), editableIndex = /* @__PURE__ */ new Map();
|
|
1639
|
+
for (const stop of editableLogical) {
|
|
1640
|
+
const id = stop.line.item.selection.storyId ?? "", story = editableStories.get(id) ?? [];
|
|
1641
|
+
editableIndex.set(stop, story.length);
|
|
1642
|
+
story.push(stop);
|
|
1643
|
+
editableStories.set(id, story);
|
|
1644
|
+
}
|
|
1645
|
+
function atOffset(offset, ordered = logical) {
|
|
1646
|
+
let first = 0, last = ordered.length;
|
|
1647
|
+
while (first < last) {
|
|
1648
|
+
const middle = first + last >>> 1;
|
|
1649
|
+
if (ordered[middle].offset < offset) first = middle + 1;
|
|
1650
|
+
else last = middle;
|
|
1651
|
+
}
|
|
1652
|
+
return ordered[first] ?? ordered.at(-1);
|
|
1653
|
+
}
|
|
1654
|
+
__name(atOffset, "atOffset");
|
|
1623
1655
|
const layers = /* @__PURE__ */ new Map();
|
|
1624
1656
|
function layerFor(index) {
|
|
1625
1657
|
const existing = layers.get(index);
|
|
@@ -1633,29 +1665,80 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1633
1665
|
return layer;
|
|
1634
1666
|
}
|
|
1635
1667
|
__name(layerFor, "layerFor");
|
|
1668
|
+
let overlays = [];
|
|
1636
1669
|
let anchor, focus, pointer, preferredX;
|
|
1637
1670
|
let frame = 0;
|
|
1638
1671
|
let pointerPosition = { x: 0, y: 0 };
|
|
1639
1672
|
let menu;
|
|
1673
|
+
let touch, touchTimer = 0, touchSelection = false;
|
|
1674
|
+
let draggedEndpoint;
|
|
1675
|
+
const handles = [document2.createElement("span"), document2.createElement("span")];
|
|
1676
|
+
for (const [index, handle] of handles.entries()) {
|
|
1677
|
+
handle.setAttribute("aria-hidden", "true");
|
|
1678
|
+
handle.dataset.selectionHandle = String(index);
|
|
1679
|
+
Object.assign(handle.style, { position: "absolute", width: "36px", height: "36px", padding: "10px", boxSizing: "border-box", zIndex: "3", touchAction: "none", display: "none" });
|
|
1680
|
+
const dot = document2.createElement("span");
|
|
1681
|
+
Object.assign(dot.style, { display: "block", width: "16px", height: "16px", borderRadius: "50%", background: "#1769c2" });
|
|
1682
|
+
handle.append(dot);
|
|
1683
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
1684
|
+
event.preventDefault();
|
|
1685
|
+
event.stopPropagation();
|
|
1686
|
+
const first = anchor && focus && anchor.offset <= focus.offset ? "anchor" : "focus";
|
|
1687
|
+
draggedEndpoint = index === 0 ? first : first === "anchor" ? "focus" : "anchor";
|
|
1688
|
+
pointer = event.pointerId;
|
|
1689
|
+
pointerPosition = { x: event.clientX, y: event.clientY - 18 };
|
|
1690
|
+
viewport.setPointerCapture(pointer);
|
|
1691
|
+
frame = window.requestAnimationFrame(drag);
|
|
1692
|
+
}, { signal });
|
|
1693
|
+
viewport.append(handle);
|
|
1694
|
+
}
|
|
1640
1695
|
viewport.tabIndex = 0;
|
|
1641
1696
|
viewport.style.userSelect = "none";
|
|
1697
|
+
viewport.style.touchAction = "pan-x pan-y pinch-zoom";
|
|
1698
|
+
viewport.style.position = "relative";
|
|
1699
|
+
if (input) {
|
|
1700
|
+
viewport.style.position = "relative";
|
|
1701
|
+
viewport.append(input);
|
|
1702
|
+
}
|
|
1642
1703
|
for (const shell of shells) shell.style.cursor = "text";
|
|
1643
1704
|
function paint() {
|
|
1705
|
+
for (const handle of handles) handle.style.display = "none";
|
|
1644
1706
|
for (const [index, layer] of layers) {
|
|
1645
1707
|
layer.remove();
|
|
1646
1708
|
layer.replaceChildren();
|
|
1647
1709
|
if (!nearby.has(index)) layers.delete(index);
|
|
1648
1710
|
}
|
|
1649
|
-
|
|
1650
|
-
const
|
|
1711
|
+
const visible = anchor && focus && (input || activeElement() === viewport || menu?.contains(activeElement()));
|
|
1712
|
+
const labels = /* @__PURE__ */ new Map();
|
|
1713
|
+
for (let index = 0; index < overlays.length; index++) {
|
|
1714
|
+
const overlay = overlays[index], start = byParagraph.get(overlay.selection.start.paragraphId)?.get(overlay.selection.start.offset), end = byParagraph.get(overlay.selection.end.paragraphId)?.get(overlay.selection.end.offset);
|
|
1715
|
+
if (start && end) {
|
|
1716
|
+
const key = `${end.line.item.pageIndex}:${end.line.index}`, row = labels.get(key) ?? 0;
|
|
1717
|
+
labels.set(key, row + 1);
|
|
1718
|
+
paintRange(start, end, overlay, index, row);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (!visible) return;
|
|
1722
|
+
if (input) {
|
|
1723
|
+
const shell = shells[focus.line.item.pageIndex], bounds = shell.getBoundingClientRect(), origin = viewport.getBoundingClientRect();
|
|
1724
|
+
const point = focus.line.transform.transformPoint({ x: focus.x, y: focus.line.item.selection.top });
|
|
1725
|
+
const scale = bounds.width / pageBounds[focus.line.item.pageIndex].width;
|
|
1726
|
+
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` });
|
|
1727
|
+
}
|
|
1728
|
+
paintRange(anchor, focus);
|
|
1729
|
+
}
|
|
1730
|
+
__name(paint, "paint");
|
|
1731
|
+
function paintRange(anchor2, focus2, overlay, overlayIndex = -1, labelRow = 0) {
|
|
1732
|
+
const start = Math.min(anchor2.offset, focus2.offset), end = Math.max(anchor2.offset, focus2.offset);
|
|
1651
1733
|
for (const index of nearby) for (const line of pages[index] ?? []) {
|
|
1652
1734
|
let rectangle2 = function(x, width, caret = false) {
|
|
1653
1735
|
const rect = document2.createElementNS(svg, "rect");
|
|
1736
|
+
if (!overlay) rect.setAttribute("data-local-selection", "");
|
|
1654
1737
|
rect.setAttribute("x", String(x));
|
|
1655
1738
|
rect.setAttribute("y", String(value.top));
|
|
1656
1739
|
rect.setAttribute("width", String(width));
|
|
1657
1740
|
rect.setAttribute("height", String(value.height));
|
|
1658
|
-
rect.setAttribute("fill", caret ? "#1769c2" : "#4285f4");
|
|
1741
|
+
rect.setAttribute("fill", overlay?.color ?? (caret ? "#1769c2" : "#4285f4"));
|
|
1659
1742
|
rect.setAttribute("fill-opacity", caret ? "1" : "0.28");
|
|
1660
1743
|
group.append(rect);
|
|
1661
1744
|
};
|
|
@@ -1667,7 +1750,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1667
1750
|
const group = document2.createElementNS(svg, "g");
|
|
1668
1751
|
const transform = line.transform;
|
|
1669
1752
|
if (transform) group.setAttribute("transform", `matrix(${transform.a} ${transform.b} ${transform.c} ${transform.d} ${transform.e} ${transform.f})`);
|
|
1670
|
-
if (start === end &&
|
|
1753
|
+
if (start === end && focus2.line === line) rectangle2(focus2.x, Number(pageBounds[line.item.pageIndex].width) / shells[line.item.pageIndex].getBoundingClientRect().width * 1.5, true);
|
|
1671
1754
|
else {
|
|
1672
1755
|
let left, right = 0;
|
|
1673
1756
|
for (let index2 = 1; index2 < line.stops.length; index2++) {
|
|
@@ -1682,12 +1765,19 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1682
1765
|
}
|
|
1683
1766
|
if (left !== void 0) rectangle2(left, right - left);
|
|
1684
1767
|
}
|
|
1768
|
+
if (overlay && focus2.line === line) {
|
|
1769
|
+
if (start !== end) rectangle2(focus2.x, Number(pageBounds[line.item.pageIndex].width) / shells[line.item.pageIndex].getBoundingClientRect().width * 1.5, true);
|
|
1770
|
+
const label = document2.createElementNS(svg, "text"), unit = Number(pageBounds[line.item.pageIndex].width) / shells[line.item.pageIndex].getBoundingClientRect().width;
|
|
1771
|
+
label.textContent = overlay.label;
|
|
1772
|
+
for (const [key, value2] of Object.entries({ x: String((line.stops.at(-1)?.x ?? focus2.x) + 8 * unit), y: String(line.item.selection.top + line.item.selection.height * 0.8 + labelRow * 14 * unit), fill: overlay.color, stroke: "white", "stroke-width": String(3 * unit), "paint-order": "stroke", "font-family": "Arial,sans-serif", "font-size": String(11 * unit), "font-weight": "600" })) label.setAttribute(key, value2);
|
|
1773
|
+
group.append(label);
|
|
1774
|
+
}
|
|
1685
1775
|
let clipped = group;
|
|
1686
1776
|
const clips = [...line.item.clip ? [{ rectangle: line.item.clip, transform: line.item.transform }] : [], ...line.item.physicalClips ?? []];
|
|
1687
1777
|
for (let index2 = 0; index2 < clips.length; index2++) {
|
|
1688
1778
|
const clip = clips[index2], rectangle3 = clip.rectangle, transform2 = matrix(clip.transform);
|
|
1689
1779
|
const path = document2.createElementNS(svg, "clipPath");
|
|
1690
|
-
path.id = `selection-${viewId}-${line.item.pageIndex}-${line.index}-${index2}`;
|
|
1780
|
+
path.id = `selection-${viewId}-${line.item.pageIndex}-${line.index}-${index2}-${overlayIndex}`;
|
|
1691
1781
|
const polygon = document2.createElementNS(svg, "polygon");
|
|
1692
1782
|
polygon.setAttribute("points", [[rectangle3.x, rectangle3.y], [rectangle3.x + rectangle3.width, rectangle3.y], [rectangle3.x + rectangle3.width, rectangle3.y + rectangle3.height], [rectangle3.x, rectangle3.y + rectangle3.height]].map(([x, y]) => {
|
|
1693
1783
|
const point = transform2.transformPoint({ x, y });
|
|
@@ -1703,11 +1793,19 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1703
1793
|
layer.append(clipped);
|
|
1704
1794
|
shells[line.item.pageIndex].append(layer);
|
|
1705
1795
|
}
|
|
1796
|
+
if (!overlay && touchSelection && anchor2.offset !== focus2.offset) {
|
|
1797
|
+
const endpoints = anchor2.offset <= focus2.offset ? [anchor2, focus2] : [focus2, anchor2], origin = viewport.getBoundingClientRect();
|
|
1798
|
+
for (const [index, stop] of endpoints.entries()) {
|
|
1799
|
+
const bounds = shells[stop.line.item.pageIndex].getBoundingClientRect(), size = pageBounds[stop.line.item.pageIndex];
|
|
1800
|
+
const point = stop.line.transform.transformPoint({ x: stop.x, y: stop.line.item.selection.top + stop.line.item.selection.height });
|
|
1801
|
+
Object.assign(handles[index].style, { display: "block", left: `${bounds.left - origin.left + point.x * bounds.width / size.width - 18}px`, top: `${bounds.top - origin.top + point.y * bounds.height / size.height - 10}px` });
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1706
1804
|
}
|
|
1707
|
-
__name(
|
|
1805
|
+
__name(paintRange, "paintRange");
|
|
1708
1806
|
function nearest(clientX, clientY) {
|
|
1709
1807
|
if (!logical.length) return;
|
|
1710
|
-
let result,
|
|
1808
|
+
let result, bestY = Infinity, bestX = Infinity;
|
|
1711
1809
|
let first = 0, last = shells.length - 1;
|
|
1712
1810
|
while (first < last) {
|
|
1713
1811
|
const middle = first + last >>> 1;
|
|
@@ -1732,9 +1830,9 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1732
1830
|
const dy = Math.max(value.top - y, 0, y - value.top - value.height) * box.height / height;
|
|
1733
1831
|
for (const stop of line.stops) {
|
|
1734
1832
|
const dx = Math.abs(stop.x - x) * box.width / width;
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1833
|
+
if (dy < bestY || dy === bestY && dx < bestX) {
|
|
1834
|
+
bestY = dy;
|
|
1835
|
+
bestX = dx;
|
|
1738
1836
|
result = stop;
|
|
1739
1837
|
}
|
|
1740
1838
|
}
|
|
@@ -1744,10 +1842,12 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1744
1842
|
}
|
|
1745
1843
|
__name(nearest, "nearest");
|
|
1746
1844
|
function select(stop, extend, scroll = false) {
|
|
1747
|
-
if (!stop) return;
|
|
1845
|
+
if (!stop || input && !input.readOnly && !stop.position) return;
|
|
1748
1846
|
focus = stop;
|
|
1749
1847
|
if (!extend || !anchor) anchor = stop;
|
|
1750
1848
|
paint();
|
|
1849
|
+
onSelectionChange?.(currentSelection());
|
|
1850
|
+
if (input && activeElement() === viewport) input.focus({ preventScroll: true });
|
|
1751
1851
|
if (scroll) {
|
|
1752
1852
|
const shell = shells[stop.line.item.pageIndex];
|
|
1753
1853
|
const point = stop.line.transform.transformPoint({ x: stop.x, y: stop.line.item.selection.top });
|
|
@@ -1766,12 +1866,49 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1766
1866
|
if (pointer === void 0) return;
|
|
1767
1867
|
const bounds = scroller?.getBoundingClientRect() ?? { top: 0, bottom: window.innerHeight };
|
|
1768
1868
|
scrollElement.scrollTop += Math.min(30, Math.max(-30, pointerPosition.y < bounds.top + 20 ? pointerPosition.y - bounds.top - 20 : pointerPosition.y > bounds.bottom - 20 ? pointerPosition.y - bounds.bottom + 20 : 0));
|
|
1769
|
-
|
|
1869
|
+
dragTo(pointerPosition.x, pointerPosition.y);
|
|
1770
1870
|
frame = window.requestAnimationFrame(drag);
|
|
1771
1871
|
}
|
|
1772
1872
|
__name(drag, "drag");
|
|
1873
|
+
function dragTo(x, y) {
|
|
1874
|
+
const stop = nearest(x, y);
|
|
1875
|
+
if (draggedEndpoint === "anchor" && stop) {
|
|
1876
|
+
anchor = stop;
|
|
1877
|
+
paint();
|
|
1878
|
+
onSelectionChange?.(currentSelection());
|
|
1879
|
+
} else select(stop, true);
|
|
1880
|
+
}
|
|
1881
|
+
__name(dragTo, "dragTo");
|
|
1882
|
+
function selectWord(x, y) {
|
|
1883
|
+
const stop = nearest(x, y);
|
|
1884
|
+
if (!stop) return;
|
|
1885
|
+
const range = paragraphRanges.get(stop.line.item.selection.paragraphId);
|
|
1886
|
+
const segment = [...new Intl.Segmenter(void 0, { granularity: "word" }).segment(text.slice(range.start, range.end))].find((value) => stop.offset >= range.start + value.index && stop.offset < range.start + value.index + value.segment.length);
|
|
1887
|
+
if (!segment) return;
|
|
1888
|
+
anchor = atOffset(range.start + segment.index);
|
|
1889
|
+
select(atOffset(range.start + segment.index + segment.segment.length), true);
|
|
1890
|
+
}
|
|
1891
|
+
__name(selectWord, "selectWord");
|
|
1773
1892
|
viewport.addEventListener("pointerdown", (event) => {
|
|
1774
|
-
if (event.button !== 0 || event.target.closest("button,input,textarea,select,a,[contenteditable],
|
|
1893
|
+
if (event.button !== 0 || event.target.closest("button,input,textarea,select,a,[contenteditable],[data-selection-handle],[data-onodocs-attachment]")) return;
|
|
1894
|
+
if (event.pointerType === "touch") {
|
|
1895
|
+
window.clearTimeout(touchTimer);
|
|
1896
|
+
if (!event.isPrimary) {
|
|
1897
|
+
touch = void 0;
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
touch = { id: event.pointerId, x: event.clientX, y: event.clientY, held: false };
|
|
1901
|
+
touchTimer = window.setTimeout(() => {
|
|
1902
|
+
if (touch) {
|
|
1903
|
+
touch.held = true;
|
|
1904
|
+
touchSelection = true;
|
|
1905
|
+
viewport.focus({ preventScroll: true });
|
|
1906
|
+
selectWord(touch.x, touch.y);
|
|
1907
|
+
}
|
|
1908
|
+
}, 500);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
touchSelection = false;
|
|
1775
1912
|
const stop = nearest(event.clientX, event.clientY);
|
|
1776
1913
|
if (!stop) return;
|
|
1777
1914
|
event.preventDefault();
|
|
@@ -1784,53 +1921,83 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1784
1921
|
frame = window.requestAnimationFrame(drag);
|
|
1785
1922
|
}, { signal });
|
|
1786
1923
|
viewport.addEventListener("pointermove", (event) => {
|
|
1924
|
+
if (touch?.id === event.pointerId && Math.hypot(event.clientX - touch.x, event.clientY - touch.y) > 8) {
|
|
1925
|
+
window.clearTimeout(touchTimer);
|
|
1926
|
+
touch = void 0;
|
|
1927
|
+
}
|
|
1787
1928
|
if (event.pointerId !== pointer) return;
|
|
1788
|
-
pointerPosition = { x: event.clientX, y: event.clientY };
|
|
1789
|
-
|
|
1929
|
+
pointerPosition = { x: event.clientX, y: event.clientY - (draggedEndpoint ? 18 : 0) };
|
|
1930
|
+
dragTo(pointerPosition.x, pointerPosition.y);
|
|
1931
|
+
}, { signal });
|
|
1932
|
+
viewport.addEventListener("dblclick", (event) => {
|
|
1933
|
+
if (event.target.closest("button,input,textarea,select,a,[data-onodocs-attachment]")) return;
|
|
1934
|
+
event.preventDefault();
|
|
1935
|
+
selectWord(event.clientX, event.clientY);
|
|
1790
1936
|
}, { signal });
|
|
1791
1937
|
for (const type of ["pointerup", "pointercancel", "lostpointercapture"]) viewport.addEventListener(type, (event) => {
|
|
1938
|
+
if (event.pointerId === touch?.id) {
|
|
1939
|
+
window.clearTimeout(touchTimer);
|
|
1940
|
+
if (type === "pointerup" && !touch.held) {
|
|
1941
|
+
event.preventDefault();
|
|
1942
|
+
touchSelection = true;
|
|
1943
|
+
viewport.focus({ preventScroll: true });
|
|
1944
|
+
select(nearest(event.clientX, event.clientY), false);
|
|
1945
|
+
}
|
|
1946
|
+
touch = void 0;
|
|
1947
|
+
}
|
|
1792
1948
|
if (event.pointerId !== pointer) return;
|
|
1793
1949
|
pointer = void 0;
|
|
1950
|
+
draggedEndpoint = void 0;
|
|
1794
1951
|
window.cancelAnimationFrame(frame);
|
|
1795
1952
|
}, { signal });
|
|
1796
1953
|
viewport.addEventListener("keydown", (event) => {
|
|
1797
|
-
|
|
1954
|
+
const ordered = input && !input.readOnly ? editableStories.get(focus?.line.item.selection.storyId ?? "") ?? editableLogical : logical, orderedIndex = input && !input.readOnly ? editableIndex : logicalIndex;
|
|
1955
|
+
if (event.target !== viewport && event.target !== input || !ordered.length || event.isComposing) return;
|
|
1798
1956
|
const command = event.ctrlKey || event.metaKey;
|
|
1799
1957
|
if (command && event.key.toLowerCase() === "a") {
|
|
1800
1958
|
event.preventDefault();
|
|
1801
|
-
anchor =
|
|
1802
|
-
select(
|
|
1959
|
+
anchor = ordered[0];
|
|
1960
|
+
select(ordered.at(-1), true);
|
|
1803
1961
|
return;
|
|
1804
1962
|
}
|
|
1805
|
-
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key) || event.altKey) return;
|
|
1963
|
+
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(event.key) || event.altKey) return;
|
|
1806
1964
|
event.preventDefault();
|
|
1807
1965
|
if (!focus) {
|
|
1808
|
-
select(
|
|
1966
|
+
select(ordered[0], false);
|
|
1809
1967
|
return;
|
|
1810
1968
|
}
|
|
1811
1969
|
let next;
|
|
1812
|
-
if (event.key === "
|
|
1970
|
+
if (event.key === "PageUp" || event.key === "PageDown") {
|
|
1971
|
+
const bounds = shells[focus.line.item.pageIndex].getBoundingClientRect(), size = pageBounds[focus.line.item.pageIndex];
|
|
1972
|
+
const point = focus.line.transform.transformPoint({ x: focus.x, y: focus.line.item.selection.top });
|
|
1973
|
+
next = nearest(bounds.left + point.x * bounds.width / size.width, bounds.top + point.y * bounds.height / size.height + (event.key === "PageUp" ? -1 : 1) * (scroller?.clientHeight ?? window.innerHeight) * 0.8);
|
|
1974
|
+
} else if (event.key === "Home" || event.key === "End") next = command ? event.key === "Home" ? ordered[0] : ordered.at(-1) : event.key === "Home" ? focus.line.stops.find((stop) => !input || input.readOnly || stop.position) : focus.line.stops.filter((stop) => !input || input.readOnly || stop.position).at(-1);
|
|
1813
1975
|
else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
1814
1976
|
const delta = event.key === "ArrowLeft" ? -1 : 1;
|
|
1815
1977
|
if (!event.shiftKey && anchor.offset !== focus.offset) next = delta < 0 ? anchor.offset < focus.offset ? anchor : focus : anchor.offset > focus.offset ? anchor : focus;
|
|
1816
|
-
else {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1978
|
+
else if (command) {
|
|
1979
|
+
const range = paragraphRanges.get(focus.line.item.selection.paragraphId);
|
|
1980
|
+
const words = [...new Intl.Segmenter(void 0, { granularity: "word" }).segment(text.slice(range.start, range.end))].filter((value) => value.isWordLike).map((value) => range.start + value.index);
|
|
1981
|
+
const offset = delta < 0 ? words.filter((value) => value < focus.offset).at(-1) ?? Math.max(0, range.start - 1) : words.find((value) => value > focus.offset) ?? Math.min(text.length, range.end + 1);
|
|
1982
|
+
next = atOffset(offset, ordered);
|
|
1983
|
+
} else {
|
|
1984
|
+
let index = orderedIndex.get(focus) + delta;
|
|
1985
|
+
while (ordered[index]?.offset === focus.offset) index += delta;
|
|
1986
|
+
next = ordered[Math.max(0, Math.min(ordered.length - 1, index))];
|
|
1820
1987
|
}
|
|
1821
1988
|
} else {
|
|
1822
1989
|
preferredX ??= focus.x;
|
|
1823
1990
|
const delta = event.key === "ArrowUp" ? -1 : 1;
|
|
1824
|
-
let index =
|
|
1825
|
-
while (
|
|
1826
|
-
const line =
|
|
1991
|
+
let index = orderedIndex.get(focus) + delta;
|
|
1992
|
+
while (ordered[index]?.line === focus.line) index += delta;
|
|
1993
|
+
const line = ordered[index]?.line;
|
|
1827
1994
|
next = line?.stops.reduce((best, stop) => Math.abs(stop.x - preferredX) < Math.abs(best.x - preferredX) ? stop : best);
|
|
1828
1995
|
}
|
|
1829
1996
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") preferredX = void 0;
|
|
1830
1997
|
select(next, event.shiftKey, true);
|
|
1831
1998
|
}, { signal });
|
|
1832
1999
|
document2.addEventListener("copy", (event) => {
|
|
1833
|
-
if (
|
|
2000
|
+
if (activeElement() !== viewport && activeElement() !== input && !menu?.contains(activeElement()) || !focus || anchor.offset === focus.offset) return;
|
|
1834
2001
|
event.clipboardData?.setData("text/plain", selectedText());
|
|
1835
2002
|
event.preventDefault();
|
|
1836
2003
|
}, { signal });
|
|
@@ -1892,7 +2059,7 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1892
2059
|
menu.append(copy);
|
|
1893
2060
|
document2.body.append(menu);
|
|
1894
2061
|
menu.showPopover();
|
|
1895
|
-
const caret = (focus ? layers.get(focus.line.item.pageIndex) : void 0)?.querySelector("
|
|
2062
|
+
const caret = (focus ? layers.get(focus.line.item.pageIndex) : void 0)?.querySelector("[data-local-selection]")?.getBoundingClientRect() ?? viewport.getBoundingClientRect();
|
|
1896
2063
|
const x = event.clientX || caret.left, y = event.clientY || caret.bottom;
|
|
1897
2064
|
const bounds = menu.getBoundingClientRect();
|
|
1898
2065
|
menu.style.left = `${Math.max(0, Math.min(x, window.innerWidth - bounds.width))}px`;
|
|
@@ -1915,10 +2082,33 @@ function mountTextSelection(pageText, viewport, shells, pageBounds, nearby, sign
|
|
|
1915
2082
|
viewport.addEventListener("blur", paint, { signal });
|
|
1916
2083
|
signal.addEventListener("abort", () => {
|
|
1917
2084
|
menu?.remove();
|
|
2085
|
+
window.clearTimeout(touchTimer);
|
|
1918
2086
|
window.cancelAnimationFrame(frame);
|
|
2087
|
+
for (const handle of handles) handle.remove();
|
|
1919
2088
|
for (const layer of layers.values()) layer.remove();
|
|
1920
2089
|
}, { once: true });
|
|
1921
|
-
|
|
2090
|
+
function currentSelection() {
|
|
2091
|
+
if (!anchor?.position || !focus?.position) return;
|
|
2092
|
+
return anchor.offset <= focus.offset ? { start: anchor.position, end: focus.position } : { start: focus.position, end: anchor.position };
|
|
2093
|
+
}
|
|
2094
|
+
__name(currentSelection, "currentSelection");
|
|
2095
|
+
const byParagraph = /* @__PURE__ */ new Map();
|
|
2096
|
+
for (const stop of logical) if (stop.position) {
|
|
2097
|
+
let offsets = byParagraph.get(stop.position.paragraphId);
|
|
2098
|
+
if (!offsets) byParagraph.set(stop.position.paragraphId, offsets = /* @__PURE__ */ new Map());
|
|
2099
|
+
offsets.set(stop.position.offset, stop);
|
|
2100
|
+
}
|
|
2101
|
+
return { paint, setSelectionOverlays(values) {
|
|
2102
|
+
overlays = values;
|
|
2103
|
+
paint();
|
|
2104
|
+
}, get selection() {
|
|
2105
|
+
return currentSelection();
|
|
2106
|
+
}, setSelection(value) {
|
|
2107
|
+
const start = byParagraph.get(value.start.paragraphId)?.get(value.start.offset), end = byParagraph.get(value.end.paragraphId)?.get(value.end.offset);
|
|
2108
|
+
if (!start || !end) return;
|
|
2109
|
+
anchor = start;
|
|
2110
|
+
select(end, true, true);
|
|
2111
|
+
} };
|
|
1922
2112
|
}
|
|
1923
2113
|
__name(mountTextSelection, "mountTextSelection");
|
|
1924
2114
|
|
|
@@ -1965,10 +2155,10 @@ function mountDocument(document2, container, options = {}, onDispose) {
|
|
|
1965
2155
|
let selection;
|
|
1966
2156
|
let selectionLifetime;
|
|
1967
2157
|
function prepareSelection(event) {
|
|
1968
|
-
if (event
|
|
2158
|
+
if (event?.target?.closest("[data-onodocs-attachment]")) return;
|
|
1969
2159
|
if (selection) return;
|
|
1970
2160
|
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]));
|
|
2161
|
+
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
2162
|
}
|
|
1973
2163
|
__name(prepareSelection, "prepareSelection");
|
|
1974
2164
|
for (const type of ["pointerdown", "keydown", "contextmenu", "focus"]) element.addEventListener(type, prepareSelection, { capture: true, signal: lifetime.signal });
|
|
@@ -1986,6 +2176,7 @@ function mountDocument(document2, container, options = {}, onDispose) {
|
|
|
1986
2176
|
Object.assign(prior.layer.style, { width: `${page.size.width * cssScale}px`, height: `${page.size.height * cssScale}px` });
|
|
1987
2177
|
}
|
|
1988
2178
|
prior.page = page;
|
|
2179
|
+
updateReading(prior.reading, page);
|
|
1989
2180
|
prior.paintedWidth = 0;
|
|
1990
2181
|
return prior;
|
|
1991
2182
|
}
|
|
@@ -1993,17 +2184,35 @@ function mountDocument(document2, container, options = {}, onDispose) {
|
|
|
1993
2184
|
shell.setAttribute("aria-label", `Page ${page.index + 1}`);
|
|
1994
2185
|
Object.assign(shell.style, { position: "relative", flex: "none", background: "white" });
|
|
1995
2186
|
const canvas = dom.createElement("canvas");
|
|
2187
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
1996
2188
|
canvas.width = 0;
|
|
1997
2189
|
canvas.height = 0;
|
|
1998
2190
|
Object.assign(canvas.style, { width: "100%", height: "100%", display: "block" });
|
|
1999
2191
|
const layer = dom.createElement("div");
|
|
2000
2192
|
Object.assign(layer.style, { position: "absolute", left: "0", top: "0", transformOrigin: "0 0", width: `${page.size.width * cssScale}px`, height: `${page.size.height * cssScale}px`, pointerEvents: "none" });
|
|
2001
|
-
|
|
2193
|
+
const reading = dom.createElement("div");
|
|
2194
|
+
Object.assign(reading.style, { position: "absolute", width: "1px", height: "1px", overflow: "hidden", clipPath: "inset(50%)", whiteSpace: "pre-wrap" });
|
|
2195
|
+
updateReading(reading, page);
|
|
2196
|
+
shell.append(canvas, layer, reading);
|
|
2002
2197
|
element.append(shell);
|
|
2003
|
-
return { shell, canvas, layer, page, width: 0, paintedWidth: 0, pixelRatio: 0 };
|
|
2198
|
+
return { shell, canvas, layer, reading, page, width: 0, paintedWidth: 0, pixelRatio: 0 };
|
|
2004
2199
|
});
|
|
2005
2200
|
}
|
|
2006
2201
|
__name(createShells, "createShells");
|
|
2202
|
+
function updateReading(element2, page) {
|
|
2203
|
+
const content = page.interaction.readingOrder;
|
|
2204
|
+
if (element2.childNodes.length === content.length && content.every((value, index) => element2.children[index].textContent === value.text && element2.children[index].getAttribute("aria-level") === (value.headingLevel ? String(value.headingLevel) : null))) return;
|
|
2205
|
+
element2.replaceChildren(...content.map((value) => {
|
|
2206
|
+
const paragraph = dom.createElement("p");
|
|
2207
|
+
paragraph.textContent = value.text;
|
|
2208
|
+
if (value.headingLevel) {
|
|
2209
|
+
paragraph.setAttribute("role", "heading");
|
|
2210
|
+
paragraph.setAttribute("aria-level", String(value.headingLevel));
|
|
2211
|
+
}
|
|
2212
|
+
return paragraph;
|
|
2213
|
+
}));
|
|
2214
|
+
}
|
|
2215
|
+
__name(updateReading, "updateReading");
|
|
2007
2216
|
const observer = new window.ResizeObserver(resize);
|
|
2008
2217
|
const visibility = new window.IntersectionObserver((entries) => {
|
|
2009
2218
|
visibilityReady = true;
|
|
@@ -2041,6 +2250,20 @@ function mountDocument(document2, container, options = {}, onDispose) {
|
|
|
2041
2250
|
}
|
|
2042
2251
|
const view = {
|
|
2043
2252
|
element,
|
|
2253
|
+
get selection() {
|
|
2254
|
+
return selection?.selection;
|
|
2255
|
+
},
|
|
2256
|
+
setSelection(value) {
|
|
2257
|
+
checkOpen();
|
|
2258
|
+
prepareSelection();
|
|
2259
|
+
selection.setSelection(value);
|
|
2260
|
+
},
|
|
2261
|
+
setSelectionOverlays(values) {
|
|
2262
|
+
checkOpen();
|
|
2263
|
+
if (!values.length && !selection) return;
|
|
2264
|
+
prepareSelection();
|
|
2265
|
+
selection.setSelectionOverlays(values);
|
|
2266
|
+
},
|
|
2044
2267
|
get disposed() {
|
|
2045
2268
|
return disposed;
|
|
2046
2269
|
},
|
|
@@ -2366,8 +2589,10 @@ function createDocument(source, options = {}) {
|
|
|
2366
2589
|
canvas.height = Math.ceil(page.size.height * ratio);
|
|
2367
2590
|
const context = canvas.getContext("2d");
|
|
2368
2591
|
if (!context) throw new Error("Canvas 2D context is unavailable.");
|
|
2369
|
-
|
|
2370
|
-
|
|
2592
|
+
if (options2.background !== "transparent") {
|
|
2593
|
+
context.fillStyle = "white";
|
|
2594
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
2595
|
+
}
|
|
2371
2596
|
context.scale(ratio, ratio);
|
|
2372
2597
|
materializePageDisplayListToCanvas(data, context, images);
|
|
2373
2598
|
if (data.evaluation) paintEvaluationWatermark(context, canvas.width, canvas.height);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onodocs/canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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-09",
|
|
19
19
|
"onodocsEnvironment": "live",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"sideEffects": false,
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
"types",
|
|
34
34
|
"README.md",
|
|
35
35
|
"SDK.md",
|
|
36
|
+
"templates.md",
|
|
37
|
+
"proposal.md",
|
|
38
|
+
"agreement-review.md",
|
|
39
|
+
"ai-report-demo.md",
|
|
40
|
+
"application-integration.md",
|
|
41
|
+
"document-forms.md",
|
|
42
|
+
"document-modes.md",
|
|
43
|
+
"document-collaboration.md",
|
|
36
44
|
"LICENSING.md",
|
|
37
45
|
"LICENSE"
|
|
38
46
|
],
|
|
@@ -40,5 +48,5 @@
|
|
|
40
48
|
"type": "git",
|
|
41
49
|
"url": "https://github.com/onodocs/onodocs.git"
|
|
42
50
|
},
|
|
43
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "3227556064a15863db186152f40ee94cff0a7d9b"
|
|
44
52
|
}
|
package/proposal.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Proposal studio
|
|
2
|
+
|
|
3
|
+
Run a complete proposal workflow with a fictional design studio and client. Change the business details, add or remove priced services, generate the Word document, refine its wording and download DOCX or searchable PDF. Everything runs in the browser; the local server serves the example assets.
|
|
4
|
+
|
|
5
|
+
## Run from the repository
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npm run build
|
|
9
|
+
npm run example:proposal
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open `http://127.0.0.1:5196`. Use `PORT` to choose another local port.
|
|
13
|
+
|
|
14
|
+
## Run from an installed SDK
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
npm install @onodocs/sdk esbuild
|
|
18
|
+
node node_modules/@onodocs/sdk/examples/proposal/serve.mjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The SDK includes the HTML, CSS, JavaScript and reusable `sample.docx`. Evaluation mode adds a watermark. For licensed use, pass your license key in the browser options supplied to both `openTemplate` and `createEditor` in `main.js`, following the SDK licensing guide.
|
|
22
|
+
|
|
23
|
+
## Try the workflow
|
|
24
|
+
|
|
25
|
+
1. Change the client and project overview. Adjust service days or rates, or add another service. The application calculates line amounts and the total in USD.
|
|
26
|
+
2. Choose **Generate proposal**. The public browser template API repeats the table rows and updates supported fields, including footer page numbers.
|
|
27
|
+
3. Choose **Edit wording**, click a passage and type. Formatting and undo use the standard editor. Return to **Preview** to review the document.
|
|
28
|
+
4. Download Word or PDF. Both exports contain the current wording. Changing business inputs does not silently overwrite the document. Regeneration requires confirmation after wording edits.
|
|
29
|
+
|
|
30
|
+
The developer panel downloads the Word template and current business data. Open the template in the existing template designer to change its bindings. It contains scalar values and a repeating `items` table row. `tests/sdk/proposal-template.mjs` reproduces the sample template in the repository.
|
|
31
|
+
|
|
32
|
+
`examples/proposal/data.js` supplies fictional starting data and calculates amounts. `main.js` calls `openTemplate(...).generate(data)`, opens the returned bytes with `createEditor`, and delegates downloads to `editor.save()` and `editor.pdf()`. The sample keeps no drafts after a page refresh. Add application-owned persistence if your workflow needs saved proposals.
|
|
33
|
+
|
|
34
|
+
## Validation
|
|
35
|
+
|
|
36
|
+
Run `npm run validate:proposal` for the complete browser journey. It changes business data, repeats and removes services, edits wording with the keyboard, checks regeneration confirmation, reopens the Word download and checks searchable PDF content. Desktop and mobile screenshots use manually reviewed references under `tests/sdk/proposal/out/`. The proposal DOCX also has a LibreOffice comparison under `tests/parity/layout/proposal-demo/`.
|