diff --git a/dev/Common/HtmlEditor.js b/dev/Common/HtmlEditor.js index f1539fe8e..084c98ffc 100644 --- a/dev/Common/HtmlEditor.js +++ b/dev/Common/HtmlEditor.js @@ -1,11 +1,10 @@ import { EventKeyCode } from 'Common/Enums'; +import { SquireUI } from 'External/SquireUI'; /** * @type {Object} */ -const doc = document, - -CKEditorDefaultConfig = { +const CKEditorDefaultConfig = { 'title': false, 'stylesSet': false, 'customConfig': '', @@ -43,35 +42,6 @@ CKEditorDefaultConfig = { 'fontSize_sizes': '10/10px;12/12px;13/13px;14/14px;16/16px;18/18px;20/20px;24/24px;28/28px;36/36px;48/48px' }, -SquireDefaultConfig = { -/* - blockTag: 'DIV', - blockAttributes: null, - tagAttributes: { - blockquote: null, - ul: null, - ol: null, - li: null, - a: null - }, - classNames: { - colour: 'colour', - fontFamily: 'font', - fontSize: 'size', - highlight: 'highlight' - }, - leafNodeNames: leafNodeNames, - undo: { - documentSizeThreshold: -1, // -1 means no threshold - undoLimit: -1 // -1 means no limit - }, - isInsertedHTMLSanitized: true, - isSetHTMLSanitized: true, - willCutCopy: null, - addLinks: true // allow_smart_html_links -*/ -}, - /** * @type {Object} */ @@ -108,232 +78,6 @@ htmlEditorLangsMap = { 'zh_tw': 'zh' }; -class SquireUI -{ - constructor(container) { - const actions = { - source: { - html: '〈〉', - cmd: () => this.doAction('bold','B') - }, -/* - bidi: { - allowHtmlEditorBitiButtons - }, -*/ - bold: { - html: '𝐁', - cmd: () => this.doAction('bold','B') - }, - italic: { - html: '𝐼', - cmd: () => this.doAction('italic','I') - }, - underline: { - html: 'U', - cmd: () => this.doAction('underline','U'), - style: 'text-decoration:underline;' - }, - strike: { - html: 'S', - cmd: () => this.doAction('strikethrough','S'), - style: 'text-decoration:line-through;' - }, - sub: { - html: 'Sx', - cmd: () => this.doAction('subscript','SUB') - }, - sup: { - html: 'Sx', - cmd: () => this.doAction('superscript','SUP') - }, - ol: { - html: '#', - cmd: () => this.doList('OL') - }, - ul: { - html: '⋮', - cmd: () => this.doList('UL') - }, - quote: { - html: '"', - cmd: () => { - let parent = this.getParentNodeName('UL,OL'); - (parent && 'BLOCKQUOTE' == parent) ? this.squire.decreaseQuoteLevel() : this.squire.increaseQuoteLevel(); - } - }, - indentDecrease: { - html: '⇤', - cmd: () => this.squire.changeIndentationLevel('decrease') - }, - indentIncrease: { - html: '⇥', - cmd: () => this.squire.changeIndentationLevel('increase') - }, - link: { - html: '🔗', - cmd: () => { - if ('A' === this.getParentNodeName()) { - this.squire.removeLink(); - } else { - let url = prompt("Link","https://"); - url != null && url.length && this.squire.makeLink(url); - } - } - }, - image: { - html: '📷🖼️', - cmd: () => { - if ('IMG' === this.getParentNodeName()) { -// wysiwyg.removeLink(); - } else { - let src = prompt("Image","https://"); - src != null && src.length && this.squire.insertImage(src); - } - } - }, - undo: { - html: '↶', - cmd: () => this.squire.undo() - }, - redo: { - html: '↷', - cmd: () => this.squire.redo() - }, - }, - - content = doc.createElement('div'), - toolbar = doc.createElement('div'), - squire = new Squire(content, SquireDefaultConfig); - - content.id = 'squire-content'; - content.style.minHeight = '200px'; - - this.squire = squire; - this.content = content; - - toolbar.id = 'squire-toolbar'; - for (let action in actions) { - if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) { - continue; - } - let cfg = actions[action], - btn = cfg.btn = doc.createElement('button'); - btn.type = 'button'; - btn.dataset.action = action; - btn.action_cmd = cfg.cmd; - btn.innerHTML = cfg.html; - btn.style.padding = 0; - cfg.style && btn.setAttribute('style', cfg.style); - toolbar.append(btn); - } - toolbar.addEventListener('click', e => { - e.target.action_cmd && e.target.action_cmd(); - }); - - actions.undo.btn.disabled = actions.redo.btn.disabled = true; - squire.addEventListener('undoStateChange', state => { - actions.undo.btn.disabled = !state.canUndo; - actions.redo.btn.disabled = !state.canRedo; - }); - - container.append(toolbar, content); - -/* -squire-raw.js:2161: this.fireEvent( 'dragover', { -squire-raw.js:2168: this.fireEvent( 'drop', { -squire-raw.js:2583: this.fireEvent( event.type, event ); -squire-raw.js:2864: this.fireEvent( 'pathChange', { path: newPath } ); -squire-raw.js:2867: this.fireEvent( range.collapsed ? 'cursor' : 'select', { -squire-raw.js:3004: this.fireEvent( 'input' ); -squire-raw.js:3080: this.fireEvent( 'input' ); -squire-raw.js:3101: this.fireEvent( 'input' ); -squire-raw.js:4036: this.fireEvent( 'willPaste', event ); -squire-raw.js:4089: this.fireEvent( 'willPaste', event ); -*/ - - // CKEditor gimmicks - this.mode = 'wysiwyg'; // 'plain' - this.plugins = { - plain: false - }; - // .plugins.plain && this.editor.__plain - this.focusManager = { - hasFocus: () => squire._isFocused, - blur: () => squire.blur() - }; - } - - doAction(name, tag) { - if (this.testPresenceinSelection(tag, new RegExp('>'+tag+'\\b'))) { - name = 'remove' + (name.toUpperCase()[0]) + name.substr(1); - } - this.squire[name](); - } - - getParentNodeName(selector) { - let parent = this.squire.getSelectionClosest(selector); - return parent ? parent.nodeName : null; - } - - doList(type) { - let parent = this.getParentNodeName('UL,OL'), - fn = {UL:'makeUnorderedList',OL:'makeOrderedList'}; - (parent && parent == type) ? this.squire.removeList() : this.squire[fn[type]](); - } - - testPresenceinSelection(format, validation) { - return validation.test(this.squire.getPath()) | this.squire.hasFormat(format); - } - - // CKeditor gimmicks - setMode(mode) { - this.mode = mode; // 'wysiwyg' or 'plain' - } - - on(type, fn) { - this.squire.addEventListener(type, fn); - } - - execCommand(cmd, cfg) { - if ('insertSignature' == cmd) { - if (cfg.clearCache) { - // remove it; - } else { - cfg.isHtml; // bool - cfg.insertBefore; // bool - cfg.signature; // string - } - } - } - - checkDirty() { - return false; - } - - resetDirty() {} - - getData() { - return this.squire.getHTML(); - } - - setData(html) { - this.squire.setHTML(html); - } - - focus() { - this.squire.focus(); - } - - resize(width, height) { - this.content.style.height = Math.max(200, (height - this.content.offsetTop)) + 'px'; - } - - setReadOnly(bool) { - this.content.contentEditable = !!bool; - } -} - class HtmlEditor { editor; blurTimer = 0; diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js new file mode 100644 index 000000000..47ca9dfce --- /dev/null +++ b/dev/External/SquireUI.js @@ -0,0 +1,389 @@ +/* eslint max-len: 0 */ + +const doc = document, + +allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,FONT,H1,H2,H3,H4,H5,H6,HR,IMG,LABEL,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL', +allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','), + +SquireDefaultConfig = { +/* + blockTag: 'P', + blockAttributes: null, + tagAttributes: { + blockquote: null, + ul: null, + ol: null, + li: null, + a: null + }, + classNames: { + colour: 'colour', + fontFamily: 'font', + fontSize: 'size', + highlight: 'highlight' + }, + leafNodeNames: leafNodeNames, + undo: { + documentSizeThreshold: -1, // -1 means no threshold + undoLimit: -1 // -1 means no limit + }, + isInsertedHTMLSanitized: true, + isSetHTMLSanitized: true, + willCutCopy: null, + addLinks: true // allow_smart_html_links +*/ + sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { + const frag = doc.createDocumentFragment(); + frag.innerHTML = html; + if (isPaste) { + frag.querySelectorAll(':not('+allowedElements+')').forEach(el => el.remove()); + frag.querySelectorAll(allowedElements).forEach(el => { + if (el.hasAttributes()) { + Array.from(el.attributes).forEach(attr => { + let name = attr.name.toLowerCase(); + if (!allowedAttributes.includes(name)) { + el.removeAttribute(name); + } + }); + } + }); + } + return frag; + } +}; + +class SquireUI +{ + constructor(container) { + const + ctrlKey = /Mac OS X/.test( navigator.userAgent ) ? 'meta + ' : 'Ctrl + ', + actions = { + group1: { + source: { + html: '〈〉', + cmd: () => console.log('TODO: toggle HTML <> Text') + } + }, + font: { + fontFamily: { + select: { + 'sans-serif': { + Arial: "'Nimbus Sans L', 'Liberation sans', 'Arial Unicode MS', Arial, Helvetica, Garuda, Utkal, FreeSans, sans-serif", + Tahoma: "'Luxi Sans', Tahoma, Loma, Geneva, Meera, sans-serif", + Trebuchet: "'DejaVu Sans Condensed', Trebuchet, 'Trebuchet MS', sans-serif", + Lucida: "'Lucida Sans Unicode', 'Lucida Sans', 'DejaVu Sans', 'Bitstream Vera Sans', 'DejaVu LGC Sans', sans-serif", + Verdana: "'DejaVu Sans', Verdana, Geneva, 'Bitstream Vera Sans', 'DejaVu LGC Sans', sans-serif" + }, + monospace: { + Courier: "'Liberation Mono', 'Courier New', FreeMono, Courier, monospace", + Lucida: "'DejaVu Sans Mono', 'DejaVu LGC Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', Monaco, monospace" + }, + sans: { + Times: "'Nimbus Roman No9 L', 'Times New Roman', Times, FreeSerif, serif", + Palatino: "'Bitstream Charter', 'Palatino Linotype', Palatino, Palladio, 'URW Palladio L', 'Book Antiqua', Times, serif", + Georgia: "'URW Palladio L', Georgia, Times, serif" + } + }, + cmd: s => squire.setFontFace(s.value) + }, + fontSize: { + select: ['11px','13px','16px','20px','24px','30px'], + cmd: s => squire.setFontSize(s.value) + } + }, + colors: { + textColor: { + input: 'color', + cmd: s => squire.setTextColour(s.value), + hint: 'text' + }, + backgroundColor: { + input: 'color', + cmd: s => squire.setHighlightColour(s.value), + hint: 'background' + }, + }, +/* + bidi: { + allowHtmlEditorBitiButtons + }, +*/ + inline: { + bold: { + html: '𝐁', + cmd: () => this.doAction('bold','B'), + key: 'B' + }, + italic: { + html: '𝐼', + cmd: () => this.doAction('italic','I'), + key: 'I' + }, + underline: { + html: 'U', + cmd: () => this.doAction('underline','U'), + key: 'U' + }, + strike: { + html: 'S', + cmd: () => this.doAction('strikethrough','S'), + key: 'Shift + 7' + }, + sub: { + html: 'Sx', + cmd: () => this.doAction('subscript','SUB'), + key: 'Shift + 5' + }, + sup: { + html: 'Sx', + cmd: () => this.doAction('superscript','SUP'), + key: 'Shift + 6' + } + }, + block: { + ol: { + html: '#', + cmd: () => this.doList('OL'), + key: 'Shift + 8' + }, + ul: { + html: '⋮', + cmd: () => this.doList('UL'), + key: 'Shift + 9' + }, + quote: { + html: '"', + cmd: () => { + let parent = this.getParentNodeName('UL,OL'); + (parent && 'BLOCKQUOTE' == parent) ? squire.decreaseQuoteLevel() : squire.increaseQuoteLevel(); + } + }, + indentDecrease: { + html: '⇤', + cmd: () => squire.changeIndentationLevel('decrease'), + key: ']' + }, + indentIncrease: { + html: '⇥', + cmd: () => squire.changeIndentationLevel('increase'), + key: '[' + } + }, + targets: { + link: { + html: '🔗', + cmd: () => { + if ('A' === this.getParentNodeName()) { + squire.removeLink(); + } else { + let url = prompt("Link","https://"); + url != null && url.length && squire.makeLink(url); + } + } + }, + image: { + html: '🖼️', + cmd: () => { + if ('IMG' === this.getParentNodeName()) { +// wysiwyg.removeLink(); + } else { + let src = prompt("Image","https://"); + src != null && src.length && squire.insertImage(src); + } + } + }, +/* + imageUpload: { + // TODO + } +*/ + }, +/* + table: { + // TODO + }, +*/ + changes: { + undo: { + html: '↶', + cmd: () => squire.undo(), + key: 'Z' + }, + redo: { + html: '↷', + cmd: () => squire.redo(), + key: 'Y' + } + } + }, + + content = doc.createElement('div'), + toolbar = doc.createElement('div'), + squire = new Squire(content, SquireDefaultConfig); + + content.className = 'squire-content cke_wysiwyg_div cke_editable'; + + this.squire = squire; + this.content = content; + + toolbar.className = 'squire-toolbar cke_top'; + for (let group in actions) { + let toolgroup = doc.createElement('div'); + toolgroup.className = 'squire-toolgroup cke_toolgroup'; + for (let action in actions[group]) { + if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) { + continue; + } + let cfg = actions[group][action], input; + if (cfg.input) { + input = doc.createElement('input'); + input.type = cfg.input; + input.addEventListener('input', () => cfg.cmd(input)); + } else if (cfg.select) { + input = doc.createElement('select'); + if (Array.isArray(cfg.select)) { + cfg.select.forEach(value => { + var option = new Option(value, value); + option.style[action] = value; + input.append(option); + }); + } else { + Object.entries(cfg.select).forEach(([label, options]) => { + let group = doc.createElement('optgroup'); + group.label = label; + Object.entries(options).forEach(([text, value]) => { + var option = new Option(text, value); + option.style[action] = value; + group.append(option); + }); + input.append(group); + }); + } + input.addEventListener('input', () => cfg.cmd(input)); + } else { + input = doc.createElement('button'); + input.type = 'button'; + input.innerHTML = cfg.html; + input.action_cmd = cfg.cmd; + } + if (cfg.hint) { + input.title = cfg.key ? cfg.hint + ' (' + ctrlKey + cfg.key + ')' : cfg.hint; + } else if (cfg.key) { + input.title = ctrlKey + cfg.key; + } + input.dataset.action = action; + cfg.input = input; + toolgroup.append(input); + } + toolgroup.children.length && toolbar.append(toolgroup); + } + toolbar.addEventListener('click', e => { + let t = e.target; + t.action_cmd && t.action_cmd(t); + }); + + let changes = actions.changes + changes.undo.input.disabled = changes.redo.input.disabled = true; + squire.addEventListener('undoStateChange', state => { + changes.undo.input.disabled = !state.canUndo; + changes.redo.input.disabled = !state.canRedo; + }); + + container.append(toolbar, content); + +/* +squire-raw.js:2161: this.fireEvent( 'dragover', { +squire-raw.js:2168: this.fireEvent( 'drop', { +squire-raw.js:2583: this.fireEvent( event.type, event ); +squire-raw.js:2864: this.fireEvent( 'pathChange', { path: newPath } ); +squire-raw.js:2867: this.fireEvent( range.collapsed ? 'cursor' : 'select', { +squire-raw.js:3004: this.fireEvent( 'input' ); +squire-raw.js:3080: this.fireEvent( 'input' ); +squire-raw.js:3101: this.fireEvent( 'input' ); +squire-raw.js:4036: this.fireEvent( 'willPaste', event ); +squire-raw.js:4089: this.fireEvent( 'willPaste', event ); +*/ + + // CKEditor gimmicks used by HtmlEditor + this.mode = 'wysiwyg'; // 'plain' + this.plugins = { + plain: false + }; + // .plugins.plain && this.editor.__plain + this.focusManager = { + hasFocus: () => squire._isFocused, + blur: () => squire.blur() + }; + } + + doAction(name, tag) { + if (this.testPresenceinSelection(tag, new RegExp('>'+tag+'\\b'))) { + name = 'remove' + (name.toUpperCase()[0]) + name.substr(1); + } + this.squire[name](); + } + + getParentNodeName(selector) { + let parent = this.squire.getSelectionClosest(selector); + return parent ? parent.nodeName : null; + } + + doList(type) { + let parent = this.getParentNodeName('UL,OL'), + fn = {UL:'makeUnorderedList',OL:'makeOrderedList'}; + (parent && parent == type) ? this.squire.removeList() : this.squire[fn[type]](); + } + + testPresenceinSelection(format, validation) { + return validation.test(this.squire.getPath()) | this.squire.hasFormat(format); + } + + // CKeditor gimmicks used by HtmlEditor + setMode(mode) { + this.mode = mode; // 'wysiwyg' or 'plain' + } + + on(type, fn) { + this.squire.addEventListener(type, fn); + } + + execCommand(cmd, cfg) { + if ('insertSignature' == cmd) { + if (cfg.clearCache) { + // remove it; + } else { + cfg.isHtml; // bool + cfg.insertBefore; // bool + cfg.signature; // string + } + } + } + + checkDirty() { + return false; + } + + resetDirty() {} + + getData() { + return this.squire.getHTML(); + } + + setData(html) { + this.squire.setHTML(html); + } + + focus() { + this.squire.focus(); + } + + resize(width, height) { + this.content.style.height = Math.max(200, (height - this.content.offsetTop)) + 'px'; + } + + setReadOnly(bool) { + this.content.contentEditable = !!bool; + } +} + +export { SquireUI, SquireUI as default }; diff --git a/dev/Styles/@Main.less b/dev/Styles/@Main.less index b987e393e..05b12eb3b 100644 --- a/dev/Styles/@Main.less +++ b/dev/Styles/@Main.less @@ -80,4 +80,7 @@ @import "SettingsFilters.less"; @import "Animations.less"; + +@import "SquireUI.less"; + @import "_End.less"; diff --git a/dev/Styles/SquireUI.less b/dev/Styles/SquireUI.less new file mode 100644 index 000000000..79060fc4f --- /dev/null +++ b/dev/Styles/SquireUI.less @@ -0,0 +1,39 @@ +.squire-toolbar { +} + +.squire-toolgroup { + display: inline-block; + margin: 0 .25em 0 0; +} + +.squire-toolgroup > button, +.squire-toolgroup > select, +.squire-toolgroup > input[type="color"] { + background: transparent; + border: 0; + box-shadow: none; + cursor: pointer; + font-size: 14px; + height: 1.5em; + line-height: 1.7em; + margin: 0; + padding: 0; + vertical-align: bottom; + width: 2em; +} + +.squire-toolgroup button:hover, .squire-toolgroup button.active { + background: #f0f0f0; +} + +.squire-toolgroup select { + width: 7em; +} +.squire-toolgroup select[data-action="fontSize"] { + width: 4em; +} + +.squire-content { + min-height: 200px; + padding: .5em; +}