@openpresentation/opf-editor 0.12.1 → 0.14.2

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 (95) hide show
  1. package/README.md +71 -60
  2. package/dist/annotations.d.ts +16 -16
  3. package/dist/annotations.js +71 -79
  4. package/dist/assets.d.ts +7 -7
  5. package/dist/assets.js +15 -15
  6. package/dist/background-options.d.ts +2 -2
  7. package/dist/background-options.js +6 -6
  8. package/dist/block-controls.js +7 -7
  9. package/dist/block-convert.d.ts +7 -7
  10. package/dist/block-convert.js +22 -21
  11. package/dist/blocks.d.ts +8 -8
  12. package/dist/blocks.js +41 -40
  13. package/dist/canvas-fields.js +35 -12
  14. package/dist/canvas.d.ts +22 -30
  15. package/dist/canvas.js +57 -64
  16. package/dist/chart-data.d.ts +10 -10
  17. package/dist/chart-data.js +21 -21
  18. package/dist/chart-options-panel.d.ts +2 -2
  19. package/dist/chart-options-panel.js +82 -8
  20. package/dist/chart-options.d.ts +35 -9
  21. package/dist/chart-options.js +133 -40
  22. package/dist/checks.js +21 -0
  23. package/dist/color-authoring.js +1 -1
  24. package/dist/content-actions.d.ts +15 -15
  25. package/dist/content-actions.js +71 -70
  26. package/dist/content-controls.js +12 -12
  27. package/dist/data-grid.js +7 -8
  28. package/dist/data.d.ts +4 -4
  29. package/dist/data.js +5 -5
  30. package/dist/dataset-refs.d.ts +2 -2
  31. package/dist/dataset-refs.js +5 -5
  32. package/dist/design-controls.js +61 -44
  33. package/dist/design-options.d.ts +16 -14
  34. package/dist/design-options.js +86 -74
  35. package/dist/edit-helpers.js +8 -7
  36. package/dist/exact-source.js +5 -5
  37. package/dist/export.d.ts +5 -7
  38. package/dist/export.js +40 -14
  39. package/dist/find-panel.js +5 -5
  40. package/dist/find-replace.d.ts +3 -3
  41. package/dist/find-replace.js +23 -20
  42. package/dist/font-gate.js +49 -58
  43. package/dist/galleries.js +36 -52
  44. package/dist/grid-model.d.ts +25 -27
  45. package/dist/grid-model.js +92 -102
  46. package/dist/image-crop.d.ts +4 -4
  47. package/dist/image-crop.js +20 -20
  48. package/dist/image-cropper.js +3 -3
  49. package/dist/index.d.ts +28 -28
  50. package/dist/index.js +74 -90
  51. package/dist/json-options.js +17 -16
  52. package/dist/layout-handles.js +5 -5
  53. package/dist/layout-placeholders.js +4 -4
  54. package/dist/layout.d.ts +2 -2
  55. package/dist/layout.js +8 -7
  56. package/dist/numbering-panel.js +3 -5
  57. package/dist/numbering.d.ts +3 -5
  58. package/dist/numbering.js +14 -26
  59. package/dist/outline-view.js +24 -24
  60. package/dist/outline.d.ts +12 -12
  61. package/dist/outline.js +52 -50
  62. package/dist/persistence.d.ts +1 -1
  63. package/dist/persistence.js +19 -18
  64. package/dist/presentation-ids.js +2 -2
  65. package/dist/react.js +3 -3
  66. package/dist/review-panel.d.ts +30 -13
  67. package/dist/review-panel.js +154 -53
  68. package/dist/review.d.ts +36 -43
  69. package/dist/review.js +92 -53
  70. package/dist/rich-text-input.js +4 -3
  71. package/dist/rich-text-toolbar.js +5 -3
  72. package/dist/rich-text.js +5 -2
  73. package/dist/schema-inspector.d.ts +2 -2
  74. package/dist/schema-inspector.js +16 -11
  75. package/dist/schema.d.ts +1 -1
  76. package/dist/schema.js +25 -5
  77. package/dist/slide-manager.d.ts +2 -2
  78. package/dist/slide-manager.js +18 -18
  79. package/dist/slides.d.ts +20 -20
  80. package/dist/slides.js +99 -97
  81. package/dist/svelte.js +1 -1
  82. package/dist/switches.d.ts +10 -10
  83. package/dist/switches.js +124 -114
  84. package/dist/table-options.d.ts +8 -8
  85. package/dist/table-options.js +28 -27
  86. package/dist/table-structure.d.ts +10 -10
  87. package/dist/table-structure.js +21 -21
  88. package/dist/template-panel.d.ts +3 -3
  89. package/dist/template-panel.js +37 -13
  90. package/dist/templates.d.ts +22 -11
  91. package/dist/templates.js +48 -55
  92. package/dist/transfer.d.ts +4 -4
  93. package/dist/transfer.js +49 -59
  94. package/package.json +8 -6
  95. package/dist/font-defaults.js +0 -25
package/dist/canvas.js CHANGED
@@ -2,16 +2,17 @@ import {
2
2
  createEditorSession,
3
3
  opfPathToJsonPointer,
4
4
  applyJsonPatch,
5
- validateOpfDocument,
6
5
  getValueAtPath,
7
6
  splitOpfPath,
8
7
  } from "./index.js";
9
8
  import {
10
- renderSvg,
9
+ renderSlideSvg,
11
10
  resolvePresentation,
12
11
  } from "@openpresentation/opf-render/svg";
13
12
  import {
14
13
  getEditableFields,
14
+ emptyLike,
15
+ fieldChoices,
15
16
  parseCanvasValue,
16
17
  createCanvasDraft,
17
18
  } from "./canvas-fields.js";
@@ -21,9 +22,10 @@ import { createRichTextInput } from "./rich-text-input.js";
21
22
  import { textInputOffsetAtPoint } from "./text-pointer.js";
