diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2019-01-17 19:17:22 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-01-17 19:17:22 +0100 |
| commit | 22efdfbecc1e8c688000be98903a5a355d1ece8a (patch) | |
| tree | e097625171b2b67ed5f5288ced67446608b9f633 /addons/web_editor/static/lib/summernote/src/js/EventHandler.js | |
| parent | f50fbc6d168f0d17fa1e932b4ecc968a92b02b65 (diff) | |
| parent | eb0ea8efbe932c140302e9f8da33a839bcd73146 (diff) | |
[IMP] web_editor,*: Refactoring the wysiwyg editor and 'html' field
_Task #1878502_
- The refactoring allows for the update of the _Summernote_ library.
- A new structure was created by transforming all the plugins in the library using the odoo inheritance system. Plugins are easier to implement with the `AbstractPlugin` to add Odoo behaviors.
- The cleanup allowed us to remove some of the code that was in `web_editor` but only used in `website` or in `mass_mailing`. Some parts are still in `web_editor` but will be moved at a later time.
- The refactoring also had to be done in order to create a set of unit tests. Each behavior can be tested, including the behaviors performed as a consequence of keyboard interactions (for instance: Enter, Tab...).
- From now on, the methods of the library (in this case _Summernote_) can no longer be called by other modules or files. Only the `wysiwyg` widgets can access it, to simplify the updating process. The `wysiwyg` object serves as an interface. A lot of tests are done, ensuring the consistency of outside behavior.
- Depending on the options the snippets will be loaded or not, the editor will be in an iframe or not... all of this is transparent from the outside.
- Regarding iframes, all controllers related to editing have been removed: the new API no longer needs them. This speeds up loading, eases testing and removes complexity for the same features.
**PUBLIC FEATURES**
There are several public methods on the `Wysiwyg` class:
- `Wysiwyg.prepare (WidgetParent)` which returns a deferred resolved when the library (xml, lazy, assets...) is loaded.
- `Wysiwyg.getRange (DOM)` returns the range (selection in the dom)
- `Wysiwyg.setRange (startNode, startOffset, endNode, endOffset)` which creates a range (selection in the dom)
- `Wysiwyg.setRangeFromNode (DOM, options)` that creates a range from an element (option available to select all, start or end)
A jQuery selector was added: `:o_editable`, which indicates whether the current element is editable. That is, if it is contained in a tag with the attribute `contentEditable = "true"` or in a tag with the class `o_editable`.
Several methods are also present:
- `focusIn`: makes a focus and places the cursor at the beginning of the element
- `focusInEnd`: makes a focus and places the cursor at the end of the element
- `selectContent`: makes a focus and selects the content
**HTML FIELD**
The HTML field can receive different options:
- `style-inline:` {boolean} transforms a class into an inline style when saving and vice versa when reading.
- `no-attachment`: {boolean} prevents the use of attachments (in media dialog)
- `cssEdit`: {xml_id} to use a template containing the css to loaded in an iframe when editing
- `cssReadonly`: {xml_id} to use a template containing the css to load into an iframe when viewing in readonly
- `snippets`: {xml_id} snippets template (can be used with or without `cssEdit`)
- `wrapper`: {template} qweb static template (containing a tag: `id = "wrapper"`) that will include the content during editing (removed on save)
**MASS MAILING**
A widget was created for `mass mailing`. There are now two fields: `body_html` and `body_arch`.
`body_arch` contains the code with the class without conversion into inline style, useful when editing and one with the inline style that is visible in readonly mode and sent by email.
Advantage: no spreading errors, able to update css/theme, able to do more changes when converting to inline style so that a maximum of mail clients have an impeccable rendering.
**REMAINING KNOW ISSUES**
These will be fixed in a subsequent commit.
- A few committed changes made during the development of this refactoring were lost in the process and should be reapplied.
- Some minor (mostly range) issues remain with Firefox.
--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
closes odoo/odoo#29775
Diffstat (limited to 'addons/web_editor/static/lib/summernote/src/js/EventHandler.js')
| -rw-r--r-- | addons/web_editor/static/lib/summernote/src/js/EventHandler.js | 569 |
1 files changed, 0 insertions, 569 deletions
diff --git a/addons/web_editor/static/lib/summernote/src/js/EventHandler.js b/addons/web_editor/static/lib/summernote/src/js/EventHandler.js deleted file mode 100644 index a924e7def8a..00000000000 --- a/addons/web_editor/static/lib/summernote/src/js/EventHandler.js +++ /dev/null @@ -1,569 +0,0 @@ -define([ - 'summernote/core/agent', - 'summernote/core/func', - 'summernote/core/dom', - 'summernote/core/async', - 'summernote/core/key', - 'summernote/core/list', - 'summernote/editing/History', - 'summernote/module/Editor', - 'summernote/module/Toolbar', - 'summernote/module/Statusbar', - 'summernote/module/Popover', - 'summernote/module/Handle', - 'summernote/module/Fullscreen', - 'summernote/module/Codeview', - 'summernote/module/DragAndDrop', - 'summernote/module/Clipboard', - 'summernote/module/LinkDialog', - 'summernote/module/ImageDialog', - 'summernote/module/HelpDialog' -], function (agent, func, dom, async, key, list, History, - Editor, Toolbar, Statusbar, Popover, Handle, Fullscreen, Codeview, - DragAndDrop, Clipboard, LinkDialog, ImageDialog, HelpDialog) { - - /** - * @class EventHandler - * - * EventHandler - * - TODO: new instance per a editor - */ - var EventHandler = function () { - var self = this; - - /** - * Modules - */ - var modules = this.modules = { - editor: new Editor(this), - toolbar: new Toolbar(this), - statusbar: new Statusbar(this), - popover: new Popover(this), - handle: new Handle(this), - fullscreen: new Fullscreen(this), - codeview: new Codeview(this), - dragAndDrop: new DragAndDrop(this), - clipboard: new Clipboard(this), - linkDialog: new LinkDialog(this), - imageDialog: new ImageDialog(this), - helpDialog: new HelpDialog(this) - }; - - /** - * invoke module's method - * - * @param {String} moduleAndMethod - ex) 'editor.redo' - * @param {...*} arguments - arguments of method - * @return {*} - */ - this.invoke = function () { - var moduleAndMethod = list.head(list.from(arguments)); - var args = list.tail(list.from(arguments)); - - var splits = moduleAndMethod.split('.'); - var hasSeparator = splits.length > 1; - var moduleName = hasSeparator && list.head(splits); - var methodName = hasSeparator ? list.last(splits) : list.head(splits); - - var module = this.getModule(moduleName); - var method = module[methodName]; - - return method && method.apply(module, args); - }; - - /** - * returns module - * - * @param {String} moduleName - name of module - * @return {Module} - defaults is editor - */ - this.getModule = function (moduleName) { - return this.modules[moduleName] || this.modules.editor; - }; - - /** - * @param {jQuery} $holder - * @param {Object} callbacks - * @param {String} eventNamespace - * @returns {Function} - */ - var bindCustomEvent = this.bindCustomEvent = function ($holder, callbacks, eventNamespace) { - return function () { - var callback = callbacks[func.namespaceToCamel(eventNamespace, 'on')]; - if (callback) { - callback.apply($holder[0], arguments); - } - return $holder.trigger('summernote.' + eventNamespace, arguments); - }; - }; - - /** - * insert Images from file array. - * - * @private - * @param {Object} layoutInfo - * @param {File[]} files - */ - this.insertImages = function (layoutInfo, files) { - var $editor = layoutInfo.editor(), - $editable = layoutInfo.editable(), - $holder = layoutInfo.holder(); - - var callbacks = $editable.data('callbacks'); - var options = $editor.data('options'); - - // If onImageUpload options setted - if (callbacks.onImageUpload) { - bindCustomEvent($holder, callbacks, 'image.upload')(files); - // else insert Image as dataURL - } else { - $.each(files, function (idx, file) { - var filename = file.name; - if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) { - bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError); - } else { - async.readFileAsDataURL(file).then(function (sDataURL) { - modules.editor.insertImage($editable, sDataURL, filename); - }).fail(function () { - bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError); - }); - } - }); - } - }; - - var commands = { - /** - * @param {Object} layoutInfo - */ - showLinkDialog: function (layoutInfo) { - modules.linkDialog.show(layoutInfo); - }, - - /** - * @param {Object} layoutInfo - */ - showImageDialog: function (layoutInfo) { - modules.imageDialog.show(layoutInfo); - }, - - /** - * @param {Object} layoutInfo - */ - showHelpDialog: function (layoutInfo) { - modules.helpDialog.show(layoutInfo); - }, - - /** - * @param {Object} layoutInfo - */ - fullscreen: function (layoutInfo) { - modules.fullscreen.toggle(layoutInfo); - }, - - /** - * @param {Object} layoutInfo - */ - codeview: function (layoutInfo) { - modules.codeview.toggle(layoutInfo); - } - }; - - var hMousedown = function (event) { - //preventDefault Selection for FF, IE8+ - if (dom.isImg(event.target)) { - event.preventDefault(); - } - }; - - var hKeyupAndMouseup = function (event) { - var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); - modules.editor.removeBogus(layoutInfo.editable()); - hToolbarAndPopoverUpdate(event); - }; - - /** - * update sytle info - * @param {Object} styleInfo - * @param {Object} layoutInfo - */ - this.updateStyleInfo = function (styleInfo, layoutInfo) { - if (!styleInfo) { - return; - } - var isAirMode = (layoutInfo.editor().data('options') || {}).airMode; - if (!isAirMode) { - modules.toolbar.update(layoutInfo.toolbar(), styleInfo); - } - - modules.popover.update(layoutInfo.popover(), styleInfo, isAirMode); - modules.handle.update(layoutInfo.handle(), styleInfo, isAirMode); - }; - - var hToolbarAndPopoverUpdate = function (event) { - var target = event.target; - // delay for range after mouseup - setTimeout(function () { - var layoutInfo = dom.makeLayoutInfo(target); - /* ODOO: (start_modification */ - var $editable = layoutInfo.editable(); - if (event.setStyleInfoFromEditable) { - var styleInfo = modules.editor.styleFromNode($editable); - } else { - if (!event.isDefaultPrevented()) { - modules.editor.saveRange($editable); - } - var styleInfo = modules.editor.currentStyle(target); - } - /* ODOO: end_modification) */ - self.updateStyleInfo(styleInfo, layoutInfo); - }, 0); - }; - - var hScroll = function (event) { - var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); - //hide popover and handle when scrolled - modules.popover.hide(layoutInfo.popover()); - modules.handle.hide(layoutInfo.handle()); - }; - - var hToolbarAndPopoverMousedown = function (event) { - // prevent default event when insertTable (FF, Webkit) - var $btn = $(event.target).closest('[data-event]'); - if ($btn.length) { - event.preventDefault(); - } - }; - - var hToolbarAndPopoverClick = function (event) { - var $btn = $(event.target).closest('[data-event]'); - - if (!$btn.length) { - return; - } - - var eventName = $btn.attr('data-event'), - value = $btn.attr('data-value'), - hide = $btn.attr('data-hide'); - - var layoutInfo = dom.makeLayoutInfo(event.target); - - // before command: detect control selection element($target) - var $target; - if ($.inArray(eventName, ['resize', 'floatMe', 'removeMedia', 'imageShape']) !== -1) { - var $selection = layoutInfo.handle().find('.note-control-selection'); - $target = $($selection.data('target')); - } - - // If requested, hide the popover when the button is clicked. - // Useful for things like showHelpDialog. - if (hide) { - $btn.parents('.popover').hide(); - } - - if ($.isFunction($.summernote.pluginEvents[eventName])) { - $.summernote.pluginEvents[eventName](event, modules.editor, layoutInfo, value); - } else if (modules.editor[eventName]) { // on command - var $editable = layoutInfo.editable(); - $editable.focus(); - modules.editor[eventName]($editable, value, $target); - event.preventDefault(); - } else if (commands[eventName]) { - commands[eventName].call(this, layoutInfo); - event.preventDefault(); - } - - // after command - if ($.inArray(eventName, ['backColor', 'foreColor']) !== -1) { - var options = layoutInfo.editor().data('options', options); - var module = options.airMode ? modules.popover : modules.toolbar; - module.updateRecentColor(list.head($btn), eventName, value); - } - - hToolbarAndPopoverUpdate(event); - }; - - var PX_PER_EM = 18; - var hDimensionPickerMove = function (event, options) { - var $picker = $(event.target.parentNode); // target is mousecatcher - var $dimensionDisplay = $picker.next(); - var $catcher = $picker.find('.note-dimension-picker-mousecatcher'); - var $highlighted = $picker.find('.note-dimension-picker-highlighted'); - var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted'); - - var posOffset; - // HTML5 with jQuery - e.offsetX is undefined in Firefox - if (event.offsetX === undefined) { - var posCatcher = $(event.target).offset(); - posOffset = { - x: event.pageX - posCatcher.left, - y: event.pageY - posCatcher.top - }; - } else { - posOffset = { - x: event.offsetX, - y: event.offsetY - }; - } - - var dim = { - c: Math.ceil(posOffset.x / PX_PER_EM) || 1, - r: Math.ceil(posOffset.y / PX_PER_EM) || 1 - }; - - $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' }); - $catcher.attr('data-value', dim.c + 'x' + dim.r); - - if (3 < dim.c && dim.c < options.insertTableMaxSize.col) { - $unhighlighted.css({ width: dim.c + 1 + 'em'}); - } - - if (3 < dim.r && dim.r < options.insertTableMaxSize.row) { - $unhighlighted.css({ height: dim.r + 1 + 'em'}); - } - - $dimensionDisplay.html(dim.c + ' x ' + dim.r); - }; - - /** - * bind KeyMap on keydown - * - * @param {Object} layoutInfo - * @param {Object} keyMap - */ - this.bindKeyMap = function (layoutInfo, keyMap) { - var $editor = layoutInfo.editor(); - var $editable = layoutInfo.editable(); - - $editable.on('keydown', function (event) { - var keys = []; - - // modifier - if (event.metaKey) { keys.push('CMD'); } - if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); } - if (event.shiftKey) { keys.push('SHIFT'); } - - // keycode - var keyName = key.nameFromCode[event.keyCode]; - if (keyName) { - keys.push(keyName); - } - - var pluginEvent; - var keyString = keys.join('+'); - var eventName = keyMap[keyString]; - - // ODOO: (start_modification - // odoo change: add visible event to overwrite the browser comportment - var keycode = event.keyCode; - if (!eventName && - !event.ctrlKey && !event.metaKey && ( // special code/command - (keycode > 47 && keycode < 58) || // number keys - keycode == 32 || keycode == 13 || // spacebar & return - (keycode > 64 && keycode < 91) || // letter keys - (keycode > 95 && keycode < 112) || // numpad keys - (keycode > 185 && keycode < 193) || // ;=,-./` (in order) - (keycode > 218 && keycode < 223))) { // [\]' (in order)) - eventName = 'visible'; - } else if (!keycode && event.key !== 'Dead') { - self.invoke('restoreRange', $editable); - } - // ODOO: end_modification) - - if (eventName) { - // FIXME Summernote doesn't support event pipeline yet. - // - Plugin -> Base Code - pluginEvent = $.summernote.pluginEvents[keyString]; - if ($.isFunction(pluginEvent)) { - if (pluginEvent(event, modules.editor, layoutInfo)) { - return false; - } - } - - pluginEvent = $.summernote.pluginEvents[eventName]; - - if ($.isFunction(pluginEvent)) { - pluginEvent(event, modules.editor, layoutInfo); - } else if (modules.editor[eventName]) { - modules.editor[eventName]($editable, $editor.data('options')); - event.preventDefault(); - } else if (commands[eventName]) { - commands[eventName].call(this, layoutInfo); - event.preventDefault(); - } - } else if (key.isEdit(event.keyCode)) { - modules.editor.afterCommand($editable); - } - }); - }; - - /** - * attach eventhandler - * - * @param {Object} layoutInfo - layout Informations - * @param {Object} options - user options include custom event handlers - */ - this.attach = function (layoutInfo, options) { - // handlers for editable - if (options.shortcuts) { - this.bindKeyMap(layoutInfo, options.keyMap[agent.isMac ? 'mac' : 'pc']); - } - layoutInfo.editable().on('mousedown', hMousedown); - layoutInfo.editable().on('keyup mouseup', hKeyupAndMouseup); - layoutInfo.editable().on('scroll', hScroll); - - // handler for clipboard - modules.clipboard.attach(layoutInfo, options); - - // handler for handle and popover - modules.handle.attach(layoutInfo, options); - layoutInfo.popover().on('click', hToolbarAndPopoverClick); - layoutInfo.popover().on('mousedown', hToolbarAndPopoverMousedown); - - // handler for drag and drop - modules.dragAndDrop.attach(layoutInfo, options); - - // handlers for frame mode (toolbar, statusbar) - if (!options.airMode) { - // handler for toolbar - layoutInfo.toolbar().on('click', hToolbarAndPopoverClick); - layoutInfo.toolbar().on('mousedown', hToolbarAndPopoverMousedown); - - // handler for statusbar - modules.statusbar.attach(layoutInfo, options); - } - - // handler for table dimension - var $catcherContainer = options.airMode ? layoutInfo.popover() : - layoutInfo.toolbar(); - var $catcher = $catcherContainer.find('.note-dimension-picker-mousecatcher'); - $catcher.css({ - width: options.insertTableMaxSize.col + 'em', - height: options.insertTableMaxSize.row + 'em' - }).on('mousemove', function (event) { - hDimensionPickerMove(event, options); - }); - - // save options on editor - layoutInfo.editor().data('options', options); - - // ret styleWithCSS for backColor / foreColor clearing with 'inherit'. - if (!agent.isMSIE) { - // [workaround] for Firefox - // - protect FF Error: NS_ERROR_FAILURE: Failure - setTimeout(function () { - document.execCommand('styleWithCSS', 0, options.styleWithSpan); - }, 0); - } - - // History - var history = new History(layoutInfo.editable()); - layoutInfo.editable().data('NoteHistory', history); - - // All editor status will be saved on editable with jquery's data - // for support multiple editor with singleton object. - layoutInfo.editable().data('callbacks', { - onInit: options.onInit, - onFocus: options.onFocus, - onBlur: options.onBlur, - onKeydown: options.onKeydown, - onKeyup: options.onKeyup, - onMousedown: options.onMousedown, - onEnter: options.onEnter, - onPaste: options.onPaste, - onBeforeCommand: options.onBeforeCommand, - onChange: options.onChange, - onImageUpload: options.onImageUpload, - onImageUploadError: options.onImageUploadError, - onMediaDelete: options.onMediaDelete, - onToolbarClick: options.onToolbarClick, - onUpload: options.onUpload, - }); - - var styleInfo = modules.editor.styleFromNode(layoutInfo.editable()); - this.updateStyleInfo(styleInfo, layoutInfo); - }; - - /** - * attach jquery custom event - * - * @param {Object} layoutInfo - layout Informations - */ - this.attachCustomEvent = function (layoutInfo, options) { - var $holder = layoutInfo.holder(); - var $editable = layoutInfo.editable(); - var callbacks = $editable.data('callbacks'); - - $editable.focus(bindCustomEvent($holder, callbacks, 'focus')); - $editable.blur(bindCustomEvent($holder, callbacks, 'blur')); - - $editable.keydown(function (event) { - if (event.keyCode === key.code.ENTER) { - bindCustomEvent($holder, callbacks, 'enter').call(this, event); - } - bindCustomEvent($holder, callbacks, 'keydown').call(this, event); - }); - $editable.keyup(bindCustomEvent($holder, callbacks, 'keyup')); - - $editable.on('mousedown', bindCustomEvent($holder, callbacks, 'mousedown')); - $editable.on('mouseup', bindCustomEvent($holder, callbacks, 'mouseup')); - $editable.on('scroll', bindCustomEvent($holder, callbacks, 'scroll')); - - $editable.on('paste', bindCustomEvent($holder, callbacks, 'paste')); - - // [workaround] IE doesn't have input events for contentEditable - // - see: https://goo.gl/4bfIvA - var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input'; - $editable.on(changeEventName, function () { - bindCustomEvent($holder, callbacks, 'change')($editable.html(), $editable); - }); - - if (!options.airMode) { - layoutInfo.toolbar().click(bindCustomEvent($holder, callbacks, 'toolbar.click')); - layoutInfo.popover().click(bindCustomEvent($holder, callbacks, 'popover.click')); - } - - // Textarea: auto filling the code before form submit. - if (dom.isTextarea(list.head($holder))) { - $holder.closest('form').submit(function (e) { - layoutInfo.holder().val(layoutInfo.holder().code()); - bindCustomEvent($holder, callbacks, 'submit').call(this, e, $holder.code()); - }); - } - - // textarea auto sync - if (dom.isTextarea(list.head($holder)) && options.textareaAutoSync) { - $holder.on('summernote.change', function () { - layoutInfo.holder().val(layoutInfo.holder().code()); - }); - } - - // fire init event - bindCustomEvent($holder, callbacks, 'init')(layoutInfo); - - // fire plugin init event - for (var i = 0, len = $.summernote.plugins.length; i < len; i++) { - if ($.isFunction($.summernote.plugins[i].init)) { - $.summernote.plugins[i].init(layoutInfo); - } - } - }; - - this.detach = function (layoutInfo, options) { - layoutInfo.holder().off(); - layoutInfo.editable().off(); - - layoutInfo.popover().off(); - layoutInfo.handle().off(); - layoutInfo.dialog().off(); - - if (!options.airMode) { - layoutInfo.dropzone().off(); - layoutInfo.toolbar().off(); - layoutInfo.statusbar().off(); - } - }; - }; - - return EventHandler; -}); |
