Files
summercms/admin/src/components/form/richeditor/lossy.ts
Jakub Zych cb2987c146 feat(17-02): richeditor field on TipTap with the untouched rule
- RichEditorField: Reka toolbar, link popover with the http/https/mailto/tel
  and relative URL policy, Edit HTML view, lossy note, skeleton while the
  lazily imported TipTap chunk loads, HTML view fallback when it fails
- untouched values are submitted byte-identical; after an edit the value is
  the editor HTML (empty paragraph -> ""); a value from outside (the save's
  sanitized answer) reloads the editor
- read-only renders through a client display allowlist, never as stored
- TipTap 3.31.4 (core, pm, starter-kit) pinned exact after the package check
- richeditor.* phrase keys (en, pl), smoke specs, forms docs
2026-10-12 15:56:59 +02:00

53 lines
1.5 KiB
TypeScript

// What the richeditor's TipTap schema can show (UI-SPEC RichEditorField,
// "Lossy note"). Stored HTML with any other element still opens: the field
// keeps the stored string until the administrator edits (the untouched
// rule), and the HTML view edits it without loss. This module has no TipTap
// import, so the field can decide before the editor chunk loads.
/** Elements the editor shows; compared by lower-cased tag name. */
export const EDITOR_TAGS: ReadonlySet<string> = new Set([
'p',
'br',
'strong',
'b',
'em',
'i',
'u',
's',
'h3',
'ul',
'ol',
'li',
'blockquote',
'a',
'div',
'span',
])
/**
* Parses html into an inert template: nothing in it runs or loads (no
* scripts, no image requests), so stored markup can be inspected safely.
*/
export function inertFragment(html: string): DocumentFragment {
const template = document.createElement('template')
template.innerHTML = html
return template.content
}
/** The distinct lower-cased tag names of html's elements, in document order. */
export function tagNames(html: string): string[] {
if (html === '') {
return []
}
const out = new Set<string>()
for (const element of Array.from(inertFragment(html).querySelectorAll('*'))) {
out.add(element.localName.toLowerCase())
}
return [...out]
}
/** Whether html holds an element the editor cannot show (the lossy note). */
export function isLossy(html: string): boolean {
return tagNames(html).some((name) => !EDITOR_TAGS.has(name))
}