@openpresentation/opf-editor 0.5.0 → 0.6.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 +3 -3
- package/dist/canvas-fields.js +15 -2
- package/dist/canvas.js +20 -8
- package/dist/rich-text-toolbar.js +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Embeddable local editor primitives for Open Presentation Format documents. The package turns traced `@openpresentation/opf-render` SVG output into JSON-path-aware edits, validates OPF after each change, and records undo/redo as JSON Patch operations.
|
|
4
4
|
|
|
5
|
-
Version 0.
|
|
5
|
+
Version 0.6.0 uses core 0.9.0 and renderer 0.7.0. Code source/metadata edits preserve line endings, literal tabs and cancellation/undo through accepted trace targets. Committed preview uses shared quote/code geometry; the active source textarea uses native browser editing. `paginateSlide` records a one-page readability-policy change in undo history, and an unchanged repeat is a no-op. Coordinated examples use PPTX 0.7.0 for exact code source/metadata recovery. Native formatting, font theme and geometry are not restored; quotes still import as editable text blocks with structure and readability-policy loss.
|
|
6
6
|
|
|
7
7
|
## Scope
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ await canvas.ready;
|
|
|
34
34
|
|
|
35
35
|
Load the same font bytes into the browser using `loadBrowserFontRegistry` from `@openpresentation/opf-render/fonts-browser` before mounting. The host owns font URLs, storage and collaboration. `onDraft` provides live document drafts; the session only changes on commit. Escape cancels. Concurrent edits to the selected payload cancel a stale draft.
|
|
36
36
|
|
|
37
|
-
These APIs were introduced in 0.1.0. Version 0.
|
|
37
|
+
These APIs were introduced in 0.1.0. Version 0.6.0 requires core 0.9.0 and renderer 0.7.0 for the canvas, including shared quote/code geometry, styled/merged cells, rich table values, headers and content-aware row heights. See the OPF repository’s `docs/live-editor.md` for setup, the support matrix and roadmap. `pnpm pack:ecosystem` in that repository also prepares local preview tarballs for coordinated development.
|
|
38
38
|
|
|
39
39
|
The canvas retains canonical SVG glyphs while a transparent native input supplies the caret. Advanced shaping, freeform object positioning, all chart/media treatments, and cross-engine pixel identity remain work in progress. The Source dialog in the playground now provides a live JSON preview; changes are validated before committing.
|
|
40
40
|
|
|
@@ -174,7 +174,7 @@ This repo does not require an npm automation token when Trusted Publishing is co
|
|
|
174
174
|
|
|
175
175
|
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See the sibling OPF repo's `docs/dynamic-composition.md` for the complete contract.
|
|
176
176
|
|
|
177
|
-
Version 0.
|
|
177
|
+
Version 0.6.0 requires `@openpresentation/opf@^0.9.0`. The optional renderer peer requires `@openpresentation/opf-render@^0.7.0`. Clean registry installs support the composition APIs without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
178
178
|
|
|
179
179
|
## Local interactive demo
|
|
180
180
|
|
package/dist/canvas-fields.js
CHANGED
|
@@ -36,7 +36,7 @@ export function getEditableFields(value, path) {
|
|
|
36
36
|
walk(value, root, []);
|
|
37
37
|
return { fields, arrays };
|
|
38
38
|
}
|
|
39
|
-
export function parseCanvasValue(text, type) {
|
|
39
|
+
export function parseCanvasValue(text, type, original) {
|
|
40
40
|
if (type === "number") {
|
|
41
41
|
if (!String(text).trim() || !Number.isFinite(Number(text)))
|
|
42
42
|
throw new Error("Enter a finite number.");
|
|
@@ -47,7 +47,20 @@ export function parseCanvasValue(text, type) {
|
|
|
47
47
|
if (text === "null") return null;
|
|
48
48
|
throw new Error("This field must be null.");
|
|
49
49
|
}
|
|
50
|
-
return text;
|
|
50
|
+
return type === 'string' && typeof original === 'string' ? preserveTextLineEndings(original,String(text)) : text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Textareas normalize line endings; retain unchanged source bytes around an edit. */
|
|
54
|
+
export function preserveTextLineEndings(original, input) {
|
|
55
|
+
const normalized=original.replace(/\r\n|\r/g,'\n'),next=input.replace(/\r\n|\r/g,'\n');
|
|
56
|
+
if(normalized===next)return original;
|
|
57
|
+
let prefix=0,suffix=0;
|
|
58
|
+
while(prefix<normalized.length&&prefix<next.length&&normalized[prefix]===next[prefix])prefix++;
|
|
59
|
+
while(suffix<normalized.length-prefix&&suffix<next.length-prefix&&normalized[normalized.length-1-suffix]===next[next.length-1-suffix])suffix++;
|
|
60
|
+
const offsets=[0];
|
|
61
|
+
for(let i=0;i<original.length;i++){if(original[i]==='\r'&&original[i+1]==='\n')i++;offsets.push(i+1);}
|
|
62
|
+
const ending=/\r\n|\r|\n/.exec(original)?.[0]??'\n';
|
|
63
|
+
return original.slice(0,offsets[prefix])+next.slice(prefix,next.length-suffix).replaceAll('\n',ending)+original.slice(offsets[normalized.length-suffix]);
|
|
51
64
|
}
|
|
52
65
|
export function createCanvasDraft(document, path, value) {
|
|
53
66
|
const draft = applyJsonPatch(
|
package/dist/canvas.js
CHANGED
|
@@ -52,7 +52,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
52
52
|
notice.hidden = true;
|
|
53
53
|
const style = doc.createElement("style");
|
|
54
54
|
style.textContent =
|
|
55
|
-
".opf-canvas-preview>svg{display:block;width:100%;height:auto}.opf-canvas [data-canvas-target]{cursor:text}.opf-canvas .opf-inline-input::selection{background:#8975d933;color:transparent}.opf-canvas [data-canvas-target]:focus{outline:none}.opf-canvas [data-canvas-target]:hover>.opf-selection{stroke:#9c90df}.opf-canvas [data-canvas-selected]>.opf-selection,.opf-canvas [data-canvas-target]:focus-visible>.opf-selection{stroke:#7765d0}.opf-canvas button:focus-visible,.opf-canvas textarea:focus-visible,.opf-canvas input:focus-visible{outline:2px solid #8170d5;outline-offset:2px}";
|
|
55
|
+
".opf-canvas-preview>svg{display:block;width:100%;height:auto}.opf-canvas [data-canvas-target]{cursor:text}.opf-canvas .opf-inline-input::selection{background:#8975d933;color:var(--opf-inline-selection-color,transparent)}.opf-canvas [data-canvas-target]:focus{outline:none}.opf-canvas [data-canvas-target]:hover>.opf-selection{stroke:#9c90df}.opf-canvas [data-canvas-selected]>.opf-selection,.opf-canvas [data-canvas-target]:focus-visible>.opf-selection{stroke:#7765d0}.opf-canvas button:focus-visible,.opf-canvas textarea:focus-visible,.opf-canvas input:focus-visible{outline:2px solid #8170d5;outline-offset:2px}";
|
|
56
56
|
root.append(style, preview, overlay, notice);
|
|
57
57
|
container.replaceChildren(root);
|
|
58
58
|
const report = (error) => {
|
|
@@ -135,6 +135,8 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
135
135
|
const value = getValueAtPath(document, path);
|
|
136
136
|
if (
|
|
137
137
|
seen.has(path) ||
|
|
138
|
+
node.getAttribute('data-opf-generated') === 'true' ||
|
|
139
|
+
(node.hasAttribute('data-opf-code-container') && typeof value === 'string') ||
|
|
138
140
|
(!item && !node.matches("g") && !node.matches("image")) ||
|
|
139
141
|
value === undefined ||
|
|
140
142
|
path === `slides.${slideIndex}` ||
|
|
@@ -157,6 +159,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
157
159
|
);
|
|
158
160
|
if (node.matches("g")) {
|
|
159
161
|
let bounds = node.getBBox();
|
|
162
|
+
if (!bounds.width && !bounds.height && node.hasAttribute('data-opf-code-role')) bounds={x:Number(node.dataset.opfBoxX),y:Number(node.dataset.opfBoxY),width:Number(node.dataset.opfBoxWidth),height:Number(node.dataset.opfBoxHeight)};
|
|
160
163
|
const lines = JSON.parse(node.getAttribute("data-opf-rich-lines") ?? "[]");
|
|
161
164
|
if (!bounds.width && !bounds.height && lines.length) bounds = {x:lines[0].x,y:lines[0].y,width:Number(node.getAttribute("data-opf-box-width"))||8,height:lines.reduce((sum,line)=>sum+line.height,0)};
|
|
162
165
|
const rect = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
@@ -228,7 +231,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
228
231
|
return;
|
|
229
232
|
}
|
|
230
233
|
try {
|
|
231
|
-
const value = active.kind === "rich-text" ? active.rich.value : parseCanvasValue(active.input.value, active.type);
|
|
234
|
+
const value = active.kind === "rich-text" ? active.rich.value : parseCanvasValue(active.input.value, active.type, active.originalValue);
|
|
232
235
|
const draft = createCanvasDraft(editor.document, active.path, value);
|
|
233
236
|
clearNotice();
|
|
234
237
|
active.valid = true;
|
|
@@ -283,7 +286,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
283
286
|
fontWeight: font.fontWeight,
|
|
284
287
|
fontStyle: font.fontStyle,
|
|
285
288
|
lineHeight: `${lineHeight * scale}px`,
|
|
286
|
-
color: active.composing ? font.fill : "transparent",
|
|
289
|
+
color: active.composing || active.isCode ? font.fill : "transparent",
|
|
287
290
|
caretColor: font.fill,
|
|
288
291
|
textAlign: anchor === "middle" ? "center" : anchor === "end" ? "right" : "left",
|
|
289
292
|
width: `${width * scale}px`,
|
|
@@ -293,8 +296,10 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
293
296
|
});
|
|
294
297
|
active.input.style.left = `${svgRect.left - rootRect.left + (left - viewBox.x) * scale}px`;
|
|
295
298
|
active.input.style.top = `${svgRect.top - rootRect.top + (y - viewBox.y) * scale}px`;
|
|
296
|
-
//
|
|
297
|
-
|
|
299
|
+
// When the input supplies visible text, its selected text needs the same fill.
|
|
300
|
+
active.input.style.setProperty('--opf-inline-selection-color',active.composing || active.isCode ? font.fill : 'transparent');
|
|
301
|
+
// Other text keeps the canonical SVG visible beneath the transparent input.
|
|
302
|
+
target.style.opacity = active.composing || active.isCode ? "0" : "1";
|
|
298
303
|
}
|
|
299
304
|
function beginEdit(path) {
|
|
300
305
|
if (disposed) return;
|
|
@@ -321,18 +326,22 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
321
326
|
input.className = "opf-inline-input";
|
|
322
327
|
input.value = String(value);
|
|
323
328
|
input.setAttribute("aria-label", `Edit ${path.split(".").at(-1)} inline`);
|
|
324
|
-
|
|
329
|
+
const isCode=text.hasAttribute('data-opf-code-role');
|
|
330
|
+
input.spellcheck = !isCode;
|
|
325
331
|
input.style.cssText =
|
|
326
332
|
"position:absolute;pointer-events:auto;resize:none;border:0;outline:1px solid #8975d9;outline-offset:4px;margin:0;padding:0;background:transparent;overflow:hidden;min-height:0;box-shadow:none;border-radius:0;white-space:pre-wrap;overflow-wrap:break-word;box-sizing:border-box;z-index:2";
|
|
327
333
|
active = {
|
|
328
334
|
kind: "text",
|
|
329
335
|
path,
|
|
330
336
|
base: JSON.stringify(value),
|
|
337
|
+
originalValue:value,
|
|
338
|
+
isCode,
|
|
331
339
|
type: typeof value,
|
|
332
340
|
input,
|
|
333
341
|
valid: true,
|
|
334
342
|
};
|
|
335
343
|
overlay.append(input);
|
|
344
|
+
if(isCode){input.style.background='#111827';input.style.tabSize='4';input.style.overflow='auto';}
|
|
336
345
|
// Only offer rich text where the canonical schema accepts TextRun[].
|
|
337
346
|
if (typeof value === "string") {
|
|
338
347
|
try {
|
|
@@ -366,7 +375,9 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
366
375
|
});
|
|
367
376
|
input.addEventListener("keydown", (event) => {
|
|
368
377
|
if (event.isComposing) return;
|
|
369
|
-
if (event.key ===
|
|
378
|
+
if (isCode && event.key === 'Tab' && !event.shiftKey) {
|
|
379
|
+
event.preventDefault();input.setRangeText('\t',input.selectionStart,input.selectionEnd,'end');input.dispatchEvent(new win.Event('input',{bubbles:true}));
|
|
380
|
+
} else if (event.key === "Escape") {
|
|
370
381
|
event.preventDefault();
|
|
371
382
|
event.stopPropagation();
|
|
372
383
|
cancel();
|
|
@@ -400,6 +411,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
400
411
|
value: parseCanvasValue(
|
|
401
412
|
field.type === "boolean" ? input.checked : input.value,
|
|
402
413
|
field.type,
|
|
414
|
+
field.value,
|
|
403
415
|
),
|
|
404
416
|
}));
|
|
405
417
|
}
|
|
@@ -594,7 +606,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
594
606
|
let value;
|
|
595
607
|
if (edit.kind === "rich-text") value = edit.rich.value;
|
|
596
608
|
else if (edit.kind === "text")
|
|
597
|
-
value = parseCanvasValue(edit.input.value, edit.type);
|
|
609
|
+
value = parseCanvasValue(edit.input.value, edit.type, edit.originalValue);
|
|
598
610
|
else value = propertyDraft(edit);
|
|
599
611
|
if (edit.kind === "text" || edit.kind === "rich-text")
|
|
600
612
|
renderSvg(createCanvasDraft(editor.document, edit.path, value), {...renderOptions,slideIndex});
|
|
@@ -102,7 +102,7 @@ export function createRichTextToolbar(root, {editor, report, beforeChange, onPro
|
|
|
102
102
|
function endpoint(node,offset) {
|
|
103
103
|
if(node?.nodeType!==3) return null;
|
|
104
104
|
const element=node.parentElement?.closest('text[data-opf-text-start]');
|
|
105
|
-
if(!element||!root.contains(element))return null;
|
|
105
|
+
if(!element||!root.contains(element)||element.hasAttribute('data-opf-code-role'))return null;
|
|
106
106
|
return {path:element.closest('[data-opf-path]')?.getAttribute('data-opf-path'),offset:Number(element.dataset.opfTextStart)+offset};
|
|
107
107
|
}
|
|
108
108
|
function selectionChanged() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -81,13 +81,14 @@
|
|
|
81
81
|
"test:packed": "node test/packed-install.mjs",
|
|
82
82
|
"prepack": "npm run build",
|
|
83
83
|
"build:playground": "node scripts/build-playground.mjs",
|
|
84
|
-
"test:playground": "npm run build:playground && node test/playground-pptx.mjs"
|
|
84
|
+
"test:playground": "npm run build:playground && node test/playground-pptx.mjs",
|
|
85
|
+
"test:code-browser": "node test/code-browser.mjs"
|
|
85
86
|
},
|
|
86
87
|
"dependencies": {
|
|
87
|
-
"@openpresentation/opf": "^0.
|
|
88
|
+
"@openpresentation/opf": "^0.9.0"
|
|
88
89
|
},
|
|
89
90
|
"peerDependencies": {
|
|
90
|
-
"@openpresentation/opf-render": "^0.
|
|
91
|
+
"@openpresentation/opf-render": "^0.7.0"
|
|
91
92
|
},
|
|
92
93
|
"peerDependenciesMeta": {
|
|
93
94
|
"@openpresentation/opf-render": {
|
|
@@ -95,8 +96,8 @@
|
|
|
95
96
|
}
|
|
96
97
|
},
|
|
97
98
|
"devDependencies": {
|
|
98
|
-
"@openpresentation/opf-pptx": "0.
|
|
99
|
-
"@openpresentation/opf-render": "0.
|
|
99
|
+
"@openpresentation/opf-pptx": "0.7.0",
|
|
100
|
+
"@openpresentation/opf-render": "0.7.0",
|
|
100
101
|
"esbuild": "0.28.2",
|
|
101
102
|
"playwright": "1.63.0"
|
|
102
103
|
}
|