Skip to content

feat: support atomic cross-slide element updates through the public API - #291

Merged
ChristopherVR merged 5 commits into
ChristopherVR:mainfrom
yunfeizhu:feat/cross-slide-element-updates
Sep 16, 2026
Merged

ChristopherVR merged 5 commits into
ChristopherVR:mainfrom
yunfeizhu:feat/cross-slide-element-updates

Conversation

@yunfeizhu

@yunfeizhu yunfeizhu commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

What does this change?

Adds updateElements(updates, { label? }) to the public viewer API across all five bindings. A host action can update titles on several slides without navigating, then undo or redo the whole action in one step.

For the two-slide report in #287, a batch changes title positions from [67, 67] to [84, 90]; one undo restores [67, 67]. The existing updateElement remains a current-slide edit. This adds an explicit batch boundary rather than treating separate edits as one operation implicitly.

Fixes #287.

  • Validate all targets and patches before committing. Missing or ambiguous targets and invalid patches reject the whole batch; empty and unchanged batches preserve history and dirty state.
  • Reject batches during active pointer gestures before committing pending text or changing slides/history. Resume after release or cancellation in all five bindings.
  • Keep consecutive batches and adjacent ordinary edits in separate undo steps. Preserve the active slide and current selection when applying a batch.
  • Reuse the existing history paths. React also tracks queued slide state so an ordinary edit immediately before a batch is preserved under both React 18 and 19.
  • Limit targets to top-level elements on ordinary editable slides. Document patch semantics, unsupported modes, pending inline text, and the async completion contract in English and Chinese.

Type of change

  • New feature (non-breaking)

Cross-binding parity

Validation, target resolution, patch copying, and no-op detection live in pptx-viewer-shared. Each binding commits through its own history implementation. No new UI controls or translated UI labels are introduced.

Binding Implemented Validation
React Yes Real public-handle tests, React 18 compatibility, browser tests
Vue Yes Public API and history tests, browser tests
Angular Yes Component API with real history service, browser tests
Svelte Yes Public deck API with real editor state, browser tests
Vanilla Yes Viewer instance and history tests, browser tests

Testing

  • Shared batch validation and guard tests; public API/history tests for all five bindings; pointer handling, Vue drawing cancellation, and demo handle lifecycle tests: 225 passed, including the React 18 compatibility run.
  • element-update-batches and inspector-undo: 75 passed across all five bindings using local Chrome. Includes active move/resize rejection before and after movement, release/cancel recovery, and separate gesture/batch undo steps.
  • The live Vue drag regression was reproduced before the guard: updateElements resolved while a gesture was still active.
  • All five demos use the same live development-handle helper with lifecycle cleanup. Production and server-rendering paths do not install the handle.

Checks run locally

  • bun run lint
  • bun run fmt:check
  • bun run typecheck
  • bun run build (plus final rebuilds for the affected bindings)
  • bun run e2e:contract

Conventional Commits

  • Commit messages follow Conventional Commits

@github-actions github-actions Bot added documentation Improvements or additions to documentation react React binding (pptx-react-viewer) vue Vue binding (pptx-vue-viewer) angular Angular binding (pptx-angular-viewer) svelte Svelte binding (pptx-svelte-viewer) vanilla Vanilla binding (pptx-vanilla-viewer) shared Framework-agnostic viewer logic (pptx-viewer-shared) e2e Playwright end-to-end suite labels Sep 15, 2026

