From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/web_editor/static/src/js/wysiwyg/fonts.js | 99 ++ addons/web_editor/static/src/js/wysiwyg/root.js | 91 ++ .../static/src/js/wysiwyg/widgets/alt_dialog.js | 62 + .../static/src/js/wysiwyg/widgets/color_palette.js | 410 ++++++ .../static/src/js/wysiwyg/widgets/dialog.js | 81 ++ .../src/js/wysiwyg/widgets/image_crop_widget.js | 213 +++ .../static/src/js/wysiwyg/widgets/link_dialog.js | 339 +++++ .../static/src/js/wysiwyg/widgets/media.js | 1463 ++++++++++++++++++++ .../static/src/js/wysiwyg/widgets/media_dialog.js | 279 ++++ .../static/src/js/wysiwyg/widgets/widgets.js | 29 + addons/web_editor/static/src/js/wysiwyg/wysiwyg.js | 274 ++++ .../static/src/js/wysiwyg/wysiwyg_iframe.js | 132 ++ .../static/src/js/wysiwyg/wysiwyg_snippets.js | 56 + .../src/js/wysiwyg/wysiwyg_translate_attributes.js | 0 14 files changed, 3528 insertions(+) create mode 100644 addons/web_editor/static/src/js/wysiwyg/fonts.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/root.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/alt_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/image_crop_widget.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/link_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/media.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/wysiwyg.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/wysiwyg_snippets.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/wysiwyg_translate_attributes.js (limited to 'addons/web_editor/static/src/js/wysiwyg') diff --git a/addons/web_editor/static/src/js/wysiwyg/fonts.js b/addons/web_editor/static/src/js/wysiwyg/fonts.js new file mode 100644 index 00000000..257ccaf2 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/fonts.js @@ -0,0 +1,99 @@ +odoo.define('wysiwyg.fonts', function (require) { +'use strict'; + +return { + /** + * Retrieves all the CSS rules which match the given parser (Regex). + * + * @param {Regex} filter + * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is + * defined by 3 values: 'selector', 'css' and 'names'. 'selector' + * is a string which contains the whole selector, 'css' is a string + * which contains the css properties and 'names' is an array of the + * first captured groups for each selector part. E.g.: if the + * filter is set to match .fa-* rules and capture the icon names, + * the rule: + * '.fa-alias1::before, .fa-alias2::before { hello: world; }' + * will be retrieved as + * { + * selector: '.fa-alias1::before, .fa-alias2::before', + * css: 'hello: world;', + * names: ['.fa-alias1', '.fa-alias2'], + * } + */ + cacheCssSelectors: {}, + getCssSelectors: function (filter) { + if (this.cacheCssSelectors[filter]) { + return this.cacheCssSelectors[filter]; + } + this.cacheCssSelectors[filter] = []; + var sheets = document.styleSheets; + for (var i = 0; i < sheets.length; i++) { + var rules; + try { + // try...catch because Firefox not able to enumerate + // document.styleSheets[].cssRules[] for cross-domain + // stylesheets. + rules = sheets[i].rules || sheets[i].cssRules; + } catch (e) { + console.warn("Can't read the css rules of: " + sheets[i].href, e); + continue; + } + if (!rules) { + continue; + } + + for (var r = 0 ; r < rules.length ; r++) { + var selectorText = rules[r].selectorText; + if (!selectorText) { + continue; + } + var selectors = selectorText.split(/\s*,\s*/); + var data = null; + for (var s = 0; s < selectors.length; s++) { + var match = selectors[s].trim().match(filter); + if (!match) { + continue; + } + if (!data) { + data = { + selector: match[0], + css: rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, ''), + names: [match[1]] + }; + } else { + data.selector += (', ' + match[0]); + data.names.push(match[1]); + } + } + if (data) { + this.cacheCssSelectors[filter].push(data); + } + } + } + return this.cacheCssSelectors[filter]; + }, + /** + * List of font icons to load by editor. The icons are displayed in the media + * editor and identified like font and image (can be colored, spinned, resized + * with fa classes). + * To add font, push a new object {base, parser} + * + * - base: class who appear on all fonts + * - parser: regular expression used to select all font in css stylesheets + * + * @type Array + */ + fontIcons: [{base: 'fa', parser: /\.(fa-(?:\w|-)+)::?before/i}], + /** + * Searches the fonts described by the @see fontIcons variable. + */ + computeFonts: _.once(function () { + var self = this; + _.each(this.fontIcons, function (data) { + data.cssData = self.getCssSelectors(data.parser); + data.alias = _.flatten(_.map(data.cssData, _.property('names'))); + }); + }), +}; +}); diff --git a/addons/web_editor/static/src/js/wysiwyg/root.js b/addons/web_editor/static/src/js/wysiwyg/root.js new file mode 100644 index 00000000..57e9f65e --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/root.js @@ -0,0 +1,91 @@ +odoo.define('web_editor.wysiwyg.root', function (require) { +'use strict'; + +var Widget = require('web.Widget'); + +var assetsLoaded = false; + +var WysiwygRoot = Widget.extend({ + assetLibs: ['web_editor.compiled_assets_wysiwyg'], + _loadLibsTplRoute: '/web_editor/public_render_template', + + publicMethods: ['isDirty', 'save', 'getValue', 'setValue', 'getEditable', 'on', 'trigger', 'focus', 'saveModifiedImages'], + + /** + * @see 'web_editor.wysiwyg' module + **/ + init: function (parent, params) { + this._super.apply(this, arguments); + this._params = params; + this.$editor = null; + }, + /** + * Load assets + * + * @override + **/ + willStart: function () { + var self = this; + + var $target = this.$el; + this.$el = null; + + return this._super().then(function () { + // FIXME: this code works by pure luck. If the web_editor.wysiwyg + // JS module was requiring a delayed module, using it here right + // away would lead to a crash. + if (!assetsLoaded) { + var Wysiwyg = odoo.__DEBUG__.services['web_editor.wysiwyg']; + _.each(['getRange', 'setRange', 'setRangeFromNode'], function (methodName) { + WysiwygRoot[methodName] = Wysiwyg[methodName].bind(Wysiwyg); + }); + assetsLoaded = true; + } + + var Wysiwyg = self._getWysiwygContructor(); + var instance = new Wysiwyg(self, self._params); + if (self.__extraAssetsForIframe) { + instance.__extraAssetsForIframe = self.__extraAssetsForIframe; + } + self._params = null; + + _.each(self.publicMethods, function (methodName) { + self[methodName] = instance[methodName].bind(instance); + }); + + return instance.attachTo($target).then(function () { + self.$editor = instance.$editor || instance.$el; + }); + }); + }, + + _getWysiwygContructor: function () { + return odoo.__DEBUG__.services['web_editor.wysiwyg']; + } +}); + +return WysiwygRoot; + +}); + +odoo.define('web_editor.wysiwyg.default_options', function (require) { +'use strict'; + +/** + * TODO this should be refactored to be done another way, same as the 'root' + * module that should be done another way. + * + * This allows to have access to default options that are used in the summernote + * editor so that they can be tweaked (instead of entirely replaced) when using + * the editor on an editable content. + */ + +var core = require('web.core'); + +var _lt = core._lt; + +return { + styleTags: ['p', 'pre', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'], + fontSizes: [_lt('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62], +}; +}); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/alt_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/alt_dialog.js new file mode 100644 index 00000000..80f143b6 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/alt_dialog.js @@ -0,0 +1,62 @@ +odoo.define('wysiwyg.widgets.AltDialog', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('wysiwyg.widgets.Dialog'); + +var _t = core._t; + +/** + * Let users change the alt & title of a media. + */ +var AltDialog = Dialog.extend({ + template: 'wysiwyg.widgets.alt', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/web_editor/static/src/xml/wysiwyg.xml'] + ), + + /** + * @constructor + */ + init: function (parent, options, media) { + options = options || {}; + this._super(parent, _.extend({}, { + title: _t("Change media description and tooltip") + }, options)); + + this.trigger_up('getRecordInfo', { + recordInfo: options, + callback: function (recordInfo) { + _.defaults(options, recordInfo); + }, + }); + + this.media = media; + var allEscQuots = /"/g; + this.alt = ($(this.media).attr('alt') || "").replace(allEscQuots, '"'); + var title = $(this.media).attr('title') || $(this.media).data('original-title') || ""; + this.tag_title = (title).replace(allEscQuots, '"'); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + save: function () { + var alt = this.$('#alt').val(); + var title = this.$('#title').val(); + var allNonEscQuots = /"/g; + $(this.media).attr('alt', alt ? alt.replace(allNonEscQuots, """) : null) + .attr('title', title ? title.replace(allNonEscQuots, """) : null); + $(this.media).trigger('content_changed'); + this.final_data = this.media; + return this._super.apply(this, arguments); + }, +}); + + +return AltDialog; +}); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js new file mode 100644 index 00000000..d00abd1a --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js @@ -0,0 +1,410 @@ +odoo.define('web_editor.ColorPalette', function (require) { +'use strict'; + +const ajax = require('web.ajax'); +const core = require('web.core'); +const session = require('web.session'); +const {ColorpickerWidget} = require('web.Colorpicker'); +const Widget = require('web.Widget'); +const summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); +const weUtils = require('web_editor.utils'); + +const qweb = core.qweb; + +const ColorPaletteWidget = Widget.extend({ + // ! for xmlDependencies, see loadDependencies function + template: 'web_editor.snippet.option.colorpicker', + events: { + 'click .o_we_color_btn': '_onColorButtonClick', + 'mouseenter .o_we_color_btn': '_onColorButtonEnter', + 'mouseleave .o_we_color_btn': '_onColorButtonLeave', + 'click .o_we_colorpicker_switch_pane_btn': '_onSwitchPaneButtonClick', + }, + custom_events: { + 'colorpicker_select': '_onColorPickerSelect', + 'colorpicker_preview': '_onColorPickerPreview', + }, + /** + * @override + * + * @param {Object} [options] + * @param {string} [options.selectedColor] The class or css attribute color selected by default. + * @param {boolean} [options.resetButton=true] Whether to display or not the reset button. + * @param {string[]} [options.excluded=[]] Sections not to display. + * @param {string[]} [options.excludeSectionOf] Extra section to exclude: the one containing the named color. + * @param {JQuery} [options.$editable=$()] Editable content from which the custom colors are retrieved. + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.summernoteCustomColorsArray = [].concat(...summernoteCustomColors); + this.style = window.getComputedStyle(document.documentElement); + this.options = _.extend({ + selectedColor: false, + resetButton: true, + excluded: [], + excludeSectionOf: null, + $editable: $(), + withCombinations: false, + }, options || {}); + + this.selectedColor = ''; + this.resetButton = this.options.resetButton; + this.withCombinations = this.options.withCombinations; + + this.trigger_up('request_editable', {callback: val => this.options.$editable = val}); + }, + /** + * @override + */ + willStart: async function () { + await this._super(...arguments); + await ColorPaletteWidget.loadDependencies(this); + }, + /** + * @override + */ + start: async function () { + const res = this._super.apply(this, arguments); + + const $colorSection = this.$('.o_colorpicker_sections[data-color-tab="theme-colors"]'); + const $clpicker = qweb.has_template('web_editor.colorpicker') + ? $(qweb.render('web_editor.colorpicker')) + : $(`
`); + $clpicker.find('button').addClass('o_we_color_btn'); + $clpicker.appendTo($colorSection); + + // Remove excluded palettes (note: only hide them to still be able + // to remove their related colors on the DOM target) + _.each(this.options.excluded, exc => { + this.$('[data-name="' + exc + '"]').addClass('d-none'); + }); + if (this.options.excludeSectionOf) { + this.$('[data-name]:has([data-color="' + this.options.excludeSectionOf + '"])').addClass('d-none'); + } + + this.el.querySelectorAll('.o_colorpicker_section').forEach(elem => { + $(elem).prepend('
' + (elem.dataset.display || '') + '
'); + }); + + // Render common colors + if (!this.options.excluded.includes('common')) { + const $commonColorSection = this.$('[data-name="common"]'); + summernoteCustomColors.forEach((colorRow, i) => { + if (i === 0) { + return; // Ignore the summernote gray palette and use ours + } + const $div = $('
', {class: 'clearfix'}).appendTo($commonColorSection); + colorRow.forEach(color => { + $div.append(this._createColorButton(color, ['o_common_color'])); + }); + }); + } + + // Compute class colors + const compatibilityColorNames = ['primary', 'secondary', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'success', 'info', 'warning', 'danger']; + this.colorNames = [...compatibilityColorNames]; + this.colorToColorNames = {}; + this.el.querySelectorAll('button[data-color]').forEach(elem => { + const colorName = elem.dataset.color; + const $color = $(elem); + const isCCName = weUtils.isColorCombinationName(colorName); + if (isCCName) { + $color.find('.o_we_cc_preview_wrapper').addClass(`o_cc o_cc${colorName}`); + } else { + $color.addClass(`bg-${colorName}`); + } + this.colorNames.push(colorName); + if (!isCCName && !elem.classList.contains('d-none')) { + const color = weUtils.getCSSVariableValue(colorName, this.style); + this.colorToColorNames[color] = colorName; + } + }); + + // Select selected Color and build customColors. + // If no color is selected selectedColor is an empty string (transparent is interpreted as no color) + if (this.options.selectedColor) { + let selectedColor = this.options.selectedColor; + if (compatibilityColorNames.includes(selectedColor)) { + selectedColor = weUtils.getCSSVariableValue(selectedColor, this.style) || selectedColor; + } + selectedColor = ColorpickerWidget.normalizeCSSColor(selectedColor); + if (selectedColor !== 'rgba(0, 0, 0, 0)') { + this.selectedColor = this.colorToColorNames[selectedColor] || selectedColor; + } + } + this._buildCustomColors(); + this._markSelectedColor(); + + // Colorpicker + let defaultColor = this.selectedColor; + if (defaultColor && !ColorpickerWidget.isCSSColor(defaultColor)) { + defaultColor = weUtils.getCSSVariableValue(defaultColor, this.style); + } + this.colorPicker = new ColorpickerWidget(this, { + defaultColor: defaultColor, + }); + await this.colorPicker.prependTo($colorSection); + + // TODO Added as a fix. In master, the widget should probably not be + // instantiated at all. + if (this.options.excluded.includes('custom')) { + this.colorPicker.$el.addClass('d-none'); + } + + return res; + }, + /** + * Return a list of the color names used in the color palette + */ + getColorNames: function () { + return this.colorNames; + }, + /** + * Sets the currently selected color + * + * @param {string} color rgb[a] + */ + setSelectedColor: function (color) { + this._selectColor({color: color}); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _buildCustomColors: function () { + if (this.options.excluded.includes('custom')) { + return; + } + this.el.querySelectorAll('.o_custom_color').forEach(el => el.remove()); + const existingColors = new Set(this.summernoteCustomColorsArray.concat( + Object.keys(this.colorToColorNames) + )); + this.trigger_up('get_custom_colors', { + onSuccess: (colors) => { + colors.forEach(color => { + this._addCustomColor(existingColors, color); + }); + }, + }); + weUtils.getCSSVariableValue('custom-colors', this.style).split(' ').forEach(v => { + const color = weUtils.getCSSVariableValue(v.substring(1, v.length - 1), this.style); + if (ColorpickerWidget.isCSSColor(color)) { + this._addCustomColor(existingColors, color); + } + }); + _.each(this.options.$editable.find('[style*="color"]'), el => { + for (const colorProp of ['color', 'backgroundColor']) { + this._addCustomColor(existingColors, el.style[colorProp]); + } + }); + if (this.selectedColor) { + this._addCustomColor(existingColors, this.selectedColor); + } + }, + /** + * Add the color to the custom color section if it is not in the existingColors. + * + * @param {string[]} existingColors Colors currently in the colorpicker + * @param {string} color Color to add to the cuustom colors + */ + _addCustomColor: function (existingColors, color) { + if (!color) { + return; + } + if (!ColorpickerWidget.isCSSColor(color)) { + color = weUtils.getCSSVariableValue(color, this.style); + } + const normColor = ColorpickerWidget.normalizeCSSColor(color); + if (!existingColors.has(normColor)) { + this._addCustomColorButton(normColor); + existingColors.add(normColor); + } + }, + /** + * Add a custom button in the coresponding section. + * + * @private + * @param {string} color + * @param {string[]} classes - classes added to the button + * @returns {jQuery} + */ + _addCustomColorButton: function (color, classes = []) { + classes.push('o_custom_color'); + const $themeSection = this.$('.o_colorpicker_section[data-name="theme"]'); + const $button = this._createColorButton(color, classes); + return $button.appendTo($themeSection); + }, + /** + * Return a color button. + * + * @param {string} color + * @param {string[]} classes - classes added to the button + * @returns {jQuery} + */ + _createColorButton: function (color, classes) { + return $('