const TOOLS = {
bold: { run: (c) => c.toggleBold(), active: (e) => e.isActive('bold') },
italic: { run: (c) => c.toggleItalic(), active: (e) => e.isActive('italic') },
underline: { run: (c) => c.toggleUnderline(), active: (e) => e.isActive('underline') },
strike: { run: (c) => c.toggleStrike(), active: (e) => e.isActive('strike') },
code: { run: (c) => c.toggleCode(), active: (e) => e.isActive('code') },
link: { run: link, active: (e) => e.isActive('link') },
'heading-1': heading(1),
'heading-2': heading(2),
'heading-3': heading(3),
'bullet-list': { run: (c) => c.toggleBulletList(), active: (e) => e.isActive('bulletList') },
'ordered-list': { run: (c) => c.toggleOrderedList(), active: (e) => e.isActive('orderedList') },
blockquote: { run: (c) => c.toggleBlockquote(), active: (e) => e.isActive('blockquote') },
'code-block': { run: (c) => c.toggleCodeBlock(), active: (e) => e.isActive('codeBlock') },
'horizontal-rule': { run: (c) => c.setHorizontalRule() },
undo: { run: (c) => c.undo(), enabled: (e) => e.can().undo() },
redo: { run: (c) => c.redo(), enabled: (e) => e.can().redo() },
};
function heading(level) {
return {
run: (c) => c.toggleHeading({ level }),
active: (e) => e.isActive('heading', { level }),
};
}
function allowedUri(url) {
const scheme = /^\s*([a-z][a-z0-9+.-]*):/i.exec(url);
return !scheme || ['http', 'https', 'mailto'].includes(scheme[1].toLowerCase());
}
function link(chain, editor, win, button) {
const current = editor.getAttributes('link').href || '';
const href = win.prompt(button.getAttribute('data-rich-text-prompt') || '', current);
if (href === null) return chain;
const range = chain.extendMarkRange('link');
return href.trim() ? range.setLink({ href: href.trim() }) : range.unsetLink();
}
function starterKit(tools) {
const on = (tool, options = {}) => (tools.has(tool) ? options : false);
const levels = [1, 2, 3].filter((level) => tools.has(`heading-${level}`));
const lists = tools.has('bullet-list') || tools.has('ordered-list') ? {} : false;
return {
bold: on('bold'),
italic: on('italic'),
underline: on('underline'),
strike: on('strike'),
code: on('code'),
link: on('link', {
openOnClick: false,
defaultProtocol: 'https',
HTMLAttributes: { target: null, rel: 'noopener noreferrer nofollow' },
isAllowedUri: allowedUri,
}),
heading: levels.length ? { levels } : false,
bulletList: on('bullet-list'),
orderedList: on('ordered-list'),
listItem: lists,
listKeymap: lists,
blockquote: on('blockquote'),
codeBlock: on('code-block'),
horizontalRule: on('horizontal-rule'),
};
}
function toolsOf(root) {
return new Set((root.getAttribute('data-rich-text-tools') || '').split(' ').filter(Boolean));
}
function editorAttributes(source) {
const attributes = { role: 'textbox', 'aria-multiline': 'true' };
const label = source.labels && source.labels[0];
if (label) {
if (!label.id) label.id = `${source.id}-label`;
attributes['aria-labelledby'] = label.id;
}
for (const name of ['aria-describedby', 'aria-invalid', 'aria-required']) {
const value = source.getAttribute(name);
if (value !== null) attributes[name] = value;
}
return attributes;
}
function syncToolbar(toolbar, editor) {
if (!toolbar || !editor.isEditable) return;
for (const button of toolbar.querySelectorAll('[data-rich-text-tool]')) {
const tool = TOOLS[button.getAttribute('data-rich-text-tool')];
if (!tool) continue;
if (tool.active) button.setAttribute('aria-pressed', String(tool.active(editor)));
if (tool.enabled) button.disabled = !tool.enabled(editor);
}
keepTabStop(toolbar);
}
function keepTabStop(toolbar) {
const buttons = Array.from(toolbar.querySelectorAll('[data-rich-text-tool]'));
const enabled = buttons.filter((button) => !button.disabled);
const stop = enabled.find((button) => button.tabIndex === 0) || enabled[0];
for (const button of buttons) button.tabIndex = button === stop ? 0 : -1;
}
function attached(root) {
const editor = root.richTextEditor;
const content = root.querySelector('[data-rich-text-content]');
return !!content && !content.hidden && !!editor.view && content.contains(editor.view.dom);
}
function unmount(root) {
root.richTextEditor.destroy();
delete root.richTextEditor;
}
function mount(root, tiptap, value) {
if (root.richTextEditor) return root.richTextEditor;
const source = root.querySelector('[data-rich-text-source]');
const content = root.querySelector('[data-rich-text-content]');
const toolbar = root.querySelector('[data-rich-text-toolbar]');
if (!source || !content) return null;
if (value !== undefined) source.value = value;
const extensions = [tiptap.StarterKit.configure(starterKit(toolsOf(root)))];
const placeholder = source.getAttribute('placeholder');
if (placeholder) extensions.push(tiptap.Placeholder.configure({ placeholder }));
const editor = new tiptap.Editor({
element: content,
content: source.value,
editable: !source.disabled,
extensions,
editorProps: { attributes: editorAttributes(source) },
onUpdate: ({ editor }) => {
source.value = editor.isEmpty ? '' : editor.getHTML();
},
onTransaction: ({ editor }) => syncToolbar(toolbar, editor),
});
source.required = false;
source.hidden = true;
content.hidden = false;
if (toolbar) {
toolbar.hidden = false;
keepTabStop(toolbar);
syncToolbar(toolbar, editor);
}
root.richTextEditor = editor;
return editor;
}
function press(button, win) {
const root = button.closest('[data-rich-text]');
const editor = root && root.richTextEditor;
const tool = TOOLS[button.getAttribute('data-rich-text-tool')];
if (!editor || !tool) return false;
return tool.run(editor.chain().focus(), editor, win, button).run();
}
function rove(button, key) {
const toolbar = button.closest('[data-rich-text-toolbar]');
if (!toolbar) return false;
const buttons = Array.from(toolbar.querySelectorAll('[data-rich-text-tool]')).filter(
(candidate) => !candidate.disabled,
);
const at = buttons.indexOf(button);
const next = {
ArrowRight: buttons[(at + 1) % buttons.length],
ArrowLeft: buttons[(at - 1 + buttons.length) % buttons.length],
Home: buttons[0],
End: buttons[buttons.length - 1],
}[key];
if (!next || at < 0) return false;
button.tabIndex = -1;
next.tabIndex = 0;
next.focus();
return true;
}
const modules = new Map();
function load(url) {
if (!modules.has(url)) {
modules.set(
url,
import(url).catch((error) => {
modules.delete(url);
throw error;
}),
);
}
return modules.get(url);
}
const mounted = new Set();
function scan(doc, loadModule = load) {
const remount = new Set();
for (const root of mounted) {
if (root.isConnected && attached(root)) continue;
unmount(root);
mounted.delete(root);
if (root.isConnected) remount.add(root);
}
const meta = doc.querySelector('meta[name="tablo-tiptap"]');
const url = meta && meta.getAttribute('content');
if (!url) return;
for (const root of doc.querySelectorAll('[data-rich-text]')) {
if (root.richTextEditor) continue;
const source = root.querySelector('[data-rich-text-source]');
const value = remount.has(root) && source ? source.defaultValue : undefined;
loadModule(url).then(
(tiptap) => {
if (root.isConnected && mount(root, tiptap, value)) mounted.add(root);
},
(error) => console.error('tablo: the rich text editor did not load', error),
);
}
}
function touchesControl(records) {
const control = (node) =>
node.nodeType === 1 &&
(node.matches('[data-rich-text]') || !!node.querySelector('[data-rich-text]'));
return records.some(
(record) =>
(record.target.matches && record.target.matches('[data-rich-text-content]')) ||
Array.from(record.addedNodes).some(control) ||
Array.from(record.removedNodes).some(control),
);
}
function install(doc, win) {
doc.addEventListener('mousedown', (e) => {
if (e.target.closest && e.target.closest('[data-rich-text-tool]')) e.preventDefault();
});
doc.addEventListener('click', (e) => {
const button = e.target.closest && e.target.closest('[data-rich-text-tool]');
if (button) press(button, win);
});
doc.addEventListener('keydown', (e) => {
const button = e.target.closest && e.target.closest('[data-rich-text-tool]');
if (button && rove(button, e.key)) e.preventDefault();
});
scan(doc);
if (typeof MutationObserver !== 'undefined' && doc.documentElement) {
new MutationObserver((records) => {
if (touchesControl(records)) scan(doc);
}).observe(doc.documentElement, {
childList: true,
subtree: true,
});
}
}
if (typeof document !== 'undefined') {
install(document, window);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { allowedUri, install, mount, press, rove, scan, starterKit, touchesControl };
}