diff --git a/src/ttfrog/themes/default/static/froghat-editor.css b/src/ttfrog/themes/default/static/froghat-editor.css index 792e3db..d538c19 100644 --- a/src/ttfrog/themes/default/static/froghat-editor.css +++ b/src/ttfrog/themes/default/static/froghat-editor.css @@ -7,21 +7,38 @@ --toolbar-height: 32px !important; --toolbar-spacing: 5px !important; - --toolbar-button-size: 24px; - --toolbar-icon-size: 16px; + --toolbar-button-size: 32px; --toolbar-button-enabled-background: rgba(128, 192, 128); --toolbar-button-active-background: rgba(192, 255, 192); --toolbar-button-enabled-border: 1px solid #000; - /* Icons by Flaticon: https://www.flaticon.com/uicons */ - --toolbar-icon-bold: url('data:image/svg+xml,'); - --toolbar-icon-italic: url('data:image/svg+xml,'); - --toolbar-icon-underline: url('data:image/svg+xml,'); - --toolbar-icon-bullet_list: url('data:image/svg+xml,'); - --toolbar-icon-center: url('data:image/svg+xml,'); + --toolbar-icon-bold: url('data:image/svg+xml,'); + --toolbar-icon-italic: url('data:image/svg+xml,'); + /* + --toolbar-icon-underline: url('data:image/svg+xml,'); + */ + --toolbar-icon-h1: url('data:image/svg+xml,'); + --toolbar-icon-h2: url('data:image/svg+xml,'); + --toolbar-icon-h3: url('data:image/svg+xml,'); + --toolbar-icon-h4: url('data:image/svg+xml,'); + --toolbar-icon-h5: url('data:image/svg+xml,'); + --toolbar-icon-h6: url('data:image/svg+xml,'); + --toolbar-icon-unordered_list: url('data:image/svg+xml,'); + --toolbar-icon-ordered_list: url('data:image/svg+xml,'); + --toolbar-icon-line: url('data:image/svg+xml,'); + --toolbar-icon-quote: url('data:image/svg+xml,'); + --toolbar-icon-link: url('data:image/svg+xml,'); + --toolbar-icon-table: url('data:image/svg+xml,'); + --toolbar-icon-macro: url('data:image/svg+xml,'); + --toolbar-icon-macro_user: url('data:image/svg+xml,'); + --toolbar-icon-macro_toc: url('data:image/svg+xml,'); + --toolbar-icon-macro_style: url('data:image/svg+xml,'); + + --toolbar-icon-markdown: url('data:image/svg+xml,'); + --toolbar-icon-save: url('data:image/svg+xml,'); + --toolbar-icon-toggle: url('data:image/svg+xml,'); - --toolbar-icon-wysiwyg: url('data:image/svg+xml,'); - --toolbar-icon-save: url('data:image/svg+xml,'); + } #froghat[contenteditable] { @@ -39,6 +56,10 @@ } #froghat.wysiwyg { + span { + display: inline; + border-bottom: 1px solid green; + } } main.editing { @@ -64,31 +85,77 @@ main.editing { li { padding: 0px; - padding-right: 5px; - margin: 0px; + margin: 2px; text-align: center; line-height: var(--toolbar-button-size); width: var(--toolbar-button-size); height: var(--toolbar-button-size); - a { - padding: var(--toolbar-spacing); + button { opacity: 0.3; display: block; - width: var(--toolbar-icon-size); - height: var(--toolbar-icon-size); background-repeat: no-repeat; background-attachment: local; background-position: center; + background-size: 1.5rem 1.5rem; border-radius: 5px; border: 1px solid transparent; + width: var(--toolbar-button-size); + height: var(--toolbar-button-size); } - #wysiwyg { background-image: var(--toolbar-icon-wysiwyg); } - #bold { background-image: var(--toolbar-icon-bold); } - #italic { background-image: var(--toolbar-icon-italic); } - #underline { background-image: var(--toolbar-icon-underline); } - #bullet_list { background-image: var(--toolbar-icon-bullet_list); } - #center { background-image: var(--toolbar-icon-center); } + + button.enabled { + opacity: 1.0; + cursor: pointer; + } + button.on { + border: var(--toolbar-button-enabled-border); + background-color: var(--toolbar-button-enabled-background); + } + button:hover { + border: var(--toolbar-button-enabled-border); + background-color: var(--toolbar-button-active-background); + } + + .dropdown-menu { + position-area: bottom; + margin: 0; + border: 0; + width: var(--toolbar-button-size); + min-height: var(--toolbar-button-size); + } + + + #bold { background-image: var(--toolbar-icon-bold); } + #italic { background-image: var(--toolbar-icon-italic); } + #underline { background-image: var(--toolbar-icon-underline); } + + #header { background-image: var(--toolbar-icon-h1); anchor-name: "header"; } + #h1 { background-image: var(--toolbar-icon-h1); } + #h2 { background-image: var(--toolbar-icon-h2); } + #h3 { background-image: var(--toolbar-icon-h3); } + #h4 { background-image: var(--toolbar-icon-h4); } + #h5 { background-image: var(--toolbar-icon-h5); } + #h6 { background-image: var(--toolbar-icon-h6); } + + #list { background-image: var(--toolbar-icon-unordered_list); anchor-name: "list"; } + #unordered_list { background-image: var(--toolbar-icon-unordered_list); } + #ordered_list { background-image: var(--toolbar-icon-ordered_list); } + + + #line { background-image: var(--toolbar-icon-line); } + #quote { background-image: var(--toolbar-icon-quote); } + #link { background-image: var(--toolbar-icon-link); } + #table { background-image: var(--toolbar-icon-table); } + + #macro { background-image: var(--toolbar-icon-macro); anchor-name: "macro"} + #macro_user { background-image: var(--toolbar-icon-macro_user); } + #macro_toc { background-image: var(--toolbar-icon-macro_toc); } + #macro_style { background-image: var(--toolbar-icon-macro_style); } + + #markdown { background-image: var(--toolbar-icon-markdown); } + #save { background-image: var(--toolbar-icon-save); } + #toggle { background-color: var(--toolbar-button-enabled-background); background-image: var(--toolbar-icon-toggle); @@ -105,7 +172,9 @@ main.editing { border: var(--toolbar-button-enabled-border); background-color: var(--toolbar-button-active-background); } - #save { background-image: var(--toolbar-icon-save); } + + #header-menu { position-anchor: "header"; } + #list-menu { position-anchor: "list"; } } li:last-child { @@ -120,20 +189,4 @@ main.editing { border-radius: 0px; border: 1px solid green; border-bottom: 1px solid transparent; - ul { - li { - a { - opacity: 1.0; - cursor: pointer; - } - a.on { - border: var(--toolbar-button-enabled-border); - background-color: var(--toolbar-button-enabled-background); - } - a:hover { - border: var(--toolbar-button-enabled-border); - background-color: var(--toolbar-button-active-background); - } - } - } } diff --git a/src/ttfrog/themes/default/static/froghat-editor.js b/src/ttfrog/themes/default/static/froghat-editor.js index 83dc5f3..b1a8d68 100644 --- a/src/ttfrog/themes/default/static/froghat-editor.js +++ b/src/ttfrog/themes/default/static/froghat-editor.js @@ -1,27 +1,109 @@ +function reEscape(string) { + return string.replaceAll('*', '\\*'); +} + class ToolbarButton { + constructor(settings) { this.id = settings.id; this.element = settings.element || document.getElementById(this.id); this.toolbar = settings.toolbar; - + this.isMenu = settings.isMenu || false; + this.open = settings.open || ''; + this.close = settings.close || ''; + this.tag = settings.tag || null; this.onclick = settings.onclick; - this.element.addEventListener('click', (e) => { - if (this.element.enabled) { - this.onclick({clickEvent: e, button: this }); + + + if (!this.isMenu) { + if (this.open) { + + var open = reEscape(this.open); + var close = this.close ? reEscape(this.close) : ''; + var leading = '(?^(?:(?!' + open + ').)*)'; + var middle = '(?(?:(?!' + open; + if (this.close && this.close != this.open) { + middle += '|' + close + ').)+)'; + } else { + middle += ').)*)'; + } + this.pattern = RegExp( + leading + + '(?' + open + ')' + + middle + + (close ? '(?(?:' + close + ')(\\s.*?|$))' : '') + + '$' + ); + console.log(this.id, this.pattern); } - }); + + this.element.addEventListener('click', (e) => { + if (this.element.enabled) { + if (this.onclick) { + this.onclick({clickEvent: e, button: this }); + } else if (this.toolbar.editor.isWysiwyg()) { + this.#applyHTMLFormatting(); + } else if (this.toolbar.editor.isMarkdown()) { + this.#applyMarkdownFormatting(); + } + } + document.querySelectorAll(".dropdown-menu:popover-open").forEach(el => { + el.hidePopover(); + }); + this.toolbar.editor.element.focus(); + }); + } if (settings.enabled) { this.enable(); } } + #applyHTMLFormatting() { + if (!this.tag) { + return; + } + var selection = window.getSelection(); + var range = selection.getRangeAt(0); + var node = document.createElement(this.tag); + try { + range.surroundContents(node); + range.setStartAfter(node); + } catch(e) { + console.log(e); + } + selection.removeAllRanges(); + selection.addRange(range); + this.toolbar.editor.moveCursorAfter(node); + } + + #applyMarkdownFormatting() { + if (!this.open) { + return; + } + var selection = window.getSelection(); + var range = selection.getRangeAt(0); + var node = document.createTextNode(this.open + range.toString() + this.close); + range.deleteContents(); + range.insertNode(node); + range.setStartAfter(node); + this.toolbar.editor.moveCursorAfter(node); + } + + click() { + if (this.element.enabled) { + this.element.click(); + } + } + enable() { this.element.enabled = true; + this.element.classList.add('enabled'); } disable() { this.element.enabled = false; + this.element.classList.remove('enabled'); } on() { @@ -37,42 +119,85 @@ class ToolbarButton { } } + class FroghatToolbar { constructor(settings) { this.editor = settings.editor; this.element = settings.toolbar || document.getElementById('toolbar'); + this.currentContext = null; this.buttons = { - 'bold': new ToolbarButton({ toolbar: this, id: 'bold', onclick: this.#click_bold }), - 'italic': new ToolbarButton({ toolbar: this, id: 'italic', onclick: this.#click_italic }), - 'underline': new ToolbarButton({ toolbar: this, id: 'underline', onclick: this.#click_underline }), - 'bullet_list': new ToolbarButton({ toolbar: this, id: 'bullet_list', onclick: this.#click_bullet_list }), - 'center': new ToolbarButton({ toolbar: this, id: 'center', onclick: this.#click_center }), - 'save': new ToolbarButton({ toolbar: this, id: 'save', onclick: this.#click_save }), - 'wysiwyg': new ToolbarButton({ toolbar: this, id: 'wysiwyg', onclick: this.#click_wysiwyg, enabled: true }), - 'toggle': new ToolbarButton({ toolbar: this, id: 'toggle', onclick: this.#click_toggle, enabled: true }) + 'line': new ToolbarButton({ toolbar: this, id: 'line', open: '***', close: '', tag: 'HR'}), + 'bold': new ToolbarButton({ toolbar: this, id: 'bold', open: '**', close: '**', tag: 'STRONG'}), + 'italic': new ToolbarButton({ toolbar: this, id: 'italic', open: '*', close: '*', tag: 'EM'}), + 'header': new ToolbarButton({ toolbar: this, id: 'header', isMenu: true}), + 'h1': new ToolbarButton({ toolbar: this, id: 'h1', markdown: '# ', close: "", tag: 'H1' }), + 'h2': new ToolbarButton({ toolbar: this, id: 'h2', markdown: '## ', close: "", tag: 'H2' }), + 'h3': new ToolbarButton({ toolbar: this, id: 'h3', markdown: '### ', close: "", tag: 'H3' }), + 'h4': new ToolbarButton({ toolbar: this, id: 'h4', markdown: '#### ', close: "", tag: 'H4' }), + 'h5': new ToolbarButton({ toolbar: this, id: 'h5', markdown: '##### ', close: "", tag: 'H5' }), + 'h6': new ToolbarButton({ toolbar: this, id: 'h6', markdown: '###### ', close: "", tag: 'H6' }), + 'list': new ToolbarButton({ toolbar: this, id: 'list', isMenu: true}), + 'unordered_list': new ToolbarButton({ toolbar: this, id: 'unordered_list', }), + 'ordered_list': new ToolbarButton({ toolbar: this, id: 'ordered_list', }), + 'link': new ToolbarButton({ toolbar: this, id: 'link', }), + 'quote': new ToolbarButton({ toolbar: this, id: 'quote', }), + 'table': new ToolbarButton({ toolbar: this, id: 'table', }), + 'macro': new ToolbarButton({ toolbar: this, id: 'macro', isMenu: true}), + 'macro_user': new ToolbarButton({ toolbar: this, id: 'macro_user', }), + 'macro_toc': new ToolbarButton({ toolbar: this, id: 'macro_toc', }), + 'macro_style': new ToolbarButton({ toolbar: this, id: 'macro_style', }), + 'save': new ToolbarButton({ toolbar: this, id: 'save', onclick: this.#click_save }), + 'markdown': new ToolbarButton({ toolbar: this, id: 'markdown', onclick: this.#click_markdown }), + 'toggle': new ToolbarButton({ toolbar: this, id: 'toggle', onclick: this.#click_toggle, enabled: true }) } } - #click_bold({clickEvent, button}) { + getContext() { + var context = null; + var node = window.getSelection().baseNode; + console.log({node}); + Object.values(this.buttons).forEach(button => { + if (button.pattern) { + var closed = false; + var leading = ""; + var matched = false; + matched = node.textContent.match(button.pattern); + if (matched && matched.groups) { + closed = matched.groups.closed; + leading = matched.groups.leading; + } + if (closed && matched.groups.middle) { + context = { + button: button, + closed: closed, + leading: leading, + node: node, + element: null + } + } + if (node.parentElement && node.parentElement.nodeName == button.tag) { + if (!context) { + context = { + button: button, + element: null + }; + } + context.element = node.parentElement + } + if (context) { + return; + } + } + }); + return context; } - #click_italic({clickEvent, button}) { - } - - #click_underline({clickEvent, button}) { - } - - #click_bullet_list({clickEvent, button}) { - } - - #click_center({clickEvent, button}) { - } #click_save({clickEvent, button}) { } - #click_wysiwyg({clickEvent, button}) { - button.toolbar.editor.toggleWysiwyg(); + #click_markdown({clickEvent, button}) { + button.toolbar.editor.toggleMarkdown(); } #click_toggle({clickEvent, button}) { @@ -80,14 +205,14 @@ class FroghatToolbar { } enable() { - button.toolbar.element.classList.add("enabled"); - button.toolbar.buttons.forEach(button => { button.enable() }); + this.element.classList.add("enabled"); + Object.values(this.buttons).forEach(button => { button.enable() }); } disable() { - button.toolbar.element.classList.remove("enabled"); - button.toolbar.buttons.forEach(button => { button.disable() }); - button.toolbar.buttons.toggle.enable(); + this.element.classList.remove("enabled"); + Object.values(this.buttons).forEach(button => { button.disable() }); + this.buttons.toggle.enable(); } } @@ -105,6 +230,8 @@ class FroghatEditor extends Froghat { this.turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', + emDelimiter: '*', + strongDelimiter: '**', }); this.turndown.use([turndownPluginGfm.gfm, turndownPluginGfm.tables]); this.turndown.keep(['pre']); @@ -114,20 +241,71 @@ class FroghatEditor extends Froghat { this.element.classList.add("loaded"); this.view(); } + + #replaceWysiwygNode(context) { + var offset = context.leading.length || 0; + var slice = context.node.textContent.slice(offset); + + var html = this.markdownToHTML(slice); + html = html.replace("

