181 lines
8 KiB
JavaScript
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
|
|
};
|
|
}
|