/** * Prebuilt CKEditor 5 plugin bundle for Drupal (sandbox quality). * * Provides a toolbar button "Footnote" that opens a Drupal dialog asking for: * - Reference ID / key (free text for now) * - Optional page * Then inserts: * * Replace this with a proper build + entity autocomplete later. */ (function () { const w = window; // Guard: CKEditor5 globals must exist. w.CKEditor5 = w.CKEditor5 || {}; w.CKEditor5.footnotepicker2 = w.CKEditor5.footnotepicker2 || {}; const Plugin = (w.CKEditor5 && w.CKEditor5.core && w.CKEditor5.core.Plugin) || null; const ButtonView = (w.CKEditor5 && w.CKEditor5.ui && w.CKEditor5.ui.ButtonView) || null; if (!Plugin || !ButtonView) { // eslint-disable-next-line no-console console.warn('[bibcite_footnotes_2] CKEditor5 globals not found; FootnotePicker2 not registered.'); return; } // Sanitize ONLY the footnote body (not the article). // Allows: , , and plain text. function sanitizeFootnoteHtml(input) { if (!input) return ''; const allowedTags = new Set(['STRONG', 'EM', 'A']); const allowedAttrs = { A: new Set(['href', 'title', 'target', 'rel']), }; const template = document.createElement('template'); template.innerHTML = input; const cleanNode = (node) => { // Remove comments. if (node.nodeType === Node.COMMENT_NODE) { node.remove(); return; } // Text nodes are fine. if (node.nodeType === Node.TEXT_NODE) return; // Element nodes. if (node.nodeType === Node.ELEMENT_NODE) { // Strip disallowed elements but keep their text content. if (!allowedTags.has(node.tagName)) { const textNode = document.createTextNode(node.textContent || ''); node.replaceWith(textNode); return; } // Strip disallowed attributes. [...node.attributes].forEach((attr) => { const name = attr.name.toLowerCase(); const ok = allowedAttrs[node.tagName] && allowedAttrs[node.tagName].has(name); if (!ok) node.removeAttribute(attr.name); }); // Extra link safety. if (node.tagName === 'A') { const href = node.getAttribute('href') || ''; // Allow only http(s), mailto, and relative URLs. if (!/^(https?:|mailto:|\/)/i.test(href)) { node.removeAttribute('href'); } node.setAttribute('rel', 'noopener noreferrer'); } } // Recurse. [...node.childNodes].forEach(cleanNode); }; [...template.content.childNodes].forEach(cleanNode); return template.innerHTML; } function openFootnoteDialog(onSubmit) { const $ = w.jQuery; const Drupal = w.Drupal; // Fallback: prompt() if dialog isn't available. if (!Drupal || !Drupal.dialog || !$) { const reference = w.prompt('Reference ID (optional):', '') || ''; const text = w.prompt('Free-form footnote text (optional):', '') || ''; const page = w.prompt('Page(s) (optional):', '') || ''; const value = w.prompt('Value (optional):', '') || ''; onSubmit({reference: reference.trim(), text: text.trim(), page: page.trim(), value: value.trim()}); return; } const $wrapper = $('
'); // Row 1: reference selector $wrapper.append('
'); const $ref = $(` `); $wrapper.find('.rf-row--ref').append($ref); // Row 2: free-form textarea $wrapper.append('
Or add free-form footnote text :
'); const $text = $(''); $wrapper.append($('
').append($text)); // Helper line $wrapper.append('
HTML tags can be used, e.g., <strong>, <em>, <a href="...">
'); // Row 3: pages + value side-by-side const $grid = $('
'); const $pageWrap = $(`
`); const $page = $(''); $pageWrap.append($page); const $valueWrap = $(`
`); const $value = $(''); $valueWrap.append($value); $grid.append($pageWrap, $valueWrap); $wrapper.append($grid); // Hint line $wrapper.append('
Leave blank for an automatic sequential reference number, or enter a custom footnote value
'); const dialog = Drupal.dialog($wrapper.get(0), { title: 'Reference Footnotes Dialog', width: 900, buttons: [ { text: 'OK', classes: 'button button--primary', click: function (event) { event.preventDefault(); console.log('[footnotes] OK clicked'); const reference = ($ref.val() || '').toString().trim(); const text = ($text.val() || '').toString().trim(); const page = ($page.val() || '').toString().trim(); const value = ($value.val() || '').toString().trim(); console.log('[footnotes] values', { reference, text, page, value }); if (!reference && !text) { console.warn('[footnotes] blocked: reference + text are empty'); $text.trigger('focus'); return; } // IMPORTANT: do the work first. console.log('[footnotes] calling onSubmit()'); onSubmit({ reference, text, page, value }); // Then close/destroy, but never let this prevent insertion. try { dialog.close(); } catch (e) { console.warn('[footnotes] dialog.close failed', e); } } }, { text: 'Cancel', click: function () { dialog.close(); dialog.destroy(); } } ] }); dialog.showModal(); setTimeout(() => $ref.trigger('focus'), 0); } class FootnotePicker2 extends Plugin { init() { const editor = this.editor; editor.ui.componentFactory.add('footnotePicker2', (locale) => { const button = new ButtonView(locale); const footnoteIcon = ` `; button.set({ label: 'Footnote', icon: footnoteIcon, tooltip: 'Insert footnote', withText: false, }); button.on('execute', () => { openFootnoteDialog(({reference, text: footnoteText, page, value}) => { const attrs = []; if (reference) attrs.push('reference="' + reference.replace(/"/g, '"') + '"'); if (page) attrs.push('page="' + page.replace(/"/g, '"') + '"'); if (value) attrs.push('value="' + value.replace(/"/g, '"') + '"'); const inner = footnoteText ? sanitizeFootnoteHtml(footnoteText) : ''; const html = '' + inner + ''; let viewFragment, modelFragment; try { viewFragment = editor.data.processor.toView(html); modelFragment = editor.data.toModel(viewFragment); } catch (e) { console.error('[footnotes] toModel failed', e); return; } console.log('[footnotes] inserting html:', html); console.log('[footnotes] modelFragment childCount:', modelFragment ? modelFragment.childCount : null); editor.model.change((writer) => { // If got stripped, modelFragment will be empty. if (!modelFragment || modelFragment.childCount === 0) { console.warn('[footnotes] was stripped; inserting as plain text fallback'); editor.model.insertContent(writer.createText(html), editor.model.document.selection); return; } editor.model.insertContent(modelFragment, editor.model.document.selection); }); }); }); return button; }); } } w.CKEditor5.footnotepicker2.FootnotePicker2 = FootnotePicker2; })();