", "").replace("

", ""); + + var el = document.createElement("span"); + el.innerHTML = context.leading + html; + console.log(el.innerHTML); + if (el.innerHTML != context.node.innerHTML) { + context.node.replaceWith(el); + } + return el; + } + + #refreshMarkdownCache() { + if (this.cachedMarkdown != this.element.textContent) { + this.changed = true; + this.toolbar.buttons.save.enable(); + this.cachedMarkdown = this.element.textContent; + } + } + + #handleEditorChanges(evt) { + var context = this.toolbar.getContext(); + if (!context) { + if (this.toolbar.currentContext) { + this.toolbar.currentContext.button.off(); + this.toolbar.currentContext = null; + } + return; + } + + if (this.toolbar.currentContext) { + this.toolbar.currentContext.button.off(); + } + + context.button.on(); + this.toolbar.currentContext = context; + + if (this.isWysiwyg()) { + if (context.closed) { + context.node = this.#replaceWysiwygNode(context); + context.button.off(); + this.toolbar.currentContext = null; + this.toolbar.editor.moveCursorAfter(context.node); + } + } else if (context.button) { + context.button.on(); + } + } #bindEvents() { this.element.addEventListener('keydown', (evt) => { - if (this.state === this.states.VIEW) { + if (! this.isEditing()) { return; } - if (this.cachedMarkdown != this.element.textContent) { - this.changed = true; - this.cachedMarkdown = this.element.textContent; - } + this.#refreshMarkdownCache(); + this.#handleEditorChanges(evt); }); }; - toggleWysiwyg() { + toggleMarkdown() { if (this.getState() === this.states.EDIT) { this.wysiwyg(); } else { @@ -136,11 +314,12 @@ class FroghatEditor extends Froghat { } toggleView() { - this.toolbar.element.classList.toggle("enabled"); if (this.getState() === this.states.VIEW) { + this.toolbar.enable(); this.wysiwyg(); this.element.focus(); } else { + this.toolbar.disable(); this.view(); } } @@ -183,7 +362,8 @@ class FroghatEditor extends Froghat { } }); this.setState(this.states.WYSIWYG); - this.toolbar.buttons.wysiwyg.on(); + this.toolbar.buttons.markdown.off(); + this.toolbar.buttons.markdown.enable(); document.getElementById("main").classList.add("editing"); } @@ -197,19 +377,45 @@ class FroghatEditor extends Froghat { this.element.contentEditable = true; this.element.innerHTML = encodeHtmlEntities(this.getMarkdown()); this.setState(this.states.EDIT); - this.toolbar.buttons.wysiwyg.off(); + this.toolbar.buttons.markdown.on(); document.getElementById("main").classList.add("editing"); } - insertAtCursor(node) { - var sel, range, html; - sel = window.getSelection(); - range = sel.getRangeAt(0); - range.deleteContents(); - range.insertNode(node); - range.setStartAfter(node); - this.element.focus(); - sel.removeAllRanges(); - sel.addRange(range); + moveCursorAfter(node) { + var dummyElement = null; + if (!node.nextElementSibling) { + // workaround for https://issues.chromium.org/issues/41239578 + var dummyElement = document.createElement('a'); + dummyElement.innerHTML="​"; + dummyElement.className = 'bugfix'; + node.parentNode.appendChild(dummyElement); + } + var nextElement = node.nextElementSibling; + nextElement.tabIndex=0; + nextElement.focus(); + var range = document.createRange(); + range.setStart(nextElement.childNodes[0], 1); + range.setEnd(nextElement.childNodes[0], 1); + + var selection = window.getSelection(); + + console.log(range); + console.log(selection); + + selection.removeAllRanges(); + selection.addRange(range); } + + isWysiwyg() { + return this.state === this.states.WYSIWYG; + } + + isMarkdown() { + return this.state === this.states.EDIT; + } + + isEditing() { + return this.isWysiwyg() || this.isMarkdown(); + } + } diff --git a/src/ttfrog/themes/default/toolbar.html b/src/ttfrog/themes/default/toolbar.html index 1d49459..9649e65 100644 --- a/src/ttfrog/themes/default/toolbar.html +++ b/src/ttfrog/themes/default/toolbar.html @@ -1,17 +1,43 @@