22
23
  import { createRichTextToolbar } from "./rich-text-toolbar.js";
23
24
  import { createImageCropper } from "./image-cropper.js";
24
- import { FONTS_PENDING, fontsPendingError, whenFontsReady } from "./font-gate.js";
25
+ import { FONTS_PENDING, fontGate, fontsPendingError, previewFonts, whenFontsReady } from "./font-gate.js";
26
+ import { checkFormat, firstErrorMessage } from "./checks.js";
25
27
  export { getEditableFields } from "./canvas-fields.js";
26
- export { createFontGate, whenFontsReady, FONTS_PENDING, FONTS_UNAVAILABLE } from "./font-gate.js";
28
+ export { whenFontsReady, FONTS_PENDING, FONTS_UNAVAILABLE } from "./font-gate.js";
27
29
 
28
30
  /** Allocated placeholder or internal-part bounds for the selection outline, not glyph ink. */
29
31
  export function allocatedSelectionBox(node, item) {
@@ -65,10 +67,12 @@ export function createCanvasEditor(container, options = {}) {
65
67
  win = doc.defaultView;
66
68
  const editor =
67
69
  options.editor ??
68
- createEditorSession(options.document, { rejectInvalid: true });
69
- // `options.fonts` is a font gate (createFontGate): a document whose faces are not loaded yet is never rendered. The canvas
70
- // loads them first and shows "Loading fonts…" meanwhile; without a gate every document renders at once, as before.
71
- const fonts = options.fonts;
70
+ createEditorSession(options.presentation, { rejectInvalid: true });
71
+ // `options.fonts` is the renderer's fonts handle (`loadFonts()`): it measures the text and draws the faces. A document whose faces
72
+ // are not loaded yet is never rendered: the canvas has the handle load them first and shows "Loading fonts…" meanwhile. Without a
73
+ // handle every document renders at once, with core's portable text estimate.
74
+ const handle = options.fonts, fonts = fontGate(handle);
75
+ const drawOptions = (extra) => ({ ...renderOptions, fonts: previewFonts(handle), ...extra });
72
76
  let slideIndex = options.slideIndex ?? 0,
73
77
  // RR-32: the canvas edits the document as authored, so a template's {{tokens}} stay visible and an inline edit never
74
78
  // overwrites one with its resolved text. The Fill template panel previews the resolved deck. Pass `variables` to override.
@@ -120,7 +124,7 @@ export function createCanvasEditor(container, options = {}) {
120
124
  onTyping: beginEdit,
121
125
  onProperties(path) { if (commit()) openProperties(path, editor.get(path)); },
122
126
  validateChange(path, value) {
123
- validateRender(createCanvasDraft(editor.document, path, value));
127
+ validateRender(createCanvasDraft(editor.presentation, path, value));
124
128
  },
125
129
  onChange(path) { clearNotice(); options.onCommit?.({path, editor}); }
126
130
  });
@@ -144,7 +148,7 @@ export function createCanvasEditor(container, options = {}) {
144
148
  const blockControls = createBlockControls(root, {
145
149
  editor, render: renderFor, beforeEdit: () => {if(!commit())return false;richToolbar.hide();return true;}, enabled: () => layoutHandles.enabled,
146
150
  isEditing: () => !!active || !!layoutHandles.editingPath, slideIndex: () => slideIndex,
147
- validate: document => validateRender(document),
151
+ validate: presentation => validateRender(presentation),
148
152
  clearError: clearNotice, onError: report,
149
153
  onMove: path => choose(splitOpfPath(path).join(".")),
150
154
  onCommit: value => options.onCommit?.(value),
@@ -175,20 +179,20 @@ export function createCanvasEditor(container, options = {}) {
175
179
  const pendingMessage = "Loading fonts for this document…";
176
180
  // A synchronous render or validation of a document whose faces are still loading fails with a clear "fonts-pending"
177
181
  // error and starts the load, so the next attempt succeeds. It never draws glyphs the registry cannot provide.
178
- function requireFonts(document) {
179
- const pending = fonts?.pending(document, renderOptions) ?? [];
182
+ function requireFonts(presentation) {
183
+ const pending = fonts?.pending(presentation, renderOptions) ?? [];
180
184
  if (!pending.length) return;
181
185
  notice.hidden = false;
182
186
  notice.textContent = pendingMessage;
183
- fonts.ensure(document, { renderOptions }).then(
187
+ fonts.ensure(presentation, { renderOptions }).then(
184
188
  () => { if (!disposed && notice.textContent === pendingMessage) clearNotice(); },
185
189
  (error) => { if (!disposed) report(error); },
186
190
  );
187
191
  throw fontsPendingError(pending);
188
192
  }
189
- function validateRender(document) {
190
- requireFonts(document);
191
- return renderSvg(document, {...renderOptions, slideIndex});
193
+ function validateRender(presentation) {
194
+ requireFonts(presentation);
195
+ return renderSlideSvg(presentation, slideIndex, drawOptions());
192
196
  }
193
197
  function fontsState(kind, detail) {
194
198
  fontsShown = true;
@@ -218,7 +222,7 @@ export function createCanvasEditor(container, options = {}) {
218
222
  function show(target) {
219
223
  if (disposed) return;
220
224
  const token = ++showToken;
221
- return whenFontsReady(fonts, target ?? editor.document, {
225
+ return whenFontsReady(fonts, target ?? editor.presentation, {
222
226
  renderOptions,
223
227
  isCurrent: () => !disposed && token === showToken,
224
228
  loading: (pending) => fontsState("loading", pending),
@@ -234,27 +238,23 @@ export function createCanvasEditor(container, options = {}) {
234
238
  });
235
239
  }
236
240
  // Renders now when the document's fonts are loaded (errors throw to the caller), otherwise after loading them.
237
- function renderFor(document) {
238
- if (fonts?.pending(document ?? editor.document, renderOptions).length) show(document);
239
- else render(document);
241
+ function renderFor(presentation) {
242
+ if (fonts?.pending(presentation ?? editor.presentation, renderOptions).length) show(presentation);
243
+ else render(presentation);
240
244
  }
241
- function render(document = editor.document) {
245
+ function render(presentation = editor.presentation) {
242
246
  if (disposed) return;
243
- requireFonts(document);
247
+ requireFonts(presentation);
244
248
  showToken++;
245
- const slides = document.slides ?? [];
249
+ const slides = presentation.slides ?? [];
246
250
  slideIndex = Math.max(0, Math.min(slideIndex, slides.length - 1));
247
- const svgText = renderSvg(document, {
248
- ...renderOptions,
249
- slideIndex,
250
- trace: true,
251
- });
251
+ const svgText = renderSlideSvg(presentation, slideIndex, drawOptions({ trace: true }));
252
252
  preview.innerHTML = svgText;
253
253
  const svg = preview.querySelector("svg");
254
254
  svg.setAttribute("role", "group");
255
255
  svg.setAttribute("aria-label", "Editable slide");
256
256
  svg.removeAttribute("aria-labelledby");
257
- const geometry = resolvePresentation(document, renderOptions).slides[
257
+ const geometry = resolvePresentation(presentation, drawOptions()).slides[
258
258
  slideIndex
259
259
  ].geometry;
260
260
  const candidates = [...svg.querySelectorAll("[data-opf-path]")];
@@ -262,7 +262,7 @@ export function createCanvasEditor(container, options = {}) {
262
262
  for (const node of candidates) {
263
263
  const path = node.getAttribute("data-opf-path");
264
264
  const item = geometry.items.find((item) => item.path === path);
265
- const value = getValueAtPath(document, path);
265
+ const value = getValueAtPath(presentation, path);
266
266
  if (
267
267
  seen.has(path) ||
268
268
  node.getAttribute('data-opf-generated') === 'true' ||
@@ -340,8 +340,8 @@ export function createCanvasEditor(container, options = {}) {
340
340
  });
341
341
  if (path === selectedPath) node.setAttribute("data-canvas-selected", "");
342
342
  }
343
- layoutHandles.update(document, geometry);
344
- blockControls.update(document, geometry);
343
+ layoutHandles.update(presentation, geometry);
344
+ blockControls.update(presentation, geometry);
345
345
  imageCropper?.update();
346
346
  if (active?.kind === "text") positionInput();
347
347
  if (active?.kind === "rich-text") active.rich?.update();
@@ -350,7 +350,7 @@ export function createCanvasEditor(container, options = {}) {
350
350
  options.onFonts?.({ state: "ready" });
351
351
  }
352
352
  options.onRender?.({
353
- document,
353
+ presentation,
354
354
  slideIndex,
355
355
  svg,
356
356
  geometry,
@@ -392,7 +392,7 @@ export function createCanvasEditor(container, options = {}) {
392
392
  clearNotice();
393
393
  render(draft);
394
394
  options.onDraft?.({
395
- document: draft,
395
+ presentation: draft,
396
396
  path: active.path,
397
397
  value: undefined,
398
398
  });
@@ -403,13 +403,13 @@ export function createCanvasEditor(container, options = {}) {
403
403
  }
404
404
  try {
405
405
  const value = active.kind === "rich-text" ? active.rich.value : parseCanvasValue(active.input.value, active.type, active.originalValue);
406
- const draft = createCanvasDraft(editor.document, active.path, value);
406
+ const draft = createCanvasDraft(editor.presentation, active.path, value);
407
407
  if (deferDraft(draft)) return;
408
408
  clearNotice();
409
409
  active.valid = true;
410
410
  active.draft = draft;
411
411
  render(draft);
412
- options.onDraft?.({ document: draft, path: active.path, value });
412
+ options.onDraft?.({ presentation: draft, path: active.path, value });
413
413
  } catch (error) {
414
414
  active.valid = false;
415
415
  report(error);
@@ -429,8 +429,8 @@ export function createCanvasEditor(container, options = {}) {
429
429
  const font = win.getComputedStyle(text),
430
430
  fontSize = parseFloat(font.fontSize),
431
431
  geometry = resolvePresentation(
432
- active.draft ?? editor.document,
433
- renderOptions,
432
+ active.draft ?? editor.presentation,
433
+ drawOptions(),
434
434
  ).slides[slideIndex].geometry;
435
435
  const item = geometry.items.find((item) => item.path === active.path);
436
436
  const metricLayout=geometry.items.find(item=>item.metricLayout?.parts.some(part=>part.path===active.path))?.metricLayout;
@@ -600,7 +600,7 @@ export function createCanvasEditor(container, options = {}) {
600
600
  // Only offer rich text where the canonical schema accepts TextRun[].
601
601
  if (typeof value === "string") {
602
602
  try {
603
- createCanvasDraft(editor.document, path, [value]);
603
+ createCanvasDraft(editor.presentation, path, [value]);
604
604
  const format = doc.createElement("button");
605
605
  format.type = "button";
606
606
  format.textContent = "Format text";
@@ -683,11 +683,11 @@ export function createCanvasEditor(container, options = {}) {
683
683
  }));
684
684
  }
685
685
  function propertyDraft(edit) {
686
- const draft = applyJsonPatch(editor.document, propertyPatches(edit)),
687
- validation = validateOpfDocument(draft);
686
+ const draft = applyJsonPatch(editor.presentation, propertyPatches(edit)),
687
+ validation = checkFormat(draft);
688
688
  if (!validation.valid)
689
689
  throw new Error(
690
- validation.errors[0]?.message ?? "This change is not valid OPF.",
690
+ firstErrorMessage(validation, "This change is not valid OPF."),
691
691
  );
692
692
  return draft;
693
693
  }
@@ -706,10 +706,20 @@ export function createCanvasEditor(container, options = {}) {
706
706
  label.style.cssText =
707
707
  "display:block;margin-top:10px;color:#756b85;font-size:11px";
708
708
  label.textContent = field.label;
709
+ const choices = field.type === "string" ? fieldChoices(field) : undefined;
709
710
  const input = doc.createElement(
710
- field.type === "boolean" ? "input" : "textarea",
711
+ choices ? "select" : field.type === "boolean" ? "input" : "textarea",
711
712
  );
712
- if (field.type === "boolean") {
713
+ if (choices) {
714
+ // A fixed set of values (a timeline event's status) is a menu, not free text.
715
+ for (const choice of choices) {
716
+ const option = doc.createElement("option");
717
+ option.value = choice;
718
+ option.textContent = choice;
719
+ input.append(option);
720
+ }
721
+ input.value = field.value;
722
+ } else if (field.type === "boolean") {
713
723
  input.type = "checkbox";
714
724
  input.checked = field.value;
715
725
  } else {
@@ -780,26 +790,9 @@ export function createCanvasEditor(container, options = {}) {
780
790
  if (!commit()) return;
781
791
  try {
782
792
  const values = editor.get(array.path);
783
- const empty = (value, key = "") =>
784
- Array.isArray(value)
785
- ? value.map((child) => empty(child))
786
- : value && typeof value === "object"
787
- ? Object.fromEntries(
788
- Object.entries(value).map(([key, child]) => [
789
- key,
790
- empty(child, key),
791
- ]),
792
- )
793
- : typeof value === "number"
794
- ? 0
795
- : typeof value === "boolean"
796
- ? false
797
- : ["type", "mode", "language"].includes(key)
798
- ? value
799
- : "";
800
793
  const next = remove
801
794
  ? values.slice(0, -1)
802
- : [...values, empty(values.at(-1) ?? "")];
795
+ : [...values, emptyLike(values.at(-1) ?? "")];
803
796
  editor.set(array.path, next, {
804
797
  source: "canvas-collection",
805
798
  rejectInvalid: true,
@@ -876,7 +869,7 @@ export function createCanvasEditor(container, options = {}) {
876
869
  value = parseCanvasValue(edit.input.value, edit.type, edit.originalValue);
877
870
  else value = propertyDraft(edit);
878
871
  if (edit.kind === "text" || edit.kind === "rich-text")
879
- validateRender(createCanvasDraft(editor.document, edit.path, value));
872
+ validateRender(createCanvasDraft(editor.presentation, edit.path, value));
880
873
  committing = true;
881
874
  if ((edit.kind === "text" || edit.kind === "rich-text") && JSON.stringify(value) !== edit.base)
882
875
  editor.set(edit.path, value, { source: "canvas", rejectInvalid: true });
@@ -928,7 +921,7 @@ export function createCanvasEditor(container, options = {}) {
928
921
  options.onCancel?.({ path });
929
922
  }
930
923
  function setSlide(index) {
931
- if (!Number.isInteger(index) || !editor.document.slides?.[index])
924
+ if (!Number.isInteger(index) || !editor.presentation.slides?.[index])
932
925
  throw new RangeError("Slide index is out of range.");
933
926
  if (index === slideIndex) {
934
927
  if (!active) renderFor();
@@ -2,29 +2,29 @@ import type { EditorSession } from "./index.js";
2
2
  import type { GridAddress, GridCellEdit, GridChange, GridOptions, GridSortOptions, PreparedGridChange } from "./grid-model.js";
3
3
 
4
4
  /** Set chart cells from text or typed values: names and categories are text, series cells are numbers read in the number format (blank is a gap, never 0). One patch; every problem is in `error.issues`. */
5
- export declare function prepareChartCells(document: unknown, chartPath: string, edits: GridCellEdit[], options?: GridOptions): PreparedGridChange;
5
+ export declare function prepareChartCells(presentation: unknown, chartPath: string, edits: GridCellEdit[], options?: GridOptions): PreparedGridChange;
6
6
  export declare function setChartCells(editor: EditorSession, chartPath: string, edits: GridCellEdit[], options?: GridOptions): GridChange;
7
- export declare function prepareChartPaste(document: unknown, chartPath: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): PreparedGridChange;
7
+ export declare function prepareChartPaste(presentation: unknown, chartPath: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): PreparedGridChange;
8
8
  export declare function pasteChartText(editor: EditorSession, chartPath: string, anchor: GridAddress, source: string | string[][], options?: GridOptions): GridChange;
9
9
  /** Insert `count` empty rows (categories) so the first is row `at`. */
10
- export declare function prepareChartInsertRows(document: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
10
+ export declare function prepareChartInsertRows(presentation: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
11
11
  export declare function insertChartRows(editor: EditorSession, chartPath: string, at: number, count?: number, options?: GridOptions): GridChange;
12
- export declare function prepareChartDeleteRows(document: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
12
+ export declare function prepareChartDeleteRows(presentation: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
13
13
  export declare function deleteChartRows(editor: EditorSession, chartPath: string, indices: number[], options?: GridOptions): GridChange;
14
- export declare function prepareChartMoveRows(document: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
14
+ export declare function prepareChartMoveRows(presentation: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
15
15
  export declare function moveChartRows(editor: EditorSession, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): GridChange;
16
16
  /** Insert `count` unnamed series so the first is column `at`. */
17
- export declare function prepareChartInsertColumns(document: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
17
+ export declare function prepareChartInsertColumns(presentation: unknown, chartPath: string, at: number, count?: number, options?: GridOptions): PreparedGridChange;
18
18
  export declare function insertChartColumns(editor: EditorSession, chartPath: string, at: number, count?: number, options?: GridOptions): GridChange;
19
- export declare function prepareChartDeleteColumns(document: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
19
+ export declare function prepareChartDeleteColumns(presentation: unknown, chartPath: string, indices: number[], options?: GridOptions): PreparedGridChange;
20
20
  export declare function deleteChartColumns(editor: EditorSession, chartPath: string, indices: number[], options?: GridOptions): GridChange;
21
- export declare function prepareChartMoveColumns(document: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
21
+ export declare function prepareChartMoveColumns(presentation: unknown, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): PreparedGridChange;
22
22
  export declare function moveChartColumns(editor: EditorSession, chartPath: string, from: number, to: number, count?: number, options?: GridOptions): GridChange;
23
23
  /** Sort the rows (categories) by a column, stably and by type. */
24
- export declare function prepareChartSort(document: unknown, chartPath: string, column: number, options?: GridSortOptions): PreparedGridChange;
24
+ export declare function prepareChartSort(presentation: unknown, chartPath: string, column: number, options?: GridSortOptions): PreparedGridChange;
25
25
  export declare function sortChartRows(editor: EditorSession, chartPath: string, column: number, options?: GridSortOptions): GridChange;
26
26
  /** Swap categories and series: the first column's values become the series names and each series becomes a row. Numeric categories become text. */
27
- export declare function prepareChartTranspose(document: unknown, chartPath: string): PreparedGridChange;
27
+ export declare function prepareChartTranspose(presentation: unknown, chartPath: string): PreparedGridChange;
28
28
  export declare function transposeChart(editor: EditorSession, chartPath: string, options?: { meta?: Record<string, unknown> }): GridChange;
29
29
  /** Rename a series: the label of data column `column` (1 or more). */
30
30
  export declare function renameChartSeries(editor: EditorSession, chartPath: string, column: number, name: string, options?: { meta?: Record<string, unknown> }): GridChange;
@@ -3,7 +3,7 @@
3
3
  // second). Cells are text for names and categories and numbers for values; a gap is null and stays a gap, never 0. Each operation is
4
4
  // one validated, undoable patch (grid-model.js has the implementation; grid-text.js documents number reading).
5
5
  //
6
- // `prepare*` computes `{ document, patches, changed }` without touching a session; the session forms apply it as one undo step.
6
+ // `prepare*` computes `{ presentation, patches, changed }` without touching a session; the session forms apply it as one undo step.
7
7
  import {
8
8
  prepareGridCells,
9
9
  preparePaste,
@@ -26,64 +26,64 @@ import {
26
26
  prepareTranspose,
27
27
  transposeGridData,
28
28
  } from "./grid-model.js";
29
- export function prepareChartCells(document, chartPath, edits, options = {}) {
30
- return prepareGridCells(document, chartPath, edits, { ...options, kind: "chart" });
29
+ export function prepareChartCells(presentation, chartPath, edits, options = {}) {
30
+ return prepareGridCells(presentation, chartPath, edits, { ...options, kind: "chart" });
31
31
  }
32
32
  export function setChartCells(editor, chartPath, edits, options = {}) {
33
33
  return setGridCells(editor, chartPath, edits, { ...options, kind: "chart" });
34
34
  }
35
- export function prepareChartPaste(document, chartPath, anchor, source, options = {}) {
36
- return preparePaste(document, chartPath, anchor, source, { ...options, kind: "chart" });
35
+ export function prepareChartPaste(presentation, chartPath, anchor, source, options = {}) {
36
+ return preparePaste(presentation, chartPath, anchor, source, { ...options, kind: "chart" });
37
37
  }
38
38
  export function pasteChartText(editor, chartPath, anchor, source, options = {}) {
39
39
  return pasteGridText(editor, chartPath, anchor, source, { ...options, kind: "chart" });
40
40
  }
41
- export function prepareChartInsertRows(document, chartPath, at, count = 1, options = {}) {
42
- return prepareInsertRows(document, chartPath, at, count, { ...options, kind: "chart" });
41
+ export function prepareChartInsertRows(presentation, chartPath, at, count = 1, options = {}) {
42
+ return prepareInsertRows(presentation, chartPath, at, count, { ...options, kind: "chart" });
43
43
  }
44
44
  export function insertChartRows(editor, chartPath, at, count = 1, options = {}) {
45
45
  return insertGridRows(editor, chartPath, at, count, { ...options, kind: "chart" });
46
46
  }
47
- export function prepareChartDeleteRows(document, chartPath, indices, options = {}) {
48
- return prepareDeleteRows(document, chartPath, indices, { ...options, kind: "chart" });
47
+ export function prepareChartDeleteRows(presentation, chartPath, indices, options = {}) {
48
+ return prepareDeleteRows(presentation, chartPath, indices, { ...options, kind: "chart" });
49
49
  }
50
50
  export function deleteChartRows(editor, chartPath, indices, options = {}) {
51
51
  return deleteGridRows(editor, chartPath, indices, { ...options, kind: "chart" });
52
52
  }
53
- export function prepareChartMoveRows(document, chartPath, from, to, count = 1, options = {}) {
54
- return prepareMoveRows(document, chartPath, from, to, count, { ...options, kind: "chart" });
53
+ export function prepareChartMoveRows(presentation, chartPath, from, to, count = 1, options = {}) {
54
+ return prepareMoveRows(presentation, chartPath, from, to, count, { ...options, kind: "chart" });
55
55
  }
56
56
  export function moveChartRows(editor, chartPath, from, to, count = 1, options = {}) {
57
57
  return moveGridRows(editor, chartPath, from, to, count, { ...options, kind: "chart" });
58
58
  }
59
- export function prepareChartInsertColumns(document, chartPath, at, count = 1, options = {}) {
60
- return prepareInsertColumns(document, chartPath, at, count, { ...options, kind: "chart" });
59
+ export function prepareChartInsertColumns(presentation, chartPath, at, count = 1, options = {}) {
60
+ return prepareInsertColumns(presentation, chartPath, at, count, { ...options, kind: "chart" });
61
61
  }
62
62
  export function insertChartColumns(editor, chartPath, at, count = 1, options = {}) {
63
63
  return insertGridColumns(editor, chartPath, at, count, { ...options, kind: "chart" });
64
64
  }
65
- export function prepareChartDeleteColumns(document, chartPath, indices, options = {}) {
66
- return prepareDeleteColumns(document, chartPath, indices, { ...options, kind: "chart" });
65
+ export function prepareChartDeleteColumns(presentation, chartPath, indices, options = {}) {
66
+ return prepareDeleteColumns(presentation, chartPath, indices, { ...options, kind: "chart" });
67
67
  }
68
68
  export function deleteChartColumns(editor, chartPath, indices, options = {}) {
69
69
  return deleteGridColumns(editor, chartPath, indices, { ...options, kind: "chart" });
70
70
  }
71
- export function prepareChartMoveColumns(document, chartPath, from, to, count = 1, options = {}) {
72
- return prepareMoveColumns(document, chartPath, from, to, count, { ...options, kind: "chart" });
71
+ export function prepareChartMoveColumns(presentation, chartPath, from, to, count = 1, options = {}) {
72
+ return prepareMoveColumns(presentation, chartPath, from, to, count, { ...options, kind: "chart" });
73
73
  }
74
74
  export function moveChartColumns(editor, chartPath, from, to, count = 1, options = {}) {
75
75
  return moveGridColumns(editor, chartPath, from, to, count, { ...options, kind: "chart" });
76
76
  }
77
- export function prepareChartSort(document, chartPath, column, options = {}) {
78
- return prepareSortRows(document, chartPath, column, { ...options, kind: "chart" });
77
+ export function prepareChartSort(presentation, chartPath, column, options = {}) {
78
+ return prepareSortRows(presentation, chartPath, column, { ...options, kind: "chart" });
79
79
  }
80
80
  export function sortChartRows(editor, chartPath, column, options = {}) {
81
81
  return sortGridRows(editor, chartPath, column, { ...options, kind: "chart" });
82
82
  }
83
83
 
84
84
  /** Swap categories and series: the first column's values become the series names and each series becomes a row. See {@link prepareTranspose}. */
85
- export function prepareChartTranspose(document, chartPath) {
86
- return prepareTranspose(document, chartPath);
85
+ export function prepareChartTranspose(presentation, chartPath) {
86
+ return prepareTranspose(presentation, chartPath);
87
87
  }
88
88
  export function transposeChart(editor, chartPath, options = {}) {
89
89
  return transposeGridData(editor, chartPath, options);
@@ -7,8 +7,8 @@ export interface ChartOptionsPanelOptions {
7
7
  onStatus?: (message: string) => void;
8
8
  }
9
9
  export interface ChartOptionsPanel {
10
- /** The panel's root element; undefined when the installed core has no chart option fields. */
11
- element: HTMLElement | undefined;
10
+ /** The panel's root element (hidden while no chart is selected). */
11
+ element: HTMLElement;
12
12
  /** Re-read the selection and the chart; the panel also refreshes on every editor change. */
13
13
  refresh(): void;
14
14
  destroy(): void;
@@ -1,14 +1,18 @@
1
- // The chart options panel (RR-35): axis titles, legend position and data labels for the selected chart.
1
+ // The chart options panel (RR-35): axis titles, legend position and data labels for the selected chart, plus (FA-09) its alt text and
2
+ // (FA-14) which series and categories to highlight.
2
3
  // It mounts into any host element over an editor session; every control commits one undoable change through
3
4
  // `setChartOptions` (src/chart-options.js), the canvas redraws, and Undo restores the chart. The panel shows only what
4
- // the selected chart type can show (core's support table), and hides itself when no chart is selected.
5
- import { CHART_LABEL_CONTENT, CHART_LEGEND_POSITIONS, chartOptionsAvailable, parseChartPath, readChartOptions, setChartOptions } from "./chart-options.js";
5
+ // the selected chart type can show (core's support table), and hides itself when no chart is selected. On a combo chart
6
+ // (FA-15) a "Series" group sets which series are lines and which lines use the secondary value axis, and a secondary axis
7
+ // title field appears while a line uses that axis.
8
+ import { CHART_LABEL_CONTENT, CHART_LEGEND_POSITIONS, parseChartPath, readChartOptions, setChartOptions } from "./chart-options.js";
6
9
 
7
10
  const LEGEND_LABELS = { default: "Default", none: "None", top: "Top", bottom: "Bottom", left: "Left", right: "Right" };
8
11
  const CONTENT_LABELS = { category: "Category", value: "Value", percent: "Percent" };
9
12
  const POSITION_LABELS = { auto: "Automatic", center: "Center", "inside-end": "Inside end", "inside-base": "Inside base", "outside-end": "Outside end", above: "Above", below: "Below", left: "Left", right: "Right" };
10
13
 
11
14
  let counter = 0;
15
+ const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
12
16
 
13
17
  /**
14
18
  * Mount the panel in `host`. `getSelectedPath()` returns the selected OPF path (any path inside a chart selects it);
@@ -16,7 +20,6 @@ let counter = 0;
16
20
  * call `refresh()` when the selection changes (the panel also refreshes on every editor change).
17
21
  */
18
22
  export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatus = () => {} }) {
19
- if (!chartOptionsAvailable()) return { refresh() {}, destroy() {}, element: undefined };
20
23
  const id = `opf-chart-options-${++counter}`;
21
24
  const root = document.createElement("section");
22
25
  root.className = "opf-chart-options";
@@ -24,10 +27,16 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
24
27
  root.hidden = true;
25
28
  root.innerHTML = `
26
29
  <h3 class="opf-co-title">Chart options</h3>
30
+ <div class="opf-co-group" data-group="alt">
31
+ <label class="opf-co-row"><span>Alt text</span><input type="text" id="${id}-alt" aria-label="Chart alt text" data-opf-chart-option="alt" autocomplete="off" placeholder="What the chart shows"></label>
32
+ <label class="opf-co-row opf-co-check"><input type="checkbox" id="${id}-decorative" aria-label="Decorative chart" data-opf-chart-option="alt.decorative"><span>Decorative (no alt text)</span></label>
33
+ </div>
27
34
  <div class="opf-co-group" data-group="axisTitles">
28
35
  <label class="opf-co-row" data-axis="category"><span>Category axis title</span><input type="text" id="${id}-category" aria-label="Category axis title" data-opf-chart-option="axisTitles.category" autocomplete="off"></label>
29
36
  <label class="opf-co-row" data-axis="value"><span>Value axis title</span><input type="text" id="${id}-value" aria-label="Value axis title" data-opf-chart-option="axisTitles.value" autocomplete="off"></label>
37
+ <label class="opf-co-row" data-axis="secondary"><span>Secondary axis title</span><input type="text" id="${id}-secondary" aria-label="Secondary axis title" data-opf-chart-option="axisTitles.secondary" autocomplete="off"></label>
30
38
  </div>
39
+ <fieldset class="opf-co-group opf-co-series" data-group="combo"><legend>Series</legend><div data-row="combo-series"></div></fieldset>
31
40
  <div class="opf-co-group" data-group="legend">
32
41
  <label class="opf-co-row"><span>Legend</span><select id="${id}-legend" aria-label="Legend" data-opf-chart-option="legend">${CHART_LEGEND_POSITIONS.map((value) => `<option value="${value}">${LEGEND_LABELS[value]}</option>`).join("")}</select></label>
33
42
  </div>
@@ -36,12 +45,18 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
36
45
  <fieldset class="opf-co-content"><legend>Label shows</legend>${CHART_LABEL_CONTENT.map((part) => `<label class="opf-co-check"><input type="checkbox" value="${part}" aria-label="Label shows ${CONTENT_LABELS[part].toLowerCase()}" data-opf-chart-option="dataLabels.content.${part}"><span>${CONTENT_LABELS[part]}</span></label>`).join("")}</fieldset>
37
46
  <label class="opf-co-row" data-row="position"><span>Label position</span><select id="${id}-position" aria-label="Label position" data-opf-chart-option="dataLabels.position"></select></label>
38
47
  <label class="opf-co-row" data-row="separator"><span>Separator</span><input type="text" id="${id}-separator" aria-label="Label separator" data-opf-chart-option="dataLabels.separator" autocomplete="off"></label>
48
+ </div>
49
+ <div class="opf-co-group" data-group="highlight">
50
+ <fieldset class="opf-co-highlight" data-part="series"><legend>Highlight series</legend><div class="opf-co-choices" data-choices="series"></div></fieldset>
51
+ <fieldset class="opf-co-highlight" data-part="categories"><legend>Highlight categories</legend><div class="opf-co-choices" data-choices="categories"></div></fieldset>
39
52
  </div>`;
40
53
  host.append(root);
41
54
  const $ = (selector) => root.querySelector(selector);
42
55
  const fields = {
43
- category: $(`#${id}-category`), value: $(`#${id}-value`), legend: $(`#${id}-legend`), labels: $(`#${id}-labels`),
56
+ alt: $(`#${id}-alt`), decorative: $(`#${id}-decorative`), category: $(`#${id}-category`), value: $(`#${id}-value`), secondary: $(`#${id}-secondary`), legend: $(`#${id}-legend`), labels: $(`#${id}-labels`),
57
+ series: $("[data-row='combo-series']"),
44
58
  position: $(`#${id}-position`), separator: $(`#${id}-separator`), contents: [...root.querySelectorAll("[data-opf-chart-option^='dataLabels.content.']")],
59
+ highlight: { series: $("[data-choices='series']"), categories: $("[data-choices='categories']") },
45
60
  };
46
61
  let chartPath;
47
62
 
@@ -55,13 +70,34 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
55
70
  return;
56
71
  }
57
72
  chartPath = path;
58
- const { fields: support, state } = readChartOptions(chart);
73
+ const { fields: support, state, choices, combo } = readChartOptions(chart, editor.presentation);
59
74
  root.hidden = false;
60
75
  const setValue = (input, value) => { if (input !== document.activeElement && input.value !== value) input.value = value; };
61
- for (const axis of ["category", "value"]) {
62
- $(`[data-axis="${axis}"]`).hidden = !support.axisTitles[axis];
76
+ setValue(fields.alt, state.alt);
77
+ fields.decorative.checked = state.decorative;
78
+ fields.alt.disabled = state.decorative;
79
+ const secondaryInUse = Boolean(combo?.series.some((entry) => entry.axis === "secondary"));
80
+ for (const axis of ["category", "value", "secondary"]) {
81
+ $(`[data-axis="${axis}"]`).hidden = !support.axisTitles[axis] || (axis === "secondary" && !secondaryInUse);
63
82
  setValue(fields[axis], state.axisTitles[axis]);
64
83
  }
84
+ // FA-15: one row per plotted series of a combo chart: "Line" and, for a line, "Secondary axis".
85
+ $("[data-group='combo']").hidden = !combo;
86
+ const series = combo?.series ?? [];
87
+ const signature = JSON.stringify(series.map((entry) => entry.name));
88
+ if (fields.series.dataset.series !== signature) {
89
+ fields.series.innerHTML = series.map((entry, index) => `<div class="opf-co-row opf-co-series-row" data-series="${index}"><span>${escapeHtml(entry.name)}</span>` +
90
+ `<label class="opf-co-check"><input type="checkbox" data-opf-chart-option="line" value="${escapeHtml(entry.name)}" aria-label="${escapeHtml(entry.name)} as a line"><span>Line</span></label>` +
91
+ `<label class="opf-co-check"><input type="checkbox" data-opf-chart-option="secondaryAxis" value="${escapeHtml(entry.name)}" aria-label="${escapeHtml(entry.name)} on the secondary axis"><span>Secondary axis</span></label></div>`).join("");
92
+ fields.series.dataset.series = signature;
93
+ }
94
+ series.forEach((entry, index) => {
95
+ const row = fields.series.querySelector(`[data-series="${index}"]`);
96
+ const line = row.querySelector("[data-opf-chart-option='line']"), secondary = row.querySelector("[data-opf-chart-option='secondaryAxis']");
97
+ line.checked = entry.role === "line";
98
+ secondary.checked = entry.axis === "secondary";
99
+ secondary.disabled = entry.role !== "line";
100
+ });
65
101
  $("[data-group='axisTitles']").hidden = !support.axisTitles.category && !support.axisTitles.value;
66
102
  $("[data-group='legend']").hidden = !support.legend;
67
103
  fields.legend.value = state.legend;
@@ -84,6 +120,31 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
84
120
  setValue(fields.separator, state.dataLabels.separator);
85
121
  fields.separator.disabled = off || state.dataLabels.content.length < 2;
86
122
  $("[data-row='separator']").hidden = support.dataLabels.content.length < 2;
123
+ // Highlight: one checkbox per plotted series and per category label the chart type can highlight.
124
+ for (const part of ["series", "categories"]) {
125
+ const offered = support.highlight[part] && choices[part].length > 0;
126
+ $(`[data-part='${part}']`).hidden = !offered;
127
+ const list = fields.highlight[part];
128
+ if (!offered) continue;
129
+ if (list.dataset.options !== JSON.stringify(choices[part])) {
130
+ list.replaceChildren(...choices[part].map((name) => {
131
+ const label = document.createElement("label");
132
+ label.className = "opf-co-check";
133
+ const input = document.createElement("input");
134
+ input.type = "checkbox";
135
+ input.value = name;
136
+ input.setAttribute("aria-label", `Highlight ${part === "series" ? "series" : "category"} ${name}`);
137
+ input.setAttribute("data-opf-chart-option", `highlight.${part}`);
138
+ const text = document.createElement("span");
139
+ text.textContent = name;
140
+ label.append(input, text);
141
+ return label;
142
+ }));
143
+ list.dataset.options = JSON.stringify(choices[part]);
144
+ }
145
+ for (const input of list.querySelectorAll("input")) input.checked = state.highlight[part].includes(input.value);
146
+ }
147
+ $("[data-group='highlight']").hidden = $("[data-part='series']").hidden && $("[data-part='categories']").hidden;
87
148
  }
88
149
 
89
150
  function commit(change, message) {
@@ -97,14 +158,27 @@ export function createChartOptionsPanel(host, { editor, getSelectedPath, onStatu
97
158
  refresh();
98
159
  }
99
160
 
161
+ const checkedNames = (part) => [...fields.highlight[part].querySelectorAll("input:checked")].map((input) => input.value);
100
162
  const selectedContent = () => fields.contents.filter((input) => input.checked && !input.closest("label").hidden).map((input) => input.value);
101
163
  const listeners = [
164
+ [fields.alt, "change", () => commit({ alt: fields.alt.value }, "Changed the chart's alt text. Undo restores it.")],
165
+ [fields.decorative, "change", () => commit({ decorative: fields.decorative.checked }, fields.decorative.checked ? "Marked the chart decorative. Undo restores it." : "The chart is no longer decorative. Undo restores it.")],
102
166
  [fields.category, "change", () => commit({ axisTitles: { category: fields.category.value } }, "Changed the category axis title. Undo restores it.")],
103
167
  [fields.value, "change", () => commit({ axisTitles: { value: fields.value.value } }, "Changed the value axis title. Undo restores it.")],
168
+ [fields.secondary, "change", () => commit({ axisTitles: { secondary: fields.secondary.value } }, "Changed the secondary axis title. Undo restores it.")],
169
+ [fields.series, "change", () => {
170
+ const checked = (option) => [...fields.series.querySelectorAll(`[data-opf-chart-option='${option}']`)].filter((input) => input.checked).map((input) => input.value);
171
+ const line = checked("line");
172
+ commit({ line, secondaryAxis: checked("secondaryAxis").filter((name) => line.includes(name)) }, "Changed which series are lines. Undo restores them.");
173
+ }],
104
174
  [fields.legend, "change", () => commit({ legend: fields.legend.value }, "Changed the legend. Undo restores it.")],
105
175
  [fields.labels, "change", () => commit({ dataLabels: fields.labels.checked }, fields.labels.checked ? "Showing data labels. Undo restores the chart." : "Hid the data labels. Undo restores them.")],
106
176
  [fields.position, "change", () => commit({ dataLabels: { position: fields.position.value } }, "Moved the data labels. Undo restores them.")],
107
177
  [fields.separator, "change", () => commit({ dataLabels: { separator: fields.separator.value } }, "Changed the label separator. Undo restores it.")],
178
+ ...["series", "categories"].map((part) => [fields.highlight[part], "change", () => {
179
+ const names = checkedNames(part);
180
+ commit({ highlight: { [part]: names } }, names.length ? `Highlighted ${names.join(", ")}. Undo restores the colors.` : `Cleared the ${part} highlight. Undo restores it.`);
181
+ }]),
108
182
  ...fields.contents.map((input) => [input, "change", () => {
109
183
  const content = selectedContent();
110
184
  // A label always shows something: unticking the last part keeps it.