@onodocs/canvas 0.2.0 → 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/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, best = Infinity;
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
- const distance = dy * dy * 16 + dx * dx;
1736
- if (distance < best) {
1737
- best = distance;
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
- return { paint };
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.target.closest("[data-onodocs-attachment]")) return;
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
  },
@@ -2420,9 +2472,30 @@ function createDocument(source, options = {}) {
2420
2472
  __name(loadFont, "loadFont");
2421
2473
  }
2422
2474
  __name(createDocument, "createDocument");
2475
+ async function openDocument(url, options = {}) {
2476
+ const endpoint = new URL(url, document.baseURI);
2477
+ const manifest = await load(endpoint, options.signal);
2478
+ return createDocument({ pages: manifest.pages.map((page) => ({
2479
+ ...page,
2480
+ load(signal) {
2481
+ const url2 = new URL(endpoint);
2482
+ url2.pathname = `${url2.pathname.replace(/\/$/, "")}/pages/${page.index}`;
2483
+ return load(url2, signal);
2484
+ }
2485
+ })) }, options);
2486
+ async function load(url2, signal) {
2487
+ const signals = [signal, options.signal].filter((value) => value !== void 0);
2488
+ const response = await fetch(url2, { cache: "no-store", ...options.request, method: "GET", signal: AbortSignal.any(signals) });
2489
+ if (!response.ok) throw new Error(`Document request failed (HTTP ${response.status}).`);
2490
+ return decode(await response.text());
2491
+ }
2492
+ __name(load, "load");
2493
+ }
2494
+ __name(openDocument, "openDocument");
2423
2495
  export {
2424
2496
  createDocument,
2425
2497
  decode,
2426
2498
  encode,
2427
- materializePageDisplayListToCanvas
2499
+ materializePageDisplayListToCanvas,
2500
+ openDocument
2428
2501
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onodocs/canvas",
3
- "version": "0.2.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-07",
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": "95967333a361ecd96d5cb764a619c07a8ab2bddf"
43
+ "gitHead": "cf082bcfda5fc7581332b4251fa27c39a9a66ec7"
44
44
  }
@@ -27,4 +27,11 @@ export declare function createDocument(source: DocumentSource, options?: Readonl
27
27
  container?: HTMLElement;
28
28
  viewOptions?: ViewOptions;
29
29
  }>): CanvasDocument;
30
+ export interface RemoteDocumentOptions {
31
+ readonly container?: HTMLElement;
32
+ readonly viewOptions?: ViewOptions;
33
+ readonly signal?: AbortSignal;
34
+ readonly request?: Omit<RequestInit, "method" | "body" | "signal">;
35
+ }
36
+ export declare function openDocument(url: string | URL, options?: RemoteDocumentOptions): Promise<CanvasDocument>;
30
37
  //# sourceMappingURL=index.d.ts.map
@@ -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