Skip to content

Editor: custom StarterKit, media node view overhaul, and fixes#765

Merged
netchampfaris merged 15 commits into
mainfrom
editor-starterkit-and-fixes
Jun 8, 2026
Merged

Editor: custom StarterKit, media node view overhaul, and fixes#765
netchampfaris merged 15 commits into
mainfrom
editor-starterkit-and-fixes

Conversation

@netchampfaris

@netchampfaris netchampfaris commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

A batch of work on the molecules editor (src/molecules/editor): a custom StarterKit, a unified media node-view experience (images, video, galleries, embeds), and a set of correctness fixes.

Highlights

Core

  • Replace TipTap's StarterKit with a custom frappeStarterKit for finer control over the bundled extensions.
  • Declarative app config via the FrappeUI plugin (no window/global reads).
  • Lifecycle guards in useEditor / useFloatingPopup; stale-update guards in the media pipeline.

Media node views

  • Unified media toolbar + custom video controls.
  • Gallery dialog and node-view UX overhaul.
  • Upload progress, cancel, and size-limit validation; shared UploadProgressIndicator.
  • Edge resize handles (left/right pill handles) replacing the corner button.
  • isolate on media roots so internal toolbar z-indexes can't leak above outside UI.
  • Consistent 8px (rounded) corners across image, video, gallery, and embed.

Embeds

  • Per-platform embed shortcuts in the slash menu (YouTube, Vimeo, CodePen, CodeSandbox, Figma, Google Docs, Notion), generated from PLATFORM_CONFIGS; each opens the embed dialog pre-titled for the platform. Custom allowlists prune unavailable platforms.
  • allow-same-origin added to the iframe sandbox so embeds render.

Slash commands

  • Dropdown-style section groups (Text, Lists, Media, Embeds, Insert). Grouping is display-only; keyboard navigation stays flat.

Fixes

  • Preserve aspect ratio when resizing media that stores only one dimension (was distorting mid-drag, snapping back on release).
  • Keep table-cell edit mode sticky on inner clicks.
  • /link inserts editable placeholder text.
  • Media upload cleanup and stale-update guards.

Testing

  • yarn vitest --run src/molecules/editor → 101 passing (incl. new resize regression tests).
  • Media resize, slash groups, embed shortcuts, rounded corners, and the image-viewer interaction verified manually in the Gameplan frontend.

🤖 Generated with Claude Code

Docs preview: https://ui.frappe.io/pr-preview/pr-765/

Coverage: 56.89% (+0.80% vs main)

netchampfaris and others added 14 commits June 8, 2026 23:06
- compose starter kit from individual tiptap extensions instead of
  @tiptap/starter-kit, so link/code/codeBlock can never double-register
  with our replacements (typed as false-only options)
- bake HeadingIds in whenever headings are enabled
- InlineKit now pushes individual marks instead of disabling members
- pin all starter member packages to 3.11.0 via resolutions
- /link now inserts a selected "Link" text and opens the link editor
  in edit mode, instead of setting an empty href on nothing
- link toolbar button only shows when openLinkEditor command exists
- delete staged local files when editor is destroyed mid-upload and
  drop the old entry after a successful reupload
- skip dimension dispatch when the node at pos no longer matches the
  probed src (reupload can swap nodes at the same position)
- render media container while upload placeholder is loading
- compare toRaw(content) against lastEmitted so reactive-wrapped JSON
  doesn't defeat the bounce-back check
- reset applyingExternalUpdate in finally so a setContent throw can't
  leave external updates permanently ignored
- don't attach popup document listeners if destroyed before the rAF
- plain Enter returns to navigate mode (spreadsheet-style)
- clicks inside the editing cell only move the caret
…edia

- upload engine passes { signal, onProgress } to the upload function;
  shared UploadFunction type used by image/video/paste/gallery options
- per-upload progress + abort tracked in the shared uploadProgressMap,
  used by both node views and the gallery dialog (one state system)
- size validation runs before staging, so over-limit files are rejected
  without base64-encoding them; error placeholder keeps retry/replace
- file-size helpers consolidated into utils/fileSize (was 4 copies)
- video poster capture for upload previews
- dragged-type detection only on dragenter (no per-dragover churn)
YouTube's player throws SecurityError reading its own cookies in an
opaque-origin sandbox and renders a black box. allow-same-origin keeps
the embed's OWN origin (not ours); the escape concern only applies to
same-origin content, which the allowlist precludes.
- MediaToolbar generalized (image/video/embed) with inline align
  buttons, no popup; embeds get the same toolbar with a Change link
  action (updateIframeAt swaps src in place, keeping caption/align)
- video nodes get an align attribute, same dispatch as images
- custom video controls (play/seek/time/mute/fullscreen) replace the
  native controls attribute in node views; hidden while resizing
- resize commit keeps inline drag styles until the attr re-render
  lands, fixing the one-frame size flash on tall media; resize drag
  uses pointer events end to end
- upload error state rendered inside the media box (overlay on
  preview, inline in the placeholder) instead of below it
- drop indicator: 3px rounded gray bar via configured Dropcursor
dialog:
- gapped, rounded grid cells with grab cursor + drag dim state
- captions stay visible once set; hover-reveal only when empty
- image count in header, single hint line, count-aware Insert CTA
- per-cell progress/cancel from the shared upload state

