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;
+}