From bdc961dfabe32818a047284e1f69c7ca128a2cda Mon Sep 17 00:00:00 2001 From: djmaze Date: Thu, 10 Sep 2020 18:47:28 +0200 Subject: [PATCH] Improved Squire with the missing 'plain' mode. --- README.md | 20 ++--- dev/Common/HtmlEditor.js | 60 ++++++-------- dev/Common/Utils.js | 6 +- dev/External/SquireUI.js | 82 ++++++++++++++----- dev/Styles/SquireUI.less | 19 ++++- .../app/localization/webmail/_source.en.yml | 2 - .../0.0.0/app/localization/webmail/de_DE.yml | 2 - .../0.0.0/app/localization/webmail/en_GB.yml | 2 - .../0.0.0/app/localization/webmail/en_US.yml | 2 - .../0.0.0/app/localization/webmail/es_ES.yml | 2 - .../0.0.0/app/localization/webmail/fr_FR.yml | 2 - .../0.0.0/app/localization/webmail/nl_NL.yml | 2 - vendors/ckeditor-plugins/plain/plugin.js | 50 +---------- 13 files changed, 119 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 2d7860569..79c186978 100644 --- a/README.md +++ b/README.md @@ -88,23 +88,23 @@ Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill |js/* |1.14.0 |native | |----------- |--------: |--------: | -|admin.js |2.130.942 | 986.303 | -|app.js |4.184.455 |2.603.659 | +|admin.js |2.130.942 | 990.560 | +|app.js |4.184.455 |2.607.924 | |boot.js | 671.522 | 5.834 | |libs.js | 647.614 | 312.343 | |polyfills.js | 325.834 | 0 | -|TOTAL |7.960.367 |3.908.139 | +|TOTAL |7.960.367 |3.916.661 | |js/min/* |1.14.0 |native |gzip 1.14 |gzip |brotli | |--------------- |--------: |--------: |--------: |--------: |--------: | -|admin.min.js | 252.147 | 134.678 | 73.657 | 39.834 | 34.181 | -|app.min.js | 511.202 | 355.203 |140.462 | 94.064 | 75.648 | +|admin.min.js | 252.147 | 135.441 | 73.657 | 40.158 | 34.434 | +|app.min.js | 511.202 | 355.966 |140.462 | 94.405 | 75.847 | |boot.min.js | 66.007 | 3.166 | 22.567 | 1.571 | 1.345 | |libs.min.js | 572.545 | 295.754 |176.720 | 91.521 | 80.871 | |polyfills.min.js | 32.452 | 0 | 11.312 | 0 | 0 | -|TOTAL |1.434.353 | 788.801 |424.718 |226.990 |192.045 | +|TOTAL |1.434.353 | 790.327 |424.718 |227.655 |192.497 | -657.410 bytes (202.330 gzip) is not much, but it feels faster. +644.026 bytes (197.063 gzip) is not much, but it feels faster. ### CSS changes @@ -129,8 +129,8 @@ Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill |css/* |1.14.0 |native |gzip 1.14 |gzip |brotli | |-------------- |-------: |-------: |------: |------: |------: | -|app.css | 340.334 | 252.797 | 46,959 | 36.394 | 30.615 | -|app.min.css | 274.791 | 206.474 | 39.618 | 32.238 | 27.339 | +|app.css | 340.334 | 253.711 | 46,959 | 36.587 | 30.802 | +|app.min.css | 274.791 | 207.227 | 39.618 | 31.993 | 27.160 | |boot.css | | 2.538 | | 837 | 668 | |boot.min.css | | 2.055 | | 732 | 560 | @@ -142,7 +142,7 @@ Still TODO: * upload image inline * support for tables (really needed?!?) -* toggle between HTML and plain +* alternative for vendors/ckeditor-plugins/signature/plugin.js | | normal | min | gzip | min gzip | |-------- |-------: |-------: |------: |--------: | diff --git a/dev/Common/HtmlEditor.js b/dev/Common/HtmlEditor.js index 084c98ffc..bb6c1e1d1 100644 --- a/dev/Common/HtmlEditor.js +++ b/dev/Common/HtmlEditor.js @@ -267,7 +267,21 @@ class HtmlEditor { init() { if (this.element && !this.editor) { - const initFunc = () => { + const onReady = () => { + if (this.editor.removeMenuItem) { + this.editor.removeMenuItem('cut'); + this.editor.removeMenuItem('copy'); + this.editor.removeMenuItem('paste'); + } + + this.__resizable = true; + this.__inited = true; + + this.resize(); + + this.onReady && this.onReady(); + }, + initFunc = () => { if (window.CKEDITOR) { const config = CKEditorDefaultConfig, language = rl.settings.get('Language'), @@ -298,15 +312,6 @@ class HtmlEditor { this.editor.on('key', event => !(event && event.data && EventKeyCode.Tab === event.data.keyCode)); - this.editor.on('blur', () => this.blurTrigger()); - - this.editor.on('mode', () => { - this.blurTrigger(); - this.onModeChange && this.onModeChange('plain' !== this.editor.mode); - }); - - this.editor.on('focus', () => this.focusTrigger()); - if (window.FileReader) { this.editor.on('drop', (event) => { if (0 < event.data.dataTransfer.getFilesCount()) { @@ -330,32 +335,13 @@ class HtmlEditor { }); } - this.editor.on('instanceReady', () => { - if (this.editor.removeMenuItem) { - this.editor.removeMenuItem('cut'); - this.editor.removeMenuItem('copy'); - this.editor.removeMenuItem('paste'); - } - - this.__resizable = true; - this.__inited = true; - - this.resize(); - - this.onReady && this.onReady(); - }); + this.editor.on('instanceReady', onReady); } else if (window.Squire) { this.editor = new SquireUI(this.element, this.editor); - this.editor.on('blur', () => this.blurTrigger()); - this.editor.on('focus', () => this.focusTrigger()); /* // TODO this.editor.on('key', event => !(event && event.data && EventKeyCode.Tab === event.data.keyCode)); - this.editor.on('mode', () => { - this.blurTrigger(); - this.onModeChange && this.onModeChange('plain' !== this.editor.mode); - }); if (window.FileReader) { this.editor.on('dragover', (event) => { event.dataTransfer = clipboardData @@ -383,12 +369,16 @@ class HtmlEditor { }); } */ - this.__resizable = true; - this.__inited = true; + setTimeout(onReady,1); + } - this.resize(); - - this.onReady && setTimeout(() => this.onReady(), 1); + if (this.editor) { + this.editor.on('blur', () => this.blurTrigger()); + this.editor.on('focus', () => this.focusTrigger()); + this.editor.on('mode', () => { + this.blurTrigger(); + this.onModeChange && this.onModeChange('plain' !== this.editor.mode); + }); } }; diff --git a/dev/Common/Utils.js b/dev/Common/Utils.js index a64bbcd5a..5777f6d95 100644 --- a/dev/Common/Utils.js +++ b/dev/Common/Utils.js @@ -498,8 +498,10 @@ export function plainToHtml(plain) { .replace(/\n/g, '
'); } -window['rainloop_Utils_htmlToPlain'] = htmlToPlain; // eslint-disable-line dot-notation -window['rainloop_Utils_plainToHtml'] = plainToHtml; // eslint-disable-line dot-notation +rl.Utils = { + htmlToPlain: htmlToPlain, + plainToHtml: plainToHtml +}; /** * @param {Array} aSystem diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index 47ca9dfce..a5a90e1a2 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -1,10 +1,13 @@ /* eslint max-len: 0 */ +'use strict'; 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(','), +i18n = (str, def) => window.rl && window.rl.i18n ? window.rl.i18n(str) : def, + SquireDefaultConfig = { /* blockTag: 'P', @@ -33,11 +36,12 @@ SquireDefaultConfig = { addLinks: true // allow_smart_html_links */ sanitizeToDOMFragment: (html, isPaste/*, squire*/) => { - const frag = doc.createDocumentFragment(); - frag.innerHTML = html; + const frag = doc.createDocumentFragment(), + tpl = doc.createElement('div'); + tpl.innerHTML = html; if (isPaste) { - frag.querySelectorAll(':not('+allowedElements+')').forEach(el => el.remove()); - frag.querySelectorAll(allowedElements).forEach(el => { + tpl.querySelectorAll(':not('+allowedElements+')').forEach(el => el.remove()); + tpl.querySelectorAll(allowedElements).forEach(el => { if (el.hasAttributes()) { Array.from(el.attributes).forEach(attr => { let name = attr.name.toLowerCase(); @@ -48,6 +52,7 @@ SquireDefaultConfig = { } }); } + frag.append(...tpl.childNodes); return frag; } }; @@ -58,10 +63,11 @@ class SquireUI const ctrlKey = /Mac OS X/.test( navigator.userAgent ) ? 'meta + ' : 'Ctrl + ', actions = { - group1: { - source: { + mode: { + plain: { html: '〈〉', - cmd: () => console.log('TODO: toggle HTML <> Text') + cmd: () => this.setMode('plain' == this.mode ? 'wysiwyg' : 'plain'), + hint: i18n('EDITOR/TEXT_SWITCHER_PLAINT_TEXT', 'Plain') } }, font: { @@ -100,7 +106,7 @@ class SquireUI backgroundColor: { input: 'color', cmd: s => squire.setHighlightColour(s.value), - hint: 'background' + hint: i18n('EDITOR/TEXT_BACKGROUND_COLOR', 'Background') }, }, /* @@ -185,7 +191,7 @@ class SquireUI html: '🖼️', cmd: () => { if ('IMG' === this.getParentNodeName()) { -// wysiwyg.removeLink(); +// squire.removeLink(); } else { let src = prompt("Image","https://"); src != null && src.length && squire.insertImage(src); @@ -217,19 +223,29 @@ class SquireUI } }, - content = doc.createElement('div'), + plain = doc.createElement('textarea'), + wysiwyg = doc.createElement('div'), toolbar = doc.createElement('div'), - squire = new Squire(content, SquireDefaultConfig); + squire = new Squire(wysiwyg, SquireDefaultConfig); - content.className = 'squire-content cke_wysiwyg_div cke_editable'; + plain.className = 'squire-plain cke_plain cke_editable'; + wysiwyg.className = 'squire-wysiwyg cke_wysiwyg_div cke_editable'; + this.mode = 'plain'; // 'wysiwyg' + this.__plain = { + getRawData: () => this.plain.value, + setRawData: plain => this.plain.value = plain + }; + this.container = container; this.squire = squire; - this.content = content; + this.plain = plain; + this.wysiwyg = wysiwyg; toolbar.className = 'squire-toolbar cke_top'; for (let group in actions) { let toolgroup = doc.createElement('div'); toolgroup.className = 'squire-toolgroup cke_toolgroup'; + toolgroup.id = 'squire-toolgroup-'+group; for (let action in actions[group]) { if ('source' == action && !rl.settings.app('allowHtmlEditorSourceButton')) { continue; @@ -279,7 +295,9 @@ class SquireUI } toolbar.addEventListener('click', e => { let t = e.target; - t.action_cmd && t.action_cmd(t); + if ('plain' != this.mode || 'plain' == t.dataset.action) { + t.action_cmd && t.action_cmd(t); + } }); let changes = actions.changes @@ -289,7 +307,7 @@ class SquireUI changes.redo.input.disabled = !state.canRedo; }); - container.append(toolbar, content); + container.append(toolbar, wysiwyg, plain); /* squire-raw.js:2161: this.fireEvent( 'dragover', { @@ -305,9 +323,8 @@ squire-raw.js:4089: this.fireEvent( 'willPaste', event ); */ // CKEditor gimmicks used by HtmlEditor - this.mode = 'wysiwyg'; // 'plain' this.plugins = { - plain: false + plain: true }; // .plugins.plain && this.editor.__plain this.focusManager = { @@ -338,13 +355,31 @@ squire-raw.js:4089: this.fireEvent( 'willPaste', event ); return validation.test(this.squire.getPath()) | this.squire.hasFormat(format); } - // CKeditor gimmicks used by HtmlEditor setMode(mode) { + let cl = this.container.classList; + cl.remove('squire-mode-'+this.mode); + if ('plain' == mode) { + let html = this.squire.getHTML(); + this.plain.value = rl.Utils.htmlToPlain(html, true).trim(); + } else { + let plain = this.plain.value; + this.squire.setHTML(rl.Utils.plainToHtml(plain, true)); + mode = 'wysiwyg'; + } this.mode = mode; // 'wysiwyg' or 'plain' + cl.add('squire-mode-'+mode); + this.onModeChange && this.onModeChange(); + setTimeout(()=>this.focus(),1); } + // CKeditor gimmicks used by HtmlEditor on(type, fn) { - this.squire.addEventListener(type, fn); + if ('mode' == type) { + this.onModeChange = fn; + } else { + this.squire.addEventListener(type, fn); + this.plain.addEventListener(type, fn); + } } execCommand(cmd, cfg) { @@ -374,15 +409,18 @@ squire-raw.js:4089: this.fireEvent( 'willPaste', event ); } focus() { - this.squire.focus(); + ('plain' == this.editor.mode ? this.plain : this.squire).focus(); } resize(width, height) { - this.content.style.height = Math.max(200, (height - this.content.offsetTop)) + 'px'; + height = Math.max(200, (height - this.wysiwyg.offsetTop)) + 'px'; + this.wysiwyg.style.height = height; + this.plain.style.height = height; } setReadOnly(bool) { - this.content.contentEditable = !!bool; + this.plain.readOnly = !!bool; + this.wysiwyg.contentEditable = !!bool; } } diff --git a/dev/Styles/SquireUI.less b/dev/Styles/SquireUI.less index 79060fc4f..ffccbc1b6 100644 --- a/dev/Styles/SquireUI.less +++ b/dev/Styles/SquireUI.less @@ -33,7 +33,24 @@ width: 4em; } -.squire-content { +.squire-wysiwyg, .squire-plain { min-height: 200px; padding: .5em; } + +.squire-plain { + width: 100%; +} + +.squire-plain { + display: none; +} +.squire-mode-plain .squire-wysiwyg { + display: none; +} +.squire-mode-plain .squire-plain { + display: block; +} +.squire-mode-plain .squire-toolgroup:not(#squire-toolgroup-mode) { + opacity: 0.5; +} diff --git a/rainloop/v/0.0.0/app/localization/webmail/_source.en.yml b/rainloop/v/0.0.0/app/localization/webmail/_source.en.yml index f7c68a2fb..29bf3d429 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/_source.en.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/_source.en.yml @@ -418,8 +418,6 @@ en: ERROR_UNKNOWN: "An unknown file upload error occurred" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> TEXT" - TEXT_SWITCHER_RICH_FORMATTING: "Rich formatting" - TEXT_SWITCHER_CONFIRM: "Text formatting and images will be lost. Are you sure you want to continue?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Personal" LABEL_GENERAL_NAME: "General" diff --git a/rainloop/v/0.0.0/app/localization/webmail/de_DE.yml b/rainloop/v/0.0.0/app/localization/webmail/de_DE.yml index a9fc19f7d..026899c96 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/de_DE.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/de_DE.yml @@ -417,8 +417,6 @@ de_DE: ERROR_UNKNOWN: "Ein unbekannter Fehler trat beim Hochladen auf" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> TEXT" - TEXT_SWITCHER_RICH_FORMATTING: "Formatierter Text (Rich Text)" - TEXT_SWITCHER_CONFIRM: "Alle Textformatierungen und Grafiken gehen verloren. Wollen Sie wirklich fortfahren?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Persönlich" LABEL_GENERAL_NAME: "Allgemein" diff --git a/rainloop/v/0.0.0/app/localization/webmail/en_GB.yml b/rainloop/v/0.0.0/app/localization/webmail/en_GB.yml index 703fba214..2370701b3 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/en_GB.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/en_GB.yml @@ -417,8 +417,6 @@ en_GB: ERROR_UNKNOWN: "An unknown file upload error occurred" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> TEXT" - TEXT_SWITCHER_RICH_FORMATTING: "Rich formatting" - TEXT_SWITCHER_CONFIRM: "Text formatting and images will be lost. Are you sure you want to continue?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Personal" LABEL_GENERAL_NAME: "General" diff --git a/rainloop/v/0.0.0/app/localization/webmail/en_US.yml b/rainloop/v/0.0.0/app/localization/webmail/en_US.yml index 16b4f2692..06083a6bd 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/en_US.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/en_US.yml @@ -417,8 +417,6 @@ en_US: ERROR_UNKNOWN: "An unknown file upload error occurred" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> TEXT" - TEXT_SWITCHER_RICH_FORMATTING: "Rich formatting" - TEXT_SWITCHER_CONFIRM: "Text formatting and images will be lost. Are you sure you want to continue?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Personal" LABEL_GENERAL_NAME: "General" diff --git a/rainloop/v/0.0.0/app/localization/webmail/es_ES.yml b/rainloop/v/0.0.0/app/localization/webmail/es_ES.yml index 94ede0012..5e2f174a2 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/es_ES.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/es_ES.yml @@ -418,8 +418,6 @@ es_ES: ERROR_UNKNOWN: "Se ha producido un error de carga de archivo desconocido" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> TEXTO" - TEXT_SWITCHER_RICH_FORMATTING: "Texto enriquecido" - TEXT_SWITCHER_CONFIRM: "Se perderá el formato de texto e imágenes. ¿Está seguro que desea continuar?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Personal" LABEL_GENERAL_NAME: "General" diff --git a/rainloop/v/0.0.0/app/localization/webmail/fr_FR.yml b/rainloop/v/0.0.0/app/localization/webmail/fr_FR.yml index 9841946dc..a6a22c2cb 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/fr_FR.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/fr_FR.yml @@ -418,8 +418,6 @@ fr_FR: ERROR_UNKNOWN: "Une erreur inconnue de téléversement de fichier s'est produite" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> Non-formaté" - TEXT_SWITCHER_RICH_FORMATTING: "Format riche" - TEXT_SWITCHER_CONFIRM: "Le formatage du texte et les images seront perdus. Êtes-vous sûr de vouloir continuer ?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Personnel" LABEL_GENERAL_NAME: "Général" diff --git a/rainloop/v/0.0.0/app/localization/webmail/nl_NL.yml b/rainloop/v/0.0.0/app/localization/webmail/nl_NL.yml index 78900dbb8..d4bee4475 100644 --- a/rainloop/v/0.0.0/app/localization/webmail/nl_NL.yml +++ b/rainloop/v/0.0.0/app/localization/webmail/nl_NL.yml @@ -416,8 +416,6 @@ nl_NL: ERROR_UNKNOWN: "Er is een onbekende fout opgetreden" EDITOR: TEXT_SWITCHER_PLAINT_TEXT: "HTML <-> TEKST" - TEXT_SWITCHER_RICH_FORMATTING: "Tekst met opmaak" - TEXT_SWITCHER_CONFIRM: "Tekst opmaak zal verloren gaan. Wenst u verder te gaan?" SETTINGS_LABELS: LABEL_PERSONAL_NAME: "Persoonlijk" LABEL_GENERAL_NAME: "Algemeen" diff --git a/vendors/ckeditor-plugins/plain/plugin.js b/vendors/ckeditor-plugins/plain/plugin.js index 87783b1f2..a88e9702e 100644 --- a/vendors/ckeditor-plugins/plain/plugin.js +++ b/vendors/ckeditor-plugins/plain/plugin.js @@ -27,46 +27,6 @@ if (el) { el.scrollTop = 0; } - }, - simplePlainToHtml = function (sPlain) { - return sPlain - .replace(/&/g, '&') - .replace(/>/g, '>').replace(/') - .replace(/\n/g, '
') - .replace(/ /g, ' ') - ; - }, - simpleHtmlToPlain = function (sHtml) { - - var sText = sHtml - .replace(/[\s]+/gm, ' ') - .replace(/]*>/gmi, '\n') - .replace(/<\/h[\d]>/gi, '\n') - .replace(/<\/p>/gi, '\n\n') - .replace(/<\/li>/gi, '\n') - .replace(/<\/td>/gi, '\n') - .replace(/<\/tr>/gi, '\n') - .replace(/<\/div>/gi, '\n') - .replace(/]*>/gmi, '\n') - .replace(/<\/blockquote>/gi, '\n') - .replace(/]*>/gmi, '\n_______________________________\n\n') - .replace(/ /gi, ' ') - .replace(/"/gi, '"') - .replace(/<[^>]*>/gm, '') - ; - - sText = $('
').html(sText).text(); - - sText = sText - .replace(/\n[ \t]+/gm, '\n') - .replace(/[\n]{3,}/gm, '\n\n') - .replace(/>/gi, '>') - .replace(/</gi, '<') - .replace(/&/gi, '&') - ; - - return sText; } ; @@ -81,14 +41,8 @@ } editor.__textUtils = { - plainToHtml: function(data) { - return window.rainloop_Utils_plainToHtml ? - window.rainloop_Utils_plainToHtml(data, true) : simplePlainToHtml(data); - }, - htmlToPlain: function(data) { - return window.rainloop_Utils_htmlToPlain ? - window.rainloop_Utils_htmlToPlain(data, true) : simpleHtmlToPlain(data); - } + plainToHtml: data => rl.Utils.plainToHtml(data, true), + htmlToPlain: data => rl.Utils.htmlToPlain(data, true) }; var plain = CKEDITOR.plugins.plain;