diff --git a/addons/web_editor/static/lib/summernote/src/js/EventHandler.js b/addons/web_editor/static/lib/summernote/src/js/EventHandler.js index 09a7b00082a..068ceff735f 100644 --- a/addons/web_editor/static/lib/summernote/src/js/EventHandler.js +++ b/addons/web_editor/static/lib/summernote/src/js/EventHandler.js @@ -1,131 +1,92 @@ 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) { + 'summernote/core/agent', 'summernote/core/dom', 'summernote/core/async', 'summernote/core/key', 'summernote/core/list', 'summernote/core/range', + 'summernote/editing/Style', 'summernote/editing/Editor', 'summernote/editing/History', + 'summernote/module/Toolbar', 'summernote/module/Popover', 'summernote/module/Handle', 'summernote/module/Dialog' +], function (agent, dom, async, key, list, range, + Style, Editor, History, + Toolbar, Popover, Handle, Dialog) { + + var CodeMirror; + if (agent.hasCodeMirror) { + if (agent.isSupportAmd) { + require(['CodeMirror'], function (cm) { + CodeMirror = cm; + }); + } else { + CodeMirror = window.CodeMirror; + } + } /** - * @class EventHandler - * * EventHandler - * - TODO: new instance per a editor */ var EventHandler = function () { - var self = this; + var $window = $(window); + var $document = $(document); + var $scrollbar = $('html, body'); - /** - * 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) + var editor = new Editor(); + var toolbar = new Toolbar(), popover = new Popover(); + var handle = new Handle(), dialog = new Dialog(); + + this.getEditor = function () { + return editor; }; /** - * invoke module's method + * returns makeLayoutInfo from editor's descendant node. * - * @param {String} moduleAndMethod - ex) 'editor.redo' - * @param {...*} arguments - arguments of method - * @return {*} + * @param {Node} descendant + * @returns {Object} */ - this.invoke = function () { - var moduleAndMethod = list.head(list.from(arguments)); - var args = list.tail(list.from(arguments)); + var makeLayoutInfo = function (descendant) { + var $target = $(descendant).closest('.note-editor, .note-air-editor, .note-air-layout'); - var splits = moduleAndMethod.split('.'); - var hasSeparator = splits.length > 1; - var moduleName = hasSeparator && list.head(splits); - var methodName = hasSeparator ? list.last(splits) : list.head(splits); + if (!$target.length) { return null; } - var module = this.getModule(moduleName); - var method = module[methodName]; + var $editor; + if ($target.is('.note-editor, .note-air-editor')) { + $editor = $target; + } else { + $editor = $('[data-note-id="' + list.last($target.attr('id').split('-')) + '"]'); + } - 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); - }; + return dom.buildLayoutInfo($editor); }; /** * insert Images from file array. * - * @private * @param {Object} layoutInfo * @param {File[]} files */ - this.insertImages = function (layoutInfo, files) { + var insertImages = function (layoutInfo, files) { var $editor = layoutInfo.editor(), - $editable = layoutInfo.editable(), - $holder = layoutInfo.holder(); + $editable = layoutInfo.editable(); var callbacks = $editable.data('callbacks'); var options = $editor.data('options'); // If onImageUpload options setted if (callbacks.onImageUpload) { - bindCustomEvent($holder, callbacks, 'image.upload')(files); + callbacks.onImageUpload(files, editor, $editable); // 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); + if (callbacks.onImageUploadError) { + callbacks.onImageUploadError(options.langInfo.image.maximumFileSizeError); + } else { + alert(options.langInfo.image.maximumFileSizeError); + } } else { async.readFileAsDataURL(file).then(function (sDataURL) { - modules.editor.insertImage($editable, sDataURL, filename); + editor.insertImage($editable, sDataURL, filename); }).fail(function () { - bindCustomEvent($holder, callbacks, 'image.upload.error')(options.langInfo.image.maximumFileSizeError); + if (callbacks.onImageUploadError) { + callbacks.onImageUploadError(); + } }); } }); @@ -137,90 +98,292 @@ define([ * @param {Object} layoutInfo */ showLinkDialog: function (layoutInfo) { - modules.linkDialog.show(layoutInfo); + var $editor = layoutInfo.editor(), + $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(), + linkInfo = editor.getLinkInfo($editable); + + var options = $editor.data('options'); + + editor.saveRange($editable); + dialog.showLinkDialog($editable, $dialog, linkInfo).then(function (linkInfo) { + editor.restoreRange($editable); + editor.createLink($editable, linkInfo, options); + // hide popover after creating link + popover.hide(layoutInfo.popover()); + }).fail(function () { + editor.restoreRange($editable); + }); }, /** * @param {Object} layoutInfo */ showImageDialog: function (layoutInfo) { - modules.imageDialog.show(layoutInfo); + var $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(); + + editor.saveRange($editable); + dialog.showImageDialog($editable, $dialog).then(function (data) { + editor.restoreRange($editable); + + if (typeof data === 'string') { + // image url + editor.insertImage($editable, data); + } else { + // array of files + insertImages(layoutInfo, data); + } + }).fail(function () { + editor.restoreRange($editable); + }); }, /** * @param {Object} layoutInfo */ showHelpDialog: function (layoutInfo) { - modules.helpDialog.show(layoutInfo); + var $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(); + + editor.saveRange($editable, true); + dialog.showHelpDialog($editable, $dialog).then(function () { + editor.restoreRange($editable); + }); }, - /** - * @param {Object} layoutInfo - */ fullscreen: function (layoutInfo) { - modules.fullscreen.toggle(layoutInfo); + var $editor = layoutInfo.editor(), + $toolbar = layoutInfo.toolbar(), + $editable = layoutInfo.editable(), + $codable = layoutInfo.codable(); + + var resize = function (size) { + $editable.css('height', size.h); + $codable.css('height', size.h); + if ($codable.data('cmeditor')) { + $codable.data('cmeditor').setsize(null, size.h); + } + }; + + $editor.toggleClass('fullscreen'); + var isFullscreen = $editor.hasClass('fullscreen'); + if (isFullscreen) { + $editable.data('orgheight', $editable.css('height')); + + $window.on('resize', function () { + resize({ + h: $window.height() - $toolbar.outerHeight() + }); + }).trigger('resize'); + + $scrollbar.css('overflow', 'hidden'); + } else { + $window.off('resize'); + resize({ + h: $editable.data('orgheight') + }); + $scrollbar.css('overflow', 'visible'); + } + + toolbar.updateFullscreen($toolbar, isFullscreen); }, - /** - * @param {Object} layoutInfo - */ codeview: function (layoutInfo) { - modules.codeview.toggle(layoutInfo); + var $editor = layoutInfo.editor(), + $toolbar = layoutInfo.toolbar(), + $editable = layoutInfo.editable(), + $codable = layoutInfo.codable(), + $popover = layoutInfo.popover(), + $handle = layoutInfo.handle(); + + var options = $editor.data('options'); + + var cmEditor, server; + + $editor.toggleClass('codeview'); + + var isCodeview = $editor.hasClass('codeview'); + if (isCodeview) { + $codable.val(dom.html($editable, true)); + $codable.height($editable.height()); + toolbar.deactivate($toolbar); + popover.hide($popover); + handle.hide($handle); + $codable.focus(); + + // activate CodeMirror as codable + if (agent.hasCodeMirror) { + cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror); + + // CodeMirror TernServer + if (options.codemirror.tern) { + server = new CodeMirror.TernServer(options.codemirror.tern); + cmEditor.ternServer = server; + cmEditor.on('cursorActivity', function (cm) { + server.updateArgHints(cm); + }); + } + + // CodeMirror hasn't Padding. + cmEditor.setSize(null, $editable.outerHeight()); + $codable.data('cmEditor', cmEditor); + } + } else { + // deactivate CodeMirror as codable + if (agent.hasCodeMirror) { + cmEditor = $codable.data('cmEditor'); + $codable.val(cmEditor.getValue()); + cmEditor.toTextArea(); + } + + $editable.html(dom.value($codable) || dom.emptyPara); + $editable.height(options.height ? $codable.height() : 'auto'); + + toolbar.activate($toolbar); + $editable.focus(); + } + + toolbar.updateCodeview(layoutInfo.toolbar(), isCodeview); } }; + this.commands = commands; // odoo change for overwrite + this.editor = editor; // odoo change for overwrite + this.toolbar = toolbar; // odoo change for overwrite + this.popover = popover; // odoo change for overwrite + this.handle = handle; // odoo change for overwrite + this.dialog = dialog; // odoo change for overwrite + var hMousedown = function (event) { //preventDefault Selection for FF, IE8+ if (dom.isImg(event.target)) { event.preventDefault(); + range.createFromNode(event.target).select(); } }; - 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 layoutInfo = makeLayoutInfo(event.target); var $editable = layoutInfo.editable(); - if (!event.isDefaultPrevented()) { - modules.editor.saveRange($editable); + editor.saveRange($editable); + var styleInfo = editor.currentStyle(); + if (!styleInfo) { return; } + + var isAirMode = layoutInfo.editor().data('options').airMode; + if (!isAirMode) { + toolbar.update(layoutInfo.toolbar(), styleInfo); } - /* ODOO: end_modification) */ - var styleInfo = modules.editor.currentStyle(target); - self.updateStyleInfo(styleInfo, layoutInfo); + + popover.update(layoutInfo.popover(), styleInfo, isAirMode); + handle.update(layoutInfo.handle(), styleInfo, isAirMode); }, 0); }; var hScroll = function (event) { - var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); //hide popover and handle when scrolled - modules.popover.hide(layoutInfo.popover()); - modules.handle.hide(layoutInfo.handle()); + popover.hide(layoutInfo.popover()); + handle.hide(layoutInfo.handle()); + }; + + /** + * paste clipboard image + * + * @param {Event} event + */ + var hPasteClipboardImage = function (event) { + var clipboardData = event.originalEvent.clipboardData; + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); + var $editable = layoutInfo.editable(); + + if (!clipboardData || !clipboardData.items || !clipboardData.items.length) { + var callbacks = $editable.data('callbacks'); + // only can run if it has onImageUpload method + if (!callbacks.onImageUpload) { + return; + } + + // save cursor + editor.saveNode($editable); + editor.saveRange($editable); + + $editable.html(''); + + setTimeout(function () { + var $img = $editable.find('img'); + var datauri = $img[0].src; + + var data = atob(datauri.split(',')[1]); + var array = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) { + array[i] = data.charCodeAt(i); + } + + var blob = new Blob([array], { type : 'image/png'}); + blob.name = 'clipboard.png'; + + editor.restoreNode($editable); + editor.restoreRange($editable); + insertImages(layoutInfo, [blob]); + + editor.afterCommand($editable); + }, 0); + + return; + } + + var item = list.head(clipboardData.items); + var isClipboardImage = item.kind === 'file' && item.type.indexOf('image/') !== -1; + + if (isClipboardImage) { + insertImages(layoutInfo, [item.getAsFile()]); + } + + editor.afterCommand($editable); + }; + + /** + * `mousedown` event handler on $handle + * - controlSizing: resize image + * + * @param {MouseEvent} event + */ + var hHandleMousedown = function (event) { + if (dom.isControlSizing(event.target)) { + event.preventDefault(); + event.stopPropagation(); + + var layoutInfo = makeLayoutInfo(event.target), + $handle = layoutInfo.handle(), $popover = layoutInfo.popover(), + $editable = layoutInfo.editable(), + $editor = layoutInfo.editor(); + + var target = $handle.find('.note-control-selection').data('target'), + $target = $(target), posStart = $target.offset(), + scrollTop = $document.scrollTop(); + + var isAirMode = $editor.data('options').airMode; + + $document.on('mousemove', function (event) { + editor.resizeTo({ + x: event.clientX - posStart.left, + y: event.clientY - (posStart.top - scrollTop) + }, $target, !event.shiftKey); + + handle.update($handle, {image: target}, isAirMode); + popover.update($popover, {image: target}, isAirMode); + }).one('mouseup', function () { + $document.off('mousemove'); + editor.afterCommand($editable); + }); + + if (!$target.data('ratio')) { // original ratio. + $target.data('ratio', $target.height() / $target.width()); + } + } }; var hToolbarAndPopoverMousedown = function (event) { @@ -234,49 +397,76 @@ define([ var hToolbarAndPopoverClick = function (event) { var $btn = $(event.target).closest('[data-event]'); - if (!$btn.length) { - return; - } + if ($btn.length && !$btn.is(':disabled')) { + var eventName = $btn.attr('data-event'), + value = $btn.attr('data-value'), + hide = $btn.attr('data-hide'); - var eventName = $btn.attr('data-event'), - value = $btn.attr('data-value'), - hide = $btn.attr('data-hide'); + var layoutInfo = makeLayoutInfo(event.target); - 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(); - } + editor.restoreRange($editable); - // 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); - } + // 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')); + } - hToolbarAndPopoverUpdate(event); + // 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, editor, layoutInfo, value); + } else if (editor[eventName]) { // on command + $editable.trigger('focus'); + 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 ? popover : toolbar; + module.updateRecentColor(list.head($btn), eventName, value); + } + + hToolbarAndPopoverUpdate(event); + } + }; + + var EDITABLE_PADDING = 24; + /** + * `mousedown` event handler on statusbar + * + * @param {MouseEvent} event + */ + var hStatusbarMousedown = function (event) { + event.preventDefault(); + event.stopPropagation(); + + var layoutInfo = makeLayoutInfo(event.target); + var $editable = layoutInfo.editable(); + var nEditableTop = $editable.offset().top - $document.scrollTop(); + var options = layoutInfo.editor().data('options'); + + $document.on('mousemove', function (event) { + var nHeight = event.clientY - (nEditableTop + EDITABLE_PADDING); + + nHeight = (options.minHeight > 0) ? Math.max(nHeight, options.minHeight) : nHeight; + nHeight = (options.maxHeight > 0) ? Math.min(nHeight, options.maxHeight) : nHeight; + + $editable.height(nHeight); + }).one('mouseup', function () { + $document.off('mousemove'); + }); }; var PX_PER_EM = 18; @@ -320,7 +510,81 @@ define([ $dimensionDisplay.html(dim.c + ' x ' + dim.r); }; - + + /** + * Drag and Drop Events + * + * @param {Object} layoutInfo - layout Informations + * @param {Object} options + */ + var handleDragAndDropEvent = function (layoutInfo, options) { + if (options.disableDragAndDrop) { + // prevent default drop event + $document.on('drop', function (e) { + e.preventDefault(); + }); + } else { + attachDragAndDropEvent(layoutInfo, options); + } + }; + + /** + * attach Drag and Drop Events + * + * @param {Object} layoutInfo - layout Informations + * @param {Object} options + */ + var attachDragAndDropEvent = function (layoutInfo, options) { + var collection = $(), + $dropzone = layoutInfo.dropzone, + $dropzoneMessage = layoutInfo.dropzone.find('.note-dropzone-message'); + + // show dropzone on dragenter when dragging a object to document. + $document.on('dragenter', function (e) { + var isCodeview = layoutInfo.editor.hasClass('codeview'); + if (!isCodeview && !collection.length) { + layoutInfo.editor.addClass('dragover'); + $dropzone.width(layoutInfo.editor.width()); + $dropzone.height(layoutInfo.editor.height()); + $dropzoneMessage.text(options.langInfo.image.dragImageHere); + } + collection = collection.add(e.target); + }).on('dragleave', function (e) { + collection = collection.not(e.target); + if (!collection.length) { + layoutInfo.editor.removeClass('dragover'); + } + }).on('drop', function () { + collection = $(); + layoutInfo.editor.removeClass('dragover'); + }); + + // change dropzone's message on hover. + $dropzone.on('dragenter', function () { + $dropzone.addClass('hover'); + $dropzoneMessage.text(options.langInfo.image.dropImage); + }).on('dragleave', function () { + $dropzone.removeClass('hover'); + $dropzoneMessage.text(options.langInfo.image.dragImageHere); + }); + + // attach dropImage + $dropzone.on('drop', function (event) { + event.preventDefault(); + + var dataTransfer = event.originalEvent.dataTransfer; + var text = dataTransfer.getData('text/plain'); + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); + layoutInfo.editable().focus(); + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + insertImages(layoutInfo, dataTransfer.files); + } else if (text) { + editor.insertText(layoutInfo.editable(), text); + } + }).on('dragover', false); // prevent default dragover event + }; + + /** * bind KeyMap on keydown * @@ -328,28 +592,25 @@ define([ * @param {Object} keyMap */ this.bindKeyMap = function (layoutInfo, keyMap) { - var $editor = layoutInfo.editor(); - var $editable = layoutInfo.editable(); + var $editor = layoutInfo.editor; + var $editable = layoutInfo.editable; + + layoutInfo = makeLayoutInfo($editable); $editable.on('keydown', function (event) { - var keys = []; + var aKey = []; // modifier - if (event.metaKey) { keys.push('CMD'); } - if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); } - if (event.shiftKey) { keys.push('SHIFT'); } + if (event.metaKey) { aKey.push('CMD'); } + if (event.ctrlKey && !event.altKey) { aKey.push('CTRL'); } + if (event.shiftKey) { aKey.push('SHIFT'); } // keycode var keyName = key.nameFromCode[event.keyCode]; - if (keyName) { - keys.push(keyName); - } + if (keyName) { aKey.push(keyName); } - var pluginEvent; - var keyString = keys.join('+'); - var eventName = keyMap[keyString]; + var eventName = keyMap[aKey.join('+')]; - // ODOO: (start_modification // odoo change: add visible event to overwrite the browser comportment var keycode = event.keyCode; if (!eventName && @@ -362,33 +623,27 @@ define([ (keycode > 218 && keycode < 223))) { // [\]' (in order)) eventName = 'visible'; } else if (!keycode) { - self.invoke('restoreRange', $editable); + editor.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; + if ($.summernote.pluginEvents[eventName]) { + var plugin = $.summernote.pluginEvents[eventName]; + if ($.isFunction(plugin)) { + plugin(event, editor, layoutInfo); } - } - - pluginEvent = $.summernote.pluginEvents[eventName]; - - if ($.isFunction(pluginEvent)) { - pluginEvent(event, modules.editor, layoutInfo); - } else if (modules.editor[eventName]) { - modules.editor[eventName]($editable, $editor.data('options')); + } else if (editor[eventName]) { + editor[eventName]($editable, $editor.data('options')); event.preventDefault(); } else if (commands[eventName]) { commands[eventName].call(this, layoutInfo); event.preventDefault(); } + if (keycode) { + editor.saveRange($editable); + } } else if (key.isEdit(event.keyCode)) { - modules.editor.afterCommand($editable); + editor.afterCommand($editable); } }); }; @@ -398,40 +653,41 @@ define([ * * @param {Object} layoutInfo - layout Informations * @param {Object} options - user options include custom event handlers + * @param {Function} options.enter - enter key handler */ 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); + layoutInfo.editable.on('mousedown', hMousedown); + layoutInfo.editable.on('keyup mouseup', hToolbarAndPopoverUpdate); + layoutInfo.editable.on('scroll', hScroll); + layoutInfo.editable.on('paste', hPasteClipboardImage); // 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); + layoutInfo.handle.on('mousedown', hHandleMousedown); + layoutInfo.popover.on('click', hToolbarAndPopoverClick); + layoutInfo.popover.on('mousedown', hToolbarAndPopoverMousedown); // handlers for frame mode (toolbar, statusbar) if (!options.airMode) { + // handler for drag and drop + handleDragAndDropEvent(layoutInfo, options); + // handler for toolbar - layoutInfo.toolbar().on('click', hToolbarAndPopoverClick); - layoutInfo.toolbar().on('mousedown', hToolbarAndPopoverMousedown); + layoutInfo.toolbar.on('click', hToolbarAndPopoverClick); + layoutInfo.toolbar.on('mousedown', hToolbarAndPopoverMousedown); // handler for statusbar - modules.statusbar.attach(layoutInfo, options); + if (!options.disableResizeEditor) { + layoutInfo.statusbar.on('mousedown', hStatusbarMousedown); + } } // handler for table dimension - var $catcherContainer = options.airMode ? layoutInfo.popover() : - layoutInfo.toolbar(); + var $catcherContainer = options.airMode ? layoutInfo.popover : + layoutInfo.toolbar; var $catcher = $catcherContainer.find('.note-dimension-picker-mousecatcher'); $catcher.css({ width: options.insertTableMaxSize.col + 'em', @@ -441,121 +697,72 @@ define([ }); // save options on editor - layoutInfo.editor().data('options', options); + 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 + // 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); + var history = new History(layoutInfo.editable); + layoutInfo.editable.data('NoteHistory', history); + + // basic event callbacks (lowercase) + // enter, focus, blur, keyup, keydown + if (options.onenter) { + layoutInfo.editable.keypress(function (event) { + if (event.keyCode === key.ENTER) { options.onenter(event); } + }); + } + + if (options.onfocus) { layoutInfo.editable.focus(options.onfocus); } + if (options.onblur) { layoutInfo.editable.blur(options.onblur); } + if (options.onkeyup) { layoutInfo.editable.keyup(options.onkeyup); } + if (options.onkeydown) { layoutInfo.editable.keydown(options.onkeydown); } + if (options.onpaste) { layoutInfo.editable.on('paste', options.onpaste); } + + // callbacks for advanced features (camel) + if (options.onToolbarClick) { layoutInfo.toolbar.click(options.onToolbarClick); } + if (options.onChange) { + var hChange = function () { + editor.triggerOnChange(layoutInfo.editable); + }; + + if (agent.isMSIE) { + var sDomEvents = 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted'; + layoutInfo.editable.on(sDomEvents, hChange); + } else { + layoutInfo.editable.on('input', hChange); + } + } // 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, + layoutInfo.editable.data('callbacks', { onChange: options.onChange, + onAutoSave: options.onAutoSave, onImageUpload: options.onImageUpload, onImageUploadError: options.onImageUploadError, - onMediaDelete: options.onMediaDelete, - onToolbarClick: options.onToolbarClick + onFileUpload: options.onFileUpload, + onFileUploadError: options.onFileUpload }); - - 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.editable.off(); - layoutInfo.popover().off(); - layoutInfo.handle().off(); - layoutInfo.dialog().off(); + layoutInfo.popover.off(); + layoutInfo.handle.off(); + layoutInfo.dialog.off(); if (!options.airMode) { - layoutInfo.dropzone().off(); - layoutInfo.toolbar().off(); - layoutInfo.statusbar().off(); + layoutInfo.dropzone.off(); + layoutInfo.toolbar.off(); + layoutInfo.statusbar.off(); } }; }; diff --git a/addons/web_editor/static/lib/summernote/src/js/Renderer.js b/addons/web_editor/static/lib/summernote/src/js/Renderer.js index 922bd6a111c..07584edc453 100644 --- a/addons/web_editor/static/lib/summernote/src/js/Renderer.js +++ b/addons/web_editor/static/lib/summernote/src/js/Renderer.js @@ -1,12 +1,7 @@ define([ - 'summernote/core/agent', - 'summernote/core/dom', - 'summernote/core/func', - 'summernote/core/list' -], function (agent, dom, func, list) { + 'summernote/core/agent', 'summernote/core/dom', 'summernote/core/func' +], function (agent, dom, func) { /** - * @class Renderer - * * renderer * * rendering toolbar and editable @@ -15,15 +10,14 @@ define([ /** * bootstrap button template - * @private - * @param {String} label button name - * @param {Object} [options] button options - * @param {String} [options.event] data-event - * @param {String} [options.className] button's class name - * @param {String} [options.value] data-value - * @param {String} [options.title] button's title for popup - * @param {String} [options.dropdown] dropdown html - * @param {String} [options.hide] data-hide + * + * @param {String} label + * @param {Object} [options] + * @param {String} [options.event] + * @param {String} [options.value] + * @param {String} [options.title] + * @param {String} [options.dropdown] + * @param {String} [options.hide] */ var tplButton = function (label, options) { var event = options.event; @@ -33,11 +27,9 @@ define([ var dropdown = options.dropdown; var hide = options.hide; - return (dropdown ? '
' + - 'Summernote @VERSION · ' + - 'Project · ' + - 'Issues' + + 'Summernote @VERSION · ' + + 'Project · ' + + 'Issues' + '
'; return tplDialog('note-help-dialog', '', body, ''); } @@ -772,7 +696,7 @@ define([ }); }; - this.createPalette = createPalette; // ODOO: allow access for override + this.createPalette = createPalette; // odoo change for overwrite /** * create summernote layout (air mode) @@ -785,25 +709,24 @@ define([ var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc']; var id = func.uniqueId(); - $holder.addClass('note-air-editor note-editable panel-body'); + $holder.addClass('note-air-editor note-editable'); $holder.attr({ - 'data-note-id': id, // ODOO: we use [data-note-id="{id}"] instead of [id="{id}"] - // 'id': 'note-editor-' + id, + 'data-note-id': id, 'contentEditable': true }); var body = document.body; // create Popover - var $popover = $(this.tplPopovers(langInfo, options)); // ODOO: user (maybe) overrided method + var $popover = $(this.tplPopovers(langInfo, options)); // odoo change for overwrite $popover.addClass('note-air-layout'); $popover.attr('id', 'note-popover-' + id); $popover.appendTo(body); createTooltip($popover, keyMap); - this.createPalette($popover, options); // ODOO: use (maybe) overrided method + this.createPalette($popover, options); // odoo change for overwrite // create Handle - var $handle = $(tplHandles(options)); + var $handle = $(tplHandles()); $handle.addClass('note-air-layout'); $handle.attr('id', 'note-handle-' + id); $handle.appendTo(body); @@ -828,7 +751,7 @@ define([ var langInfo = options.langInfo; //01. create Editor - var $editor = $(''); + var $editor = $(''); if (options.width) { $editor.width(options.width); } @@ -838,81 +761,70 @@ define([ $('').prependTo($editor); } - //03 editing area - var $editingArea = $(''); - //03. create editable + //03. create Editable var isContentEditable = !$holder.is(':disabled'); - var $editable = $('').prependTo($editingArea); - + var $editable = $('') + .prependTo($editor); if (options.height) { $editable.height(options.height); } if (options.direction) { $editable.attr('dir', options.direction); } - var placeholder = $holder.attr('placeholder') || options.placeholder; - if (placeholder) { - $editable.attr('data-placeholder', placeholder); + if (options.placeholder) { + $editable.attr('data-placeholder', options.placeholder); } - $editable.html(dom.html($holder) || dom.emptyPara); + $editable.html(dom.html($holder)); //031. create codable - $('').prependTo($editingArea); + $('').prependTo($editor); - //04. create Popover - var $popover = $(this.tplPopovers(langInfo, options)).prependTo($editingArea); // ODOO: use (maybe) overrided method - this.createPalette($popover, options); // ODOO: use (maybe) overrided method - createTooltip($popover, keyMap); - - //05. handle(control selection, ...) - $(tplHandles(options)).prependTo($editingArea); - - $editingArea.prependTo($editor); - - //06. create Toolbar - var $toolbar = $(''); + //04. create Toolbar + var toolbarHTML = ''; for (var idx = 0, len = options.toolbar.length; idx < len; idx ++) { var groupName = options.toolbar[idx][0]; var groupButtons = options.toolbar[idx][1]; - var $group = $(''); + toolbarHTML += '] var ancestors = listAncestor(point.node, func.eq(root)); if (!ancestors.length) { return null; } else if (ancestors.length === 1) { - return splitNode(point, options); + return splitNode(point, isSkipPaddingBlankHTML); } return ancestors.reduce(function (node, parent) { + var clone = insertAfter(parent.cloneNode(false), parent); + if (node === point.node) { - node = splitNode(point, options); + node = splitNode(point, isSkipPaddingBlankHTML); } - return splitNode({ - node: parent, - offset: node ? dom.position(node) : nodeLength(parent) - }, options); + appendChildNodes(clone, listNext(node)); + + if (!isSkipPaddingBlankHTML) { + paddingBlankHTML(parent); + paddingBlankHTML(clone); + } + return clone; }); }; - /** - * split point - * - * @param {Point} point - * @param {Boolean} isInline - * @return {Object} - */ - var splitPoint = function (point, isInline) { - // find splitRoot, container - // - inline: splitRoot is a child of paragraph - // - block: splitRoot is a child of bodyContainer - var pred = isInline ? isPara : isBodyContainer; - var ancestors = listAncestor(point.node, pred); - var topAncestor = list.last(ancestors) || point.node; - - var splitRoot, container; - if (pred(topAncestor)) { - splitRoot = ancestors[ancestors.length - 2]; - container = topAncestor; - } else { - splitRoot = topAncestor; - container = splitRoot.parentNode; - } - - // if splitRoot is exists, split with splitTree - var pivot = splitRoot && splitTree(splitRoot, point, { - isSkipPaddingBlankHTML: isInline, - isNotSplitEdgePoint: isInline - }); - - // if container is point.node, find pivot with point.offset - if (!pivot && container === point.node) { - pivot = point.node.childNodes[point.offset]; - } - - return { - rightNode: pivot, - container: container - }; - }; - var create = function (nodeName) { return document.createElement(nodeName); }; @@ -917,10 +692,7 @@ define([ }; /** - * @method remove - * * remove node, (isRemoveChild: remove child or not) - * * @param {Node} node * @param {Boolean} isRemoveChild */ @@ -945,8 +717,6 @@ define([ }; /** - * @method removeWhile - * * @param {Node} node * @param {Function} pred */ @@ -963,8 +733,6 @@ define([ }; /** - * @method replace - * * replace node with provided nodeName * * @param {Node} node @@ -992,27 +760,13 @@ define([ var isTextarea = makePredByNodeName('TEXTAREA'); /** - * @param {jQuery} $node - * @param {Boolean} [stripLinebreaks] - default: false - */ - var value = function ($node, stripLinebreaks) { - var val = isTextarea($node[0]) ? $node.val() : $node.html(); - if (stripLinebreaks) { - return val.replace(/[\n\r]/g, ''); - } - return val; - }; - - /** - * @method html - * - * get the HTML contents of node + * get the HTML contents of node * * @param {jQuery} $node * @param {Boolean} [isNewlineOnBlock] */ var html = function ($node, isNewlineOnBlock) { - var markup = value($node); + var markup = isTextarea($node[0]) ? $node.val() : $node.html(); if (isNewlineOnBlock) { var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g; @@ -1030,31 +784,30 @@ define([ return markup; }; + var value = function ($textarea) { + var val = $textarea.val(); + // strip line breaks + return val.replace(/[\n\r]/g, ''); + }; + return { - /** @property {String} NBSP_CHAR */ NBSP_CHAR: NBSP_CHAR, - /** @property {String} ZERO_WIDTH_NBSP_CHAR */ ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR, - /** @property {String} blank */ blank: blankHTML, - /** @property {String} emptyPara */ emptyPara: '
' + blankHTML + '
', - makePredByNodeName: makePredByNodeName, isEditable: isEditable, isControlSizing: isControlSizing, buildLayoutInfo: buildLayoutInfo, - makeLayoutInfo: makeLayoutInfo, isText: isText, isVoid: isVoid, isPara: isPara, isPurePara: isPurePara, isInline: isInline, - isBlock: func.not(isInline), isBodyInline: isBodyInline, isBody: isBody, isParaInline: isParaInline, isList: isList, - isTable: isTable, + isTable: makePredByNodeName('TABLE'), isCell: isCell, isBlockquote: isBlockquote, isBodyContainer: isBodyContainer, @@ -1071,26 +824,20 @@ define([ isTextarea: isTextarea, isEmpty: isEmpty, isEmptyAnchor: func.and(isAnchor, isEmpty), - isClosestSibling: isClosestSibling, - withClosestSiblings: withClosestSiblings, nodeLength: nodeLength, isLeftEdgePoint: isLeftEdgePoint, isRightEdgePoint: isRightEdgePoint, isEdgePoint: isEdgePoint, isLeftEdgeOf: isLeftEdgeOf, isRightEdgeOf: isRightEdgeOf, - isLeftEdgePointOf: isLeftEdgePointOf, - isRightEdgePointOf: isRightEdgePointOf, prevPoint: prevPoint, nextPoint: nextPoint, isSamePoint: isSamePoint, isVisiblePoint: isVisiblePoint, prevPointUntil: prevPointUntil, nextPointUntil: nextPointUntil, - isCharPoint: isCharPoint, walkPoint: walkPoint, ancestor: ancestor, - singleChildAncestor: singleChildAncestor, listAncestor: listAncestor, lastAncestor: lastAncestor, listNext: listNext, @@ -1105,7 +852,6 @@ define([ makeOffsetPath: makeOffsetPath, fromOffsetPath: fromOffsetPath, splitTree: splitTree, - splitPoint: splitPoint, create: create, createText: createText, remove: remove, diff --git a/addons/web_editor/static/lib/summernote/src/js/core/func.js b/addons/web_editor/static/lib/summernote/src/js/core/func.js index 11bfdf546e0..1acb9f98e97 100644 --- a/addons/web_editor/static/lib/summernote/src/js/core/func.js +++ b/addons/web_editor/static/lib/summernote/src/js/core/func.js @@ -1,11 +1,6 @@ define('summernote/core/func', function () { /** - * @class core.func - * * func utils (for high-order func's arg) - * - * @singleton - * @alternateClassName func */ var func = (function () { var eq = function (itemA) { @@ -98,18 +93,6 @@ define('summernote/core/func', function () { return inverted; }; - /** - * @param {String} namespace - * @param {String} [prefix] - * @return {String} - */ - var namespaceToCamel = function (namespace, prefix) { - prefix = prefix || ''; - return prefix + namespace.split('.').map(function (name) { - return name.substring(0, 1).toUpperCase() + name.substring(1); - }).join(''); - }; - return { eq: eq, eq2: eq2, @@ -121,8 +104,7 @@ define('summernote/core/func', function () { and: and, uniqueId: uniqueId, rect2bnd: rect2bnd, - invertObject: invertObject, - namespaceToCamel: namespaceToCamel + invertObject: invertObject }; })(); diff --git a/addons/web_editor/static/lib/summernote/src/js/core/key.js b/addons/web_editor/static/lib/summernote/src/js/core/key.js index a17b2756328..80631125bfa 100644 --- a/addons/web_editor/static/lib/summernote/src/js/core/key.js +++ b/addons/web_editor/static/lib/summernote/src/js/core/key.js @@ -1,96 +1,49 @@ define([ - 'summernote/core/list', - 'summernote/core/func' -], function (list, func) { + 'summernote/core/list' +], function (list) { /** - * @class core.key - * * Object for keycodes. - * - * @singleton - * @alternateClassName key */ - var key = (function () { - var keyMap = { - 'BACKSPACE': 8, - 'TAB': 9, - 'ENTER': 13, - 'SPACE': 32, - - // Arrow - 'LEFT': 37, - 'UP': 38, - 'RIGHT': 39, - 'DOWN': 40, + var key = { + isEdit: function (keyCode) { + return list.contains([8, 9, 13, 32], keyCode); + }, + nameFromCode: { + '8': 'BACKSPACE', + '9': 'TAB', + '13': 'ENTER', + '32': 'SPACE', // Number: 0-9 - 'NUM0': 48, - 'NUM1': 49, - 'NUM2': 50, - 'NUM3': 51, - 'NUM4': 52, - 'NUM5': 53, - 'NUM6': 54, - 'NUM7': 55, - 'NUM8': 56, + '48': 'NUM0', + '49': 'NUM1', + '50': 'NUM2', + '51': 'NUM3', + '52': 'NUM4', + '53': 'NUM5', + '54': 'NUM6', + '55': 'NUM7', + '56': 'NUM8', // Alphabet: a-z - 'B': 66, - 'E': 69, - 'I': 73, - 'J': 74, - 'K': 75, - 'L': 76, - 'R': 82, - 'S': 83, - 'U': 85, - 'V': 86, - 'Y': 89, - 'Z': 90, + '66': 'B', + '69': 'E', + '73': 'I', + '74': 'J', + '75': 'K', + '76': 'L', + '82': 'R', + '83': 'S', + '85': 'U', + '89': 'Y', + '90': 'Z', - 'SLASH': 191, - 'LEFTBRACKET': 219, - 'BACKSLASH': 220, - 'RIGHTBRACKET': 221 - }; - - return { - /** - * @method isEdit - * - * @param {Number} keyCode - * @return {Boolean} - */ - isEdit: function (keyCode) { - return list.contains([ - keyMap.BACKSPACE, - keyMap.TAB, - keyMap.ENTER, - keyMap.SPACe - ], keyCode); - }, - /** - * @method isMove - * - * @param {Number} keyCode - * @return {Boolean} - */ - isMove: function (keyCode) { - return list.contains([ - keyMap.LEFT, - keyMap.UP, - keyMap.RIGHT, - keyMap.DOWN - ], keyCode); - }, - /** - * @property {Object} nameFromCode - * @property {String} nameFromCode.8 "BACKSPACE" - */ - nameFromCode: func.invertObject(keyMap), - code: keyMap - }; - })(); + '191': 'SLASH', + '219': 'LEFTBRACKET', + '220': 'BACKSLASH', + '221': 'RIGHTBRACKET' + } + }; return key; }); diff --git a/addons/web_editor/static/lib/summernote/src/js/core/list.js b/addons/web_editor/static/lib/summernote/src/js/core/list.js index 2d6708838df..cc78781ea7f 100644 --- a/addons/web_editor/static/lib/summernote/src/js/core/list.js +++ b/addons/web_editor/static/lib/summernote/src/js/core/list.js @@ -1,11 +1,6 @@ define(['summernote/core/func'], function (func) { /** - * @class core.list - * * list utils - * - * @singleton - * @alternateClassName list */ var list = (function () { /** @@ -68,18 +63,11 @@ define(['summernote/core/func'], function (func) { return true; }; - /** - * returns index of item - */ - var indexOf = function (array, item) { - return $.inArray(item, array); - }; - /** * returns true if the value is present in the list. */ var contains = function (array, item) { - return indexOf(array, item) !== -1; + return $.inArray(item, array) !== -1; }; /** @@ -164,7 +152,7 @@ define(['summernote/core/func'], function (func) { * @param {Array} array */ var next = function (array, item) { - var idx = indexOf(array, item); + var idx = array.indexOf(item); if (idx === -1) { return null; } return array[idx + 1]; @@ -175,11 +163,12 @@ define(['summernote/core/func'], function (func) { * @param {Array} array */ var prev = function (array, item) { - var idx = indexOf(array, item); + var idx = array.indexOf(item); if (idx === -1) { return null; } return array[idx - 1]; }; + return { head: head, last: last, initial: initial, tail: tail, prev: prev, next: next, find: find, contains: contains, diff --git a/addons/web_editor/static/lib/summernote/src/js/core/range.js b/addons/web_editor/static/lib/summernote/src/js/core/range.js index cc8d0683d14..9611e09c72b 100644 --- a/addons/web_editor/static/lib/summernote/src/js/core/range.js +++ b/addons/web_editor/static/lib/summernote/src/js/core/range.js @@ -5,6 +5,13 @@ define([ 'summernote/core/dom' ], function (agent, func, list, dom) { + /** + * Data structure + * - {BoundaryPoint}: a point of dom tree + * - {BoundaryPoints}: two boundaryPoints corresponding to the start and the end of the Range + * + * @see http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position + */ var range = (function () { /** @@ -111,7 +118,6 @@ define([ /** * Wrapped Range * - * @constructor * @param {Node} sc - start container * @param {Number} so - start offset * @param {Node} ec - end container @@ -183,48 +189,25 @@ define([ } else { nativeRng.select(); } - - return this; }; /** * @return {WrappedRange} */ this.normalize = function () { - - /** - * @param {BoundaryPoint} point - * @param {Boolean} isLeftToRight - * @return {BoundaryPoint} - */ - var getVisiblePoint = function (point, isLeftToRight) { - if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) || - (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) || - (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) || - (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) { - return point; - } - - // point on block's edge - var block = dom.ancestor(point.node, dom.isBlock); - if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) || - ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) { - - // returns point already on visible point - if (dom.isVisiblePoint(point)) { - return point; + var getVisiblePoint = function (point) { + if (!dom.isVisiblePoint(point)) { + if (dom.isLeftEdgePoint(point)) { + point = dom.nextPointUntil(point, dom.isVisiblePoint); + } else if (dom.isRightEdgePoint(point)) { + point = dom.prevPointUntil(point, dom.isVisiblePoint); } - // reverse direction - isLeftToRight = !isLeftToRight; } - - var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) : - dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint); - return nextPoint || point; + return point; }; - var endPoint = getVisiblePoint(this.getEndPoint(), false); - var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true); + var startPoint = getVisiblePoint(this.getStartPoint()); + var endPoint = getVisiblePoint(this.getStartPoint()); return new WrappedRange( startPoint.node, @@ -370,44 +353,42 @@ define([ * delete contents on range * @return {WrappedRange} */ - if(_.isUndefined(this.deleteContents)) // ODOO: ability to override by prototype - this.deleteContents = function () { - if (this.isCollapsed()) { - return this; - } + // this.deleteContents = function () { + // if (this.isCollapsed()) { + // return this; + // } - var rng = this.splitText(); - var nodes = rng.nodes(null, { - fullyContains: true - }); + // var rng = this.splitText(); + // var nodes = rng.nodes(null, { + // fullyContains: true + // }); - // find new cursor point - var point = dom.prevPointUntil(rng.getStartPoint(), function (point) { - return !list.contains(nodes, point.node); - }); + // var point = dom.prevPointUntil(rng.getStartPoint(), function (point) { + // return !list.contains(nodes, point.node); + // }); - var emptyParents = []; - $.each(nodes, function (idx, node) { - // find empty parents - var parent = node.parentNode; - if (point.node !== parent && dom.nodeLength(parent) === 1) { - emptyParents.push(parent); - } - dom.remove(node, false); - }); + // var emptyParents = []; + // $.each(nodes, function (idx, node) { + // // find empty parents + // var parent = node.parentNode; + // if (point.node !== parent && dom.nodeLength(parent) === 1) { + // emptyParents.push(parent); + // } + // dom.remove(node, false); + // }); - // remove empty parents - $.each(emptyParents, function (idx, node) { - dom.remove(node, false); - }); + // // remove empty parents + // $.each(emptyParents, function (idx, node) { + // dom.remove(node, false); + // }); - return new WrappedRange( - point.node, - point.offset, - point.node, - point.offset - ).normalize(); - }; + // return new WrappedRange( + // point.node, + // point.offset, + // point.node, + // point.offset + // ); + // }; /** * makeIsOn: return isOn(pred) function @@ -427,6 +408,36 @@ define([ this.isOnAnchor = makeIsOn(dom.isAnchor); // isOnAnchor: judge whether range is on cell node or not this.isOnCell = makeIsOn(dom.isCell); + // isOnAnchor: judge whether range is an image node or not + this.isOnImg = function () { + var nb = 0; + var image; + var startPoint = {node: sc.childNodes.length && sc.childNodes[so] || sc}; + startPoint.offset = startPoint.node === sc ? so : 0; + var endPoint = {node: ec.childNodes.length && ec.childNodes[eo] || ec}; + endPoint.offset = endPoint.node === ec ? eo : 0; + + if (dom.isImg(startPoint.node)) { + nb ++; + image = startPoint.node; + } + dom.walkPoint(startPoint, endPoint, function (point) { + if (!dom.isText(endPoint.node) && point.node === endPoint.node && point.offset === endPoint.offset) { + return; + } + var node = point.node.childNodes.length && point.node.childNodes[point.offset] || point.node; + var offset = node === point.node ? point.offset : 0; + var isImg = dom.ancestor(node, dom.isImg); + if (!isImg && ((!dom.isBR(node) && !dom.isText(node)) || (offset && node.textContent.length !== offset && node.textContent.match(/\S|\u00A0/)))) { + nb++; + } + if (isImg && image !== isImg) { + image = isImg; + nb ++; + } + }); + return nb === 1 && image; + }; /** * @param {Function} pred @@ -456,20 +467,13 @@ define([ this.wrapBodyInlineWithPara = function () { if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) { sc.innerHTML = dom.emptyPara; - return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0); + return new WrappedRange(sc.firstChild, 0); } - /** - * [workaround] firefox often create range on not visible point. so normalize here. - * - firefox: |text
| - * - chrome:|text|
- */ - var rng = this.normalize(); if (dom.isParaInline(sc) || dom.isPara(sc)) { - return rng; + return this.normalize(); } - // ODOO: insert a p tag when try to insert a br with insertNode method, if the editor is inside a p, li, h1... (start_modification // if apply the editor to a P, LI... or inside a P, LI... if (dom.isText(sc)) { var node = sc; @@ -480,18 +484,17 @@ define([ } } } - // ODOO: end_modification) // find inline top ancestor var topAncestor; - if (dom.isInline(rng.sc)) { - var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline)); + if (dom.isInline(sc)) { + var ancestors = dom.listAncestor(sc, func.not(dom.isInline)); topAncestor = list.last(ancestors); if (!dom.isInline(topAncestor)) { - topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so]; + topAncestor = ancestors[ancestors.length - 2] || sc.childNodes[so]; } } else { - topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0]; + topAncestor = sc.childNodes[so - 1]; } // siblings not in paragraph @@ -511,79 +514,52 @@ define([ * insert node at current cursor * * @param {Node} node + * @param {Boolean} [isInline] * @return {Node} */ - this.insertNode = function (node) { - var rng = this.wrapBodyInlineWithPara().deleteContents(); - var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node)); + this.insertNode = function (node, isInline) { + var rng = this.wrapBodyInlineWithPara(); + var point = rng.getStartPoint(); - if (info.rightNode) { - info.rightNode.parentNode.insertBefore(node, info.rightNode); + var splitRoot, container, pivot; + if (isInline) { + container = dom.isPara(point.node) ? point.node : point.node.parentNode; + if (dom.isPara(point.node)) { + pivot = point.node.childNodes[point.offset]; + } else { + pivot = dom.splitTree(point.node, point); + } } else { - info.container.appendChild(node); + // splitRoot will be childNode of container + var ancestors = dom.listAncestor(point.node, dom.isBodyContainer); + var topAncestor = list.last(ancestors) || point.node; + + if (dom.isBodyContainer(topAncestor)) { + splitRoot = ancestors[ancestors.length - 2]; + container = topAncestor; + } else { + splitRoot = topAncestor; + container = splitRoot.parentNode; + } + pivot = splitRoot && dom.splitTree(splitRoot, point); + } + + if (pivot) { + pivot.parentNode.insertBefore(node, pivot); + } else { + container.appendChild(node); } return node; }; - - /** - * insert html at current cursor - */ - this.pasteHTML = function (markup) { - var contentsContainer = $('').html(markup)[0]; - var childNodes = list.from(contentsContainer.childNodes); - - var rng = this.wrapBodyInlineWithPara().deleteContents(); - - return childNodes.reverse().map(function (childNode) { - return rng.insertNode(childNode); - }).reverse(); - }; - /** - * returns text in range - * - * @return {String} - */ this.toString = function () { var nativeRng = nativeRange(); return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text; }; - - /** - * returns range for word before cursor - * - * @param {Boolean} [findAfter] - find after cursor, default: false - * @return {WrappedRange} - */ - this.getWordRange = function (findAfter) { - var endPoint = this.getEndPoint(); - - if (!dom.isCharPoint(endPoint)) { - return this; - } - - var startPoint = dom.prevPointUntil(endPoint, function (point) { - return !dom.isCharPoint(point); - }); - - if (findAfter) { - endPoint = dom.nextPointUntil(endPoint, function (point) { - return !dom.isCharPoint(point); - }); - } - - return new WrappedRange( - startPoint.node, - startPoint.offset, - endPoint.node, - endPoint.offset - ); - }; /** * create offsetPath bookmark - * * @param {Node} editable */ this.bookmark = function (editable) { @@ -599,24 +575,6 @@ define([ }; }; - /** - * create offsetPath bookmark base on paragraph - * - * @param {Node[]} paras - */ - this.paraBookmark = function (paras) { - return { - s: { - path: list.tail(dom.makeOffsetPath(list.head(paras), sc)), - offset: so - }, - e: { - path: list.tail(dom.makeOffsetPath(list.last(paras), ec)), - offset: eo - } - }; - }; - /** * getClientRects * @return {Rect[]} @@ -626,43 +584,30 @@ define([ return nativeRng.getClientRects(); }; }; - - /** - * @class core.range - * - * Data structure - * * BoundaryPoint: a point of dom tree - * * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range - * - * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position - * - * @singleton - * @alternateClassName range - */ + return { - WrappedRange: WrappedRange, // ODOO: give access to WrappedRange + + WrappedRange: WrappedRange, // odoo change for overwrite + /** - * @method - * * create Range Object From arguments or Browser Selection * * @param {Node} sc - start container * @param {Number} so - start offset * @param {Node} ec - end container * @param {Number} eo - end offset - * @return {WrappedRange} */ create : function (sc, so, ec, eo) { if (!arguments.length) { // from Browser Selection if (agent.isW3CRangeSupport) { var selection = document.getSelection(); - if (!selection || selection.rangeCount === 0) { + if (selection.rangeCount === 0) { return null; } else if (dom.isBody(selection.anchorNode)) { // Firefox: returns entire body as range on initialization. We won't never need it. return null; } - + var nativeRng = selection.getRangeAt(0); sc = nativeRng.startContainer; so = nativeRng.startOffset; @@ -690,6 +635,7 @@ define([ ec = endPoint.cont; eo = endPoint.offset; } + } else if (arguments.length === 2) { //collapsed ec = sc; eo = so; @@ -698,8 +644,6 @@ define([ }, /** - * @method - * * create WrappedRange from node * * @param {Node} node @@ -713,13 +657,13 @@ define([ // browsers can't target a picture or void node if (dom.isVoid(sc)) { - so = dom.listPrev(sc).length - 1; + so = dom.listPrev(sc).length-1; sc = sc.parentNode; } if (dom.isBR(ec)) { - eo = dom.listPrev(ec).length - 1; + eo = dom.listPrev(ec).length-1; ec = ec.parentNode; - } else if (dom.isVoid(ec)) { + } else if (dom.isVoid(ec) || dom.isImg(sc)) { eo = dom.listPrev(ec).length; ec = ec.parentNode; } @@ -728,32 +672,10 @@ define([ }, /** - * create WrappedRange from node after position - * - * @param {Node} node - * @return {WrappedRange} - */ - createFromNodeBefore: function (node) { - return this.createFromNode(node).collapse(true); - }, - - /** - * create WrappedRange from node after position - * - * @param {Node} node - * @return {WrappedRange} - */ - createFromNodeAfter: function (node) { - return this.createFromNode(node).collapse(); - }, - - /** - * @method - * - * create WrappedRange from bookmark + * create WrappedRange from Bookmark * * @param {Node} editable - * @param {Object} bookmark + * @param {Obkect} bookmark * @return {WrappedRange} */ createFromBookmark : function (editable, bookmark) { @@ -761,24 +683,6 @@ define([ var so = bookmark.s.offset; var ec = dom.fromOffsetPath(editable, bookmark.e.path); var eo = bookmark.e.offset; - return new WrappedRange(sc, so, ec, eo); - }, - - /** - * @method - * - * create WrappedRange from paraBookmark - * - * @param {Object} bookmark - * @param {Node[]} paras - * @return {WrappedRange} - */ - createFromParaBookmark: function (bookmark, paras) { - var so = bookmark.s.offset; - var eo = bookmark.e.offset; - var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path); - var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path); - return new WrappedRange(sc, so, ec, eo); } }; diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/Bullet.js b/addons/web_editor/static/lib/summernote/src/js/editing/Bullet.js index 6a115edaa90..d0b0f2e9169 100644 --- a/addons/web_editor/static/lib/summernote/src/js/editing/Bullet.js +++ b/addons/web_editor/static/lib/summernote/src/js/editing/Bullet.js @@ -5,17 +5,9 @@ define([ 'summernote/core/range' ], function (list, func, dom, range) { - /** - * @class editing.Bullet - * - * @alternateClassName Bullet - */ var Bullet = function () { /** - * @method insertOrderedList - * * toggle ordered list - * * @type command */ this.insertOrderedList = function () { @@ -23,10 +15,7 @@ define([ }; /** - * @method insertUnorderedList - * * toggle unordered list - * * @type command */ this.insertUnorderedList = function () { @@ -34,10 +23,7 @@ define([ }; /** - * @method indent - * * indent - * * @type command */ this.indent = function () { @@ -64,10 +50,7 @@ define([ }; /** - * @method outdent - * * outdent - * * @type command */ this.outdent = function () { @@ -95,10 +78,7 @@ define([ }; /** - * @method toggleList - * * toggle list - * * @param {String} listName - OL or UL */ this.toggleList = function (listName) { @@ -106,16 +86,13 @@ define([ var rng = range.create().wrapBodyInlineWithPara(); var paras = rng.nodes(dom.isPara, { includeAncestor: true }); - var bookmark = rng.paraBookmark(paras); var clustereds = list.clusterBy(paras, func.peq2('parentNode')); // paragraph to list if (list.find(paras, dom.isPurePara)) { - var wrappedParas = []; $.each(clustereds, function (idx, paras) { - wrappedParas = wrappedParas.concat(self.wrapList(paras, listName)); + self.wrapList(paras, listName); }); - paras = wrappedParas; // list to paragraph or change list style } else { var diffLists = rng.nodes(dom.isList, { @@ -129,19 +106,16 @@ define([ dom.replace(listNode, listName); }); } else { - paras = this.releaseList(clustereds, true); + this.releaseList(clustereds, true); } } - range.createFromParaBookmark(bookmark, paras).select(); + rng.select(); }; /** - * @method wrapList - * * @param {Node[]} paras * @param {String} listName - * @return {Node[]} */ this.wrapList = function (paras, listName) { var head = list.head(paras); @@ -153,7 +127,7 @@ define([ var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last); // P to LI - paras = paras.map(function (para) { + paras = $.map(paras, function (para) { return dom.isPurePara(para) ? dom.replace(para, 'LI') : para; }); @@ -164,13 +138,9 @@ define([ dom.appendChildNodes(listNode, list.from(nextList.childNodes)); dom.remove(nextList); } - - return paras; }; /** - * @method releaseList - * * @param {Array[]} clustereds * @param {Boolean} isEscapseToBody * @return {Node[]} @@ -187,23 +157,19 @@ define([ var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, { node: last.parentNode, offset: dom.position(last) + 1 - }, { - isSkipPaddingBlankHTML: true - }) : null; + }, true) : null; var middleList = dom.splitTree(headList, { node: head.parentNode, offset: dom.position(head) - }, { - isSkipPaddingBlankHTML: true - }); + }, true); paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) : list.from(middleList.childNodes).filter(dom.isLi); // LI to P if (isEscapseToBody || !dom.isList(headList.parentNode)) { - paras = paras.map(function (para) { + paras = $.map(paras, function (para) { return dom.replace(para, 'P'); }); } diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/Editor.js b/addons/web_editor/static/lib/summernote/src/js/editing/Editor.js new file mode 100644 index 00000000000..1acd957096d --- /dev/null +++ b/addons/web_editor/static/lib/summernote/src/js/editing/Editor.js @@ -0,0 +1,495 @@ +define([ + 'summernote/core/agent', + 'summernote/core/func', + 'summernote/core/list', + 'summernote/core/dom', + 'summernote/core/range', + 'summernote/core/async', + 'summernote/editing/Style', + 'summernote/editing/Typing', + 'summernote/editing/Table', + 'summernote/editing/Bullet' +], function (agent, func, list, dom, range, async, + Style, Typing, Table, Bullet) { + /** + * Editor + * @class + */ + var Editor = function () { + + var style = new Style(); + var table = new Table(); + var typing = new Typing(); + var bullet = new Bullet(); + + this.style = style; // odoo change for overwrite + this.table = table; // odoo change for overwrite + this.typing = typing; // odoo change for overwrite + this.bullet = bullet; // odoo change for overwrite + + /** + * create range + */ + this.createRange = function ($editable) { + $editable.focus(); + return range.create(); + }; + + /** + * save current range + * + * @param {jQuery} $editable + */ + this.saveRange = function ($editable, thenCollapse) { + var r = range.create(); + if (!r || ($editable[0] !== r.sc && !$.contains($editable[0], r.sc))) { + $editable.focus(); + } + $editable.data('range', range.create()); + if (thenCollapse) { + range.create().collapse().select(); + } + }; + + this.saveNode = function ($editable) { + // copy child node reference + var copy = []; + for (var key = 0, len = $editable[0].childNodes.length; key < len; key++) { + copy.push($editable[0].childNodes[key]); + } + $editable.data('childNodes', copy); + }; + + /** + * restore lately range + * + * @param {jQuery} $editable + */ + this.restoreRange = function ($editable) { + var rng = $editable.data('range'); + if (rng) { + rng.select(); + $editable.focus(); + } + }; + + this.restoreNode = function ($editable) { + $editable.html(''); + var child = $editable.data('childNodes'); + for (var index = 0, len = child.length; index < len; index++) { + $editable[0].appendChild(child[index]); + } + }; + /** + * current style + */ + this.currentStyle = function () { + var rng = range.create(); + return rng ? rng.isOnEditable() && style.current(rng) : false; + }; + + var triggerOnChange = this.triggerOnChange = function ($editable) { + var onChange = $editable.data('callbacks').onChange; + if (onChange) { + onChange($editable.html(), $editable); + } + }; + + /** + * undo + * @param {jQuery} $editable + */ + this.undo = function ($editable) { + $editable.data('NoteHistory').undo(); + triggerOnChange($editable); + }; + + /** + * redo + * @param {jQuery} $editable + */ + this.redo = function ($editable) { + $editable.data('NoteHistory').redo(); + triggerOnChange($editable); + }; + + /** + * after command + * @param {jQuery} $editable + */ + var afterCommand = this.afterCommand = function ($editable) { + $editable.data('NoteHistory').recordUndo(); + triggerOnChange($editable); + }; + + /* jshint ignore:start */ + // native commands(with execCommand), generate function for execCommand + var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', + 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', + 'formatBlock', 'removeFormat', + 'backColor', 'foreColor', 'insertHorizontalRule', 'fontName']; + + for (var idx = 0, len = commands.length; idx < len; idx ++) { + this[commands[idx]] = (function (sCmd) { + return function ($editable, value) { + document.execCommand(sCmd, false, value); + + afterCommand($editable); + }; + })(commands[idx]); + } + /* jshint ignore:end */ + + /** + * handle tab key + * + * @param {jQuery} $editable + * @param {Object} options + */ + this.tab = function ($editable, options) { + var rng = range.create(); + if (rng.isCollapsed() && rng.isOnCell()) { + table.tab(rng); + } else { + typing.insertTab($editable, rng, options.tabsize); + afterCommand($editable); + } + }; + + /** + * handle shift+tab key + */ + this.untab = function () { + var rng = range.create(); + if (rng.isCollapsed() && rng.isOnCell()) { + table.tab(rng, true); + } + }; + + /** + * insert paragraph + * + * @param {Node} $editable + */ + this.insertParagraph = function ($editable) { + typing.insertParagraph($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.insertOrderedList = function ($editable) { + bullet.insertOrderedList($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.insertUnorderedList = function ($editable) { + bullet.insertUnorderedList($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.indent = function ($editable) { + bullet.indent($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.outdent = function ($editable) { + bullet.outdent($editable); + afterCommand($editable); + }; + + /** + * insert image + * + * @param {jQuery} $editable + * @param {String} sUrl + */ + this.insertImage = function ($editable, sUrl, filename) { + async.createImage(sUrl, filename).then(function ($image) { + $image.css({ + display: '', + width: Math.min($editable.width(), $image.width()) + }); + range.create().insertNode($image[0]); + afterCommand($editable); + }).fail(function () { + var callbacks = $editable.data('callbacks'); + if (callbacks.onImageUploadError) { + callbacks.onImageUploadError(); + } + }); + }; + + /** + * insert node + * @param {Node} $editable + * @param {Node} node + * @param {Boolean} [isInline] + */ + this.insertNode = function ($editable, node, isInline) { + range.create().insertNode(node, isInline); + afterCommand($editable); + }; + + /** + * insert text + * @param {Node} $editable + * @param {String} text + */ + this.insertText = function ($editable, text) { + var textNode = this.createRange($editable).insertNode(dom.createText(text), true); + range.create(textNode, dom.nodeLength(textNode)).select(); + afterCommand($editable); + }; + + /** + * formatBlock + * + * @param {jQuery} $editable + * @param {String} tagName + */ + this.formatBlock = function ($editable, tagName) { + tagName = agent.isMSIE ? '<' + tagName + '>' : tagName; + document.execCommand('FormatBlock', false, tagName); + afterCommand($editable); + }; + + this.formatPara = function ($editable) { + this.formatBlock($editable, 'P'); + afterCommand($editable); + }; + + /* jshint ignore:start */ + for (var idx = 1; idx <= 6; idx ++) { + this['formatH' + idx] = function (idx) { + return function ($editable) { + this.formatBlock($editable, 'H' + idx); + }; + }(idx); + }; + /* jshint ignore:end */ + + /** + * fontsize + * FIXME: Still buggy + * + * @param {jQuery} $editable + * @param {String} value - px + */ + this.fontSize = function ($editable, value) { + document.execCommand('fontSize', false, 3); + if (agent.isFF) { + // firefox: to , buggy + $editable.find('font[size=3]').removeAttr('size').css('font-size', value + 'px'); + } else { + // chrome: to + $editable.find('span').filter(function () { + return this.style.fontSize === 'medium'; + }).css('font-size', value + 'px'); + } + + afterCommand($editable); + }; + + /** + * lineHeight + * @param {jQuery} $editable + * @param {String} value + */ + this.lineHeight = function ($editable, value) { + style.stylePara(range.create(), { + lineHeight: value + }); + afterCommand($editable); + }; + + /** + * unlink + * + * @type command + * + * @param {jQuery} $editable + */ + this.unlink = function ($editable) { + var rng = range.create(); + if (rng.isOnAnchor()) { + var anchor = dom.ancestor(rng.sc, dom.isAnchor); + rng = range.createFromNode(anchor); + rng.select(); + document.execCommand('unlink'); + + afterCommand($editable); + } + }; + + /** + * create link + * + * @type command + * + * @param {jQuery} $editable + * @param {Object} linkInfo + * @param {Object} options + */ + this.createLink = function ($editable, linkInfo, options) { + var linkUrl = linkInfo.url; + var linkText = linkInfo.text; + var isNewWindow = linkInfo.newWindow; + var className = linkInfo.className || null; + var rng = linkInfo.range; + + if (options.onCreateLink) { + linkUrl = options.onCreateLink(linkUrl); + } + + rng = rng.deleteContents(true); + var anchor = dom.ancestor(rng.sc, dom.isAnchor); + + // Create a new link when there is no anchor on range. + if (!anchor) { + anchor = rng.insertNode($('' + linkText + '')[0], true); + } + $(anchor).html(linkText).attr({ + 'href': linkUrl, + 'target': isNewWindow ? '_blank' : null, + 'class': className + }); + + range.createFromNode(anchor).select(); + afterCommand($editable); + }; + + /** + * returns link info + * + * @return {Object} + */ + this.getLinkInfo = function ($editable) { + $editable.focus(); + + var rng = range.create().expand(dom.isAnchor); + + // Get the first anchor on range(for edit). + var $anchor = $(list.head(rng.nodes(dom.isAnchor))); + + return { + range: rng, + text: rng.toString(), + isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : true, + url: $anchor.length ? $anchor.attr('href') : '' + }; + }; + + this.color = function ($editable, sObjColor) { + var oColor = JSON.parse(sObjColor); + var foreColor = oColor.foreColor, backColor = oColor.backColor; + + if (foreColor) { document.execCommand('foreColor', false, foreColor); } + if (backColor) { document.execCommand('backColor', false, backColor); } + + afterCommand($editable); + }; + + this.insertTable = function ($editable, sDim) { + var dimension = sDim.split('x'); + var rng = range.create(); + rng = rng.deleteContents(); + rng.insertNode(table.createTable(dimension[0], dimension[1])); + afterCommand($editable); + }; + + /** + * @return {Node} image from the current range selection + */ + function getImgTarget () { + var rng = range.create(); + var target = rng.sc.childNodes.length && rng.sc.childNodes[rng.so] || rng.sc; + return target; + } + + /** + * @param {jQuery} $editable + * @param {String} value + */ + this.floatMe = function ($editable, value) { + var $target = $( getImgTarget() ); + $target.css('float', value); + afterCommand($editable); + }; + + this.imageShape = function ($editable, value) { + var $target = $( getImgTarget() ); + $target.removeClass('img-rounded img-circle img-thumbnail'); + + if (value) { + $target.addClass(value); + } + + afterCommand($editable); + }; + + /** + * resize overlay element + * @param {jQuery} $editable + * @param {String} value + */ + this.resize = function ($editable, value) { + var $target = $( getImgTarget() ); + $target.css({ + width: value * 100 + '%', + height: '' + }); + + afterCommand($editable); + }; + + /** + * @param {Position} pos + * @param {jQuery} $target - target element + * @param {Boolean} [bKeepRatio] - keep ratio + */ + this.resizeTo = function (pos, $target, bKeepRatio) { + var imageSize; + if (bKeepRatio) { + var newRatio = pos.y / pos.x; + var ratio = $target.data('ratio'); + imageSize = { + width: ratio > newRatio ? pos.x : pos.y / ratio, + height: ratio > newRatio ? pos.x * ratio : pos.y + }; + } else { + imageSize = { + width: pos.x, + height: pos.y + }; + } + + $target.css(imageSize); + }; + + /** + * remove media object + * + * @param {jQuery} $editable + * @param {String} value - dummy argument (for keep interface) + */ + this.removeMedia = function ($editable, value) { + var $target = $( getImgTarget() ); + $target.detach(); + + afterCommand($editable); + }; + }; + + return Editor; +}); diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/History.js b/addons/web_editor/static/lib/summernote/src/js/editing/History.js index f50e87fe166..dc301bf957b 100644 --- a/addons/web_editor/static/lib/summernote/src/js/editing/History.js +++ b/addons/web_editor/static/lib/summernote/src/js/editing/History.js @@ -1,12 +1,10 @@ -define(['summernote/core/range'], function (range) { +define(['summernote/core/range', 'summernote/core/dom'], function (range, dom) { /** - * @class editing.History - * - * Editor History - * + * History + * @class */ var History = function ($editable) { - var stack = [], stackOffset = -1; + var stack = [], stackOffset = -1, hasUndo = false; var editable = $editable[0]; var makeSnapshot = function () { @@ -15,7 +13,7 @@ define(['summernote/core/range'], function (range) { return { contents: $editable.html(), - bookmark: (rng ? rng.bookmark(editable) : emptyBookmark) + bookmark: (rng && dom.ancestor(rng.sc, dom.isEditable) ? rng.bookmark(editable) : emptyBookmark) }; }; @@ -28,29 +26,21 @@ define(['summernote/core/range'], function (range) { } }; - /** - * undo - */ this.undo = function () { - // Create snap shot if not yet recorded - if ($editable.html() !== stack[stackOffset].contents) { - this.recordUndo(); - } - - if (0 < stackOffset) { + if (0 < stackOffset || (stack.length - 1 === stackOffset && hasUndo)) { + if (stack.length - 1 === stackOffset) { + this.recordUndo(); + } stackOffset--; applySnapshot(stack[stackOffset]); + hasUndo = !!stackOffset; } }; - /* ODOO: to suggest upstream */ this.hasUndo = function () { - return 0 < stackOffset; + return 0 < stackOffset || hasUndo; }; - /** - * redo - */ this.redo = function () { if (stack.length - 1 > stackOffset) { stackOffset++; @@ -58,24 +48,49 @@ define(['summernote/core/range'], function (range) { } }; - /* ODOO: to suggest upstream */ this.hasRedo = function () { return stack.length - 1 > stackOffset; }; - /** - * recorded undo - */ + var last; this.recordUndo = function () { - stackOffset++; + // test event for firefox: remove stack of history because event doesn't exists + var key = typeof event !== 'undefined' ? event : false; + if (key && !event.metaKey && !event.ctrlKey && !event.altKey && event.type === "keydown") { + key = event.type + "-"; + if (event.which === 8 || event.which === 46) key += 'delete'; + else if (event.which === 13) key += 'enter'; + else key += 'other'; + if (key === last) return; + hasUndo = true; + } + last = key; // Wash out stack after stackOffset - if (stack.length > stackOffset) { - stack = stack.slice(0, stackOffset); + if (stack.length > stackOffset+1) { + stack = stack.slice(0, stackOffset+1); } + if (stack[stackOffset] && stack[stackOffset].contents === $editable.html()) { + return; + } + + stackOffset++; + // Create new snapshot and push it to the end stack.push(makeSnapshot()); + return true; + }; + + this.splitNext = function () { + last = false; + }; + + this.reset = function () { + last = false; + stack = []; + stackOffset = -1; + this.recordUndo(); }; // Create first undo stack diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/Style.js b/addons/web_editor/static/lib/summernote/src/js/editing/Style.js index bf749cd817b..835d575be1b 100644 --- a/addons/web_editor/static/lib/summernote/src/js/editing/Style.js +++ b/addons/web_editor/static/lib/summernote/src/js/editing/Style.js @@ -1,28 +1,20 @@ define([ 'summernote/core/agent', - 'summernote/core/func', - 'summernote/core/list', 'summernote/core/dom' -], function (agent, func, list, dom) { +], function (agent, dom) { /** - * @class editing.Style - * * Style - * + * @class */ var Style = function () { /** - * @method jQueryCSS - * - * [workaround] for old jQuery * passing an array of style properties to .css() * will result in an object of property-value pairs. * (compability with version < 1.9) * - * @private * @param {jQuery} $obj * @param {Array} propertyNames - An array of one or more CSS properties. - * @return {Object} + * @returns {Object} */ var jQueryCSS = function ($obj, propertyNames) { if (agent.jqueryVersion < 1.9) { @@ -35,19 +27,6 @@ define([ return $obj.css.call($obj, propertyNames); }; - /** - * returns style object from node - * - * @param {jQuery} $node - * @return {Object} - */ - this.fromNode = function ($node) { - var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height']; - var styleInfo = jQueryCSS($node, properties) || {}; - styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10); - return styleInfo; - }; - /** * paragraph level style * @@ -62,58 +41,6 @@ define([ }); }; - /** - * insert and returns styleNodes on range. - * - * @param {WrappedRange} rng - * @param {Object} [options] - options for styleNodes - * @param {String} [options.nodeName] - default: `SPAN` - * @param {Boolean} [options.expandClosestSibling] - default: `false` - * @param {Boolean} [options.onlyPartialContains] - default: `false` - * @return {Node[]} - */ - this.styleNodes = function (rng, options) { - rng = rng.splitText(); - - var nodeName = options && options.nodeName || 'SPAN'; - var expandClosestSibling = !!(options && options.expandClosestSibling); - var onlyPartialContains = !!(options && options.onlyPartialContains); - - if (rng.isCollapsed()) { - return [rng.insertNode(dom.create(nodeName))]; - } - - var pred = dom.makePredByNodeName(nodeName); - var nodes = rng.nodes(dom.isText, { - fullyContains: true - }).map(function (text) { - return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName); - }); - - if (expandClosestSibling) { - if (onlyPartialContains) { - var nodesInRange = rng.nodes(); - // compose with partial contains predication - pred = func.and(pred, function (node) { - return list.contains(nodesInRange, node); - }); - } - - return nodes.map(function (node) { - var siblings = dom.withClosestSiblings(node, pred); - var head = list.head(siblings); - var tails = list.tail(siblings); - $.each(tails, function (idx, elem) { - dom.appendChildNodes(head, elem.childNodes); - dom.remove(elem); - }); - return list.head(siblings); - }); - } else { - return nodes; - } - }; - /** * get current style on cursor * @@ -122,7 +49,10 @@ define([ */ this.current = function (rng) { var $cont = $(dom.isText(rng.sc) ? rng.sc.parentNode : rng.sc); - var styleInfo = this.fromNode($cont); + var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height']; + var styleInfo = jQueryCSS($cont, properties) || {}; + + styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10); // document.queryCommandState for toggle state styleInfo['font-bold'] = document.queryCommandState('bold') ? 'bold' : 'normal'; @@ -149,6 +79,7 @@ define([ styleInfo['line-height'] = lineHeight.toFixed(1); } + styleInfo.image = rng.isOnImg(); styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor); styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable); styleInfo.range = rng; diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/Table.js b/addons/web_editor/static/lib/summernote/src/js/editing/Table.js index 634d5a79d7e..291f85128e5 100644 --- a/addons/web_editor/static/lib/summernote/src/js/editing/Table.js +++ b/addons/web_editor/static/lib/summernote/src/js/editing/Table.js @@ -2,10 +2,8 @@ define([ 'summernote/core/dom', 'summernote/core/range', 'summernote/core/list' ], function (dom, range, list) { /** - * @class editing.Table - * * Table - * + * @class */ var Table = function () { /** diff --git a/addons/web_editor/static/lib/summernote/src/js/editing/Typing.js b/addons/web_editor/static/lib/summernote/src/js/editing/Typing.js index f95eba9e9f3..2a094cafa22 100644 --- a/addons/web_editor/static/lib/summernote/src/js/editing/Typing.js +++ b/addons/web_editor/static/lib/summernote/src/js/editing/Typing.js @@ -1,24 +1,12 @@ define([ 'summernote/core/dom', - 'summernote/core/range', - 'summernote/editing/Bullet' -], function (dom, range, Bullet) { + 'summernote/core/range' +], function (dom, range) { - /** - * @class editing.Typing - * - * Typing - * - */ var Typing = function () { - // a Bullet instance to toggle lists off - var bullet = new Bullet(); - /** - * insert tab - * - * @param {jQuery} $editable + * @param {jQuery} $editable * @param {WrappedRange} rng * @param {Number} tabsize */ @@ -49,22 +37,14 @@ define([ var nextPara; // on paragraph: split paragraph if (splitRoot) { - // if it is an empty line with li - if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) { - // disable UL/OL and escape! - bullet.toggleList(splitRoot.parentNode.nodeName); - return; - // if new line has content (not a line break) - } else { - nextPara = dom.splitTree(splitRoot, rng.getStartPoint()); + nextPara = dom.splitTree(splitRoot, rng.getStartPoint()); - var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor); - emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor)); + var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor); + emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor)); - $.each(emptyAnchors, function (idx, anchor) { - dom.remove(anchor); - }); - } + $.each(emptyAnchors, function (idx, anchor) { + dom.remove(anchor); + }); // no paragraph: insert empty paragraph } else { var next = rng.sc.childNodes[rng.so]; @@ -77,7 +57,6 @@ define([ } range.create(nextPara, 0).normalize().select(); - }; }; diff --git a/addons/web_editor/static/lib/summernote/src/js/intro.js b/addons/web_editor/static/lib/summernote/src/js/intro.js index 357132eb110..dd5e9763077 100644 --- a/addons/web_editor/static/lib/summernote/src/js/intro.js +++ b/addons/web_editor/static/lib/summernote/src/js/intro.js @@ -1,9 +1,9 @@ /** * Super simple wysiwyg editor on Bootstrap v@VERSION - * http://summernote.org/ + * http://hackerwins.github.io/summernote/ * * summernote.js - * Copyright 2013-2015 Alan Hong. and other contributors + * Copyright 2013-2014 Alan Hong. and other contributors * summernote may be freely distributed under the MIT license./ * * Date: @DATE diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Button.js b/addons/web_editor/static/lib/summernote/src/js/module/Button.js index b7a1614d4db..512822aeba8 100644 --- a/addons/web_editor/static/lib/summernote/src/js/module/Button.js +++ b/addons/web_editor/static/lib/summernote/src/js/module/Button.js @@ -1,10 +1,7 @@ define([ - 'summernote/core/list', - 'summernote/core/agent' -], function (list, agent) { + 'summernote/core/list' +], function (list) { /** - * @class module.Button - * * Button */ var Button = function () { @@ -31,7 +28,6 @@ define([ /** * update button state(active or not). * - * @private * @param {String} selector * @param {Function} pred */ @@ -44,30 +40,30 @@ define([ var $img = $(styleInfo.image); btnState('button[data-event="imageShape"][data-value="img-rounded"]', function () { - return $img.hasClass('img-rounded'); + return $img.hasClass("img-rounded"); }); btnState('button[data-event="imageShape"][data-value="img-circle"]', function () { - return $img.hasClass('img-circle'); + return $img.hasClass("img-circle"); }); btnState('button[data-event="imageShape"][data-value="img-thumbnail"]', function () { - return $img.hasClass('img-thumbnail'); + return $img.hasClass("img-thumbnail"); }); btnState('button[data-event="imageShape"]:not([data-value])', function () { - return !$img.is('.img-rounded, .img-circle, .img-thumbnail'); + return !$img.is(".img-rounded, .img-circle, .img-thumbnail"); }); - var imgFloat = $img.css('float'); + var imgFloat = $img.css("float"); btnState('button[data-event="floatMe"][data-value="left"]', function () { - return imgFloat === 'left'; + return imgFloat === "left"; }); btnState('button[data-event="floatMe"][data-value="right"]', function () { - return imgFloat === 'right'; + return imgFloat === "right"; }); btnState('button[data-event="floatMe"][data-value="none"]', function () { - return imgFloat !== 'left' && imgFloat !== 'right'; + return imgFloat !== "left" && imgFloat !== "right"; }); - var style = $img.attr('style'); + var style = $img.attr("style"); btnState('button[data-event="resize"][data-value="1"]', function () { return !!/(^|\s)(max-)?width\s*:\s*100%/.test(style); }); @@ -85,18 +81,10 @@ define([ if ($fontname.length) { var selectedFont = styleInfo['font-family']; if (!!selectedFont) { - - var list = selectedFont.split(','); - for (var i = 0, len = list.length; i < len; i++) { - selectedFont = list[i].replace(/[\'\"]/g, '').replace(/\s+$/, '').replace(/^\s+/, ''); - if (agent.isFontInstalled(selectedFont)) { - break; - } - } - + selectedFont = list.head(selectedFont.split(',')); + selectedFont = selectedFont.replace(/\'/g, ''); $fontname.find('.note-current-fontname').text(selectedFont); checkDropdownMenu($fontname, selectedFont); - } } @@ -152,7 +140,7 @@ define([ * * @param {Node} button * @param {String} eventName - * @param {Mixed} value + * @param {value} value */ this.updateRecentColor = function (button, eventName, value) { var $color = $(button).closest('.note-color'); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js b/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js deleted file mode 100644 index e2d234c9dbb..00000000000 --- a/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js +++ /dev/null @@ -1,90 +0,0 @@ -define([ - 'summernote/core/list', - 'summernote/core/dom', - 'summernote/core/key', - 'summernote/core/agent' -], function (list, dom, key, agent) { - var Clipboard = function (handler) { - var $paste; - - this.attach = function (layoutInfo) { - // [workaround] getting image from clipboard - // - IE11 and Firefox: CTRL+v hook - // - Webkit: event.clipboardData - if ((agent.isMSIE && agent.browserVersion > 10) || agent.isFF) { - $paste = $('').attr('contenteditable', true).css({ - position : 'absolute', - left : -100000, - opacity : 0 - }); - - layoutInfo.editable().on('keydown', function (e) { - if (e.ctrlKey && e.keyCode === key.code.V) { - handler.invoke('saveRange', layoutInfo.editable()); - $paste.focus(); - - setTimeout(function () { - pasteByHook(layoutInfo); - }, 0); - } - }); - - layoutInfo.editable().before($paste); - } else { - layoutInfo.editable().on('paste', pasteByEvent); - } - }; - - var pasteByHook = function (layoutInfo) { - var $editable = layoutInfo.editable(); - var node = $paste[0].firstChild; - - if (dom.isImg(node)) { - var dataURI = node.src; - var decodedData = atob(dataURI.split(',')[1]); - var array = new Uint8Array(decodedData.length); - for (var i = 0; i < decodedData.length; i++) { - array[i] = decodedData.charCodeAt(i); - } - - var blob = new Blob([array], { type : 'image/png' }); - blob.name = 'clipboard.png'; - - handler.invoke('restoreRange', $editable); - handler.invoke('focus', $editable); - handler.insertImages(layoutInfo, [blob]); - } else { - var pasteContent = $('').html($paste.html()).html(); - handler.invoke('restoreRange', $editable); - handler.invoke('focus', $editable); - - if (pasteContent) { - handler.invoke('pasteHTML', $editable, pasteContent); - } - } - - $paste.empty(); - }; - - /** - * paste by clipboard event - * - * @param {Event} event - */ - var pasteByEvent = function (event) { - var clipboardData = event.originalEvent.clipboardData; - var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); - var $editable = layoutInfo.editable(); - - if (clipboardData && clipboardData.items && clipboardData.items.length) { - var item = list.head(clipboardData.items); - if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { - handler.insertImages(layoutInfo, [item.getAsFile()]); - } - handler.invoke('editor.afterCommand', $editable); - } - }; - }; - - return Clipboard; -}); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Codeview.js b/addons/web_editor/static/lib/summernote/src/js/module/Codeview.js deleted file mode 100644 index 46f3d96ab12..00000000000 --- a/addons/web_editor/static/lib/summernote/src/js/module/Codeview.js +++ /dev/null @@ -1,137 +0,0 @@ -define([ - 'summernote/core/agent', - 'summernote/core/dom' -], function (agent, dom) { - - var CodeMirror; - if (agent.hasCodeMirror) { - if (agent.isSupportAmd) { - require(['CodeMirror'], function (cm) { - CodeMirror = cm; - }); - } else { - CodeMirror = window.CodeMirror; - } - } - - /** - * @class Codeview - */ - var Codeview = function (handler) { - - this.sync = function (layoutInfo) { - var isCodeview = handler.invoke('codeview.isActivated', layoutInfo); - if (isCodeview && agent.hasCodeMirror) { - layoutInfo.codable().data('cmEditor').save(); - } - }; - - /** - * @param {Object} layoutInfo - * @return {Boolean} - */ - this.isActivated = function (layoutInfo) { - var $editor = layoutInfo.editor(); - return $editor.hasClass('codeview'); - }; - - /** - * toggle codeview - * - * @param {Object} layoutInfo - */ - this.toggle = function (layoutInfo) { - if (this.isActivated(layoutInfo)) { - this.deactivate(layoutInfo); - } else { - this.activate(layoutInfo); - } - }; - - /** - * activate code view - * - * @param {Object} layoutInfo - */ - this.activate = function (layoutInfo) { - var $editor = layoutInfo.editor(), - $toolbar = layoutInfo.toolbar(), - $editable = layoutInfo.editable(), - $codable = layoutInfo.codable(), - $popover = layoutInfo.popover(), - $handle = layoutInfo.handle(); - - var options = $editor.data('options'); - - $codable.val(dom.html($editable, options.prettifyHtml)); - $codable.height($editable.height()); - - handler.invoke('toolbar.updateCodeview', $toolbar, true); - handler.invoke('popover.hide', $popover); - handler.invoke('handle.hide', $handle); - - $editor.addClass('codeview'); - - $codable.focus(); - - // activate CodeMirror as codable - if (agent.hasCodeMirror) { - var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror); - - // CodeMirror TernServer - if (options.codemirror.tern) { - var server = new CodeMirror.TernServer(options.codemirror.tern); - cmEditor.ternServer = server; - cmEditor.on('cursorActivity', function (cm) { - server.updateArgHints(cm); - }); - } - - // CodeMirror hasn't Padding. - cmEditor.setSize(null, $editable.outerHeight()); - $codable.data('cmEditor', cmEditor); - } - }; - - /** - * deactivate code view - * - * @param {Object} layoutInfo - */ - this.deactivate = function (layoutInfo) { - var $holder = layoutInfo.holder(), - $editor = layoutInfo.editor(), - $toolbar = layoutInfo.toolbar(), - $editable = layoutInfo.editable(), - $codable = layoutInfo.codable(); - - var options = $editor.data('options'); - - // deactivate CodeMirror as codable - if (agent.hasCodeMirror) { - var cmEditor = $codable.data('cmEditor'); - $codable.val(cmEditor.getValue()); - cmEditor.toTextArea(); - } - - var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara; - var isChange = $editable.html() !== value; - - $editable.html(value); - $editable.height(options.height ? $codable.height() : 'auto'); - $editor.removeClass('codeview'); - - if (isChange) { - handler.bindCustomEvent( - $holder, $editable.data('callbacks'), 'change' - )($editable.html(), $editable); - } - - $editable.focus(); - - handler.invoke('toolbar.updateCodeview', $toolbar, false); - }; - }; - - return Codeview; -}); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Dialog.js b/addons/web_editor/static/lib/summernote/src/js/module/Dialog.js new file mode 100644 index 00000000000..8e7cf07b323 --- /dev/null +++ b/addons/web_editor/static/lib/summernote/src/js/module/Dialog.js @@ -0,0 +1,158 @@ +define('summernote/module/Dialog', function () { + /** + * Dialog + * + * @class + */ + var Dialog = function () { + + /** + * toggle button status + * + * @param {jQuery} $btn + * @param {Boolean} isEnable + */ + var toggleBtn = function ($btn, isEnable) { + $btn.toggleClass('disabled', !isEnable); + $btn.attr('disabled', !isEnable); + }; + + /** + * show image dialog + * + * @param {jQuery} $editable + * @param {jQuery} $dialog + * @return {Promise} + */ + this.showImageDialog = function ($editable, $dialog) { + return $.Deferred(function (deferred) { + var $imageDialog = $dialog.find('.note-image-dialog'); + + var $imageInput = $dialog.find('.note-image-input'), + $imageUrl = $dialog.find('.note-image-url'), + $imageBtn = $dialog.find('.note-image-btn'); + + $imageDialog.one('shown.bs.modal', function () { + // Cloning imageInput to clear element. + $imageInput.replaceWith($imageInput.clone() + .on('change', function () { + deferred.resolve(this.files || this.value); + $imageDialog.modal('hide'); + }) + .val('') + ); + + $imageBtn.click(function (event) { + event.preventDefault(); + + deferred.resolve($imageUrl.val()); + $imageDialog.modal('hide'); + }); + + $imageUrl.on('keyup paste', function (event) { + var url; + + if (event.type === 'paste') { + url = event.originalEvent.clipboardData.getData('text'); + } else { + url = $imageUrl.val(); + } + + toggleBtn($imageBtn, url); + }).val('').trigger('focus'); + }).one('hidden.bs.modal', function () { + $imageInput.off('change'); + $imageUrl.off('keyup paste'); + $imageBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }).modal('show'); + }); + }; + + /** + * Show link dialog and set event handlers on dialog controls. + * + * @param {jQuery} $dialog + * @param {Object} linkInfo + * @return {Promise} + */ + this.showLinkDialog = function ($editable, $dialog, linkInfo) { + return $.Deferred(function (deferred) { + var $linkDialog = $dialog.find('.note-link-dialog'); + + var $linkText = $linkDialog.find('.note-link-text'), + $linkUrl = $linkDialog.find('.note-link-url'), + $linkBtn = $linkDialog.find('.note-link-btn'), + $openInNewWindow = $linkDialog.find('input[type=checkbox]'); + + $linkDialog.one('shown.bs.modal', function () { + $linkText.val(linkInfo.text); + + $linkText.on('input', function () { + // if linktext was modified by keyup, + // stop cloning text from linkUrl + linkInfo.text = $linkText.val(); + }); + + // if no url was given, copy text to url + if (!linkInfo.url) { + linkInfo.url = linkInfo.text; + toggleBtn($linkBtn, linkInfo.text); + } + + $linkUrl.on('input', function () { + toggleBtn($linkBtn, $linkUrl.val()); + // display same link on `Text to display` input + // when create a new link + if (!linkInfo.text) { + $linkText.val($linkUrl.val()); + } + }).val(linkInfo.url).trigger('focus').trigger('select'); + + $openInNewWindow.prop('checked', linkInfo.newWindow); + + $linkBtn.one('click', function (event) { + event.preventDefault(); + + deferred.resolve({ + range: linkInfo.range, + url: $linkUrl.val(), + text: $linkText.val(), + newWindow: $openInNewWindow.is(':checked') + }); + $linkDialog.modal('hide'); + }); + }).one('hidden.bs.modal', function () { + // detach events + $linkText.off('input'); + $linkUrl.off('input'); + $linkBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }).modal('show'); + }).promise(); + }; + + /** + * show help dialog + * + * @param {jQuery} $dialog + */ + this.showHelpDialog = function ($editable, $dialog) { + return $.Deferred(function (deferred) { + var $helpDialog = $dialog.find('.note-help-dialog'); + + $helpDialog.one('hidden.bs.modal', function () { + deferred.resolve(); + }).modal('show'); + }).promise(); + }; + }; + + return Dialog; +}); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/DragAndDrop.js b/addons/web_editor/static/lib/summernote/src/js/module/DragAndDrop.js deleted file mode 100644 index b8871599c6f..00000000000 --- a/addons/web_editor/static/lib/summernote/src/js/module/DragAndDrop.js +++ /dev/null @@ -1,98 +0,0 @@ -define([ - 'summernote/core/dom' -], function (dom) { - var DragAndDrop = function (handler) { - var $document = $(document); - - /** - * attach Drag and Drop Events - * - * @param {Object} layoutInfo - layout Informations - * @param {Object} options - */ - this.attach = function (layoutInfo, options) { - if (options.airMode || options.disableDragAndDrop) { - // prevent default drop event - $document.on('drop', function (e) { - e.preventDefault(); - }); - } else { - this.attachDragAndDropEvent(layoutInfo, options); - } - }; - - /** - * attach Drag and Drop Events - * - * @param {Object} layoutInfo - layout Informations - * @param {Object} options - */ - this.attachDragAndDropEvent = function (layoutInfo, options) { - var collection = $(), - $editor = layoutInfo.editor(), - $dropzone = layoutInfo.dropzone(), - $dropzoneMessage = $dropzone.find('.note-dropzone-message'); - - // show dropzone on dragenter when dragging a object to document - // -but only if the editor is visible, i.e. has a positive width and height - $document.on('dragenter', function (e) { - var isCodeview = handler.invoke('codeview.isActivated', layoutInfo); - var hasEditorSize = $editor.width() > 0 && $editor.height() > 0; - if (!isCodeview && !collection.length && hasEditorSize) { - $editor.addClass('dragover'); - $dropzone.width($editor.width()); - $dropzone.height($editor.height()); - $dropzoneMessage.text(options.langInfo.image.dragImageHere); - } - collection = collection.add(e.target); - }).on('dragleave', function (e) { - collection = collection.not(e.target); - if (!collection.length) { - $editor.removeClass('dragover'); - } - }).on('drop', function () { - collection = $(); - $editor.removeClass('dragover'); - }); - - // change dropzone's message on hover. - $dropzone.on('dragenter', function () { - $dropzone.addClass('hover'); - $dropzoneMessage.text(options.langInfo.image.dropImage); - }).on('dragleave', function () { - $dropzone.removeClass('hover'); - $dropzoneMessage.text(options.langInfo.image.dragImageHere); - }); - - // attach dropImage - $dropzone.on('drop', function (event) { - - var dataTransfer = event.originalEvent.dataTransfer; - var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target); - - if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { - event.preventDefault(); - layoutInfo.editable().focus(); - handler.insertImages(layoutInfo, dataTransfer.files); - } else { - var insertNodefunc = function () { - layoutInfo.holder().summernote('insertNode', this); - }; - - for (var i = 0, len = dataTransfer.types.length; i < len; i++) { - var type = dataTransfer.types[i]; - var content = dataTransfer.getData(type); - - if (type.toLowerCase().indexOf('text') > -1) { - layoutInfo.holder().summernote('pasteHTML', content); - } else { - $(content).each(insertNodefunc); - } - } - } - }).on('dragover', false); // prevent default dragover event - }; - }; - - return DragAndDrop; -}); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Editor.js b/addons/web_editor/static/lib/summernote/src/js/module/Editor.js deleted file mode 100644 index 732072b5d24..00000000000 --- a/addons/web_editor/static/lib/summernote/src/js/module/Editor.js +++ /dev/null @@ -1,861 +0,0 @@ -define([ - 'summernote/core/agent', - 'summernote/core/func', - 'summernote/core/list', - 'summernote/core/dom', - 'summernote/core/range', - 'summernote/core/async', - 'summernote/editing/Style', - 'summernote/editing/Typing', - 'summernote/editing/Table', - 'summernote/editing/Bullet' -], function (agent, func, list, dom, range, async, - Style, Typing, Table, Bullet) { - - var KEY_BOGUS = 'bogus'; - - /** - * @class editing.Editor - * - * Editor - * - */ - var Editor = function (handler) { - - var self = this; - var style = new Style(); - var table = new Table(); - var typing = new Typing(); - var bullet = new Bullet(); - - this.style = style; // ODOO: allow access for override - this.table = table; // ODOO: allow access for override - this.typing = typing; // ODOO: allow access for override - this.bullet = bullet; // ODOO: allow access for override - - /** - * @method createRange - * - * create range - * - * @param {jQuery} $editable - * @return {WrappedRange} - */ - this.createRange = function ($editable) { - this.focus($editable); - return range.create(); - }; - - /** - * @method saveRange - * - * save current range - * - * @param {jQuery} $editable - * @param {Boolean} [thenCollapse=false] - */ - this.saveRange = function ($editable, thenCollapse) { - // ODOO: scroll to top when click on input in editable m (start_modification - // this.focus($editable); - var r = range.create(); - if (!r || ($editable[0] !== r.sc && !$.contains($editable[0], r.sc))) { - $editable.focus(); - } - // ODOO: end_modication) - $editable.data('range', range.create()); - if (thenCollapse) { - range.create().collapse().select(); - } - }; - - /** - * @method saveRange - * - * save current node list to $editable.data('childNodes') - * - * @param {jQuery} $editable - */ - this.saveNode = function ($editable) { - // copy child node reference - var copy = []; - for (var key = 0, len = $editable[0].childNodes.length; key < len; key++) { - copy.push($editable[0].childNodes[key]); - } - $editable.data('childNodes', copy); - }; - - /** - * @method restoreRange - * - * restore lately range - * - * @param {jQuery} $editable - */ - this.restoreRange = function ($editable) { - var rng = $editable.data('range'); - if (rng) { - rng.select(); - this.focus($editable); - } - }; - - /** - * @method restoreNode - * - * restore lately node list - * - * @param {jQuery} $editable - */ - this.restoreNode = function ($editable) { - $editable.html(''); - var child = $editable.data('childNodes'); - for (var index = 0, len = child.length; index < len; index++) { - $editable[0].appendChild(child[index]); - } - }; - - /** - * @method currentStyle - * - * current style - * - * @param {Node} target - * @return {Object|Boolean} unfocus - */ - this.currentStyle = function (target) { - var rng = range.create(); - var styleInfo = rng && rng.isOnEditable() ? style.current(rng.normalize()) : {}; - if (dom.isImg(target)) { - styleInfo.image = target; - } - return styleInfo; - }; - - /** - * style from node - * - * @param {jQuery} $node - * @return {Object} - */ - this.styleFromNode = function ($node) { - return style.fromNode($node); - }; - - var triggerOnBeforeChange = function ($editable) { - var $holder = dom.makeLayoutInfo($editable).holder(); - handler.bindCustomEvent( - $holder, $editable.data('callbacks'), 'before.command' - )($editable.html(), $editable); - }; - - var triggerOnChange = function ($editable) { - var $holder = dom.makeLayoutInfo($editable).holder(); - handler.bindCustomEvent( - $holder, $editable.data('callbacks'), 'change' - )($editable.html(), $editable); - }; - - /** - * @method undo - * undo - * @param {jQuery} $editable - */ - this.undo = function ($editable) { - triggerOnBeforeChange($editable); - $editable.data('NoteHistory').undo(); - triggerOnChange($editable); - }; - - /** - * @method redo - * redo - * @param {jQuery} $editable - */ - this.redo = function ($editable) { - triggerOnBeforeChange($editable); - $editable.data('NoteHistory').redo(); - triggerOnChange($editable); - }; - - /** - * @method beforeCommand - * before command - * @param {jQuery} $editable - */ - var beforeCommand = this.beforeCommand = function ($editable) { - triggerOnBeforeChange($editable); - // keep focus on editable before command execution - self.focus($editable); - }; - - /** - * @method afterCommand - * after command - * @param {jQuery} $editable - * @param {Boolean} isPreventTrigger - */ - var afterCommand = this.afterCommand = function ($editable, isPreventTrigger) { - $editable.data('NoteHistory').recordUndo(); - if (!isPreventTrigger) { - triggerOnChange($editable); - } - }; - - /** - * @method bold - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method italic - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method underline - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method strikethrough - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method formatBlock - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method superscript - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method subscript - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method justifyLeft - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method justifyCenter - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method justifyRight - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method justifyFull - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method formatBlock - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method removeFormat - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method backColor - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method foreColor - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method insertHorizontalRule - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /** - * @method fontName - * - * change font name - * - * @param {jQuery} $editable - * @param {Mixed} value - */ - - /* jshint ignore:start */ - // native commands(with execCommand), generate function for execCommand - var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', - 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', - 'formatBlock', 'removeFormat', - 'backColor', 'foreColor', 'fontName']; - - for (var idx = 0, len = commands.length; idx < len; idx ++) { - this[commands[idx]] = (function (sCmd) { - return function ($editable, value) { - beforeCommand($editable); - - document.execCommand(sCmd, false, value); - - afterCommand($editable, true); - }; - })(commands[idx]); - } - /* jshint ignore:end */ - - /** - * @method tab - * - * handle tab key - * - * @param {jQuery} $editable - * @param {Object} options - */ - this.tab = function ($editable, options) { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - table.tab(rng); - } else { - beforeCommand($editable); - typing.insertTab($editable, rng, options.tabsize); - afterCommand($editable); - } - }; - - /** - * @method untab - * - * handle shift+tab key - * - */ - this.untab = function ($editable) { - var rng = this.createRange($editable); - if (rng.isCollapsed() && rng.isOnCell()) { - table.tab(rng, true); - } - }; - - /** - * @method insertParagraph - * - * insert paragraph - * - * @param {Node} $editable - */ - this.insertParagraph = function ($editable) { - beforeCommand($editable); - typing.insertParagraph($editable); - afterCommand($editable); - }; - - /** - * @method insertOrderedList - * - * @param {jQuery} $editable - */ - this.insertOrderedList = function ($editable) { - beforeCommand($editable); - bullet.insertOrderedList($editable); - afterCommand($editable); - }; - - /** - * @param {jQuery} $editable - */ - this.insertUnorderedList = function ($editable) { - beforeCommand($editable); - bullet.insertUnorderedList($editable); - afterCommand($editable); - }; - - /** - * @param {jQuery} $editable - */ - this.indent = function ($editable) { - beforeCommand($editable); - bullet.indent($editable); - afterCommand($editable); - }; - - /** - * @param {jQuery} $editable - */ - this.outdent = function ($editable) { - beforeCommand($editable); - bullet.outdent($editable); - afterCommand($editable); - }; - - /** - * insert image - * - * @param {jQuery} $editable - * @param {String} sUrl - */ - this.insertImage = function ($editable, sUrl, filename) { - async.createImage(sUrl, filename).then(function ($image) { - beforeCommand($editable); - $image.css({ - display: '', - width: Math.min($editable.width(), $image.width()) - }); - range.create().insertNode($image[0]); - range.createFromNodeAfter($image[0]).select(); - afterCommand($editable); - }).fail(function () { - var $holder = dom.makeLayoutInfo($editable).holder(); - handler.bindCustomEvent( - $holder, $editable.data('callbacks'), 'image.upload.error' - )(); - }); - }; - - /** - * @method insertNode - * insert node - * @param {Node} $editable - * @param {Node} node - */ - this.insertNode = function ($editable, node) { - beforeCommand($editable); - range.create().insertNode(node); - range.createFromNodeAfter(node).select(); - afterCommand($editable); - }; - - /** - * insert text - * @param {Node} $editable - * @param {String} text - */ - this.insertText = function ($editable, text) { - beforeCommand($editable); - var textNode = range.create().insertNode(dom.createText(text)); - range.create(textNode, dom.nodeLength(textNode)).select(); - afterCommand($editable); - }; - - /** - * paste HTML - * @param {Node} $editable - * @param {String} markup - */ - this.pasteHTML = function ($editable, markup) { - beforeCommand($editable); - var contents = range.create().pasteHTML(markup); - range.createFromNodeAfter(list.last(contents)).select(); - afterCommand($editable); - }; - - /** - * formatBlock - * - * @param {jQuery} $editable - * @param {String} tagName - */ - this.formatBlock = function ($editable, tagName) { - beforeCommand($editable); - // [workaround] for MSIE, IE need `<` - tagName = agent.isMSIE ? '<' + tagName + '>' : tagName; - document.execCommand('FormatBlock', false, tagName); - afterCommand($editable); - }; - - this.formatPara = function ($editable) { - beforeCommand($editable); - this.formatBlock($editable, 'P'); - afterCommand($editable); - }; - - /* jshint ignore:start */ - for (var idx = 1; idx <= 6; idx ++) { - this['formatH' + idx] = function (idx) { - return function ($editable) { - this.formatBlock($editable, 'H' + idx); - }; - }(idx); - }; - /* jshint ignore:end */ - - /** - * fontSize - * - * @param {jQuery} $editable - * @param {String} value - px - */ - this.fontSize = function ($editable, value) { - var rng = range.create(); - - if (rng.isCollapsed()) { - var spans = style.styleNodes(rng); - var firstSpan = list.head(spans); - - $(spans).css({ - 'font-size': value + 'px' - }); - - // [workaround] added styled bogus span for style - // - also bogus character needed for cursor position - if (firstSpan && !dom.nodeLength(firstSpan)) { - firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR; - range.createFromNodeAfter(firstSpan.firstChild).select(); - $editable.data(KEY_BOGUS, firstSpan); - } - } else { - beforeCommand($editable); - $(style.styleNodes(rng)).css({ - 'font-size': value + 'px' - }); - afterCommand($editable); - } - }; - - /** - * insert horizontal rule - * @param {jQuery} $editable - */ - this.insertHorizontalRule = function ($editable) { - beforeCommand($editable); - - var rng = range.create(); - var hrNode = rng.insertNode($('