summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/widgets
diff options
context:
space:
mode:
authorChristophe Matthieu <chm@odoo.com>2018-12-28 17:49:21 +0000
committerChristophe Matthieu <chm@odoo.com>2019-01-17 08:40:21 +0000
commitde444335b7da30a2ab3a4c5b92c9731a023f3f07 (patch)
tree40ceda25aa7157ae0923cf567a448d685798e911 /addons/web_editor/static/src/js/widgets
parent17c6d00531c55d0c392ddb6916bf1957bc490c1c (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.js863
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js1795
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(/&quot;/g, '"');
- this.tag_title = ($(this.media).attr('title') || "").replace(/&quot;/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, "&quot;") : null).attr('title', title ? title.replace(/"/g, "&quot;") : 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">&nbsp;</div>' +
- '<div class="media_iframe_video_size" contenteditable="false">&nbsp;</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(/</, "&lt;").replace(/>/, "&gt;").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,
-};
-});