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 @@