From 5990addfef85f96770424660aa36746a5afbf5e3 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Thu, 24 Feb 2022 11:19:19 +0100 Subject: [PATCH] Added: addEventsListener() and addEventsListeners() --- dev/App/User.js | 9 +- dev/Common/Audio.js | 5 +- dev/Common/Enums.js | 8 +- dev/Common/Globals.js | 15 +- dev/Common/Selector.js | 40 +-- dev/Component/EmailAddresses.js | 180 ++++++----- dev/External/User/ko.js | 392 ++++++++++++------------ dev/External/ko.js | 202 ++++++------ dev/View/Popup/Ask.js | 7 +- dev/View/Popup/Compose.js | 25 +- dev/View/Popup/Contacts.js | 15 +- dev/View/Popup/KeyboardShortcutsHelp.js | 4 +- dev/View/Popup/OpenPgpKey.js | 3 +- dev/View/User/MailBox/MessageList.js | 43 +-- 14 files changed, 484 insertions(+), 464 deletions(-) diff --git a/dev/App/User.js b/dev/App/User.js index febb678a1..6ed9b9dd7 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -19,7 +19,8 @@ import { $htmlCL, Settings, SettingsGet, - leftPanelDisabled + leftPanelDisabled, + addEventsListener } from 'Common/Globals'; import { UNUSED_OPTION_VALUE } from 'Common/Consts'; @@ -89,7 +90,7 @@ class AppUser extends AbstractApp { }, interval); const fn = (ev=>$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey)).debounce(500); - ['keydown','keyup'].forEach(t => doc.addEventListener(t, fn)); + addEventsListener(doc, ['keydown','keyup'], fn); shortcuts.add('escape,enter', '', Scope.All, () => rl.Dropdowns.detectVisibility()); } @@ -344,9 +345,7 @@ class AppUser extends AbstractApp { } // add pointermove ? - ['touchstart','mousemove','keydown'].forEach( - t => doc.addEventListener(t, SettingsUserStore.delayLogout, {passive:true}) - ); + addEventsListener(doc, ['touchstart','mousemove','keydown'], SettingsUserStore.delayLogout, {passive:true}); SettingsUserStore.delayLogout(); // initLeftSideLayoutResizer diff --git a/dev/Common/Audio.js b/dev/Common/Audio.js index 32f98d187..a77efbcd5 100644 --- a/dev/Common/Audio.js +++ b/dev/Common/Audio.js @@ -1,5 +1,5 @@ import * as Links from 'Common/Links'; -import { doc, SettingsGet, fireEvent } from 'Common/Globals'; +import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals'; let notificator = null, player = null, @@ -73,8 +73,7 @@ export const SMAudio = new class { this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"'); if (player) { const stopFn = () => this.pause(); - player.addEventListener('ended', stopFn); - player.addEventListener('error', stopFn); + addEventsListener(player, ['ended','error'], stopFn); addEventListener('audio.api.stop', stopFn); } } diff --git a/dev/Common/Enums.js b/dev/Common/Enums.js index 60aaf65a7..f161a3cda 100644 --- a/dev/Common/Enums.js +++ b/dev/Common/Enums.js @@ -29,16 +29,10 @@ export const Capa = { export const Scope = { All: 'all', None: 'none', - Contacts: 'Contacts', MessageList: 'MessageList', FolderList: 'FolderList', MessageView: 'MessageView', - Compose: 'Compose', - Settings: 'Settings', - Menu: 'Menu', - OpenPgpKey: 'OpenPgpKey', - KeyboardShortcutsHelp: 'KeyboardShortcutsHelp', - Ask: 'Ask' + Settings: 'Settings' }; /** diff --git a/dev/Common/Globals.js b/dev/Common/Globals.js index 0f768db53..37551a4b5 100644 --- a/dev/Common/Globals.js +++ b/dev/Common/Globals.js @@ -4,6 +4,7 @@ import { Scope } from 'Common/Enums'; let keyScopeFake = Scope.All; export const + ScopeMenu = 'Menu', doc = document, @@ -31,14 +32,20 @@ export const fireEvent = (name, detail) => dispatchEvent(new CustomEvent(name, {detail:detail})), + addEventsListener = (element, events, fn, options) => + events.forEach(event => element.addEventListener(event, fn, options)), + + addEventsListeners = (element, events) => + Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)), + // keys keyScopeReal = ko.observable(Scope.All), keyScope = value => { if (value) { - if (Scope.Menu !== value) { + if (ScopeMenu !== value) { keyScopeFake = value; if (dropdownVisibility()) { - value = Scope.Menu; + value = ScopeMenu; } } keyScopeReal(value); @@ -50,8 +57,8 @@ export const dropdownVisibility.subscribe(value => { if (value) { - keyScope(Scope.Menu); - } else if (Scope.Menu === shortcuts.getScope()) { + keyScope(ScopeMenu); + } else if (ScopeMenu === shortcuts.getScope()) { keyScope(keyScopeFake); } }); diff --git a/dev/Common/Selector.js b/dev/Common/Selector.js index 56bd4012a..8153b2607 100644 --- a/dev/Common/Selector.js +++ b/dev/Common/Selector.js @@ -1,4 +1,5 @@ import ko from 'ko'; +import { addEventsListeners } from 'Common/Globals'; import { isArray } from 'Common/Utils'; import { koComputable } from 'External/ko'; @@ -239,27 +240,28 @@ export class Selector { return el ? ko.dataFor(el) : null; }; - contentScrollable.addEventListener('click', event => { - let el = event.target.closestWithin(this.sItemSelector, contentScrollable); - el && this.actionClick(ko.dataFor(el), event); + addEventsListeners(contentScrollable, { + click: event => { + let el = event.target.closestWithin(this.sItemSelector, contentScrollable); + el && this.actionClick(ko.dataFor(el), event); - const item = getItem(this.sItemCheckedSelector); - if (item) { - if (event.shiftKey) { - this.actionClick(item, event); - } else { - this.focusedItem(item); - item.checked(!item.checked()); - } - } - }); - - contentScrollable.addEventListener('auxclick', event => { - if (1 == event.button) { - const item = getItem(this.sItemSelector); + const item = getItem(this.sItemCheckedSelector); if (item) { - this.focusedItem(item); - (this.oCallbacks.MiddleClick || (()=>0))(item); + if (event.shiftKey) { + this.actionClick(item, event); + } else { + this.focusedItem(item); + item.checked(!item.checked()); + } + } + }, + auxclick: event => { + if (1 == event.button) { + const item = getItem(this.sItemSelector); + if (item) { + this.focusedItem(item); + (this.oCallbacks.MiddleClick || (()=>0))(item); + } } } }); diff --git a/dev/Component/EmailAddresses.js b/dev/Component/EmailAddresses.js index 829168fe9..f0a9e1244 100644 --- a/dev/Component/EmailAddresses.js +++ b/dev/Component/EmailAddresses.js @@ -1,4 +1,4 @@ -import { doc, createElement } from 'Common/Globals'; +import { doc, createElement, addEventsListeners } from 'Common/Globals'; import { EmailModel } from 'Model/Email'; const contentType = 'snappymail/emailaddress', @@ -41,49 +41,55 @@ export class EmailAddressesComponent { // Create the elements self.ul = createElement('ul',{class:"emailaddresses"}); - self.ul.addEventListener('click', e => self._focus(e)); - self.ul.addEventListener('dblclick', e => self._editTag(e)); - self.ul.addEventListener("dragenter", fnDrag); - self.ul.addEventListener("dragover", fnDrag); - self.ul.addEventListener("drop", e => { - if (validDropzone() && dragAddress.value) { - e.preventDefault(); - dragAddress.source._removeDraggedTag(dragAddress.li); - self._parseValue(dragAddress.value); + addEventsListeners(self.ul, { + click: e => self._focus(e), + dblclick: e => self._editTag(e), + dragenter: fnDrag, + dragover: fnDrag, + drop: e => { + if (validDropzone() && dragAddress.value) { + e.preventDefault(); + dragAddress.source._removeDraggedTag(dragAddress.li); + self._parseValue(dragAddress.value); + } } }); self.input = createElement('input',{type:"text", list:datalist.id, autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"}); - self.input.addEventListener('focus', () => self._focusTrigger(true)); - self.input.addEventListener('blur', () => { - // prevent autoComplete menu click from causing a false 'blur' - self._parseInput(true); - self._focusTrigger(false); - }); - self.input.addEventListener('keydown', e => { - if ('Backspace' === e.key || 'ArrowLeft' === e.key) { - // if our input contains no value and backspace has been pressed, select the last tag - var lastTag = self.inputCont.previousElementSibling, - input = self.input; - if (lastTag && (!input.value - || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0)) - ) { - e.preventDefault(); - lastTag.querySelector('a').focus(); - } - self._updateDatalist(); - } else if (e.key == 'Enter') { - e.preventDefault(); + addEventsListeners(self.input, { + focus: () => { + self._focusTrigger(true); + self.input.value || self._resetDatalist(); + }, + blur: () => { + // prevent autoComplete menu click from causing a false 'blur' self._parseInput(true); + self._focusTrigger(false); + }, + keydown: e => { + if ('Backspace' === e.key || 'ArrowLeft' === e.key) { + // if our input contains no value and backspace has been pressed, select the last tag + var lastTag = self.inputCont.previousElementSibling, + input = self.input; + if (lastTag && (!input.value + || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0)) + ) { + e.preventDefault(); + lastTag.querySelector('a').focus(); + } + self._updateDatalist(); + } else if (e.key == 'Enter') { + e.preventDefault(); + self._parseInput(true); + } + }, + input: () => { + self._parseInput(); + self._updateDatalist(); } }); - self.input.addEventListener('input', () => { - self._parseInput(); - self._updateDatalist(); - }); - self.input.addEventListener('focus', () => self.input.value || self._resetDatalist()); // define starting placeholder if (element.placeholder) { @@ -261,66 +267,70 @@ export class EmailAddressesComponent { el = createElement('a',{href:'#', class:'ficon'}); el.append('✖'); - el.addEventListener('click', e => self._removeTag(e, li)); - el.addEventListener('focus', () => li.className = 'emailaddresses-selected'); - el.addEventListener('blur', () => li.className = null); - el.addEventListener('keydown', e => { - switch (e.key) { - case 'Delete': - case 'Backspace': - self._removeTag(e, li); - break; + addEventsListeners(el, { + click: e => self._removeTag(e, li), + focus: () => li.className = 'emailaddresses-selected', + blur: () => li.className = null, + keydown: e => { + switch (e.key) { + case 'Delete': + case 'Backspace': + self._removeTag(e, li); + break; - // 'e' - edit tag (removes tag and places value into visible input - case 'e': - case 'Enter': - self._editTag(e); - break; + // 'e' - edit tag (removes tag and places value into visible input + case 'e': + case 'Enter': + self._editTag(e); + break; - case 'ArrowLeft': - // select the previous tag or input if no more tags exist - var previous = el.closest('li').previousElementSibling; - if (previous.matches('li')) { - previous.querySelector('a').focus(); - } else { - self.focus(); - } - break; + case 'ArrowLeft': + // select the previous tag or input if no more tags exist + var previous = el.closest('li').previousElementSibling; + if (previous.matches('li')) { + previous.querySelector('a').focus(); + } else { + self.focus(); + } + break; - case 'ArrowRight': - // select the next tag or input if no more tags exist - var next = el.closest('li').nextElementSibling; - if (next !== this.inputCont) { - next.querySelector('a').focus(); - } else { - this.focus(); - } - break; + case 'ArrowRight': + // select the next tag or input if no more tags exist + var next = el.closest('li').nextElementSibling; + if (next !== this.inputCont) { + next.querySelector('a').focus(); + } else { + this.focus(); + } + break; - case 'ArrowDown': - self._focus(e); - break; + case 'ArrowDown': + self._focus(e); + break; + } } }); li.append(el); li.emailaddress = v; - li.addEventListener("dragstart", e => { - dragAddress = { - source: self, - li: li, - value: li.emailaddress.obj.toLine() - }; -// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine()); - e.dataTransfer.setData('text/plain', contentType); -// e.dataTransfer.setDragImage(li, 0, 0); - e.dataTransfer.effectAllowed = 'move'; - li.style.opacity = 0.25; - }); - li.addEventListener("dragend", () => { - dragAddress = null; - li.style.cssText = ''; + addEventsListeners(li, { + dragstart: e => { + dragAddress = { + source: self, + li: li, + value: li.emailaddress.obj.toLine() + }; +// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine()); + e.dataTransfer.setData('text/plain', contentType); +// e.dataTransfer.setDragImage(li, 0, 0); + e.dataTransfer.effectAllowed = 'move'; + li.style.opacity = 0.25; + }, + dragend: () => { + dragAddress = null; + li.style.cssText = ''; + } }); self.inputCont.before(li); diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index e56815fe0..64c2299b9 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -2,7 +2,7 @@ import 'External/ko'; import ko from 'ko'; import { HtmlEditor } from 'Common/Html'; import { timeToNode } from 'Common/Translator'; -import { elementById } from 'Common/Globals'; +import { elementById, addEventsListeners } from 'Common/Globals'; import { isArray } from 'Common/Utils'; import { EmailAddressesComponent } from 'Component/EmailAddresses'; import { ThemeStore } from 'Stores/Theme'; @@ -29,226 +29,236 @@ const rlContentType = 'snappymail/action', id: 0, stop: () => clearTimeout(dragTimer.id), start: fn => dragTimer.id = setTimeout(fn, 500) - }; + }, + + ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor())); let dragImage, dragData; -ko.bindingHandlers.editor = { - init: (element, fValueAccessor) => { - let editor = null; +Object.assign(ko.bindingHandlers, { - const fValue = fValueAccessor(), - fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()), - fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()), - fOnReady = () => { - fValue.__editor = editor; - fUpdateEditorValue(); - }; + editor: { + init: (element, fValueAccessor) => { + let editor = null; - if (ko.isObservable(fValue) && HtmlEditor) { - editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue); + const fValue = fValueAccessor(), + fUpdateEditorValue = () => fValue && fValue.__editor && fValue.__editor.setHtmlOrPlain(fValue()), + fUpdateKoValue = () => fValue && fValue.__editor && fValue(fValue.__editor.getDataWithHtmlMark()), + fOnReady = () => { + fValue.__editor = editor; + fUpdateEditorValue(); + }; - fValue.__fetchEditorValue = fUpdateKoValue; + if (ko.isObservable(fValue) && HtmlEditor) { + editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue); - fValue.subscribe(fUpdateEditorValue); + fValue.__fetchEditorValue = fUpdateKoValue; - // ko.utils.domNodeDisposal.addDisposeCallback(element, () => { - // }); - } - } -}; + fValue.subscribe(fUpdateEditorValue); -let ttn = (element, fValueAccessor) => timeToNode(element, ko.unwrap(fValueAccessor())); -ko.bindingHandlers.moment = { - init: ttn, - update: ttn -}; - -ko.bindingHandlers.emailsTags = { - init: (element, fValueAccessor, fAllBindings) => { - const fValue = fValueAccessor(); - - element.addresses = new EmailAddressesComponent(element, { - focusCallback: value => fValue.focused && fValue.focused(!!value), - autoCompleteSource: fAllBindings.get('autoCompleteSource'), - onChange: value => fValue(value) - }); - - if (fValue.focused && fValue.focused.subscribe) { - fValue.focused.subscribe(value => - element.addresses[value ? 'focus' : 'blur']() - ); + // ko.utils.domNodeDisposal.addDisposeCallback(element, () => { + // }); + } } }, - update: (element, fValueAccessor) => { - element.addresses.value = ko.unwrap(fValueAccessor()); - } -}; -// Start dragging selected messages -ko.bindingHandlers.dragmessages = { - init: (element, fValueAccessor) => { - element.addEventListener("dragstart", e => { - let data = fValueAccessor()(e); - dragImage || (dragImage = elementById('messagesDragImage')); - if (data && dragImage && !ThemeStore.isMobile()) { - dragImage.querySelector('.text').textContent = data.uids.length; - let img = dragImage.querySelector('i'); - img.classList.toggle('icon-copy', e.ctrlKey); - img.classList.toggle('icon-mail', !e.ctrlKey); + moment: { + init: ttn, + update: ttn + }, - // Else Chrome doesn't show it - dragImage.style.left = e.clientX + 'px'; - dragImage.style.top = e.clientY + 'px'; - dragImage.style.right = 'auto'; + emailsTags: { + init: (element, fValueAccessor, fAllBindings) => { + const fValue = fValueAccessor(); - setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage); + element.addresses = new EmailAddressesComponent(element, { + focusCallback: value => fValue.focused && fValue.focused(!!value), + autoCompleteSource: fAllBindings.get('autoCompleteSource'), + onChange: value => fValue(value) + }); - // Remove the Chrome visibility - dragImage.style.cssText = ''; - } else { - e.preventDefault(); + if (fValue.focused && fValue.focused.subscribe) { + fValue.focused.subscribe(value => + element.addresses[value ? 'focus' : 'blur']() + ); } + }, + update: (element, fValueAccessor) => { + element.addresses.value = ko.unwrap(fValueAccessor()); + } + }, - }, false); - element.addEventListener("dragend", () => dragData = null); - element.setAttribute('draggable', true); - } -}; + // Start dragging selected messages + dragmessages: { + init: (element, fValueAccessor) => { + element.addEventListener("dragstart", e => { + let data = fValueAccessor()(e); + dragImage || (dragImage = elementById('messagesDragImage')); + if (data && dragImage && !ThemeStore.isMobile()) { + dragImage.querySelector('.text').textContent = data.uids.length; + let img = dragImage.querySelector('i'); + img.classList.toggle('icon-copy', e.ctrlKey); + img.classList.toggle('icon-mail', !e.ctrlKey); -// Drop selected messages on folder -ko.bindingHandlers.dropmessages = { - init: (element, fValueAccessor) => { - const folder = fValueAccessor(), -// folder = ko.dataFor(element), - fnStop = e => { - e.preventDefault(); - element.classList.remove('droppableHover'); - dragTimer.stop(); - }, - fnHover = e => { - if ('messages' === getDragAction(e)) { - fnStop(e); - element.classList.add('droppableHover'); - if (folder && folder.collapsed()) { - dragTimer.start(() => { - folder.collapsed(false); - setExpandedFolder(folder.fullName, true); - }, 500); - } - } - }; - element.addEventListener("dragenter", fnHover); - element.addEventListener("dragover", fnHover); - element.addEventListener("dragleave", fnStop); - element.addEventListener("drop", e => { - fnStop(e); - if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) { - let data = dragData.data; - if (folder && data && data.folder && isArray(data.uids)) { - moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey); - } - } - }); - } -}; + // Else Chrome doesn't show it + dragImage.style.left = e.clientX + 'px'; + dragImage.style.top = e.clientY + 'px'; + dragImage.style.right = 'auto'; -ko.bindingHandlers.sortableItem = { - init: (element, fValueAccessor) => { - let options = ko.unwrap(fValueAccessor()) || {}, - parent = element.parentNode, - fnHover = e => { - if ('sortable' === getDragAction(e)) { + setDragAction(e, 'messages', e.ctrlKey ? 'copy' : 'move', data, dragImage); + + // Remove the Chrome visibility + dragImage.style.cssText = ''; + } else { e.preventDefault(); - let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]'); - if (node && node !== dragData.data && parent.contains(node)) { - let rect = node.getBoundingClientRect(); - if (rect.top + (rect.height / 2) <= e.clientY) { - if (node.nextElementSibling !== dragData.data) { - node.after(dragData.data); - } - } else if (node.previousElementSibling !== dragData.data) { - node.before(dragData.data); + } + + }, false); + element.addEventListener("dragend", () => dragData = null); + element.setAttribute('draggable', true); + } + }, + + // Drop selected messages on folder + dropmessages: { + init: (element, fValueAccessor) => { + const folder = fValueAccessor(), + // folder = ko.dataFor(element), + fnStop = e => { + e.preventDefault(); + element.classList.remove('droppableHover'); + dragTimer.stop(); + }, + fnHover = e => { + if ('messages' === getDragAction(e)) { + fnStop(e); + element.classList.add('droppableHover'); + if (folder && folder.collapsed()) { + dragTimer.start(() => { + folder.collapsed(false); + setExpandedFolder(folder.fullName, true); + }, 500); + } + } + }; + addEventsListeners(element, { + dragenter: fnHover, + dragover: fnHover, + dragleave: fnStop, + drop: e => { + fnStop(e); + if ('messages' === getDragAction(e) && ['move','copy'].includes(e.dataTransfer.effectAllowed)) { + let data = dragData.data; + if (folder && data && data.folder && isArray(data.uids)) { + moveMessagesToFolder(data.folder, data.uids, folder.fullName, data.copy && e.ctrlKey); } } } - }; - element.addEventListener("dragstart", e => { - dragData = { - action: 'sortable', - element: element - }; - setDragAction(e, 'sortable', 'move', element, element); - element.style.opacity = 0.25; - }); - element.addEventListener("dragend", e => { - element.style.opacity = null; - if ('sortable' === getDragAction(e)) { - dragData.data.style.cssText = ''; - let row = parent.rows[options.list.indexOf(ko.dataFor(element))]; - if (row != dragData.data) { - row.before(dragData.data); - } - dragData = null; - } - }); - if (!parent.sortable) { - parent.sortable = true; - parent.addEventListener("dragenter", fnHover); - parent.addEventListener("dragover", fnHover); - parent.addEventListener("drop", e => { - if ('sortable' === getDragAction(e)) { - e.preventDefault(); - let data = ko.dataFor(dragData.data), - from = options.list.indexOf(data), - to = [...parent.children].indexOf(dragData.data); - if (from != to) { - let arr = options.list(); - arr.splice(to, 0, ...arr.splice(from, 1)); - options.list(arr); - } - dragData = null; - options.afterMove && options.afterMove(); - } }); } - } -}; + }, -ko.bindingHandlers.initDom = { - init: (element, fValueAccessor) => fValueAccessor()(element) -}; - -ko.bindingHandlers.onEsc = { - init: (element, fValueAccessor, fAllBindings, viewModel) => { - let fn = event => { - if ('Escape' == event.key) { - element.dispatchEvent(new Event('change')); - fValueAccessor().call(viewModel); + sortableItem: { + init: (element, fValueAccessor) => { + let options = ko.unwrap(fValueAccessor()) || {}, + parent = element.parentNode, + fnHover = e => { + if ('sortable' === getDragAction(e)) { + e.preventDefault(); + let node = (e.target.closest ? e.target : e.target.parentNode).closest('[draggable]'); + if (node && node !== dragData.data && parent.contains(node)) { + let rect = node.getBoundingClientRect(); + if (rect.top + (rect.height / 2) <= e.clientY) { + if (node.nextElementSibling !== dragData.data) { + node.after(dragData.data); + } + } else if (node.previousElementSibling !== dragData.data) { + node.before(dragData.data); + } + } + } + }; + addEventsListeners(element, { + dragstart: e => { + dragData = { + action: 'sortable', + element: element + }; + setDragAction(e, 'sortable', 'move', element, element); + element.style.opacity = 0.25; + }, + dragend: e => { + element.style.opacity = null; + if ('sortable' === getDragAction(e)) { + dragData.data.style.cssText = ''; + let row = parent.rows[options.list.indexOf(ko.dataFor(element))]; + if (row != dragData.data) { + row.before(dragData.data); + } + dragData = null; + } + } + }); + if (!parent.sortable) { + parent.sortable = true; + addEventsListeners(parent, { + dragenter: fnHover, + dragover: fnHover, + drop: e => { + if ('sortable' === getDragAction(e)) { + e.preventDefault(); + let data = ko.dataFor(dragData.data), + from = options.list.indexOf(data), + to = [...parent.children].indexOf(dragData.data); + if (from != to) { + let arr = options.list(); + arr.splice(to, 0, ...arr.splice(from, 1)); + options.list(arr); + } + dragData = null; + options.afterMove && options.afterMove(); + } + } + }); } - }; - element.addEventListener('keyup', fn); - ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn)); - } -}; + } + }, -ko.bindingHandlers.registerBootstrapDropdown = { - init: element => { - rl.Dropdowns.register(element); - element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle')); - } -}; + initDom: { + init: (element, fValueAccessor) => fValueAccessor()(element) + }, -ko.bindingHandlers.openDropdownTrigger = { - update: (element, fValueAccessor) => { - if (ko.unwrap(fValueAccessor())) { - const el = element.ddBtn; - el.open || el.toggle(); -// el.focus(); + onEsc: { + init: (element, fValueAccessor, fAllBindings, viewModel) => { + let fn = event => { + if ('Escape' == event.key) { + element.dispatchEvent(new Event('change')); + fValueAccessor().call(viewModel); + } + }; + element.addEventListener('keyup', fn); + ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn)); + } + }, - rl.Dropdowns.detectVisibility(); - fValueAccessor()(false); + registerBootstrapDropdown: { + init: element => { + rl.Dropdowns.register(element); + element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle')); + } + }, + + openDropdownTrigger: { + update: (element, fValueAccessor) => { + if (ko.unwrap(fValueAccessor())) { + const el = element.ddBtn; + el.open || el.toggle(); + // el.focus(); + + rl.Dropdowns.detectVisibility(); + fValueAccessor()(false); + } } } -}; +}); diff --git a/dev/External/ko.js b/dev/External/ko.js index 3eba5045c..69abed526 100644 --- a/dev/External/ko.js +++ b/dev/External/ko.js @@ -5,6 +5,10 @@ import { SaveSettingsStep } from 'Common/Enums'; import { arrayLength, isFunction, forEachObjectEntry } from 'Common/Utils'; export const + errorTip = (element, value) => value + ? setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100) + : element.removeAttribute('data-rainloopErrorTip'), + /** * The value of the pureComputed observable shouldn’t vary based on the * number of evaluations or other “hidden” information. Its value should be @@ -35,118 +39,116 @@ export const return data; }; -ko.bindingHandlers.tooltipErrorTip = { - init: (element, fValueAccessor) => { - doc.addEventListener('click', () => { - let value = fValueAccessor(); - ko.isObservable(value) && !ko.isComputed(value) && value(''); - element.removeAttribute('data-rainloopErrorTip'); - }); - }, - update: (element, fValueAccessor) => { - let value = ko.unwrap(fValueAccessor()); - value = isFunction(value) ? value() : value; - if (value) { - setTimeout(() => element.setAttribute('data-rainloopErrorTip', value), 100); - } else { - element.removeAttribute('data-rainloopErrorTip'); +Object.assign(ko.bindingHandlers, { + tooltipErrorTip: { + init: (element, fValueAccessor) => { + doc.addEventListener('click', () => { + let value = fValueAccessor(); + ko.isObservable(value) && !ko.isComputed(value) && value(''); + errorTip(element); + }); + }, + update: (element, fValueAccessor) => { + let value = ko.unwrap(fValueAccessor()); + value = isFunction(value) ? value() : value; + errorTip(element, value); } - } -}; + }, -ko.bindingHandlers.onEnter = { - init: (element, fValueAccessor, fAllBindings, viewModel) => { - let fn = event => { - if ('Enter' == event.key) { - element.dispatchEvent(new Event('change')); - fValueAccessor().call(viewModel); + onEnter: { + init: (element, fValueAccessor, fAllBindings, viewModel) => { + let fn = event => { + if ('Enter' == event.key) { + element.dispatchEvent(new Event('change')); + fValueAccessor().call(viewModel); + } + }; + element.addEventListener('keydown', fn); + ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn)); + } + }, + + onSpace: { + init: (element, fValueAccessor, fAllBindings, viewModel) => { + let fn = event => { + if (' ' == event.key) { + fValueAccessor().call(viewModel, event); + } + }; + element.addEventListener('keyup', fn); + ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn)); + } + }, + + i18nInit: { + init: element => i18nToNodes(element) + }, + + i18nUpdate: { + update: (element, fValueAccessor) => { + ko.unwrap(fValueAccessor()); + i18nToNodes(element); + } + }, + + title: { + update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor()) + }, + + command: { + init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => { + const command = fValueAccessor(); + + if (!command || !command.enabled || !command.canExecute) { + throw new Error('Value should be a command'); } - }; - element.addEventListener('keydown', fn); - ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn)); - } -}; -ko.bindingHandlers.onSpace = { - init: (element, fValueAccessor, fAllBindings, viewModel) => { - let fn = event => { - if (' ' == event.key) { - fValueAccessor().call(viewModel, event); + ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init( + element, + fValueAccessor, + fAllBindings, + viewModel, + bindingContext + ); + }, + update: (element, fValueAccessor) => { + const cl = element.classList, + command = fValueAccessor(); + + let disabled = !command.enabled(); + + disabled = disabled || !command.canExecute(); + cl.toggle('disabled', disabled); + + if (element.matches('INPUT,TEXTAREA,BUTTON')) { + element.disabled = disabled; } - }; - element.addEventListener('keyup', fn); - ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keyup', fn)); - } -}; - -ko.bindingHandlers.i18nInit = { - init: element => i18nToNodes(element) -}; - -ko.bindingHandlers.i18nUpdate = { - update: (element, fValueAccessor) => { - ko.unwrap(fValueAccessor()); - i18nToNodes(element); - } -}; - -ko.bindingHandlers.title = { - update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor()) -}; - -ko.bindingHandlers.command = { - init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => { - const command = fValueAccessor(); - - if (!command || !command.enabled || !command.canExecute) { - throw new Error('Value should be a command'); } - - ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init( - element, - fValueAccessor, - fAllBindings, - viewModel, - bindingContext - ); }, - update: (element, fValueAccessor) => { - const cl = element.classList, - command = fValueAccessor(); - let disabled = !command.enabled(); - - disabled = disabled || !command.canExecute(); - cl.toggle('disabled', disabled); - - if (element.matches('INPUT,TEXTAREA,BUTTON')) { - element.disabled = disabled; - } - } -}; - -ko.bindingHandlers.saveTrigger = { - init: (element) => { - let icon = element; - if (element.matches('input,select,textarea')) { - element.classList.add('settings-saved-trigger-input'); - element.after(element.saveTriggerIcon = icon = createElement('span')); - } - icon.classList.add('settings-save-trigger'); - }, - update: (element, fValueAccessor) => { - const value = parseInt(ko.unwrap(fValueAccessor()),10); - let cl = (element.saveTriggerIcon || element).classList; - if (element.saveTriggerIcon) { - cl.toggle('saving', value === SaveSettingsStep.Animate); + saveTrigger: { + init: (element) => { + let icon = element; + if (element.matches('input,select,textarea')) { + element.classList.add('settings-saved-trigger-input'); + element.after(element.saveTriggerIcon = icon = createElement('span')); + } + icon.classList.add('settings-save-trigger'); + }, + update: (element, fValueAccessor) => { + const value = parseInt(ko.unwrap(fValueAccessor()),10); + let cl = (element.saveTriggerIcon || element).classList; + if (element.saveTriggerIcon) { + cl.toggle('saving', value === SaveSettingsStep.Animate); + cl.toggle('success', value === SaveSettingsStep.TrueResult); + cl.toggle('error', value === SaveSettingsStep.FalseResult); + } + cl = element.classList; cl.toggle('success', value === SaveSettingsStep.TrueResult); cl.toggle('error', value === SaveSettingsStep.FalseResult); } - cl = element.classList; - cl.toggle('success', value === SaveSettingsStep.TrueResult); - cl.toggle('error', value === SaveSettingsStep.FalseResult); } -}; +}); // extenders diff --git a/dev/View/Popup/Ask.js b/dev/View/Popup/Ask.js index bb8a71890..82a995984 100644 --- a/dev/View/Popup/Ask.js +++ b/dev/View/Popup/Ask.js @@ -1,4 +1,3 @@ -import { Scope } from 'Common/Enums'; import { i18n } from 'Common/Translator'; import { isFunction } from 'Common/Utils'; @@ -58,8 +57,8 @@ class AskPopupView extends AbstractViewPopup { } onBuild() { -// shortcuts.add('tab', 'shift', Scope.Ask, () => { - shortcuts.add('tab,arrowright,arrowleft', '', Scope.Ask, () => { +// shortcuts.add('tab', 'shift', 'Ask', () => { + shortcuts.add('tab,arrowright,arrowleft', '', 'Ask', () => { let btn = this.querySelector('.buttonYes'); if (btn.matches(':focus')) { btn = this.querySelector('.buttonNo'); @@ -68,7 +67,7 @@ class AskPopupView extends AbstractViewPopup { return false; }); - shortcuts.add('escape', '', Scope.Ask, () => { + shortcuts.add('escape', '', 'Ask', () => { this.noClick(); return false; }); diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 3a09eda92..9ea7ec71c 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -1,7 +1,6 @@ import ko from 'ko'; import { - Scope, Notification, UploadErrorCode } from 'Common/Enums'; @@ -50,6 +49,8 @@ import { ContactsPopupView } from 'View/Popup/Contacts'; import { ThemeStore } from 'Stores/Theme'; const + ScopeCompose = 'Compose', + base64_encode = text => btoa(text).match(/.{1,76}/g).join('\r\n'), email = new EmailModel(), @@ -1323,42 +1324,42 @@ class ComposePopupView extends AbstractViewPopup { this.addAttachmentEnabled(true); - shortcuts.add('q', 'meta', Scope.Compose, ()=>false); - shortcuts.add('w', 'meta', Scope.Compose, ()=>false); + shortcuts.add('q', 'meta', ScopeCompose, ()=>false); + shortcuts.add('w', 'meta', ScopeCompose, ()=>false); - shortcuts.add('contextmenu', '', Scope.Compose, e => this.popupMenu(e)); - shortcuts.add('m', 'meta', Scope.Compose, e => this.popupMenu(e)); + shortcuts.add('contextmenu', '', ScopeCompose, e => this.popupMenu(e)); + shortcuts.add('m', 'meta', ScopeCompose, e => this.popupMenu(e)); - shortcuts.add('escape,close', '', Scope.Compose, () => { + shortcuts.add('escape,close', '', ScopeCompose, () => { this.skipCommand(); return false; }); - shortcuts.add('arrowdown', 'meta', Scope.Compose, () => { + shortcuts.add('arrowdown', 'meta', ScopeCompose, () => { this.skipCommand(); return false; }); - shortcuts.add('s', 'meta', Scope.Compose, () => { + shortcuts.add('s', 'meta', ScopeCompose, () => { this.saveCommand(); return false; }); - shortcuts.add('save', '', Scope.Compose, () => { + shortcuts.add('save', '', ScopeCompose, () => { this.saveCommand(); return false; }); if (Settings.app('allowCtrlEnterOnCompose')) { - shortcuts.add('enter', 'meta', Scope.Compose, () => { + shortcuts.add('enter', 'meta', ScopeCompose, () => { this.sendCommand(); return false; }); } - shortcuts.add('mailsend', '', Scope.Compose, () => { + shortcuts.add('mailsend', '', ScopeCompose, () => { this.sendCommand(); return false; }); - shortcuts.add('escape,close', 'shift', Scope.Compose, () => { + shortcuts.add('escape,close', 'shift', ScopeCompose, () => { this.modalVisibility() && this.tryToClosePopup(); return false; }); diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js index 93b7572ab..e74c5003e 100644 --- a/dev/View/Popup/Contacts.js +++ b/dev/View/Popup/Contacts.js @@ -1,9 +1,6 @@ import { koArrayWithDestroy } from 'External/ko'; -import { - SaveSettingsStep, - Scope -} from 'Common/Enums'; +import { SaveSettingsStep } from 'Common/Enums'; import { ComposeType } from 'Common/EnumsUser'; @@ -26,7 +23,9 @@ import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty import { decorateKoCommands } from 'Knoin/Knoin'; import { AbstractViewPopup } from 'Knoin/AbstractViews'; -const CONTACTS_PER_PAGE = 50, +const + CONTACTS_PER_PAGE = 50, + ScopeContacts = 'Contacts', propertyIsMail = prop => prop.isType(ContactPropertyType.Email), propertyIsName = prop => prop.isType(ContactPropertyType.FirstName) || prop.isType(ContactPropertyType.LastName); @@ -446,14 +445,14 @@ class ContactsPopupView extends AbstractViewPopup { } onBuild(dom) { - this.selector.init(dom.querySelector('.b-list-content'), Scope.Contacts); + this.selector.init(dom.querySelector('.b-list-content'), ScopeContacts); - shortcuts.add('delete', '', Scope.Contacts, () => { + shortcuts.add('delete', '', ScopeContacts, () => { this.deleteCommand(); return false; }); - shortcuts.add('c,w', '', Scope.Contacts, () => { + shortcuts.add('c,w', '', ScopeContacts, () => { this.newMessageCommand(); return false; }); diff --git a/dev/View/Popup/KeyboardShortcutsHelp.js b/dev/View/Popup/KeyboardShortcutsHelp.js index bb208b478..82d5b4231 100644 --- a/dev/View/Popup/KeyboardShortcutsHelp.js +++ b/dev/View/Popup/KeyboardShortcutsHelp.js @@ -1,5 +1,3 @@ -import { Scope } from 'Common/Enums'; - import { AbstractViewPopup } from 'Knoin/AbstractViews'; export class KeyboardShortcutsHelpPopupView extends AbstractViewPopup { @@ -14,7 +12,7 @@ export class KeyboardShortcutsHelpPopupView extends AbstractViewPopup { // shortcuts.add('tab', 'shift', shortcuts.add('tab,arrowleft,arrowright', '', - Scope.KeyboardShortcutsHelp, + 'KeyboardShortcutsHelp', event => { let next = 0; tabs.forEach((node, index) => { diff --git a/dev/View/Popup/OpenPgpKey.js b/dev/View/Popup/OpenPgpKey.js index 4469f3630..772b20d34 100644 --- a/dev/View/Popup/OpenPgpKey.js +++ b/dev/View/Popup/OpenPgpKey.js @@ -1,4 +1,3 @@ -import { Scope } from 'Common/Enums'; import { doc } from 'Common/Globals'; import { AbstractViewPopup } from 'Knoin/AbstractViews'; @@ -119,7 +118,7 @@ class OpenPgpKeyPopupView extends AbstractViewPopup { } onBuild() { - shortcuts.add('a', 'meta', Scope.OpenPgpKey, () => { + shortcuts.add('a', 'meta', 'OpenPgpKey', () => { this.selectKey(); return false; }); diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js index 6c6a25be6..2d6e10d37 100644 --- a/dev/View/User/MailBox/MessageList.js +++ b/dev/View/User/MailBox/MessageList.js @@ -13,7 +13,7 @@ import { import { UNUSED_OPTION_VALUE } from 'Common/Consts'; -import { doc, leftPanelDisabled, moveAction, Settings, SettingsCapa, SettingsGet, fireEvent } from 'Common/Globals'; +import { doc, leftPanelDisabled, moveAction, Settings, SettingsCapa, SettingsGet, fireEvent, addEventsListeners } from 'Common/Globals'; import { computedPaginatorHelper, showMessageComposer, populateMessageBody } from 'Common/UtilsUser'; import { FileInfo } from 'Common/File'; @@ -642,28 +642,29 @@ export class MailMessageList extends AbstractViewRight { this.selector.init(dom.querySelector('.b-content'), Scope.MessageList); - dom.addEventListener('click', event => { - ThemeStore.isMobile() && !eqs(event, '.toggleLeft') && leftPanelDisabled(true); + addEventsListeners(dom, { + click: event => { + ThemeStore.isMobile() && !eqs(event, '.toggleLeft') && leftPanelDisabled(true); - if (eqs(event, '.messageList') && Scope.MessageView === AppUserStore.focusedState()) { - AppUserStore.focusedState(Scope.MessageList); + if (eqs(event, '.messageList') && Scope.MessageView === AppUserStore.focusedState()) { + AppUserStore.focusedState(Scope.MessageList); + } + + let el = eqs(event, '.e-paginator a'); + el && this.gotoPage(ko.dataFor(el)); + + eqs(event, '.checkboxCheckAll') && this.checkAll(!this.checkAll()); + + el = eqs(event, '.flagParent'); + el && this.flagMessages(ko.dataFor(el)); + + el = eqs(event, '.threads-len'); + el && this.gotoThread(ko.dataFor(el)); + }, + dblclick: event => { + let el = eqs(event, '.actionHandle'); + el && this.gotoThread(ko.dataFor(el)); } - - let el = eqs(event, '.e-paginator a'); - el && this.gotoPage(ko.dataFor(el)); - - eqs(event, '.checkboxCheckAll') && this.checkAll(!this.checkAll()); - - el = eqs(event, '.flagParent'); - el && this.flagMessages(ko.dataFor(el)); - - el = eqs(event, '.threads-len'); - el && this.gotoThread(ko.dataFor(el)); - }); - - dom.addEventListener('dblclick', event => { - let el = eqs(event, '.actionHandle'); - el && this.gotoThread(ko.dataFor(el)); }); // initUploaderForAppend