node view:
- on-selection dark toolbar (edit + inline 2/3/4 column buttons)
  replaces the always-visible Edit button and columns dropdown
- click selects the node with the standard selection ring; selection
  survives column switches (pointerdown swallowed on the toolbar)
- cells match the dialog look (gap + rounding)
- menu label: Image -> Image / Gallery
- app.use(FrappeUI, { config: {...} }) applies config keys at install,
  one declarative entry point instead of scattered setConfig calls
- new maxFileSize config key; file-size helpers read it first and fall
  back to the legacy window/frappe.boot globals (deprecated)
- export FrappeUIConfig type and the fileSize utils
- Replace the bottom-right diagonal resize button on media/embed node
  views with Notion-style pill handles on the left and right edges
  (shared MediaResizeHandles component); left-edge drags invert the
  pointer delta in useNodeViewResize.
- Fix the post-resize layout bug where the container stretched to full
  editor width: stop clearing inline drag styles that Vue style
  bindings own (Vue skips re-writing unchanged values), and clear only
  attribute-sized media elements (new mediaSizing option, the iframe
  opts into 'style').
- Restyle the gallery's remove button from a white circle to the dark
  media-toolbar idiom.
- Add `isolate` to media node roots so internal toolbar z-indexes
  can't leak above outside UI.
- Extract the inline upload progress overlay into a shared
  UploadProgressIndicator component (media node view + gallery cells).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Surface every first-party embed integration (YouTube, Vimeo, CodePen,
  CodeSandbox, Figma, Google Docs, Notion) directly in the slash menu,
  generated from PLATFORM_CONFIGS; each opens the same embed dialog
  pre-titled for the platform with a platform example placeholder.
  openIframeDialog(platform?) threads the platform through the command,
  controller, and dialog. A custom iframe allowlist that excludes a
  platform's hosts prunes its shortcut (allowlistPermitsHosts).
- Group slash commands under dropdown-style section headers (Text,
  Lists, Media, Embeds, Insert). Grouping is display-only: headers
  derive from consecutive runs of item.group in the filtered list, so
  keyboard navigation stays flat and empty groups vanish on filter.
- Align the legacy TextEditor's Commands.iframe declaration with the
  new openIframeDialog signature (divergent merged declarations are a
  TS2717 error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stored dimension

The resize locked its aspect ratio from the node's stored attrs
(height / width). When a media node stores only `width` — its height
coming from CSS `height: auto` — that ratio disagreed with what was
painted: a tall image distorted to a wider shape mid-drag, then snapped
back on release once the inline drag styles were cleared and `height:
auto` re-derived the true ratio.

Lock the ratio from the element's rendered box (offsetHeight /
offsetWidth) at drag start instead — the painted box is the source of
truth for what the user sees, so the shape is preserved for any attr
state. `getAspectRatio` remains a fallback for the pre-layout case
(offsetWidth 0). Adds regression tests for both paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Images/videos rendered at 2px, the gallery at 10px, and embeds at 12px.
Standardize every media node — image, video, gallery, embed, and their
caption overlays — on the design system's default `rounded` (8px) token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@barista-for-frappe

Copy link
Copy Markdown

Minor nits — large editor PR, but well-tested and the new public surface is clean. No src/components/ API drift. A couple of things worth a conscious look.

  • iframe sandboxsrc/molecules/editor/extensions/iframe/iframe-embed-utils.ts adds allow-same-origin alongside allow-scripts. That combo normally lets a frame script its way out of the sandbox, so it's the one change worth signing off on deliberately. The reasoning holds: the allowlist only permits absolute http(s) URLs on external hosts (never our origin), and renderHTML forces the fixed sandbox so pasted markup can't widen it. Looks safe — just flagging it as the highest-risk line in the diff.
  • Public exports — new fileSize.ts (getMaxFileSize, formatBytes, fileSizeLimitMessage) plus FrappeUIConfig.maxFileSize and the FrappeUI { config } option are a good consolidation: useFileUpload and fileUploadHandler now import the shared helpers instead of duplicating the size lookup, and the window/frappe.boot globals are kept as documented deprecated fallbacks. Nice direction.
  • PR size — 2560/659 across ~60 files, bundling StarterKit, media node-views, embeds, slash groups, and several unrelated fixes. Fine to merge, but hard to bisect later if one piece regresses; smaller follow-ups would be easier to revert in isolation.

Tests are solid (8 new .test.ts, all new .vue use <script setup lang="ts">, resize regression covered). No blockers.

@netchampfaris netchampfaris force-pushed the editor-starterkit-and-fixes branch from fb6691b to 0b5d2ea Compare June 8, 2026 17:37
getMaxFileSize now reads only the app-provided maxFileSize config
(setConfig). The window/frappe.boot fallback and its Window global
augmentation were a migration shim with no remaining consumers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@netchampfaris netchampfaris added the beta-release Auto-publish a beta to npm when the PR is merged label Jun 8, 2026
@netchampfaris netchampfaris merged commit 7dbc3d8 into main Jun 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beta-release Auto-publish a beta to npm when the PR is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant