pediatric-ai-scribe-v3/public/js/notes/editor.js
2026-05-08 02:44:22 +02:00

181 lines
8 KiB
JavaScript

import { esc, stripTags } from './utils.js';
export function createNotesEditor(options) {
options = options || {};
var editor = null;
function mount(container, initialHtml) {
var T = window.Tiptap || {};
if (!T.Editor) {
container.innerHTML = '<textarea class="notes-body-fallback" placeholder="Write your note...">' + esc(stripTags(initialHtml)) + '</textarea>';
editor = null;
var fb = container.querySelector('.notes-body-fallback');
if (fb) fb.addEventListener('input', function() { markDirty(); });
return;
}
container.innerHTML = toolbarHTML() + '<div class="tp-content"></div>';
editor = new T.Editor({
element: container.querySelector('.tp-content'),
extensions: [
T.StarterKit,
T.Link.configure({ openOnClick: false, autolink: true }),
T.Underline
],
content: initialHtml || '',
autofocus: false,
onUpdate: function() {
markDirty();
updateToolbarState(container);
},
onSelectionUpdate: function() { updateToolbarState(container); }
});
wireToolbar(container, editor);
}
function destroy() {
if (!editor) return;
editor.destroy();
editor = null;
}
function getBody() {
if (!editor) {
var fb = document.querySelector('.notes-body-fallback');
return fb ? fb.value : '';
}
var html = editor.getHTML();
return (html === '<p></p>' || html === '') ? '' : html;
}
function applyGenerated(container, body) {
if (editor && typeof editor.commands === 'object') {
try {
editor.commands.setContent(body || '<p></p>', { emitUpdate: true });
if (typeof options.onDirty === 'function') options.onDirty();
return;
} catch (err) {
console.warn('[notes] Tiptap setContent failed, rebuilding:', err && err.message);
}
}
destroy();
mount(container, body || '<p></p>');
if (typeof options.onDirty === 'function') options.onDirty();
}
function markDirty() {
if (typeof options.onDirty === 'function') options.onDirty();
if (typeof options.onStatus === 'function') options.onStatus('Saving...', 'saving');
if (typeof options.onAutosave === 'function') options.onAutosave();
}
function toolbarHTML() {
var btns = ''
+ '<button type="button" class="tp-btn" data-cmd="bold" title="Bold"><i class="fas fa-bold"></i></button>'
+ '<button type="button" class="tp-btn" data-cmd="italic" title="Italic"><i class="fas fa-italic"></i></button>'
+ '<button type="button" class="tp-btn" data-cmd="underline" title="Underline"><i class="fas fa-underline"></i></button>'
+ '<button type="button" class="tp-btn" data-cmd="strike" title="Strike"><i class="fas fa-strikethrough"></i></button>'
+ '<span class="tp-sep"></span>'
+ '<button type="button" class="tp-btn" data-cmd="h2" title="Heading 2">H2</button>'
+ '<button type="button" class="tp-btn" data-cmd="h3" title="Heading 3">H3</button>'
+ '<span class="tp-sep"></span>'
+ '<button type="button" class="tp-btn" data-cmd="bulletList" title="Bullet list"><i class="fas fa-list-ul"></i></button>'
+ '<button type="button" class="tp-btn" data-cmd="orderedList" title="Numbered list"><i class="fas fa-list-ol"></i></button>'
+ '<button type="button" class="tp-btn" data-cmd="blockquote" title="Quote"><i class="fas fa-quote-left"></i></button>'
+ '<button type="button" class="tp-btn" data-cmd="codeBlock" title="Code"><i class="fas fa-code"></i></button>'
+ '<span class="tp-sep"></span>'
+ '<button type="button" class="tp-btn" data-cmd="link" title="Link"><i class="fas fa-link"></i></button>'
+ '<span class="tp-sep"></span>'
+ '<button type="button" class="tp-btn" data-cmd="clear" title="Clear formatting"><i class="fas fa-remove-format"></i></button>';
return ''
+ '<div class="tp-toolbar">' + btns + '</div>'
+ '<div class="tp-link-bar" style="display:none;">'
+ '<input type="url" class="tp-link-input" placeholder="https://">'
+ '<button type="button" class="tp-link-apply">Apply</button>'
+ '<button type="button" class="tp-link-remove">Remove</button>'
+ '<button type="button" class="tp-link-cancel">x</button>'
+ '</div>';
}
function wireToolbar(wrap, ed) {
var toolbar = wrap.querySelector('.tp-toolbar');
var linkBar = wrap.querySelector('.tp-link-bar');
var linkInput = wrap.querySelector('.tp-link-input');
toolbar.addEventListener('mousedown', function(e) {
var btn = e.target.closest('.tp-btn[data-cmd]');
if (!btn) return;
e.preventDefault();
var cmd = btn.dataset.cmd;
switch (cmd) {
case 'bold': ed.chain().focus().toggleBold().run(); break;
case 'italic': ed.chain().focus().toggleItalic().run(); break;
case 'underline': ed.chain().focus().toggleUnderline().run(); break;
case 'strike': ed.chain().focus().toggleStrike().run(); break;
case 'h2': ed.chain().focus().toggleHeading({ level: 2 }).run(); break;
case 'h3': ed.chain().focus().toggleHeading({ level: 3 }).run(); break;
case 'bulletList': ed.chain().focus().toggleBulletList().run(); break;
case 'orderedList': ed.chain().focus().toggleOrderedList().run(); break;
case 'blockquote': ed.chain().focus().toggleBlockquote().run(); break;
case 'codeBlock': ed.chain().focus().toggleCodeBlock().run(); break;
case 'clear': ed.chain().focus().unsetAllMarks().clearNodes().run(); break;
case 'link':
if (linkBar.style.display === 'none') {
linkInput.value = ed.getAttributes('link').href || '';
linkBar.style.display = 'flex';
setTimeout(function() { linkInput.focus(); }, 0);
} else {
linkBar.style.display = 'none';
}
break;
}
updateToolbarState(wrap);
});
wrap.querySelector('.tp-link-apply').addEventListener('mousedown', function(e) {
e.preventDefault();
var url = linkInput.value.trim();
if (url) ed.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
linkBar.style.display = 'none';
});
wrap.querySelector('.tp-link-remove').addEventListener('mousedown', function(e) {
e.preventDefault();
ed.chain().focus().unsetLink().run();
linkBar.style.display = 'none';
});
wrap.querySelector('.tp-link-cancel').addEventListener('mousedown', function(e) {
e.preventDefault();
linkBar.style.display = 'none';
});
linkInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); wrap.querySelector('.tp-link-apply').dispatchEvent(new MouseEvent('mousedown')); }
if (e.key === 'Escape') { linkBar.style.display = 'none'; }
});
}
function updateToolbarState(wrap) {
if (!editor) return;
wrap.querySelectorAll('.tp-btn[data-cmd]').forEach(function(btn) {
var cmd = btn.dataset.cmd;
var active = false;
if (cmd === 'bold') active = editor.isActive('bold');
else if (cmd === 'italic') active = editor.isActive('italic');
else if (cmd === 'underline') active = editor.isActive('underline');
else if (cmd === 'strike') active = editor.isActive('strike');
else if (cmd === 'h2') active = editor.isActive('heading', { level: 2 });
else if (cmd === 'h3') active = editor.isActive('heading', { level: 3 });
else if (cmd === 'bulletList') active = editor.isActive('bulletList');
else if (cmd === 'orderedList') active = editor.isActive('orderedList');
else if (cmd === 'blockquote') active = editor.isActive('blockquote');
else if (cmd === 'codeBlock') active = editor.isActive('codeBlock');
else if (cmd === 'link') active = editor.isActive('link');
btn.classList.toggle('active', active);
});
}
return {
mount: mount,
destroy: destroy,
getBody: getBody,
applyGenerated: applyGenerated
};
}