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, '&') 92 .replace(/</g, '<') 93 .replace(/>/g, '>') 94 .replace(/"/g, '"') 95 .replace(/'/g, ''') 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.