diff options
| author | Christophe Matthieu <chm@odoo.com> | 2018-12-28 17:49:21 +0000 |
|---|---|---|
| committer | Christophe Matthieu <chm@odoo.com> | 2019-01-17 08:40:21 +0000 |
| commit | de444335b7da30a2ab3a4c5b92c9731a023f3f07 (patch) | |
| tree | 40ceda25aa7157ae0923cf567a448d685798e911 /addons/web_editor/static/src/js/widgets | |
| parent | 17c6d00531c55d0c392ddb6916bf1957bc490c1c (diff) | |
[REF] web_editor,*: Move and rename files to prepare refactoring
The refactoring of the wysiwyg editor and 'html' field allows us to
move some of the code that was in web_editor but only used in website or
in mass_mailing. Some parts are still in web_editor but will be
moved at a later time.
Diffstat (limited to 'addons/web_editor/static/src/js/widgets')
| -rw-r--r-- | addons/web_editor/static/src/js/widgets/ace.js | 863 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/widgets/widgets.js | 1795 |
2 files changed, 0 insertions, 2658 deletions
diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js deleted file mode 100644 index f3c47f30050..00000000000 --- a/addons/web_editor/static/src/js/widgets/ace.js +++ /dev/null @@ -1,863 +0,0 @@ -odoo.define('web_editor.ace', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var Dialog = require('web.Dialog'); -var Widget = require('web.Widget'); -var weContext = require('web_editor.context'); -var localStorage = require('web.local_storage'); -var session = require('web.session'); - -var _t = core._t; - -/** - * Formats a content-check result (@see checkXML, checkSCSS). - * - * @param {boolean} isValid - * @param {integer} [errorLine] needed if isValid is false - * @param {string} [errorMessage] needed if isValid is false - * @returns {Object} - */ -function _getCheckReturn(isValid, errorLine, errorMessage) { - return { - isValid: isValid, - error: isValid ? null : { - line: errorLine, - message: errorMessage, - }, - }; -} -/** - * Checks the syntax validity of some XML. - * - * @param {string} xml - * @returns {Object} @see _getCheckReturn - */ -function checkXML(xml) { - if (typeof window.DOMParser != 'undefined') { - var xmlDoc = (new window.DOMParser()).parseFromString(xml, 'text/xml'); - var error = xmlDoc.getElementsByTagName('parsererror'); - if (error.length > 0) { - return _getCheckReturn(false, parseInt(error[0].innerHTML.match(/[Ll]ine[^\d]+(\d+)/)[1], 10), error[0].innerHTML); - } - } else if (typeof window.ActiveXObject != 'undefined' && new window.ActiveXObject('Microsoft.XMLDOM')) { - var xmlDocIE = new window.ActiveXObject('Microsoft.XMLDOM'); - xmlDocIE.async = 'false'; - xmlDocIE.loadXML(xml); - if (xmlDocIE.parseError.line > 0) { - return _getCheckReturn(false, xmlDocIE.parseError.line, xmlDocIE.parseError.reason); - } - } - return _getCheckReturn(true); -} -/** - * Formats some XML so that it has proper indentation and structure. - * - * @param {string} xml - * @returns {string} formatted xml - */ -function formatXML(xml) { - // do nothing if an inline script is present to avoid breaking it - if (/<script(?: [^>]*)?>[^<][\s\S]*<\/script>/i.test(xml)) { - return xml; - } - return window.vkbeautify.xml(xml, 4); -} -/** - * Checks the syntax validity of some SCSS. - * - * @param {string} scss - * @returns {Object} @see _getCheckReturn - */ -var checkSCSS = (function () { - var mapping = { - '{': '}', '}': '{', - '(': ')', ')': '(', - '[': ']', ']': '[', - }; - var openings = ['{', '(', '[']; - var closings = ['}', ')', ']']; - - return function (scss) { - var stack = []; - var line = 1; - for (var i = 0 ; i < scss.length ; i++) { - if (_.contains(openings, scss[i])) { - stack.push(scss[i]); - } else if (_.contains(closings, scss[i])) { - if (stack.pop() !== mapping[scss[i]]) { - return _getCheckReturn(false, line, _t("Unexpected ") + scss[i]); - } - } else if (scss[i] === '\n') { - line++; - } - } - if (stack.length > 0) { - return _getCheckReturn(false, line, _t("Expected ") + mapping[stack.pop()]); - } - return _getCheckReturn(true); - }; -})(); -/** - * Formats some SCSS so that it has proper indentation and structure. - * - * @todo Right now, this does return the given SCSS content, untouched. - * @param {string} scss - * @returns {string} formatted scss - */ -function formatSCSS(scss) { - return scss; -} - -/** - * Allows to visualize resources (by default, XML views) and edit them. - */ -var ViewEditor = Widget.extend({ - template: 'web_editor.ace_view_editor', - xmlDependencies: ['/web_editor/static/src/xml/ace.xml'], - jsLibs: [ - '/web/static/lib/ace/ace.js', - [ - '/web/static/lib/ace/mode-xml.js', - '/web/static/lib/ace/mode-scss.js', - '/web/static/lib/ace/theme-monokai.js' - ] - ], - events: { - 'click .o_ace_type_switcher_choice': '_onTypeChoice', - 'change .o_res_list': '_onResChange', - 'click .js_include_bundles': '_onIncludeBundlesChange', - 'click .js_include_all_scss': '_onIncludeAllSCSSChange', - 'click button[data-action=save]': '_onSaveClick', - 'click button[data-action=reset]': '_onResetClick', - 'click button[data-action=format]': '_onFormatClick', - 'click button[data-action=close]': '_onCloseClick', - }, - - /** - * Initializes the parameters so that the ace editor knows which information - * it has to load. - * - * @constructor - * @param {Widget} parent - * @param {string|integer} viewKey - * xml_id or id of the view whose linked resources have to be loaded. - * @param {Object} [options] - * @param {string|integer} [options.initialResID] - * a specific view ID / SCSS URL to load on start (otherwise the main - * view ID associated with the specified viewKey will be used) - * @param {string} [options.position=right] - * @param {boolean} [options.doNotLoadViews=false] - * @param {boolean} [options.doNotLoadSCSS=false] - * @param {boolean} [options.includeBundles=false] - * @param {boolean} [options.includeAllSCSS=false] - * @param {string[]} [options.defaultBundlesRestriction] - */ - init: function (parent, viewKey, options) { - this._super.apply(this, arguments); - - this.viewKey = viewKey; - this.options = _.defaults({}, options, { - position: 'right', - doNotLoadViews: false, - doNotLoadSCSS: false, - includeBundles: false, - includeAllSCSS: false, - defaultBundlesRestriction: [], - }); - - this.resources = {xml: {}, scss: {}}; - this.editingSessions = {xml: {}, scss: {}}; - this.currentType = 'xml'; - - // Alias - this.views = this.resources.xml; - this.scss = this.resources.scss; - }, - /** - * Loads everything the ace library needs to work. - * It also loads the resources to visualize (@see _loadResources). - * - * @override - */ - willStart: function () { - return $.when( - this._super.apply(this, arguments), - ajax.loadLibs(this), - this._loadResources() - ); - }, - /** - * Initializes the library and initial view once the DOM is ready. It also - * initializes the resize feature of the ace editor. - * - * @override - */ - start: function () { - this.$viewEditor = this.$('#ace-view-editor'); - - this.$typeSwitcherChoices = this.$('.o_ace_type_switcher_choice'); - this.$typeSwitcherBtn = this.$('.o_ace_type_switcher > .dropdown-toggle'); - - this.$lists = { - xml: this.$('#ace-view-list'), - scss: this.$('#ace-scss-list') - }; - this.$includeBundlesArea = this.$('.oe_include_bundles'); - this.$includeAllSCSSArea = this.$('.o_include_all_scss'); - this.$viewID = this.$('#ace-view-id > span'); - - this.$formatButton = this.$('button[data-action=format]'); - this.$resetButton = this.$('button[data-action=reset]'); - - this.aceEditor = window.ace.edit(this.$viewEditor[0]); - this.aceEditor.setTheme('ace/theme/monokai'); - this.$editor = this.$('.ace_editor'); - - var refX = 0; - var resizing = false; - var minWidth = 400; - var debounceStoreEditorWidth = _.debounce(storeEditorWidth, 500); - - this._updateViewSelectDOM(); - - var initResID; - var initType; - if (this.options.initialResID) { - initResID = this.options.initialResID; - initType = (_.isString(initResID) && initResID[0] === '/') ? 'scss' : 'xml'; - } else { - if (!this.options.doNotLoadSCSS) { - initResID = this.sortedSCSS[0][1][0].url; // first bundle, scss files, first one - initType = 'scss'; - } - if (!this.options.doNotLoadViews) { - if (typeof this.viewKey === "number") { - initResID = this.viewKey; - } else { - var view = _.findWhere(this.views, {xml_id: this.viewKey}); - if (!view) { - view = _.findWhere(this.views, {key: this.viewKey}); - } - initResID = view.id; - } - initType = 'xml'; - } - } - if (initResID) { - this._displayResource(initResID, initType); - } - - if (!this.sortedViews.length || !this.sortedSCSS.length) { - _.defer((function () { - this._switchType(this.sortedViews.length ? 'xml' : 'scss'); - this.$typeSwitcherBtn.parent('.btn-group').addClass('d-none'); - }).bind(this)); - } - - $(document).on('mouseup.ViewEditor', stopResizing.bind(this)).on('mousemove.ViewEditor', updateWidth.bind(this)); - if (this.options.position === 'left') { - this.$('.ace_scroller').after($('<div>').addClass('ace_resize_bar')); - this.$('.ace_gutter').css({'cursor': 'default'}); - this.$el.on('mousedown.ViewEditor', '.ace_resize_bar', startResizing.bind(this)); - } else { - this.$el.on('mousedown.ViewEditor', '.ace_gutter', startResizing.bind(this)); - } - - resizeEditor.call(this, readEditorWidth.call(this)); - - return this._super.apply(this, arguments); - - function resizeEditor(target) { - var width = Math.min(document.body.clientWidth, Math.max(parseInt(target, 10), minWidth)); - this.$editor.width(width); - this.aceEditor.resize(); - this.$el.width(width); - } - function storeEditorWidth() { - localStorage.setItem('ace_editor_width', this.$el.width()); - } - function readEditorWidth() { - var width = localStorage.getItem('ace_editor_width'); - return parseInt(width || 720, 10); - } - function startResizing(e) { - refX = e.pageX; - resizing = true; - } - function stopResizing() { - resizing = false; - } - function updateWidth(e) { - if (!resizing) return; - - var offset = e.pageX - refX; - if (this.options.position === 'left') { - offset = - offset; - } - var width = this.$el.width() - offset; - refX = e.pageX; - resizeEditor.call(this, width); - debounceStoreEditorWidth.call(this); - } - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - this.$el.off('.ViewEditor'); - $(document).off('.ViewEditor'); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Initializes a text editor for the specified resource. - * - * @private - * @param {integer|string} resID - the ID/URL of the view/scss file - * @param {string} [type] (default to the currently selected one) - * @returns {ace.EditSession} - */ - _buildEditingSession: function (resID, type) { - var self = this; - type = type || this.currentType; - var editingSession = new window.ace.EditSession(this.resources[type][resID].arch); - editingSession.setUseWorker(false); - editingSession.setMode('ace/mode/' + (type || this.currentType)); - editingSession.setUndoManager(new window.ace.UndoManager()); - editingSession.on('change', function () { - _.defer(function () { - self._toggleDirtyInfo(resID); - self._showErrorLine(); - }); - }); - return editingSession; - }, - /** - * Forces the view/scss file identified by its ID/URL to be displayed in the - * editor. The method will update the resource select DOM element as well if - * necessary. - * - * @private - * @param {integer|string} resID - * @param {string} [type] - the type of resource (either 'xml' or 'scss') - */ - _displayResource: function (resID, type) { - if (type) { - this._switchType(type); - } - - var editingSession = this.editingSessions[this.currentType][resID]; - if (!editingSession) { - editingSession = this.editingSessions[this.currentType][resID] = this._buildEditingSession(resID); - } - this.aceEditor.setSession(editingSession); - - if (this.currentType === 'xml') { - this.$viewID.text(_.str.sprintf(_t("Template ID: %s"), this.views[resID].xml_id)); - } else { - this.$viewID.text(_.str.sprintf(_t("SCSS file: %s"), resID)); - } - this.$lists[this.currentType].select2('val', resID); - - this.$resetButton.toggleClass('d-none', this.currentType === 'xml' || !this.scss[resID].customized); - }, - /** - * Formats the current resource being vizualized. - * (@see formatXML, formatSCSS) - * - * @private - */ - _formatResource: function () { - var res = this.aceEditor.getValue(); - var check = (this.currentType === 'xml' ? checkXML : checkSCSS)(res); - if (check.isValid) { - this.aceEditor.setValue((this.currentType === 'xml' ? formatXML : formatSCSS)(res)); - } else { - this._showErrorLine(check.error.line, check.error.message, this._getSelectedResource()); - } - }, - /** - * Returns the currently selected resource data. - * - * @private - * @returns {integer|string} view ID or scss file URL - */ - _getSelectedResource: function () { - var value = this.$lists[this.currentType].select2('val'); - return parseInt(value, 10) || value; - }, - /** - * Loads data the ace editor will vizualize and process it. Default behavior - * is loading the activate views, index them and build their hierarchy. - * - * @private - * @returns {Deferred} - */ - _loadResources: function () { - // Reset resources - this.resources = {xml: {}, scss: {}}; - this.editingSessions = {xml: {}, scss: {}}; - this.views = this.resources.xml; - this.scss = this.resources.scss; - - // Load resources - return this._rpc({ - route: '/web_editor/get_assets_editor_resources', - params: { - key: this.viewKey, - get_views: !this.options.doNotLoadViews, - get_scss: !this.options.doNotLoadSCSS, - bundles: this.options.includeBundles, - bundles_restriction: this.options.includeAllSCSS ? [] : this.options.defaultBundlesRestriction, - }, - }).then((function (resources) { - _processViews.call(this, resources.views || []); - _processSCSS.call(this, resources.scss || []); - }).bind(this)); - - function _processViews(views) { - // Only keep the active views and index them by ID. - _.extend(this.views, _.indexBy(_.filter(views, function (view) { - return view.active; - }), 'id')); - - // Initialize a 0 level for each view and assign them an array containing their children. - var self = this; - var roots = []; - _.each(this.views, function (view) { - view.level = 0; - view.children = []; - }); - _.each(this.views, function (view) { - var parentId = view.inherit_id[0]; - var parent = parentId && self.views[parentId]; - if (parent) { - parent.children.push(view); - } else { - roots.push(view); - } - }); - - // Assign the correct level based on children key and save a sorted array where - // each view is followed by their children. - this.sortedViews = []; - function visit(view, level) { - view.level = level; - self.sortedViews.push(view); - _.each(view.children, function (child) { - visit(child, level + 1); - }); - } - _.each(roots, function (root) { - visit(root, 0); - }); - } - - function _processSCSS(scss) { - // The received scss data is already sorted by bundle and DOM order - this.sortedSCSS = scss; - - // Store the URL ungrouped by bundle and use the URL as key (resource ID) - var self = this; - _.each(scss, function (bundleInfos) { - _.each(bundleInfos[1], function (info) { info.bundle_xmlid = bundleInfos[0].xmlid; }); - _.extend(self.scss, _.indexBy(bundleInfos[1], 'url')); - }); - } - }, - /** - * Forces the view/scss file identified by its ID/URL to be reset to the way - * it was before the user started editing it. - * - * @todo views reset is not supported yet - * - * @private - * @param {integer|string} [resID] (default to the currently selected one) - * @param {string} [type] (default to the currently selected one) - * @returns {Deferred} - */ - _resetResource: function (resID, type) { - resID = resID || this._getSelectedResource(); - type = type || this.currentType; - - if (this.currentType === 'xml') { - return $.Defered().reject(_t("Reseting views is not supported yet")); - } else { - return this._rpc({ - route: '/web_editor/reset_scss', - params: { - url: resID, - bundle_xmlid: this.scss[resID].bundle_xmlid, - }, - }); - } - }, - /** - * Saves an unique SCSS file. - * - * @private - * @param {Object} session - contains the 'id' (url) and the 'text' of the - * SCSS file to save. - * @return {Deferred} status indicates if the save is finished or if an - * error occured. - */ - _saveSCSS: function (session) { - var def = $.Deferred(); - - var self = this; - this._rpc({ - route: '/web_editor/save_scss', - params: { - url: session.id, - bundle_xmlid: this.scss[session.id].bundle_xmlid, - content: session.text, - }, - }).then(function () { - self._toggleDirtyInfo(session.id, 'scss', false); - def.resolve(); - }, function (source, error) { - def.reject(session, error); - }); - - return def; - }, - /** - * Saves every resource that has been modified. If one cannot be saved, none - * is saved and an error message is displayed. - * - * @private - * @return {Deferred} status indicates if the save is finished or if an - * error occured. - */ - _saveResources: function () { - var toSave = {}; - var errorFound = false; - _.each(this.editingSessions, (function (editingSessions, type) { - if (errorFound) return; - - var dirtySessions = _.pick(editingSessions, function (session) { - return session.getUndoManager().hasUndo(); - }); - toSave[type] = _.map(dirtySessions, function (session, resID) { - return { - id: parseInt(resID, 10) || resID, - text: session.getValue(), - }; - }); - - this._showErrorLine(); - for (var i = 0 ; i < toSave[type].length && !errorFound ; i++) { - var check = (type === 'xml' ? checkXML : checkSCSS)(toSave[type][i].text); - if (!check.isValid) { - this._showErrorLine(check.error.line, check.error.message, toSave[type][i].id, type); - errorFound = toSave[type][i]; - } - } - }).bind(this)); - if (errorFound) return $.Deferred().reject(errorFound); - - var defs = []; - _.each(toSave, (function (_toSave, type) { - defs = defs.concat(_.map(_toSave, (type === 'xml' ? this._saveView : this._saveSCSS).bind(this))); - }).bind(this)); - - return $.when.apply($, defs).fail((function (session, error) { - Dialog.alert(this, '', { - title: _t("Server error"), - $content: $('<div/>').html( - _t("A server error occured. Please check you correctly signed in and that the file you are saving is correctly formatted.") - + '<br/>' - + error - ) - }); - }).bind(this)); - }, - /** - * Saves an unique XML view. - * - * @private - * @param {Object} session - the 'id' and the 'text' of the view to save. - * @returns {Deferred} status indicates if the save is finished or if an - * error occured. - */ - _saveView: function (session) { - var def = $.Deferred(); - - var self = this; - this._rpc({ - model: 'ir.ui.view', - method: 'write', - args: [[session.id], {arch: session.text}], - }, { - noContextKeys: 'lang', - }).then(function () { - self._toggleDirtyInfo(session.id, 'xml', false); - def.resolve(); - }, function (source, error) { - def.reject(session, error); - }); - - return def; - }, - /** - * Shows a line which produced an error. Red color is added to the editor, - * the cursor move to the line and a message is opened on click on the line - * number. If called without argument, the effects are removed. - * - * @private - * @param {integer} [line] - the line number to highlight - * @param {string} [message] - to show on click on the line number - * @param {integer|string} [resID] - * @param {string} [type] - */ - _showErrorLine: function (line, message, resID, type) { - if (line === undefined || line <= 0) { - if (this.$errorLine) { - this.$errorLine.removeClass('o_error'); - this.$errorLine.off('.o_error'); - this.$errorLine = undefined; - this.$errorContent.removeClass('o_error'); - this.$errorContent = undefined; - } - return; - } - - if (type) this._switchType(type); - - if (this._getSelectedResource() === resID) { - __showErrorLine.call(this, line); - } else { - var onChangeSession = (function () { - this.aceEditor.off('changeSession', onChangeSession); - _.delay(__showErrorLine.bind(this, line), 400); - }).bind(this); - this.aceEditor.on('changeSession', onChangeSession); - this._displayResource(resID, this.currentType); - } - - function __showErrorLine(line) { - this.aceEditor.gotoLine(line); - this.$errorLine = this.$viewEditor.find('.ace_gutter-cell').filter(function () { - return parseInt($(this).text()) === line; - }).addClass('o_error'); - this.$errorLine.addClass('o_error').on('click.o_error', function () { - var $message = $('<div/>').html(message); - $message.text($message.text()); - Dialog.alert(this, "", {$content: $message}); - }); - this.$errorContent = this.$viewEditor.find('.ace_scroller').addClass('o_error'); - } - }, - /** - * Switches to the SCSS or XML edition. Calling this method will adapt all - * DOM elements to keep the editor consistent. - * - * @private - * @param {string} type - either 'xml' or 'scss' - */ - _switchType: function (type) { - this.currentType = type; - this.$typeSwitcherBtn.html(this.$typeSwitcherChoices.filter('[data-type=' + type + ']').html()); - _.each(this.$lists, function ($list, _type) { $list.toggleClass('d-none', type !== _type); }); - this.$lists[type].change(); - - this.$includeBundlesArea.toggleClass('d-none', this.currentType === 'scss' || !session.debug); - this.$includeAllSCSSArea.toggleClass('d-none', this.currentType === 'xml' || !session.debug || this.options.defaultBundlesRestriction.length === 0); - this.$formatButton.toggleClass('d-none', this.currentType === 'scss'); - }, - /** - * Updates the select option DOM element associated with a particular resID - * to indicate if the option is dirty or not. - * - * @private - * @param {integer|string} resID - * @param {string} [type] (default to the currently selected one) - * @param {boolean} [isDirty] true if the view is dirty, default to content - * of UndoManager - */ - _toggleDirtyInfo: function (resID, type, isDirty) { - type = type || this.currentType; - - if (!resID || !this.editingSessions[type][resID]) return; - - var $option = this.$lists[type].find('[value="' + resID + '"]'); - if (isDirty === undefined) { - isDirty = this.editingSessions[type][resID].getUndoManager().hasUndo(); - } - $option.data('dirty', isDirty); - }, - /** - * Renders the content of the view/file <select/> DOM element according to - * current widget data. - * - * @private - */ - _updateViewSelectDOM: function () { - var currentId = this._getSelectedResource(); - - var self = this; - this.$lists.xml.empty(); - _.each(this.sortedViews, function (view) { - self.$lists.xml.append($('<option/>', { - value: view.id, - text: view.name, - selected: currentId === view.id, - 'data-level': view.level, - 'data-debug': view.xml_id, - })); - }); - - this.$lists.scss.empty(); - _.each(this.sortedSCSS, function (bundleInfos) { - var $optgroup = $('<optgroup/>', { - label: bundleInfos[0].name, - }).appendTo(self.$lists.scss); - _.each(bundleInfos[1], function (scssInfo) { - var name = scssInfo.url.substring(_.lastIndexOf(scssInfo.url, '/') + 1, scssInfo.url.length - 5); - $optgroup.append($('<option/>', { - value: scssInfo.url, - text: name, - selected: currentId === scssInfo.url, - 'data-debug': scssInfo.url, - 'data-customized': scssInfo.customized - })); - }); - }); - - this.$lists.xml.select2('destroy'); - this.$lists.xml.select2({ - formatResult: _formatDisplay.bind(this, false), - formatSelection: _formatDisplay.bind(this, true), - }); - this.$lists.xml.data('select2').dropdown.addClass('o_ace_select2_dropdown'); - this.$lists.scss.select2('destroy'); - this.$lists.scss.select2({ - formatResult: _formatDisplay.bind(this, false), - formatSelection: _formatDisplay.bind(this, true), - }); - this.$lists.scss.data('select2').dropdown.addClass('o_ace_select2_dropdown'); - - function _formatDisplay(isSelected, data) { - var $elem = $(data.element); - - var text = data.text || ''; - if (!isSelected) { - text = Array(($elem.data('level') || 0) + 1).join('-') + ' ' + text; - } - var $div = $('<div/>', { - text: text, - class: 'o_ace_select2_result', - }); - - if ($elem.data('dirty') || $elem.data('customized')) { - $div.prepend($('<span/>', { - class: 'mr8 fa fa-floppy-o ' + ($elem.data('dirty') ? 'text-warning' : 'text-success'), - })); - } - - if (!isSelected && session.debug && $elem.data('debug')) { - $div.append($('<span/>', { - text: ' (' + $elem.data('debug') + ')', - class: 'ml4 small text-muted', - })); - } - - return $div; - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the close button is clicked -> hides the ace editor. - * - * @private - */ - _onCloseClick: function () { - this.do_hide(); - }, - /** - * Called when the format button is clicked -> format the current resource. - * - * @private - */ - _onFormatClick: function () { - this._formatResource(); - }, - /** - * Called when the checkbox which indicates if all SCSS should be included - * is toggled -> reloads the resources accordingly. - * - * @private - * @param {Event} ev - */ - _onIncludeAllSCSSChange: function (ev) { - this.options.includeAllSCSS = $(ev.target).prop('checked'); - this._loadResources().then(this._updateViewSelectDOM.bind(this)); - }, - /** - * Called when the checkbox which indicates if assets bundles should be - * included is toggled -> reloads the resources accordingly. - * - * @private - * @param {Event} ev - */ - _onIncludeBundlesChange: function (ev) { - this.options.includeBundles = $(ev.target).prop('checked'); - this._loadResources().then(this._updateViewSelectDOM.bind(this)); - }, - /** - * Called when another resource is selected -> displays it. - * - * @private - */ - _onResChange: function () { - this._displayResource(this._getSelectedResource()); - }, - /** - * Called when the reset button is clicked -> resets the resources to its - * original standard odoo state. - * - * @private - */ - _onResetClick: function () { - var self = this; - Dialog.confirm(this, _t("If you reset this file, all your customizations will be lost as it will be reverted to the default file."), { - title: _t("Careful !"), - confirm_callback: function () { - self._resetResource(self._getSelectedResource()); - }, - }); - }, - /** - * Called when the save button is clicked -> saves the dirty resources and - * reloads. - * - * @private - */ - _onSaveClick: function () { - this._saveResources(); - }, - /** - * Called when the user wants to switch from xml to scss or vice-versa -> - * adapt resources choices and displays a resource of that type. - * - * @private - * @param {Event} ev - */ - _onTypeChoice: function (ev) { - ev.preventDefault(); - this._switchType($(ev.target).data('type')); - }, -}); - -return ViewEditor; -}); diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js deleted file mode 100644 index 9dad13a0b2b..00000000000 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ /dev/null @@ -1,1795 +0,0 @@ -odoo.define('web_editor.widget', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var base = require('web_editor.base'); -var core = require('web.core'); -var DialogBase = require('web.Dialog'); -var Widget = require('web.Widget'); -var weContext = require("web_editor.context"); - -var QWeb = core.qweb; -var range = $.summernote.core.range; -var dom = $.summernote.core.dom; - -var _t = core._t; - -/** - * @todo we should either get rid of this or move it somewhere else - */ -function simulateMouseEvent(el, type) { - var evt = document.createEvent("MouseEvents"); - evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); - el.dispatchEvent(evt); -} - -/** - * Extend Dialog class to handle save/cancel of edition components. - */ -var Dialog = DialogBase.extend({ - /** - * @constructor - */ - init: function (parent, options) { - options = options || {}; - this._super(parent, _.extend({}, { - buttons: [ - {text: options.save_text || _t("Save"), classes: 'btn-primary', click: this.save}, - {text: _t("Discard"), close: true} - ] - }, options)); - - this.destroyAction = 'cancel'; - - var self = this; - this.opened().then(function () { - self.$('input:first').focus(); - }); - this.on('closed', this, function () { - this.trigger(this.destroyAction, this.final_data || null); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Called when the dialog is saved. Set the destroy action type to "save" - * and should set the final_data variable correctly before closing. - */ - save: function () { - this.destroyAction = "save"; - this.close(); - }, -}); - -/** - * Let users change the alt & title of a media. - */ -var AltDialog = Dialog.extend({ - template: 'web_editor.dialog.alt', - xmlDependencies: Dialog.prototype.xmlDependencies.concat( - ['/web_editor/static/src/xml/editor.xml'] - ), - - /** - * @constructor - */ - init: function (parent, options, $editable, media) { - this._super(parent, _.extend({}, { - title: _t("Change media description and tooltip") - }, options)); - this.$editable = $editable; - this.media = media; - this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"'); - this.tag_title = ($(this.media).attr('title') || "").replace(/"/g, '"'); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - save: function () { - var self = this; - range.createFromNode(this.media).select(); - this.$editable.data('NoteHistory').recordUndo(); - var alt = this.$('#alt').val(); - var title = this.$('#title').val(); - $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null); - _.defer(function () { - simulateMouseEvent(self.media, 'mouseup'); - }); - return this._super.apply(this, arguments); - }, -}); - -var MediaWidget = Widget.extend({ - xmlDependencies: ['/web_editor/static/src/xml/editor.xml'], - events: { - 'input input.o_we_search': '_onSearchInput', - }, - - /** - * @constructor - */ - init: function (parent, media, options) { - this._super.apply(this, arguments); - this.media = media; - this.$media = $(media); - this._onSearchInput = _.debounce(this._onSearchInput, 500); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @todo comment - */ - clear: function () { - if (!this.media) { - return; - } - this._clear(); - }, - /** - * @abstract - * @param {string} needle - * @returns {Deferred} - */ - search: function (needle) {}, - /** - * @abstract - * @returns {*} - */ - save: function () {}, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @abstract - */ - _clear: function () {}, - /** - * @private - */ - _replaceMedia: function ($media) { - this.$media.replaceWith($media); - this.$media = $media; - this.media = $media[0]; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onSearchInput: function (ev) { - this.search($(ev.currentTarget).val() || ''); - this.hasSearched = true; - }, -}); - -/** - * Let users choose an image, including uploading a new image in odoo. - */ -var ImageWidget = MediaWidget.extend({ - template: 'web_editor.dialog.image', - events: _.extend({}, MediaWidget.prototype.events || {}, { - 'click .o_upload_media_button': '_onUploadButtonClick', - 'click .o_upload_media_button_no_optimization': '_onUploadButtonNoOptimizationClick', - 'change input[type=file]': '_onImageSelection', - 'click .o_upload_media_url_button': '_onUploadURLButtonClick', - 'input input.o_we_url_input': '_onURLInputChange', - 'click .existing-attachments [data-src]': '_onImageClick', - 'dblclick .existing-attachments [data-src]': '_onImageDblClick', - 'click .o_existing_attachment_remove': '_onRemoveClick', - 'click .o_load_more': '_onLoadMoreClick', - }), - - IMAGES_PER_ROW: 6, - IMAGES_ROWS: 5, - - /** - * @constructor - */ - init: function (parent, media, options) { - this._super.apply(this, arguments); - - this.imagesRows = this.IMAGES_ROWS; - this.IMAGES_DISPLAYED_TOTAL = this.IMAGES_PER_ROW * this.imagesRows; - - this.options = options; - this.accept = options.accept || (options.document ? '*/*' : 'image/*'); - if (options.domain) { - this.domain = typeof options.domain === 'function' ? options.domain() : options.domain; - } else if (options.res_id) { - this.domain = ['|', - '&', ['res_model', '=', options.res_model], ['res_id', '=', options.res_id], - ['res_model', '=', 'ir.ui.view']]; - } else { - this.domain = [['res_model', '=', 'ir.ui.view']]; - } - - this.multiImages = options.multiImages; - - this.firstFilters = options.firstFilters || []; - this.lastFilters = options.lastFilters || []; - - this.images = []; - }, - /** - * @override - */ - willStart: function () { - return $.when( - this._super.apply(this, arguments), - this.search('', true) - ); - }, - /** - * @override - */ - start: function () { - var def = this._super.apply(this, arguments); - var self = this; - - this._renderImages(true); - - var o = { - url: null, - alt: null, - }; - if (this.$media.is('img')) { - o.url = this.$media.attr('src'); - } else if (this.$media.is('a.o_image')) { - o.url = this.$media.attr('href').replace(/[?].*/, ''); - o.id = +o.url.match(/\/web\/content\/(\d+)/, '')[1]; - } - if (o.url) { - self._toggleImage(_.find(self.records, function (record) { return record.url === o.url;}) || o, true); - } - - return def; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - save: function () { - var self = this; - if (this.multiImages) { - return this.images; - } - - var img = this.images[0]; - if (!img) { - return this.media; - } - - var def = $.when(); - if (!img.access_token) { - def = this._rpc({ - model: 'ir.attachment', - method: 'generate_access_token', - args: [[img.id]] - }).then(function (access_token) { - img.access_token = access_token[0]; - }); - } - - return def.then(function () { - if (!img.isDocument) { - if (img.access_token && self.options.res_model !== 'ir.ui.view') { - img.src += _.str.sprintf('?access_token=%s', img.access_token); - } - if (!self.$media.is('img')) { - // Note: by default the images receive the bootstrap opt-in - // img-fluid class. We cannot make them all responsive - // by design because of libraries and client databases img. - self._replaceMedia($('<img/>', {class: 'img-fluid o_we_custom_image'})); - } - self.$media.attr('src', img.src); - - } else { - if (!self.$media.is('a')) { - $('.note-control-selection').hide(); - self._replaceMedia($('<a/>')); - } - var href = '/web/content/' + img.id + '?'; - if (img.access_token && self.options.res_model !== 'ir.ui.view') { - href += _.str.sprintf('access_token=%s&', img.access_token); - } - href += 'unique=' + img.checksum + '&download=true'; - self.$media.attr('href', href); - self.$media.addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype); - } - - self.$media.attr('alt', img.alt); - var style = self.style; - if (style) { - self.$media.css(style); - } - - if (self.options.onUpload) { - // We consider that when selecting an image it is as if we upload it in the html content. - self.options.onUpload([img]); - } - - // Remove crop related attributes - if (self.$media.attr('data-aspect-ratio')) { - var attrs = ['aspect-ratio', 'x', 'y', 'width', 'height', 'rotate', 'scale-x', 'scale-y']; - _.each(attrs, function (attr) { - self.$media.removeData(attr); - self.$media.removeAttr('data-' + attr); - }); - } - return self.media; - }); - }, - /** - * @override - */ - search: function (needle, noRender) { - var self = this; - if (!noRender) { - this.$('input.o_we_url_input').val('').trigger('input').trigger('change'); - } - // TODO: Expand this for adding SVG - var domain = this.domain.concat(['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]); - if (needle && needle.length) { - domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]); - } - domain.push('|', ['datas_fname', '=', false], '!', ['datas_fname', '=like', '%.crop'], '!', ['name', '=like', '%.crop']); - return this._rpc({ - model: 'ir.attachment', - method: 'search_read', - args: [], - kwargs: { - domain: domain, - fields: ['name', 'datas_fname', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'], - order: [{name: 'id', asc: false}], - context: weContext.get(), - }, - }).then(function (records) { - self.records = _.chain(records) - .filter(function (r) { - return (r.type === "binary" || r.url && r.url.length > 0); - }) - .uniq(function (r) { - return (r.url || r.id); - }) - .sortBy(function (r) { - if (_.any(self.firstFilters, function (filter) { - var regex = new RegExp(filter, 'i'); - return r.name.match(regex) || r.datas_fname && r.datas_fname.match(regex); - })) { - return -1; - } - if (_.any(self.lastFilters, function (filter) { - var regex = new RegExp(filter, 'i'); - return r.name.match(regex) || r.datas_fname && r.datas_fname.match(regex); - })) { - return 1; - } - return 0; - }) - .value(); - - _.each(self.records, function (record) { - record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, encodeURI(record.name)); // Name is added for SEO purposes - record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype)); - }); - if (!noRender) { - self._renderImages(); - self._adaptLoadMore(); - } - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - */ - _adaptLoadMore: function () { - var noMoreImgToLoad = this.IMAGES_DISPLAYED_TOTAL >= this.records.length; - this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad); - this.$('.o_load_done_msg').toggleClass('d-none', !noMoreImgToLoad); - }, - /** - * @override - */ - _clear: function () { - this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' '); - }, - /** - * @private - */ - _highlightSelectedImages: function () { - var self = this; - this.$('.o_existing_attachment_cell.o_selected').removeClass("o_selected"); - var $select = this.$('.o_existing_attachment_cell [data-src]').filter(function () { - var $img = $(this); - return !!_.find(self.images, function (v) { - return (v.url === $img.data("src") || ($img.data("url") && v.url === $img.data("url")) || v.id === $img.data("id")); - }); - }); - $select.closest('.o_existing_attachment_cell').addClass("o_selected"); - return $select; - }, - /** - * @private - */ - _loadMoreImages: function (forceSearch) { - this.imagesRows += 2; - this.IMAGES_DISPLAYED_TOTAL = this.imagesRows * this.IMAGES_PER_ROW; - if (!forceSearch) { - this._renderImages(); - this._adaptLoadMore(); - } else { - this.search(this.$('.o_we_search').val() || ''); - } - }, - /** - * @private - */ - _renderImages: function (withEffect) { - var self = this; - var rows = _(this.records).chain() - .slice(0, this.IMAGES_DISPLAYED_TOTAL) - .groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); }) - .values() - .value(); - - this.$('.form-text').empty(); - - // Render menu & content - this.$('.existing-attachments').replaceWith( - QWeb.render('web_editor.dialog.files.existing.content', { - rows: rows, - isDocument: this.options.document, - withEffect: withEffect, - }) - ); - - var $divs = this.$('.o_image'); - var imageDefs = _.map($divs, function (el) { - var $div = $(el); - if (/gif|jpe|jpg|png/.test($div.data('mimetype'))) { - var $img = $('<img/>', { - class: 'img-fluid', - src: $div.data('url') || $div.data('src'), - }); - var def = $.Deferred(); - $img[0].onload = def.resolve.bind(def); - $div.addClass('o_webimage').append($img); - return def; - } - }); - if (withEffect) { - $.when.apply($, imageDefs).then(function () { - _.delay(function () { - $divs.removeClass('o_image_loading'); - }, 400); - }); - } - this._highlightSelectedImages(); - }, - /** - * @private - */ - _toggleImage: function (attachment, clearSearch, forceSelect) { - if (this.multiImages) { - var img = _.select(this.images, function (v) { return v.id === attachment.id; }); - if (img.length) { - if (!forceSelect) { - this.images.splice(this.images.indexOf(img[0]), 1); - } - } else { - this.images.push(attachment); - } - } else { - this.images = [attachment]; - } - this._highlightSelectedImages(); - - if (clearSearch) { - this.search(''); - } - }, - /** - * @private - */ - _uploadFile: function () { - var self = this; - - /** - * @todo file upload cannot be handled with _rpc smoothly. This uses the - * form posting in iframe trick to handle the upload. - */ - var $iframe = this.$('.o_file_upload_iframe'); - $iframe.on('load', function () { - var iWindow = $iframe[0].contentWindow; - var attachments = iWindow.attachments; - var error = iWindow.error; - - self.$('.well > span').remove(); - self.$('.well > div').show(); - _.each(attachments, function (record) { - record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, encodeURI(record.name)); // Name is added for SEO purposes - record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype)); - }); - if (error || !attachments.length) { - _processFile(null, error || !attachments.length); - } - self.images = attachments; - for (var i = 0; i < attachments.length; i++) { - _processFile(attachments[i], error); - } - - if (self.options.onUpload) { - self.options.onUpload(attachments); - } - - function _processFile(attachment, error) { - if (!error) { - self._toggleImage(attachment, true); - } else { - self.$el.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); - self.$el.find('.form-text').text(error); - } - } - }); - this.$el.submit(); - - // Empty file input - this.$('.o_file_input').val(''); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onImageClick: function (ev, forceSelect) { - var $img = $(ev.currentTarget); - var attachment = _.find(this.records, function (record) { - return record.id === $img.data('id'); - }); - this._toggleImage(attachment, false, forceSelect); - }, - /** - * @private - */ - _onImageDblClick: function (ev) { - this._onImageClick(ev, true); - this.trigger_up('save_request'); - }, - /** - * @private - */ - _onImageSelection: function () { - var $form = this.$('form'); - this.$el.addClass('nosave'); - $form.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); - $form.find('.form-text').empty(); - this.$('.o_upload_media_button').removeClass('btn-danger btn-success'); - this._uploadFile(); - }, - /** - * @private - */ - _onRemoveClick: function (ev) { - var self = this; - DialogBase.confirm(this, _t("Are you sure you want to delete this file ?"), { - confirm_callback: function () { - var $helpBlock = this.$('.form-text').empty(); - var $a = $(ev.currentTarget); - var id = parseInt($a.data('id'), 10); - var attachment = _.findWhere(this.records, {id: id}); - - return self._rpc({ - route: '/web_editor/attachment/remove', - params: { - ids: [id], - }, - }).then(function (prevented) { - if (_.isEmpty(prevented)) { - self.records = _.without(self.records, attachment); - self.search(''); - return; - } - $helpBlock.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', { - views: prevented[id], - })); - }); - } - }); - }, - /** - * @private - */ - _onURLInputChange: function (ev) { - var $input = $(ev.currentTarget); - var $button = this.$('.o_upload_media_url_button'); - var $success = this.$('.o_we_url_success'); - var $warning = this.$('.o_we_url_warning'); - var $error = this.$('.o_we_url_error'); - - var inputValue = $input.val(); - var emptyValue = (inputValue === ''); - - var isURL = /^.+\..+$/.test(inputValue); // TODO improve - var isImage = _.any(['.gif', '.jpe', '.jpg', '.png'], function (format) { - return inputValue.endsWith(format); - }); - - $button.toggleClass('btn-secondary', emptyValue).toggleClass('btn-primary', !emptyValue) - .prop('disabled', !isURL); - if (!this.options.document) { - $button.text((isURL && !isImage) ? _t("Add as document") : _t("Add image")); - } - $success.toggleClass('d-none', !isURL); - $warning.toggleClass('d-none', !isURL || this.options.document || isImage); - $error.toggleClass('d-none', emptyValue || isURL); - }, - /** - * @private - */ - _onUploadButtonClick: function () { - this.$('input[type=file]').click(); - }, - /** - * @private - */ - _onUploadButtonNoOptimizationClick: function () { - this.$('input[name="disable_optimization"]').val('1'); - this.$('.o_upload_media_button').click(); - }, - /** - * @private - */ - _onUploadURLButtonClick: function () { - this._uploadFile(); - }, - /** - * @private - */ - _onLoadMoreClick: function () { - this._loadMoreImages(); - }, - /** - * @override - */ - _onSearchInput: function () { - this.imagesRows = this.IMAGES_ROWS; - this.IMAGES_DISPLAYED_TOTAL = this.IMAGES_PER_ROW * this.imagesRows; - this._super.apply(this, arguments); - }, -}); - -/** - * Let users choose a font awesome icon, support all font awesome loaded in the - * css files. - */ -var IconWidget = MediaWidget.extend({ - template: 'web_editor.dialog.font-icons', - events: _.extend({}, MediaWidget.prototype.events || {}, { - 'click .font-icons-icon': '_onIconClick', - 'dblclick .font-icons-icon': '_onIconDblClick', - }), - - /** - * @constructor - */ - init: function (parent, media) { - this._super.apply(this, arguments); - - base.computeFonts(); - this.iconsParser = base.fontIcons; - this.alias = _.flatten(_.map(this.iconsParser, function (data) { - return data.alias; - })); - }, - /** - * @override - */ - start: function () { - this.$icons = this.$('.font-icons-icon'); - var classes = (this.media && this.media.className || '').split(/\s+/); - for (var i = 0; i < classes.length; i++) { - var cls = classes[i]; - if (_.contains(this.alias, cls)) { - this.selectedIcon = cls; - this._highlightSelectedIcon(); - } - } - this.nonIconClasses = _.without(classes, this.selectedIcon); - - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - save: function () { - var style = this.$media.attr('style') || ''; - var iconFont = this._getFont(this.selectedIcon) || {base: 'fa', font: ''}; - var finalClasses = _.uniq(this.nonIconClasses.concat([iconFont.base, iconFont.font])); - if (!this.$media.is('span')) { - var $span = $('<span/>'); - $span.data(this.$media.data()); - this._replaceMedia($span); - style = style.replace(/\s*width:[^;]+/, ''); - } - this.$media.attr({ - class: _.compact(finalClasses).join(' '), - style: style, - }); - return this.media; - }, - /** - * @override - */ - search: function (needle) { - var iconsParser = this.iconsParser; - if (needle && needle.length) { - iconsParser = []; - _.filter(this.iconsParser, function (data) { - var cssData = _.filter(data.cssData, function (cssData) { - return _.find(cssData.names, function (alias) { - return alias.indexOf(needle) >= 0; - }); - }); - if (cssData.length) { - iconsParser.push({ - base: data.base, - cssData: cssData, - }); - } - }); - } - this.$('div.font-icons-icons').html( - QWeb.render('web_editor.dialog.font-icons.icons', {iconsParser: iconsParser}) - ); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _clear: function () { - this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' '); - }, - /** - * @private - */ - _getFont: function (classNames) { - if (!(classNames instanceof Array)) { - classNames = (classNames || "").split(/\s+/); - } - var fontIcon, cssData; - for (var k = 0; k < this.iconsParser.length; k++) { - fontIcon = this.iconsParser[k]; - for (var s = 0; s < fontIcon.cssData.length; s++) { - cssData = fontIcon.cssData[s]; - if (_.intersection(classNames, cssData.names).length) { - return { - base: fontIcon.base, - parser: fontIcon.parser, - font: cssData.names[0], - }; - } - } - } - return null; - }, - /** - * @private - */ - _highlightSelectedIcon: function () { - var self = this; - this.$icons.removeClass('o_selected'); - this.$icons.filter(function (i, el) { - return _.contains($(el).data('alias').split(','), self.selectedIcon); - }).addClass('o_selected'); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onIconClick: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - this.selectedIcon = $(ev.currentTarget).data('id'); - this._highlightSelectedIcon(); - }, - /** - * @private - */ - _onIconDblClick: function () { - this.trigger_up('save_request'); - }, -}); - -/** - * Let users choose a video, support all summernote video, and embed iframe. - */ -var VideoWidget = MediaWidget.extend({ - template: 'web_editor.dialog.video', - events: _.extend({}, MediaWidget.prototype.events || {}, { - 'change .o_video_dialog_options input': '_onUpdateVideoOption', - 'input textarea#o_video_text': '_onVideoCodeInput', - 'change textarea#o_video_text': '_onVideoCodeChange', - }), - - /** - * @constructor - */ - init: function (parent, media) { - this._super.apply(this, arguments); - this._onVideoCodeInput = _.debounce(this._onVideoCodeInput, 1000); - }, - /** - * @override - */ - start: function () { - this.$content = this.$('.o_video_dialog_iframe'); - - var $media = $(this.media); - if ($media.hasClass('media_iframe_video')) { - var src = $media.data('oe-expression') || $media.data('src') || ''; - this.$('textarea#o_video_text').val(src); - - this.$('input#o_video_autoplay').prop('checked', src.indexOf('autoplay=1') >= 0); - this.$('input#o_video_hide_controls').prop('checked', src.indexOf('controls=0') >= 0); - this.$('input#o_video_loop').prop('checked', src.indexOf('loop=1') >= 0); - this.$('input#o_video_hide_fullscreen').prop('checked', src.indexOf('fs=0') >= 0); - this.$('input#o_video_hide_yt_logo').prop('checked', src.indexOf('modestbranding=1') >= 0); - this.$('input#o_video_hide_dm_logo').prop('checked', src.indexOf('ui-logo=0') >= 0); - this.$('input#o_video_hide_dm_share').prop('checked', src.indexOf('sharing-enable=0') >= 0); - - this._updateVideo(); - } - - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - save: function () { - this._updateVideo(); - if (this.$('.o_video_dialog_iframe').is('iframe')) { - this._replaceMedia($( - '<div class="media_iframe_video" data-oe-expression="' + this.$content.attr('src') + '">' + - '<div class="css_editable_mode_display"> </div>' + - '<div class="media_iframe_video_size" contenteditable="false"> </div>' + - '<iframe src="' + this.$content.attr('src') + '" frameborder="0" contenteditable="false"></iframe>' + - '</div>' - )); - } - return this.media; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _clear: function () { - if (this.media.dataset.src) { - try { - delete this.media.dataset.src; - } catch (e) { - this.media.dataset.src = undefined; - } - } - this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' '); - }, - /** - * Creates a video node according to the given URL and options. If not - * possible, returns an error code. - * - * @private - * @param {string} url - * @param {Object} options - * @returns {Object} - * $video -> the created video jQuery node - * type -> the type of the created video - * errorCode -> if defined, either '0' for invalid URL or '1' for - * unsupported video provider - */ - _createVideoNode: function (url, options) { - options = options || {}; - - // Video url patterns(youtube, instagram, vimeo, dailymotion, youku, ...) - var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; - var ytMatch = url.match(ytRegExp); - - var insRegExp = /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/; - var insMatch = url.match(insRegExp); - - var vinRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/; - var vinMatch = url.match(vinRegExp); - - var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; - var vimMatch = url.match(vimRegExp); - - var dmRegExp = /.+dailymotion.com\/(video|hub|embed)\/([^_]+)[^#]*(#video=([^_&]+))?/; - var dmMatch = url.match(dmRegExp); - - var ykuRegExp = /(.*).youku\.com\/(v_show\/id_|embed\/)(.+)/; - var ykuMatch = url.match(ykuRegExp); - - var $video = $('<iframe>').width(1280).height(720).attr('frameborder', 0).addClass('o_video_dialog_iframe'); - var videoType = 'yt'; - - if (!/^(http:\/\/|https:\/\/|\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(url)){ - return {errorCode: 0}; - } - - var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0'; - - if (ytMatch && ytMatch[1].length === 11) { - $video.attr('src', '//www.youtube.com/embed/' + ytMatch[1] + autoplay); - } else if (insMatch && insMatch[2].length) { - $video.attr('src', '//www.instagram.com/p/' + insMatch[2] + '/embed/'); - videoType = 'ins'; - } else if (vinMatch && vinMatch[0].length) { - $video.attr('src', vinMatch[0] + '/embed/simple'); - videoType = 'vin'; - } else if (vimMatch && vimMatch[3].length) { - $video.attr('src', '//player.vimeo.com/video/' + vimMatch[3] + autoplay); - videoType = 'vim'; - } else if (dmMatch && dmMatch[2].length) { - var justId = dmMatch[2].replace('video/', ''); - $video.attr('src', '//www.dailymotion.com/embed/video/' + justId + autoplay); - videoType = 'dm'; - } else if (ykuMatch && ykuMatch[3].length) { - var ykuId = ykuMatch[3].indexOf('.html?') >= 0 ? ykuMatch[3].substring(0, ykuMatch[3].indexOf('.html?')) : ykuMatch[3]; - $video.attr('src', '//player.youku.com/embed/' + ykuId); - videoType = 'yku'; - } else { - return {errorCode: 1}; - } - - if (ytMatch) { - $video.attr('src', $video.attr('src') + '&rel=0'); - } - if (options.loop && (ytMatch || vimMatch)) { - $video.attr('src', $video.attr('src') + '&loop=1'); - } - if (options.hide_controls && (ytMatch || dmMatch)) { - $video.attr('src', $video.attr('src') + '&controls=0'); - } - if (options.hide_fullscreen && ytMatch) { - $video.attr('src', $video.attr('src') + '&fs=0'); - } - if (options.hide_yt_logo && ytMatch) { - $video.attr('src', $video.attr('src') + '&modestbranding=1'); - } - if (options.hide_dm_logo && dmMatch) { - $video.attr('src', $video.attr('src') + '&ui-logo=0'); - } - if (options.hide_dm_share && dmMatch) { - $video.attr('src', $video.attr('src') + '&sharing-enable=0'); - } - - return {$video: $video, type: videoType}; - }, - /** - * Updates the video preview according to video code and enabled options. - * - * @private - */ - _updateVideo: function () { - // Reset the feedback - this.$content.empty(); - this.$('#o_video_form_group').removeClass('o_has_error o_has_success').find('.form-control, .custom-select').removeClass('is-invalid is-valid'); - this.$('.o_video_dialog_options div').addClass('d-none'); - - // Check video code - var $textarea = this.$('textarea#o_video_text'); - var code = $textarea.val().trim(); - if (!code) { - return; - } - - // Detect if we have an embed code rather than an URL - var embedMatch = code.match(/(src|href)=["']?([^"']+)?/); - if (embedMatch && embedMatch[2].length > 0 && embedMatch[2].indexOf('instagram')) { - embedMatch[1] = embedMatch[2]; // Instagram embed code is different - } - var url = embedMatch ? embedMatch[1] : code; - - var query = this._createVideoNode(url, { - autoplay: this.$('input#o_video_autoplay').is(':checked'), - hide_controls: this.$('input#o_video_hide_controls').is(':checked'), - loop: this.$('input#o_video_loop').is(':checked'), - hide_fullscreen: this.$('input#o_video_hide_fullscreen').is(':checked'), - hide_yt_logo: this.$('input#o_video_hide_yt_logo').is(':checked'), - hide_dm_logo: this.$('input#o_video_hide_dm_logo').is(':checked'), - hide_dm_share: this.$('input#o_video_hide_dm_share').is(':checked'), - }); - - var $optBox = this.$('.o_video_dialog_options'); - - // Show / Hide preview elements - this.$el.find('.o_video_dialog_preview_text, .media_iframe_video_size').add($optBox).toggleClass('d-none', !query.$video); - // Toggle validation classes - this.$el.find('#o_video_form_group') - .toggleClass('o_has_error', !query.$video).find('.form-control, .custom-select').toggleClass('is-invalid', !query.$video) - .end() - .toggleClass('o_has_success', !!query.$video).find('.form-control, .custom-select').toggleClass('is-valid', !!query.$video); - - // Individually show / hide options base on the video provider - $optBox.find('div.o_' + query.type + '_option').removeClass('d-none'); - - // Hide the entire options box if no options are available - $optBox.toggleClass('d-none', $optBox.find('div:not(.d-none)').length === 0); - - if (query.type === 'yt') { - // Youtube only: If 'hide controls' is checked, hide 'fullscreen' - // and 'youtube logo' options too - this.$('input#o_video_hide_fullscreen, input#o_video_hide_yt_logo').closest('div').toggleClass('d-none', this.$('input#o_video_hide_controls').is(':checked')); - } - - var $content = query.$video; - if (!$content) { - switch (query.errorCode) { - case 0: - $content = $('<div/>', { - class: 'alert alert-danger o_video_dialog_iframe mb-2 mt-2', - text: _t("The provided url is not valid"), - }); - break; - case 1: - $content = $('<div/>', { - class: 'alert alert-warning o_video_dialog_iframe mb-2 mt-2', - text: _t("The provided url does not reference any supported video"), - }); - break; - } - } - this.$content.replaceWith($content); - this.$content = $content; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when a video option changes -> Updates the video preview. - * - * @private - */ - _onUpdateVideoOption: function () { - this._updateVideo(); - }, - /** - * Called when the video code (URL / Iframe) change is confirmed -> Updates - * the video preview immediately. - * - * @private - */ - _onVideoCodeChange: function () { - this._updateVideo(); - }, - /** - * Called when the video code (URL / Iframe) changes -> Updates the video - * preview (note: this function is automatically debounced). - * - * @private - */ - _onVideoCodeInput: function () { - this._updateVideo(); - }, -}); - -/** - * MediaDialog widget. Lets users change a media, including uploading a - * new image, font awsome or video and can change a media into an other - * media. - */ -var MediaDialog = Dialog.extend({ - template: 'web_editor.dialog.media', - xmlDependencies: Dialog.prototype.xmlDependencies.concat( - ['/web_editor/static/src/xml/editor.xml'] - ), - events: _.extend({}, Dialog.prototype.events, { - 'shown.bs.tab a[data-toggle="tab"]': '_onTabChange', - }), - custom_events: _.extend({}, Dialog.prototype.custom_events || {}, { - save_request: '_onSaveRequest', - }), - - /** - * @constructor - */ - init: function (parent, options, $editable, media) { - var self = this; - this._super(parent, _.extend({}, { - title: _t("Select a Media"), - save_text: _t("Add"), - }, options)); - - if ($editable) { - this.$editable = $editable; - this.rte = this.$editable.rte || this.$editable.data('rte'); - } - - this.media = media; - this.$media = $(media); - this.range = range.create(); - - this.multiImages = options.multiImages; - var onlyImages = options.onlyImages || this.multiImages || (this.media && (this.$media.parent().data('oeField') === 'image' || this.$media.parent().data('oeType') === 'image')); - this.noImages = options.noImages; - this.noDocuments = onlyImages || options.noDocuments; - this.noIcons = onlyImages || options.noIcons; - this.noVideos = onlyImages || options.noVideos; - - if (!this.noImages) { - this.imageDialog = new ImageWidget(this, this.media, options); - } - if (!this.noDocuments) { - this.documentDialog = new ImageWidget(this, this.media, _.extend({}, options, {document: true})); - } - if (!this.noIcons) { - this.iconDialog = new IconWidget(this, this.media, options); - } - if (!this.noVideos) { - this.videoDialog = new VideoWidget(this, this.media, options); - } - - this.opened(function () { - var tabToShow = 'icon'; - if (!self.media || self.$media.is('img')) { - tabToShow = 'image'; - } else if (self.$media.is('a.o_image')) { - tabToShow = 'document'; - } else if (self.$media.attr('class').match(/(^|\s)media_iframe_video($|\s)/)) { - tabToShow = 'video'; - } else if (self.$media.parent().attr('class').match(/(^|\s)media_iframe_video($|\s)/)) { - self.$media = self.$media.parent(); - self.media = self.$media[0]; - tabToShow = 'video'; - } - self.$('[href="#editor-media-' + tabToShow + '"]').tab('show'); - }); - }, - /** - * @override - */ - start: function () { - var self = this; - var defs = [this._super.apply(this, arguments)]; - this.$modal.addClass('note-image-dialog'); - this.$modal.find('.modal-dialog').addClass('o_select_media_dialog'); - - if (this.imageDialog) { - defs.push(this.imageDialog.appendTo(this.$("#editor-media-image"))); - } - if (this.documentDialog) { - defs.push(this.documentDialog.appendTo(this.$("#editor-media-document"))); - } - if (this.iconDialog) { - defs.push(this.iconDialog.appendTo(this.$("#editor-media-icon"))); - } - if (this.videoDialog) { - defs.push(this.videoDialog.appendTo(this.$("#editor-media-video"))); - } - - return $.when.apply($, defs).then(function () { - self._setActive(self.imageDialog); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - save: function () { - var self = this; - var args = arguments; - var _super = this._super; - if (this.multiImages) { - // In the case of multi images selection we suppose this was not to - // replace an old media, so we only retrieve the images and save. - return $.when(this.active.save()).then(function (data) { - self.final_data = data; - return _super.apply(self, args); - }); - } - - if (this.rte) { - this.range.select(); - this.rte.historyRecordUndo(this.media); - } - - if (this.media) { - this.$media.html(''); - _.each(['imageDialog', 'documentDialog', 'iconDialog', 'videoDialog'], function (v) { - // Note: hack since imageDialog is the same type as the documentDialog - if (self[v] && self.active._clear.toString() !== self[v]._clear.toString()) { - self[v].clear(); - } - }); - } - - return $.when(this.active.save()).then(function (media) { - if (!self.media && media) { - self.range.insertNode(media, true); - } - self.media = media; - self.$media = $(media); - - self.final_data = self.media; - $(self.final_data).trigger('input').trigger('save'); - $(document.body).trigger("media-saved", self.final_data); // TODO get rid of this - - // Update editor bar after image edition (in case the image change to icon or other) - _.defer(function () { - if (!self.media || !self.media.parentNode) { - return; - } - range.createFromNode(self.media).select(); - simulateMouseEvent(self.media, 'mousedown'); - simulateMouseEvent(self.media, 'mouseup'); - }); - return _super.apply(self, args); - }); - }, - - //-------------------------------------------------------------------------- - // - //-------------------------------------------------------------------------- - - /** - * @private - */ - _setActive: function (widget) { - this.active = widget; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onSaveRequest: function (ev) { - ev.stopPropagation(); - this.save(); - }, - /** - * @private - */ - _onTabChange: function (ev) { - var $target = $(ev.target); - if ($target.is('[href="#editor-media-image"]')) { - this._setActive(this.imageDialog); - } else if ($target.is('[href="#editor-media-document"]')) { - this._setActive(this.documentDialog); - } else if ($target.is('[href="#editor-media-icon"]')) { - this._setActive(this.active = this.iconDialog); - } else if ($target.is('[href="#editor-media-video"]')) { - this._setActive(this.active = this.videoDialog); - } - }, -}); - -/** - * Allows to customize link content and style. - */ -var LinkDialog = Dialog.extend({ - template: 'web_editor.dialog.link', - xmlDependencies: Dialog.prototype.xmlDependencies.concat( - ['/web_editor/static/src/xml/editor.xml'] - ), - events: _.extend({}, Dialog.prototype.events || {}, { - 'input': '_onAnyChange', - 'change': '_onAnyChange', - 'input input[name="url"]': '_onURLInput', - }), - - /** - * @constructor - */ - init: function (parent, options, editable, linkInfo) { - this._super(parent, _.extend({ - title: _t("Link to"), - }, options || {})); - - this.editable = editable; - this.data = linkInfo || {}; - - this.data.className = ""; - - var r = this.data.range; - this.needLabel = !r || (r.sc === r.ec && r.so === r.eo); - - if (this.data.range) { - this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || ""; - this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' '); - - var isLink = this.data.range.isOnAnchor(); - - var sc = r.sc; - var so = r.so; - var ec = r.ec; - var eo = r.eo; - - var nodes; - if (!isLink) { - if (sc.tagName) { - sc = dom.firstChild(so ? sc.childNodes[so] : sc); - so = 0; - } else if (so !== sc.textContent.length) { - if (sc === ec) { - ec = sc = sc.splitText(so); - eo -= so; - } else { - sc = sc.splitText(so); - } - so = 0; - } - if (ec.tagName) { - ec = dom.lastChild(eo ? ec.childNodes[eo - 1] : ec); - eo = ec.textContent.length; - } else if (eo !== ec.textContent.length) { - ec.splitText(eo); - } - - nodes = dom.listBetween(sc, ec); - - // browsers can't target a picture or void node - if (dom.isVoid(sc) || dom.isImg(sc)) { - so = dom.listPrev(sc).length - 1; - sc = sc.parentNode; - } - if (dom.isBR(ec)) { - eo = dom.listPrev(ec).length - 1; - ec = ec.parentNode; - } else if (dom.isVoid(ec) || dom.isImg(sc)) { - eo = dom.listPrev(ec).length; - ec = ec.parentNode; - } - - this.data.range = range.create(sc, so, ec, eo); - this.data.range.select(); - } else { - nodes = dom.ancestor(sc, dom.isAnchor).childNodes; - } - - if (dom.isImg(sc) && nodes.indexOf(sc) === -1) { - nodes.push(sc); - } - if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) { - var text = ""; - this.data.images = []; - for (var i = 0; i < nodes.length; i++) { - if (dom.ancestor(nodes[i], dom.isImg)) { - this.data.images.push(dom.ancestor(nodes[i], dom.isImg)); - text += '[IMG]'; - } else if (!isLink && nodes[i].nodeType === 1) { - // just use text nodes from listBetween - } else if (!isLink && i === 0) { - text += nodes[i].textContent.slice(so, Infinity); - } else if (!isLink && i === nodes.length - 1) { - text += nodes[i].textContent.slice(0, eo); - } else { - text += nodes[i].textContent; - } - } - this.data.text = text; - } - } - - this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' '); - - this._onURLInput = _.debounce(this._onURLInput, 400); - }, - /** - * @override - */ - start: function () { - var self = this; - - this.$('input.link-style').prop('checked', false).first().prop('checked', true); - if (this.data.iniClassName) { - _.each(this.$('input.link-style, select.link-style > option'), function (el) { - var $option = $(el); - if ($option.val() && self.data.iniClassName.indexOf($option.val()) >= 0) { - if ($option.is("input")) { - $option.prop("checked", true); - } else { - $option.parent().val($option.val()); - } - } - }); - } - if (this.data.url) { - var match = /mailto:(.+)/.exec(this.data.url); - this.$('input[name="url"]').val(match ? match[1] : this.data.url); - } - - // Hide the duplicate color buttons (most of the times, primary = alpha - // and secondary = beta for example but this may depend on the theme) - this.opened().then(function () { - var colors = []; - _.each(self.$('.o_btn_preview.o_link_dialog_color_item'), function (btn) { - var $btn = $(btn); - var color = $btn.css('background-color'); - if (_.contains(colors, color)) { - $btn.hide(); // Not remove to be able to edit buttons with those styles - } else { - colors.push(color); - } - }); - }); - - this._adaptPreview(); - - this.$('input:visible:first').focus(); - - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - save: function () { - var data = this._getData(); - if (data === null) { - var $url = this.$('input[name="url"]'); - $url.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); - $url.focus(); - return $.Deferred().reject(); - } - this.data.text = data.label; - this.data.url = data.url; - this.data.className = data.classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, ''); - if (data.classes.replace(/(^|[ ])(btn-secondary|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi, ' ')) { - this.data.style = {'background-color': '', 'color': ''}; - } - this.data.isNewWindow = data.isNewWindow; - this.final_data = this.data; - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - */ - _adaptPreview: function () { - var $preview = this.$("#link-preview"); - var data = this._getData(); - if (data === null) { - return; - } - $preview.attr({ - target: data.isNewWindow ? '_blank' : '', - href: data.url && data.url.length ? data.url : '#', - class: data.classes.replace(/float-\w+/, '') + ' o_btn_preview', - }).html((data.label && data.label.length) ? data.label : data.url); - }, - /** - * @private - */ - _getData: function () { - var $url = this.$('input[name="url"]'); - var url = $url.val(); - var label = this.$('input[name="label"]').val() || url; - - if (label && this.data.images) { - for (var i = 0; i < this.data.images.length; i++) { - label = label.replace(/</, "<").replace(/>/, ">").replace(/\[IMG\]/, this.data.images[i].outerHTML); - } - } - - if ($url.prop('required') && (!url || !$url[0].checkValidity())) { - return null; - } - - var style = this.$('input[name="link_style_color"]:checked').val() || ''; - var shape = this.$('select[name="link_style_shape"]').val() || ''; - var size = this.$('select[name="link_style_size"]').val() || ''; - var shapes = shape.split(','); - var outline = shapes[0] === 'outline'; - shape = shapes.slice(outline ? 1 : 0).join(' '); - var classes = (this.data.className || '') - + (style ? (' btn btn-' + (outline ? 'outline-' : '') + style) : '') - + (shape ? (' ' + shape) : '') - + (size ? (' btn-' + size) : ''); - var isNewWindow = this.$('input[name="is_new_window"]').prop('checked'); - - if (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0 && !url.match(/^http[s]?/i)) { - url = ('mailto:' + url); - } - return { - label: label, - url: url, - classes: classes, - isNewWindow: isNewWindow, - }; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onAnyChange: function () { - this._adaptPreview(); - }, - /** - * @private - */ - _onURLInput: function () { - var $linkUrlInput = this.$('#o_link_dialog_url_input'); - $linkUrlInput.closest('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); - var isLink = $linkUrlInput.val().indexOf('@') < 0; - this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('d-none', !isLink); - }, -}); - -/** - * CropImageDialog widget. Let users crop an image. - */ -var CropImageDialog = Dialog.extend({ - template: 'web_editor.dialog.crop_image', - xmlDependencies: Dialog.prototype.xmlDependencies.concat( - ['/web_editor/static/src/xml/editor.xml'] - ), - jsLibs: [ - '/web_editor/static/lib/cropper/js/cropper.js', - ], - cssLibs: [ - '/web_editor/static/lib/cropper/css/cropper.css', - ], - events: _.extend({}, Dialog.prototype.events, { - 'click .o_crop_options [data-event]': '_onCropOptionClick', - }), - - /** - * @constructor - */ - init: function (parent, options, $editable, media) { - this.media = media; - this.$media = $(this.media); - var src = this.$media.attr('src').split('?')[0]; - this.aspectRatioList = [ - [_t("Free"), '0/0', 0], - ["16:9", '16/9', 16 / 9], - ["4:3", '4/3', 4 / 3], - ["1:1", '1/1', 1], - ["2:3", '2/3', 2 / 3], - ]; - this.imageData = { - imageSrc: src, - originalSrc: this.$media.data('crop:originalSrc') || src, // the original src for cropped DB images will be fetched later - mimetype: this.$media.data('crop:mimetype') || (_.str.endsWith(src, '.png') ? 'image/png' : 'image/jpeg'), // the mimetype for DB images will be fetched later - aspectRatio: this.$media.data('aspectRatio') || this.aspectRatioList[0][1], - isExternalImage: src.substr(0, 5) !== 'data:' && src[0] !== '/' && src.indexOf(window.location.host) < 0, - }; - this.options = _.extend({ - title: _t("Crop Image"), - buttons: this.imageData.isExternalImage ? [{ - text: _t("Close"), - close: true, - }] : [{ - text: _t("Save"), - classes: 'btn-primary', - click: this.save, - }, { - text: _t("Discard"), - close: true, - }], - }, options || {}); - this._super(parent, this.options); - }, - /** - * @override - */ - willStart: function () { - var self = this; - var def = this._super.apply(this, arguments); - if (this.imageData.isExternalImage) { - return def; - } - - var defs = [def, ajax.loadLibs(this)]; - var params = {}; - var isDBImage = false; - var matchImageID = this.imageData.imageSrc.match(/^\/web\/image\/(\d+)/); - if (matchImageID) { - params['image_id'] = parseInt(matchImageID[1]); - isDBImage = true; - } else { - var matchXmlID = this.imageData.imageSrc.match(/^\/web\/image\/([^/?]+)/); - if (matchXmlID) { - params['xml_id'] = matchXmlID[1]; - isDBImage = true; - } - } - if (isDBImage) { - defs.push(this._rpc({ - route: '/web_editor/get_image_info', - params: params, - }).then(function (res) { - _.extend(self.imageData, res); - })); - } - return $.when.apply($, defs); - }, - /** - * @override - */ - start: function () { - this.$cropperImage = this.$('.o_cropper_image'); - if (this.$cropperImage.length) { - var data = this.$media.data(); - var ratio = 0; - for (var i = 0; i < this.aspectRatioList.length; i++) { - if (this.aspectRatioList[i][1] === data.aspectRatio) { - ratio = this.aspectRatioList[i][2]; - break; - } - } - this.$cropperImage.cropper({ - viewMode: 1, - autoCropArea: 1, - aspectRatio: ratio, - data: _.pick(data, 'x', 'y', 'width', 'height', 'rotate', 'scaleX', 'scaleY') - }); - } - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - if (this.$cropperImage.length) { - this.$cropperImage.cropper('destroy'); - } - this._super.apply(this, arguments); - }, - /** - * Updates the DOM image with cropped data and associates required - * information for a potential future save (where required cropped data - * attachments will be created). - * - * @override - */ - save: function () { - var self = this; - var cropperData = this.$cropperImage.cropper('getData'); - - // Mark the media for later creation of required cropped attachments... - this.$media.addClass('o_cropped_img_to_save'); - - // ... and attach required data - this.$media.data('crop:resModel', this.options.res_model); - this.$media.data('crop:resID', this.options.res_id); - this.$media.data('crop:id', this.imageData.id); - this.$media.data('crop:mimetype', this.imageData.mimetype); - this.$media.data('crop:originalSrc', this.imageData.originalSrc); - - // Mark the media with the cropping information which is required for - // a future crop edition - this.$media - .attr('data-aspect-ratio', this.imageData.aspectRatio) - .data('aspectRatio', this.imageData.aspectRatio); - _.each(cropperData, function (value, key) { - key = _.str.dasherize(key); - self.$media.attr('data-' + key, value); - self.$media.data(key, value); - }); - - // Update the media with base64 source for preview before saving - var canvas = this.$cropperImage.cropper('getCroppedCanvas', { - width: cropperData.width, - height: cropperData.height, - }); - this.$media.attr('src', canvas.toDataURL(this.imageData.mimetype)); - - this.$media.trigger('content_changed'); - - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when a crop option is clicked -> change the crop area accordingly. - * - * @private - * @param {MouseEvent} ev - */ - _onCropOptionClick: function (ev) { - var $option = $(ev.currentTarget); - var opt = $option.data('event'); - var value = $option.data('value'); - switch (opt) { - case 'ratio': - this.$cropperImage.cropper('reset'); - this.imageData.aspectRatio = $option.data('label'); - this.$cropperImage.cropper('setAspectRatio', value); - break; - case 'zoom': - case 'rotate': - case 'reset': - this.$cropperImage.cropper(opt, value); - break; - case 'flip': - var direction = value === 'horizontal' ? 'x' : 'y'; - var scaleAngle = -$option.data(direction); - $option.data(direction, scaleAngle); - this.$cropperImage.cropper('scale' + direction.toUpperCase(), scaleAngle); - break; - } - }, -}); - -return { - Dialog: Dialog, - AltDialog: AltDialog, - MediaDialog: MediaDialog, - LinkDialog: LinkDialog, - CropImageDialog: CropImageDialog, - ImageWidget: ImageWidget, -}; -}); |