@ChristopherVR ChristopherVR left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice extraction: element-update-batch.ts is a clean pure-decision-function (validate/no-op-detect without mutating, then a thin commitElementUpdateBatch adapter each binding wires to its own history), and Angular/Vanilla reuse existing proven commit paths (applyReplacement, pushHistory+store.set) rather than inventing new ones. Traced the React flushSync/ref choreography against the existing history-tracking effect and label propagation through undo/redo — looks correctly synchronized (the shared cloneHistorySnapshot drops actionLabel, which is what stops a batch's label leaking into a later unrelated edit). Left a few inline notes below, nothing blocking.

Comment thread demos/demo-react/main.tsx Outdated
return;
}
// Expose the public handle for development and cross-binding API tests.
Object.defineProperty(window, '__pptxViewer', {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This window.__pptxViewer dev-hook is added identically (modulo syntax) in this file, demos/demo-vue/src/App.vue, demos/demo-angular/src/app.component.ts, and demos/demo-svelte/src/App.svelte. The actual logic (Object.defineProperty(window, '__pptxViewer', { get }) + Reflect.deleteProperty cleanup) has no framework dependency — could this be one small shared helper (e.g. installDevViewerHandle(getHandle)) that each binding's lifecycle hook just calls, instead of four copies?

Separately: demos/demo-vanilla/src/main.ts:185 already does window.__pptxViewer = viewer, but unconditionally, not gated by import.meta.env.DEV like these four new ones. After this PR, 1 of 5 demos leaks the live handle in production builds and 4 don't. Since these demos are the ones embedded live on the docs site, worth reconciling (either gate vanilla's too, or note why the asymmetry is fine).

state.shapeAdjustmentDragStateRef.current ||
state.isDrawingRef.current
) {
throw new Error('Finish the current pointer interaction before updating elements.');

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pointer-interaction guard (reusing the same refs as the existing hasActivePointerInteraction() gate for the ordinary history-tracking effect) only exists in React's useElementUpdateBatch. commitElementUpdateBatch — the path Vue, Angular, Svelte, and Vanilla all go through — has no equivalent check.

Is that safe because those four bindings apply the live drag/resize/marquee preview outside the authoritative slides store and only commit it at pointer-up (so an intervening updateElements just operates on the last-committed, pre-gesture state)? If so a one-line comment on commitElementUpdateBatch would help; if not, calling updateElements mid-drag in Vue/Angular/Svelte/Vanilla might be able to race the gesture's own commit in a way this PR doesn't test for.

commitElementUpdateBatch(updates, options, {
getTarget: () => ({
canEdit: deps.canEdit(),
mode: editor.masterViewTarget ? 'master' : deps.getMode(),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deps.getMode() here is wired to derived.viewerMode (see create-viewer-state-derived.svelte.ts), which already resolves to 'master' whenever editor.masterViewTarget is set:

const viewerMode = $derived<ViewerMode>(
	editor.masterViewTarget
		? 'master'
		: viewer.isFullscreen
			? 'present'
			: deps.getEditable()
				? 'edit'
				: 'preview',
);

So editor.masterViewTarget ? 'master' : deps.getMode() is redundant (the left branch can never differ from what the right branch would already return). Not wrong, just dead/confusing — Angular's equivalent trusts getMode() alone. Could probably just be mode: deps.getMode().

yunfeizhu and others added 2 commits September 16, 2026 18:22
Read live gesture state in every binding before touching pending text, slides,
or history, and release canceled gestures so batching can resume. Share the
development-only demo handle lifecycle and simplify the Svelte mode check.

Co-Authored-By: Codex <noreply@openai.com>
Reproduce a live Vue drag racing with a cross-slide batch. Exercise move and
resize rejection, cancellation recovery, and separate undo entries in all
five demos. Verify production handle gating and ownership-aware cleanup.

Co-Authored-By: Codex <noreply@openai.com>
@github-actions github-actions Bot added the ci Workflows, scripts, release tooling label Sep 16, 2026
@yunfeizhu

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I pushed follow-up commits da3bf490 and 62331e27 addressing all three notes.

  • Extracted installDevViewerHandle(getHandle, development) and wired it into all five demos. Vanilla is now development-only too. The getter follows the current viewer, and cleanup only removes the installation it owns.
  • Confirmed the gesture race with a failing Vue regression: drag previews write into the authoritative slide store, so accepting a batch mid-drag is unsafe. Vue, Angular, Svelte, and Vanilla now pass their live, per-instance gesture state into the shared commit adapter. Like React, they reject before touching pending text, slides, or history. The recovery tests also exposed missing pointercancel cleanup in React/Angular and Vue drawing; those paths now release the guard as well.
  • Simplified Svelte's batch target mode to deps.getMode() and updated the English/Chinese contract docs.

Validation: 225 targeted tests passed, including React 18 and the demo-handle lifecycle tests; 75 browser tests passed across all five bindings. The browser matrix covers move/resize while held, normal release, cancellation, and independent gesture/batch undo steps. Build, typecheck, lint, formatting, and the framework-neutral E2E contract check passed locally.

@ChristopherVR
ChristopherVR merged commit 264b167 into ChristopherVR:main Sep 16, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

angular Angular binding (pptx-angular-viewer) ci Workflows, scripts, release tooling documentation Improvements or additions to documentation e2e Playwright end-to-end suite react React binding (pptx-react-viewer) shared Framework-agnostic viewer logic (pptx-viewer-shared) svelte Svelte binding (pptx-svelte-viewer) vanilla Vanilla binding (pptx-vanilla-viewer) vue Vue binding (pptx-vue-viewer)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: support atomic cross-slide element updates through the public API

2 participants