PageSourceSearch

https://www.theatrefolk.com/assets/src/image_replacement_panel-0b13af7f.js

js theatrefolk.com collected 2026-10-03 19:09:54 UTC 4,703 bytes, 130 lines download raw bytes

1// Click-to-copy panel for "image needs replacement" placeholders inside the
2// Lexxy editor. The placeholder is an atomic action-text-attachment
3// (contenteditable=false with user-select: none under Lexical), so its
4// description text cannot be drag-selected in the editor, and Lexical's
5// mousedown handling selects the whole node before the browser can start a
6// text selection. Clicking the placeholder instead opens a small panel
7// anchored beneath it with the description in a read-only textarea,
8// pre-selected for an immediate Cmd/Ctrl+C, plus a Copy button wired to the
9// existing Stimulus clipboard controller (which supplies the "Copied!"
10// feedback).
11//
12// Mirrors the note_aside_extension click-to-edit panel: document-level
13// capture-phase click handler so we run before Lexxy's selection logic,
14// panel appended to the lexxy-editor element so it anchors correctly.
15
16const ATTACHMENT_SELECTOR = 'action-text-attachment[content-type$="profile=image-replacement"]'
17
18let activePanel = null
19
20function closeActivePanel() {
21  if (activePanel) {
22    activePanel.remove()
23    activePanel = null
24  }
25}
26
27document.addEventListener(
28  'click',
29  (event) => {
30    if (event.target.closest('lexxy-attachment-toolbar')) {
31      closeActivePanel()
32      return
33    }
34
35    if (event.target.closest('.image-replacement-panel')) return
36
37    const attachment = event.target.closest(ATTACHMENT_SELECTOR)
38    const editor = attachment?.closest('lexxy-editor')
39    if (!attachment || !editor) {
40      closeActivePanel()
41      return
42    }
43
44    openPanel(editor, attachment)
45  },
46  true
47)
48
49document.addEventListener('keydown', (event) => {
50  if (event.key === 'Escape') closeActivePanel()
51})
52
53// Pull the description out of the placeholder's rendered aside, dropping the
54// leading "Description:" label. Falls back to the attachment's textContent
55// for unexpected structures (legacy or hand-edited HTML).
56function descriptionOf(attachment) {
57  const p = attachment.querySelector('.image-needs-replacement__description')
58  if (!p) return attachment.textContent.trim()
59
60  const clone = p.cloneNode(true)
61  const label = clone.querySelector(':scope > strong:first-child')
62  if (label && /^\s*Description:?\s*$/i.test(label.textContent)) {
63    label.remove()
64  }
65  return clone.textContent.trim()
66}
67
68function pageOf(attachment) {
69  return attachment.querySelector('.image-needs-replacement')?.getAttribute('data-pdf-page') || ''
70}
71
72function openPanel(editorElement, attachment) {
73  closeActivePanel()
74
75  const page = pageOf(attachment)
76  const panel = document.createElement('div')
77  panel.className = 'image-replacement-panel'
78  panel.setAttribute('role', 'dialog')
79  panel.setAttribute('aria-label', 'Image description')
80  panel.dataset.controller = 'clipboard'
81  panel.dataset.clipboardSuccessContentValue = 'Copied!'
82  panel.innerHTML = `
83    <label class="image-replacement-panel__row">
84      <span>Image description${page ? ` (page ${escapeHtml(page)})` : ''}</span>
85      <textarea rows="4" readonly data-clipboard-target="source"></textarea>
86    </label>
87    <div class="image-replacement-panel__actions">
88      <button type="button" data-action="image-replacement-close">Close</button>
89      <button type="button" class="image-replacement-panel__copy" data-clipboard-target="button" data-action="clipboard#copy">
90        Copy
91      </button>
92    </div>
93  `
94
95  // Assigned via the property, not interpolated into the markup, so the
96  // description can never be parsed as HTML.
97  const textarea = panel.querySelector('textarea')
98  textarea.value = descriptionOf(attachment)
99
100  // lexxy-editor is both the panel's containing block AND a scroll container
101  // (overflow-y: auto with a capped height), so absolute children are
102  // positioned in content coordinates and scroll with the text. Viewport
103  // rect differences alone would anchor the panel scrollTop pixels too high;
104  // add the editor's scroll offsets to land next to the placeholder.
105  const rect = attachment.getBoundingClientRect()
106  const editorRect = editorElement.getBoundingClientRect()
107  panel.style.top = `${rect.bottom - editorRect.top + editorElement.scrollTop + 4}px`
108  panel.style.left = `${Math.max(0, rect.left - editorRect.left + editorElement.scrollLeft)}px`
109
110  editorElement.appendChild(panel)
111  activePanel = panel
112
113  textarea.focus()
114  textarea.select()
115
116  panel.addEventListener('click', (event) => {
117    if (event.target.closest('button[data-action="image-replacement-close"]')) {
118      closeActivePanel()
119    }
120  })
121}
122
123function escapeHtml(str) {
124  return String(str || '')
125    .replace(/&/g, '&amp;')
126    .replace(/</g, '&lt;')
127    .replace(/>/g, '&gt;')
128    .replace(/"/g, '&quot;')
129    .replace(/'/g, '&#39;')
130}

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.