From 9eb5646aeccc5c800535dcf39c522ac48f9f704f Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 30 Jan 2024 18:16:39 +0100 Subject: [PATCH] Redesign WYSIWYG handling to allow any editor (quill, tiny, etc. etc.) --- dev/Common/Html.js | 48 ++++------- dev/External/SquireUI.js | 27 +++--- dev/External/User/ko.js | 2 +- dev/Settings/User/General.js | 8 +- dev/Stores/User/Settings.js | 4 +- dev/View/Popup/Compose.js | 1 - plugins/wysiwyg-example/example.js | 82 +++++++++++++++++++ plugins/wysiwyg-example/index.php | 41 ++++++++++ plugins/wysiwyg-example/static/example.min.js | 1 + .../0.0.0/app/libraries/RainLoop/Actions.php | 2 + .../app/libraries/RainLoop/Actions/User.php | 1 + .../templates/Views/User/SettingsGeneral.html | 14 ++++ 12 files changed, 180 insertions(+), 51 deletions(-) create mode 100644 plugins/wysiwyg-example/example.js create mode 100644 plugins/wysiwyg-example/index.php create mode 100644 plugins/wysiwyg-example/static/example.min.js diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 063caaab7..b2d929afa 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -725,20 +725,12 @@ export const WYSIWYGS = ko.observableArray(); -WYSIWYGS.push(['Squire', (owner, container, onReady)=>{ - let squire = new SquireUI(container); - setTimeout(()=>onReady(squire), 1); -/* - squire.on('blur', () => owner.blurTrigger()); - squire.on('focus', () => clearTimeout(owner.blurTimer)); - squire.on('mode', () => { - owner.blurTrigger(); - owner.onModeChange?.(!owner.isPlain()); - }); -*/ -}]); +WYSIWYGS.push({ + name: 'Squire', + construct: (owner, container, onReady) => onReady(new SquireUI(container)) +}); -rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push([name, construct]); +rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct}); export class HtmlEditor { /** @@ -747,7 +739,7 @@ export class HtmlEditor { * @param {Function=} onReady * @param {Function=} onModeChange */ - constructor(element, onBlur = null, onReady = null, onModeChange = null) { + constructor(element, onReady = null, onModeChange = null, onBlur = null) { this.blurTimer = 0; this.onBlur = onBlur; @@ -757,10 +749,10 @@ export class HtmlEditor { onReady = onReady ? [onReady] : []; this.onReady = fn => onReady.push(fn); // TODO: make 'which' user configurable -// const which = 'CKEditor4', -// wysiwyg = WYSIWYGS.find(item => which == item[0]) || WYSIWYGS.find(item => 'Squire' == item[0]); - const wysiwyg = WYSIWYGS.find(item => 'Squire' == item[0]); - wysiwyg[1](this, element, editor => { + const which = SettingsUserStore.editorWysiwyg(), + wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name); +// const wysiwyg = WYSIWYGS.find(item => 'Squire' == item.name); + wysiwyg.construct(this, element, editor => setTimeout(()=>{ this.editor = editor; editor.on('blur', () => this.blurTrigger()); editor.on('focus', () => clearTimeout(this.blurTimer)); @@ -770,7 +762,7 @@ export class HtmlEditor { }); this.onReady = fn => fn(); onReady.forEach(fn => fn()); - }); + },1)); } } @@ -826,8 +818,8 @@ export class HtmlEditor { let result = ''; if (this.editor) { try { - if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) { - result = this.editor.__plain.getRawData(); + if (this.isPlain()) { + result = this.editor.getPlainData(); } else { result = this.editor.getData(); } @@ -862,8 +854,8 @@ export class HtmlEditor { this.clearCachedSignature(); try { editor.setMode(mode); - if (this.isPlain() && editor.plugins.plain && editor.__plain) { - editor.__plain.setRawData(data); + if (this.isPlain()) { + editor.setPlainData(data); } else { editor.setData(data); } @@ -883,16 +875,8 @@ export class HtmlEditor { this.onReady(() => this.editor.focus()); } - hasFocus() { - try { - return !!this.editor?.focusManager.hasFocus; - } catch (e) { - return false; - } - } - blur() { - this.onReady(() => this.editor.focusManager.blur(true)); + this.onReady(() => this.editor.blur()); } clear() { diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index 18d1ba0fd..015b9ece7 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -318,11 +318,6 @@ class SquireUI wysiwyg.className = 'squire-wysiwyg'; wysiwyg.dir = 'auto'; this.mode = ''; // 'plain' | 'wysiwyg' - this.__plain = { - getRawData: () => this.plain.value, - setRawData: plain => this.plain.value = plain - }; - this.container = container; this.squire = squire; this.plain = plain; @@ -530,15 +525,6 @@ class SquireUI console.dir({select:e.range}); }); */ - - // CKEditor gimmicks used by HtmlEditor - this.plugins = { - plain: true - }; - this.focusManager = { - hasFocus: () => squire._isFocused, - blur: () => squire.blur() - }; } doAction(name) { @@ -576,7 +562,6 @@ class SquireUI this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0; } - // CKeditor gimmicks used by HtmlEditor on(type, fn) { if ('mode' == type) { this.onModeChange = fn; @@ -643,6 +628,18 @@ class SquireUI squire.setSelection( range ); } + getPlainData() { + return this.plain.value; + } + + setPlainData(text) { + this.plain.value = text; + } + + blur() { + this.squire.blur(); + } + focus() { if ('plain' == this.mode) { this.plain.focus(); diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index 8fc26ffd5..306162bc6 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -91,7 +91,7 @@ Object.assign(ko.bindingHandlers, { }; if (ko.isObservable(fValue)) { - editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue); + editor = new HtmlEditor(element, fOnReady, fUpdateKoValue, fUpdateKoValue); fValue.__fetchEditorValue = fUpdateKoValue; diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js index ebb12c97d..3c2584c5b 100644 --- a/dev/Settings/User/General.js +++ b/dev/Settings/User/General.js @@ -5,6 +5,7 @@ import { SaveSettingStatus } from 'Common/Enums'; import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser'; import { setRefreshFoldersInterval } from 'Common/Folders'; import { Settings, SettingsGet } from 'Common/Globals'; +import { WYSIWYGS } from 'Common/Html'; import { isArray } from 'Common/Utils'; import { addSubscribablesTo, addComputablesTo } from 'External/ko'; import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator'; @@ -44,7 +45,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { ['useThreads', // These use addSetting() 'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', - 'editorDefaultType', 'msgDefaultAction', 'maxBlockquotesLevel', + 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', // These are in addSettings() 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave', @@ -59,6 +60,8 @@ export class UserSettingsGeneral extends AbstractViewSettings { this.identities = IdentityUserStore; + this.wysiwygs = WYSIWYGS; + addComputablesTo(this, { languageFullName: () => convertLangName(this.language()), @@ -80,6 +83,8 @@ export class UserSettingsGeneral extends AbstractViewSettings { ]; }, + hasWysiwygs: () => 1 < WYSIWYGS().length, + msgDefaultActions: () => { translateTrigger(); return [ @@ -99,6 +104,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { }); this.addSetting('EditorDefaultType'); + this.addSetting('editorWysiwyg'); this.addSetting('MsgDefaultAction'); this.addSetting('MessageReadDelay'); this.addSetting('MessagesPerPage'); diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js index 234690783..9c577715e 100644 --- a/dev/Stores/User/Settings.js +++ b/dev/Stores/User/Settings.js @@ -47,6 +47,7 @@ export const SettingsUserStore = new class { layout: 1, editorDefaultType: 'Html', + editorWysiwyg: 'Squire', msgDefaultAction: 1 }); @@ -78,9 +79,10 @@ export const SettingsUserStore = new class { init() { const self = this; - self.editorDefaultType(SettingsGet('EditorDefaultType')); [ + 'EditorDefaultType', + 'editorWysiwyg', 'messageNewWindow', 'messageReadAuto', 'MsgDefaultAction', diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 04cd423fc..ec4b3381d 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -704,7 +704,6 @@ export class ComposePopupView extends AbstractViewPopup { // setTimeout(() => { this.oEditor = new HtmlEditor( this.editorArea(), - null, () => fOnInit(this.oEditor), bHtml => this.isHtml(!!bHtml) ); diff --git a/plugins/wysiwyg-example/example.js b/plugins/wysiwyg-example/example.js new file mode 100644 index 000000000..a26fe5158 --- /dev/null +++ b/plugins/wysiwyg-example/example.js @@ -0,0 +1,82 @@ +(rl => { + class Example + { + constructor(owner, editor) { + this.mode = 'wysiwyg'; + this.owner = owner; + this.editor = editor; +console.dir({editor}); + } + + setMode(mode) { + console.log(`WYSIWYG-Example.setMode(${mode})`); + this.mode = mode; + } + + on(type, fn) { + console.log(`WYSIWYG-Example.on(${type}, ${fn})`); + } + + execCommand(cmd, cfg) { + console.log(`WYSIWYG-Example.execCommand(${cmd}, ${cfg})`); +/* + execCommand('insertSignature', { + clearCache: true + })); + + execCommand('insertSignature', { + isHtml: html, + insertBefore: insertBefore, + signature: signature + })); +*/ + } + + getData() { + console.log(`WYSIWYG-Example.getData()`); + return this.editor.innerHTML; + } + + setData(html) { + console.log(`WYSIWYG-Example.setData(${html})`); + this.editor.innerHTML = html; + } + + getPlainData() { + console.log(`WYSIWYG-Example.getPlainData()`); + return this.editor.innerText; + } + + setPlainData(text) { + console.log(`WYSIWYG-Example.setPlainData(${text})`); + return this.editor.textContent = text; + } + + blur() { + console.log(`WYSIWYG-Example.blur()`); + this.editor.blur(); + } + + focus() { + console.log(`WYSIWYG-Example.focus()`); + } + } + + if (rl) { + const path = rl.settings.app('webVersionPath'), + script = document.createElement('script'); + script.src = path + 'static/wysiwyg-example/example.min.js'; + document.head.append(script); + + /** + * owner = HtmlEditor + * container = HTMLElement + * onReady = callback(SMQuill) + */ + rl.registerWYSIWYG('Example', (owner, container, onReady) => { + const editor = new Example(owner, container); + onReady(editor); + }); + } + +})(window.rl); diff --git a/plugins/wysiwyg-example/index.php b/plugins/wysiwyg-example/index.php new file mode 100644 index 000000000..a2613ecbd --- /dev/null +++ b/plugins/wysiwyg-example/index.php @@ -0,0 +1,41 @@ +isDir()) { + \mkdir($path . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } else { + \copy($item, $path . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + } + umask($old_mask); + } + + if (\is_file("{$path}/example.min.js")) { +// $this->addCss('style.css'); + $this->addJs('example.js'); + } + } +} diff --git a/plugins/wysiwyg-example/static/example.min.js b/plugins/wysiwyg-example/static/example.min.js new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/plugins/wysiwyg-example/static/example.min.js @@ -0,0 +1 @@ + diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 10535e2d1..eebc01df7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -702,6 +702,7 @@ class Actions 'DesktopNotifications' => true, 'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW), 'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')), + 'editorWysiwyg' => 'Squire', 'UseCheckboxesInList' => (bool) $oConfig->Get('defaults', 'view_use_checkboxes', true), 'showNextMessage' => (bool) $oConfig->Get('defaults', 'view_show_next_message', false), 'AutoLogout' => (int) $oConfig->Get('defaults', 'autologout', 30), @@ -766,6 +767,7 @@ class Actions } $aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType'])); + $aResult['editorWysiwyg'] = $oSettings->GetConf('editorWysiwyg', $aResult['editorWysiwyg']); $aResult['requestReadReceipt'] = (bool) $oSettings->GetConf('requestReadReceipt', false); $aResult['requestDsn'] = (bool) $oSettings->GetConf('requestDsn', false); $aResult['requireTLS'] = (bool) $oSettings->GetConf('requireTLS', false); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php index a58b4e82e..2805c5222 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php @@ -177,6 +177,7 @@ trait User }); $this->setSettingsFromParams($oSettings, 'EditorDefaultType', 'string'); + $this->setSettingsFromParams($oSettings, 'editorWysiwyg', 'string'); $this->setSettingsFromParams($oSettings, 'requestReadReceipt', 'bool'); $this->setSettingsFromParams($oSettings, 'requestDsn', 'bool'); $this->setSettingsFromParams($oSettings, 'requireTLS', 'bool'); diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html index d18d11235..dc2bd1350 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html @@ -119,6 +119,20 @@ } }"> +
+ +
+
+