Files
sm-journal-plugin/tests/blogeditor/helpers.ts
Jakub Zych 4b09bd9312 feat: editor heading levels, outline, link rel, generic-anchor warning and image naming
- the blogeditor and mlblogeditor types serve headingLevels and
  genericAnchors through RequestOptions, per schema request
- J1: H2 to H4 with a pasted or loaded H1 parsed as H2 while the H1
  setting is on, H1 to H4 while it is off; H5 and H6 still parse
- J2: the outline bar and panel (skipped, empty, link-only, H1 in Source,
  generic link texts), 300 ms after a change, collapsed, read-only
- J3: nofollow/sponsored/ugc checkboxes for absolute http(s) links only,
  and a non-blocking warning for a generic link text
- J4: one form per picked file with the slugged file name, alt required
  unless decorative (alt=""); Insert uploads the renamed file and the
  image is inserted on the upload result
- assets/dist rebuilt
2026-11-04 10:35:19 +01:00

97 lines
4.0 KiB
TypeScript

import type { Editor } from '@tiptap/core'
import '../../assets/src/blogeditor/index'
import { BlogEditorElement, TAG } from '../../assets/src/blogeditor/index'
export interface Mounted {
el: BlogEditorElement
editor: Editor
changes: string[]
uploads: { id: string; file: File }[]
}
/** mount creates a connected editor element, optionally with a value. */
export function mount(value?: string, attributes: Record<string, string> = {}): Mounted {
const el = document.createElement(TAG) as BlogEditorElement
for (const [name, val] of Object.entries(attributes)) el.setAttribute(name, val)
const changes: string[] = []
const uploads: { id: string; file: File }[] = []
el.addEventListener('summer-change', (e) => changes.push((e as CustomEvent<{ value: string }>).detail.value))
el.addEventListener('summer-upload', (e) => uploads.push((e as CustomEvent<{ id: string; file: File }>).detail))
if (value !== undefined) el.value = value
document.body.append(el)
return { el, editor: el.editor!, changes, uploads }
}
/** type inserts text at the selection, as typing does. */
export function type(editor: Editor, text: string): void {
editor.view.dispatch(editor.state.tr.insertText(text))
}
/** insertAtBlock types text at the start of the top-level textblock index. */
export function insertAtBlock(editor: Editor, index: number, text: string): void {
let pos = -1
editor.state.doc.forEach((node, offset, i) => {
if (i === index) pos = node.isTextblock ? offset + 1 : -1
})
if (pos < 0) throw new Error(`block ${index} is not a textblock`)
editor.view.dispatch(editor.state.tr.insertText(text, pos))
}
/** insertAtEveryTextblock types text at the start of every textblock. */
export function insertAtEveryTextblock(editor: Editor, text: string): void {
const positions: number[] = []
editor.state.doc.descendants((node, pos) => {
if (node.isTextblock) positions.push(pos + 1)
return true
})
let tr = editor.state.tr
for (const pos of positions.reverse()) tr = tr.insertText(text, pos)
editor.view.dispatch(tr)
}
/** button returns the toolbar button for a command. */
export function button(el: HTMLElement, command: string): HTMLButtonElement {
const found = el.querySelector<HTMLButtonElement>(`button[data-command="${command}"]`)
if (!found) throw new Error(`no toolbar button ${command}`)
return found
}
/** form returns the inline form of a kind, or null when it is closed. */
export function form(el: HTMLElement, kind: string): HTMLElement | null {
const found = el.querySelector<HTMLElement>(`[data-form="${kind}"]`)
return found && !found.hidden ? found : null
}
export function click(el: Element | null | undefined): void {
if (!el) throw new Error('nothing to click')
;(el as HTMLElement).click()
}
export function fill(input: Element | null | undefined, value: string): void {
if (!input) throw new Error('no input')
;(input as HTMLInputElement).value = value
input.dispatchEvent(new Event('input', { bubbles: true }))
}
export function status(el: HTMLElement): string {
return el.querySelector('[role="status"]')?.textContent ?? ''
}
/**
* chooseImage fills the open J4 image form (alt, caption, decorative, file
* name) and presses Insert, which uploads the file.
*/
export function chooseImage(el: HTMLElement, choice: { alt?: string; caption?: string; decorative?: boolean; fileName?: string } = {}): void {
const imageForm = form(el, 'image')
if (!imageForm) throw new Error('the image form is not open')
if (choice.fileName !== undefined) fill(imageForm.querySelector('input[name="fileName"]'), choice.fileName)
if (choice.decorative) {
const box = imageForm.querySelector<HTMLInputElement>('input[name="decorative"]')!
box.checked = true
box.dispatchEvent(new Event('change', { bubbles: true }))
}
if (choice.alt !== undefined) fill(imageForm.querySelector('input[name="alt"]'), choice.alt)
if (choice.caption !== undefined) fill(imageForm.querySelector('input[name="caption"]'), choice.caption)
click(imageForm.querySelector('[data-action="insert"]'))
}