PageSourceSearch

https://www.theatrefolk.com/assets/src/page_break_extension-918dd10e.js

js theatrefolk.com collected 2026-09-25 14:04:02 UTC 3,888 bytes, 108 lines download raw bytes

1// Lexxy extension that adds a toolbar button for inserting a forced page
2// break into a rich-text section. The insert produces an action-text-
3// attachment with a unique content-type so Lexxy preserves it as an atomic,
4// non-editable node across save round-trips. The PDF layout
5// (app/views/layouts/pdf.html.haml) targets this content-type and applies
6// `break-before: page`, forcing the next content onto a new page.
7//
8// No params, no edit panel — a page break has nothing to configure.
9
10import { Extension } from 'lexxy'
11
12const CONTENT_TYPE = 'text/html; profile=page-break'
13
14class PageBreakExtension extends Extension {
15  get allowedElements() {
16    return ['div']
17  }
18
19  initializeToolbar(toolbar) {
20    const button = document.createElement('button')
21    button.type = 'button'
22    button.className = 'lexxy-editor__toolbar-button'
23    button.name = 'page-break'
24    button.title = 'Insert a forced page break (PDF only)'
25    button.innerHTML = '<i class="fas fa-page-break" aria-hidden="true"></i>'
26
27    button.addEventListener('click', (event) => {
28      event.preventDefault()
29      this.editorElement.contents.insertHtml(buildAttachmentHtml())
30      // Lexxy's insertHtml drops the attachment at the cursor. If the
31      // cursor was inside a heading / paragraph / list item, the
32      // attachment ends up nested inside that block, where Chrome's
33      // print engine doesn't reliably honor `break-after: page` on a
34      // descendant. Lift any nested page-break attachments out so they
35      // become siblings of their containing top-level block.
36      liftPageBreaksToTopLevel(this.editorElement)
37    })
38
39    toolbar.appendChild(button)
40  }
41}
42
43const ATTACHMENT_SELECTOR = 'action-text-attachment[content-type*="page-break"]'
44
45// Walks every page-break attachment in the editor and, if it's nested
46// inside a top-level block (heading, paragraph, list item, etc.),
47// reparents it to be a sibling immediately before that block. The break
48// then occurs cleanly between the previous block and the lifted-out
49// attachment. Idempotent — already-top-level attachments are left alone.
50function liftPageBreaksToTopLevel(editorElement) {
51  const editor = editorElement.editor
52  if (!editor) return
53
54  editor.update(() => {
55    const attachments = editorElement.querySelectorAll(ATTACHMENT_SELECTOR)
56    for (const dom of attachments) {
57      const nodeKey = findNodeKeyForDom(editor, dom)
58      if (!nodeKey) continue
59
60      const node = editor.getEditorState()._nodeMap.get(nodeKey)
61      if (!node) continue
62
63      // Already a direct child of root — nothing to do.
64      const parent = node.getParent()
65      if (!parent || !parent.getParent()) continue
66
67      // Walk up to the highest ancestor whose own parent is root.
68      let topLevel = node
69      while (topLevel.getParent() && topLevel.getParent().getParent()) {
70        topLevel = topLevel.getParent()
71      }
72
73      // insertBefore on a Lexical node detaches the source from its
74      // current parent and reattaches at the new position.
75      topLevel.insertBefore(node)
76    }
77  })
78}
79
80function findNodeKeyForDom(editor, targetElement) {
81  const map = editor._keyToDOMMap
82  if (!map) return null
83  for (const [key, dom] of map.entries()) {
84    if (dom === targetElement) return key
85  }
86  return null
87}
88
89function escapeHtml(str) {
90  return String(str || '')
91    .replace(/&/g, '&amp;')
92    .replace(/</g, '&lt;')
93    .replace(/>/g, '&gt;')
94    .replace(/"/g, '&quot;')
95    .replace(/'/g, '&#39;')
96}
97
98// Mirrors app/views/page_breaks/_page_break.html.haml.
99function buildContentHtml() {
100  return '<div class="page-break" role="separator" aria-label="Page break"></div>'
101}
102
103function buildAttachmentHtml() {
104  const content = escapeHtml(buildContentHtml())
105  return `<action-text-attachment content-type="${CONTENT_TYPE}" content="${content}"></action-text-attachment>`
106}
107
108export { PageBreakExtension }

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.