[REF] web_editor: prevent duplicate code in LinkDialog and LinkTools
LinkTools were recently introduced to provide a new UI from link editing within a sidebar where there is one. It was implemented separately from the existing LinkDialog despite them having a lot of code in common. In order to prevent future discrepancies between them, this commit introduces the Link widget which contains the common logic for link editing, to be extended by LinkTools and LinkDialog (via an intermediary widget). closes odoo/odoo#69137 X-original-commit: 5415ea46f8d3ee4732111a21b0cd9fa8bdb9526e Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
odoo.define('wysiwyg.widgets.Link', function (require) {
|
||||
'use strict';
|
||||
|
||||
const core = require('web.core');
|
||||
const OdooEditorLib = require('web_editor.odoo-editor');
|
||||
const wysiwygUtils = require('@web_editor/js/wysiwyg/wysiwyg_utils');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const getDeepRange = OdooEditorLib.getDeepRange;
|
||||
const getInSelection = OdooEditorLib.getInSelection;
|
||||
const _t = core._t;
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
*/
|
||||
const Link = Widget.extend({
|
||||
xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],
|
||||
events: {
|
||||
'input': '_onAnyChange',
|
||||
'change': '_onAnyChange',
|
||||
'input input[name="url"]': '_onURLInput',
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Boolean} data.isButton - whether if the target is a button element.
|
||||
*/
|
||||
init: function (parent, options, editable, data, $button, link) {
|
||||
this.options = options || {};
|
||||
this._super(parent, _.extend({
|
||||
title: _t("Link to"),
|
||||
}, this.options));
|
||||
|
||||
this.data = data || {};
|
||||
this.isButton = this.data.isButton;
|
||||
this.$button = $button;
|
||||
|
||||
this.data.className = this.data.className || "";
|
||||
this.data.iniClassName = this.data.iniClassName || "";
|
||||
|
||||
// Using explicit type 'link' to preserve style when the target is <button class="...btn-link"/>.
|
||||
this.colorsData = [
|
||||
{type: this.isButton ? 'link' : '', label: _t("Link"), btnPreview: 'link'},
|
||||
{type: 'primary', label: _t("Primary"), btnPreview: 'primary'},
|
||||
{type: 'secondary', label: _t("Secondary"), btnPreview: 'secondary'},
|
||||
// Note: by compatibility the dialog should be able to remove old
|
||||
// colors that were suggested like the BS status colors or the
|
||||
// alpha -> epsilon classes. This is currently done by removing
|
||||
// all btn-* classes anyway.
|
||||
];
|
||||
|
||||
this.editable = editable;
|
||||
this.$editable = $(editable);
|
||||
|
||||
if (link) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(link);
|
||||
this.data.range = range;
|
||||
this.$link = $(link);
|
||||
} else {
|
||||
const selection = editable && editable.ownerDocument.getSelection();
|
||||
this.data.range = selection && selection.rangeCount && selection.getRangeAt(0);
|
||||
}
|
||||
|
||||
if (this.data.range) {
|
||||
this.$link = this.$link || $(OdooEditorLib.getInSelection(this.editable.ownerDocument, 'a'));
|
||||
this.data.iniClassName = this.$link.attr('class') || '';
|
||||
this.colorCombinationClass = false;
|
||||
let $node = this.$link;
|
||||
while ($node.length && !$node.is('body')) {
|
||||
const className = $node.attr('class') || '';
|
||||
const m = className.match(/\b(o_cc\d+)\b/g);
|
||||
if (m) {
|
||||
this.colorCombinationClass = m[0];
|
||||
break;
|
||||
}
|
||||
$node = $node.parent();
|
||||
}
|
||||
this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');
|
||||
|
||||
const textLink = this.$link[0] || this.data.range.cloneContents();
|
||||
const [encodedText, images] = this._encodeNodeToText(textLink);
|
||||
this.data.originalText = this._decodeText(encodedText, images);
|
||||
this.data.content = encodedText;
|
||||
this.data.images = images;
|
||||
this.data.url = this.$link.attr('href') || '';
|
||||
} else {
|
||||
this.data.content = this.data.content ? this.data.content.replace(/[ \t\r\n]+/g, ' ') : '';
|
||||
}
|
||||
|
||||
const allBtnClassSuffixes = /(^|\s+)btn(-[a-z0-9_-]*)?/gi;
|
||||
const allBtnShapes = /\s*(rounded-circle|flat)\s*/gi;
|
||||
this.data.className = this.data.iniClassName
|
||||
.replace(allBtnClassSuffixes, ' ')
|
||||
.replace(allBtnShapes, ' ');
|
||||
// 'o_submit' class will force anchor to be handled as a button in linkdialog.
|
||||
if (/(?:s_website_form_send|o_submit)/.test(this.data.className)) {
|
||||
this.isButton = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
for (const option of this._getLinkOptions()) {
|
||||
const $option = $(option);
|
||||
const value = $option.is('input') ? $option.val() : $option.data('value');
|
||||
let active = false;
|
||||
if (value) {
|
||||
const subValues = value.split(',');
|
||||
let subActive = true;
|
||||
for (let subValue of subValues) {
|
||||
const classPrefix = new RegExp('(^|btn-| |btn-outline-)' + subValue);
|
||||
subActive = subActive && classPrefix.test(this.data.iniClassName);
|
||||
}
|
||||
active = subActive;
|
||||
} else {
|
||||
active = !this.data.iniClassName || !this.data.iniClassName.includes('btn-');
|
||||
}
|
||||
this._setSelectOption($option, active);
|
||||
}
|
||||
if (this.data.url) {
|
||||
var match = /mailto:(.+)/.exec(this.data.url);
|
||||
this.$('input[name="url"]').val(match ? match[1] : this.data.url);
|
||||
this._onURLInput();
|
||||
}
|
||||
|
||||
this._updateOptionsUI();
|
||||
this._adaptPreview(false);
|
||||
|
||||
this.$('input:visible:first').focus();
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Apply the new link to the DOM (via `this.$link`).
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
applyLinkToDom: function (data) {
|
||||
const attrs = Object.assign({}, this.data.oldAttributes, {
|
||||
href: data.url,
|
||||
target: data.isNewWindow ? '_blank' : '',
|
||||
class: `${data.classes}`,
|
||||
});
|
||||
this.$link.attr(attrs);
|
||||
if (!this.$link.attr('target')) {
|
||||
this.$link[0].removeAttribute('target');
|
||||
}
|
||||
if (data.content !== this.data.originalText || data.url !== this.data.url) {
|
||||
const content = (data.content && data.content.length) ? data.content : data.url;
|
||||
// If there is a this.data.originalText, it means that we selected
|
||||
// the text and we could not change the content through the text input.
|
||||
// .html() is needed in case we selected images.
|
||||
if (this.data.originalText) {
|
||||
this.$link.html(content);
|
||||
} else {
|
||||
this.$link.text(content);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Return the link element to edit. Create one from selection if none was
|
||||
* present in selection.
|
||||
*
|
||||
* @param {Node} editable
|
||||
* @returns {Node}
|
||||
*/
|
||||
getOrCreateLink: function (editable) {
|
||||
const doc = editable.ownerDocument;
|
||||
const range = getDeepRange(editable, {splitText: true, select: true, correctTripleClick: true});
|
||||
this.needLabel = false;
|
||||
let link = getInSelection(doc, 'a');
|
||||
const $link = $(link);
|
||||
if (link && (!$link.has(range.startContainer).length || !$link.has(range.endContainer).length)) {
|
||||
// Expand the current link to include the whole selection.
|
||||
let before = link.previousSibling;
|
||||
while (before !== null && range.intersectsNode(before)) {
|
||||
link.insertBefore(before, link.firstChild);
|
||||
before = link.previousSibling;
|
||||
}
|
||||
let after = link.nextSibling;
|
||||
while (after !== null && range.intersectsNode(after)) {
|
||||
link.appendChild(after);
|
||||
after = link.nextSibling;
|
||||
}
|
||||
} else if (!link) {
|
||||
link = document.createElement('a');
|
||||
if (range.collapsed) {
|
||||
range.insertNode(link);
|
||||
this.needLabel = true;
|
||||
} else {
|
||||
link.appendChild(range.extractContents());
|
||||
range.insertNode(link);
|
||||
}
|
||||
}
|
||||
return link;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Abstract method: adapt the link to changes.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
*/
|
||||
_adaptPreview: function (createStep = true) {},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_correctLink: function (url) {
|
||||
if (url.indexOf('mailto:') === 0 || url.indexOf('tel:') === 0) {
|
||||
url = url.replace(/^tel:([0-9]+)$/, 'tel://$1');
|
||||
} else if (url.indexOf('@') !== -1 && url.indexOf(':') === -1) {
|
||||
url = 'mailto:' + url;
|
||||
} else if (url.indexOf('://') === -1 && url[0] !== '/'
|
||||
&& url[0] !== '#' && url.slice(0, 2) !== '${') {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
/**
|
||||
* Take an HTML string and return an escape HTML string with each instance
|
||||
* of the string "[IMG]" is replaced with the HTML from the given images
|
||||
* respectively.
|
||||
*
|
||||
* @private
|
||||
* @param {string} text
|
||||
* @param {Node[]} images
|
||||
* @returns {string}
|
||||
*/
|
||||
_decodeText: function (text, images = []) {
|
||||
text = _.escape(text);
|
||||
for (const img of images) {
|
||||
text = text.replace(/\[IMG\]/, img.outerHTML);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
/**
|
||||
* Abstract method: return true if the URL should be stripped of its domain.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_doStripDomain: function () {},
|
||||
/**
|
||||
* Take an element and return a tuple with its inner text where each image
|
||||
* is replaced with the string "[IMG]", and a list of the replaced image
|
||||
* elements.
|
||||
*
|
||||
* @param {Node} rootNode
|
||||
* @returns {[string, Node[]]}
|
||||
*/
|
||||
_encodeNodeToText: function (rootNode) {
|
||||
const fake = document.createElement('fake');
|
||||
for (const node of [...rootNode.cloneNode(true).childNodes]) {
|
||||
fake.appendChild(node);
|
||||
}
|
||||
|
||||
const nodes = this._getDescendants(fake);
|
||||
const images = [];
|
||||
for (const node of nodes) {
|
||||
if (wysiwygUtils.isImg(node)) {
|
||||
node.before(document.createTextNode('[IMG]'));
|
||||
node.remove();
|
||||
images.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const text = fake.innerText.replace(/[ \t\r\n]+/g, ' ');
|
||||
return [text, images];
|
||||
},
|
||||
/**
|
||||
* Get the link's data (url, content and styles).
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} {content: String, url: String, classes: String, isNewWindow: Boolean}
|
||||
*/
|
||||
_getData: function () {
|
||||
var $url = this.$('input[name="url"]');
|
||||
var url = $url.val();
|
||||
var content = this.$('input[name="label"]').val() || url;
|
||||
content = this._decodeText(content, this.data.images);
|
||||
|
||||
if (!this.isButton && $url.prop('required') && (!url || !$url[0].checkValidity())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = this._getLinkType();
|
||||
const size = this._getLinkSize();
|
||||
const shape = this._getLinkShape();
|
||||
const shapes = shape ? shape.split(',') : [];
|
||||
const style = ['outline', 'fill'].includes(shapes[0]) ? `${shapes[0]}-` : '';
|
||||
const shapeClasses = shapes.slice(style ? 1 : 0).join(' ');
|
||||
const classes = (this.data.className || '') +
|
||||
(type ? (` btn btn-${style}${type}`) : '') +
|
||||
(type && shapeClasses ? (` ${shapeClasses}`) : '') +
|
||||
(type && size ? (' btn-' + size) : '');
|
||||
var isNewWindow = this._isNewWindow(url);
|
||||
var doStripDomain = this._doStripDomain();
|
||||
if (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0 && !url.match(/^http[s]?/i)) {
|
||||
url = ('mailto:' + url);
|
||||
} else if (url.indexOf(location.origin) === 0 && doStripDomain) {
|
||||
url = url.slice(location.origin.length);
|
||||
}
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
return {
|
||||
content: content,
|
||||
url: this._correctLink(url),
|
||||
classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '').replace(/oe_edited_link/, ''),
|
||||
oldAttributes: this.data.oldAttributes,
|
||||
isNewWindow: isNewWindow,
|
||||
doStripDomain: doStripDomain,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Return a list of all the descendants of a given element.
|
||||
*
|
||||
* @private
|
||||
* @param {Node} rootNode
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
_getDescendants: function (rootNode) {
|
||||
const nodes = [];
|
||||
for (const node of rootNode.childNodes) {
|
||||
nodes.push(node);
|
||||
nodes.push(...this._getDescendants(node));
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
/**
|
||||
* Abstract method: return a JQuery object containing the UI elements
|
||||
* holding the styling options of the link (eg: color, size, shape).
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
_getLinkOptions: function () {},
|
||||
/**
|
||||
* Abstract method: return the shape(s) to apply to the link (eg:
|
||||
* "outline", "rounded-circle", "outline,rounded-circle").
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLinkShape: function () {},
|
||||
/**
|
||||
* Abstract method: return the size to apply to the link (eg:
|
||||
* "sm", "lg").
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLinkSize: function () {},
|
||||
/**
|
||||
* Abstract method: return the type to apply to the link (eg:
|
||||
* "primary", "secondary").
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLinkType: function () {},
|
||||
/**
|
||||
* Abstract method: return true if the link should open in a new window.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isNewWindow: function (url) {},
|
||||
/**
|
||||
* Abstract method: mark one or several options as active or inactive.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @param {JQuery} $option
|
||||
* @param {boolean} [active]
|
||||
*/
|
||||
_setSelectOption: function ($option, active) {},
|
||||
/**
|
||||
* @abstract
|
||||
* @private
|
||||
*/
|
||||
_updateOptionsUI: function () {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAnyChange: function () {
|
||||
this._adaptPreview();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onURLInput: function () {
|
||||
var $linkUrlInput = this.$('#o_link_dialog_url_input');
|
||||
let value = $linkUrlInput.val();
|
||||
let isLink = value.indexOf('@') < 0;
|
||||
this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('d-none', !isLink);
|
||||
this.$('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0);
|
||||
},
|
||||
});
|
||||
|
||||
return Link;
|
||||
});
|
||||
@@ -1,150 +1,27 @@
|
||||
odoo.define('wysiwyg.widgets.LinkDialog', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Dialog = require('wysiwyg.widgets.Dialog');
|
||||
const OdooEditorLib = require('web_editor.odoo-editor');
|
||||
const wysiwygUtils = require('@web_editor/js/wysiwyg/wysiwyg_utils');
|
||||
const Dialog = require('wysiwyg.widgets.Dialog');
|
||||
const Link = require('wysiwyg.widgets.Link');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
*/
|
||||
var LinkDialog = Dialog.extend({
|
||||
// This widget is there only to extend Link and be instantiated by LinkDialog.
|
||||
const _DialogLinkWidget = Link.extend({
|
||||
template: 'wysiwyg.widgets.link',
|
||||
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat([
|
||||
'/web_editor/static/src/xml/wysiwyg.xml'
|
||||
]),
|
||||
events: _.extend({}, Dialog.prototype.events || {}, {
|
||||
'input': '_onAnyChange',
|
||||
events: _.extend({}, Link.prototype.events || {}, {
|
||||
'change [name="link_style_color"]': '_onTypeChange',
|
||||
'change': '_onAnyChange',
|
||||
'input input[name="url"]': '_onURLInput',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Boolean} linkInfo.isButton - whether if the target is a button element.
|
||||
*/
|
||||
init: function (parent, options, editable, data, link) {
|
||||
this.options = options || {};
|
||||
this._super(parent, _.extend({
|
||||
title: _t("Link to"),
|
||||
}, this.options));
|
||||
|
||||
this.data = data || {};
|
||||
this.isButton = this.data.isButton;
|
||||
// Using explicit type 'link' to preserve style when the target is <button class="...btn-link"/>.
|
||||
this.colorsData = [
|
||||
{type: this.isButton ? 'link' : '', label: _t("Link"), btnPreview: 'link'},
|
||||
{type: 'primary', label: _t("Primary"), btnPreview: 'primary'},
|
||||
{type: 'secondary', label: _t("Secondary"), btnPreview: 'secondary'},
|
||||
// Note: by compatibility the dialog should be able to remove old
|
||||
// colors that were suggested like the BS status colors or the
|
||||
// alpha -> epsilon classes. This is currently done by removing
|
||||
// all btn-* classes anyway.
|
||||
];
|
||||
|
||||
this.editable = editable;
|
||||
this.$editable = $(editable);
|
||||
this.data = data || {};
|
||||
|
||||
this.data.className = this.data.className || "";
|
||||
this.data.iniClassName = this.data.iniClassName || "";
|
||||
|
||||
if (link) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(link);
|
||||
this.data.range = range;
|
||||
} else {
|
||||
const selection = editable && editable.ownerDocument.getSelection();
|
||||
this.data.range = selection && selection.rangeCount && selection.getRangeAt(0);
|
||||
}
|
||||
|
||||
if (this.data.range) {
|
||||
this.needLabel = this.data.range.collapsed;
|
||||
let $link;
|
||||
if (link) {
|
||||
$link = $(link);
|
||||
} else {
|
||||
const firstLinkInSelection = OdooEditorLib.getInSelection(this.editable.ownerDocument, 'a');
|
||||
$link = $(firstLinkInSelection);
|
||||
}
|
||||
this.data.iniClassName = $link.attr("class") || "";
|
||||
this.colorCombinationClass = false;
|
||||
let $node = $link;
|
||||
while ($node.length && !$node.is('body')) {
|
||||
const className = $node.attr('class') || '';
|
||||
const m = className.match(/\b(o_cc\d+)\b/g);
|
||||
if (m) {
|
||||
this.colorCombinationClass = m[0];
|
||||
break;
|
||||
}
|
||||
$node = $node.parent();
|
||||
}
|
||||
this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');
|
||||
|
||||
let textLink = link;
|
||||
if (!textLink) {
|
||||
const startLink = $(this.data.range.startContainer).closest('a')[0];
|
||||
const endLink = $(this.data.range.endContainer).closest('a')[0];
|
||||
const isLink = startLink && (startLink === endLink);
|
||||
textLink = isLink ? startLink : this.data.range.cloneContents();
|
||||
}
|
||||
const [encodedText, images] = wysiwygUtils.encodeNodeToText(textLink);
|
||||
this.data.originalText = wysiwygUtils.decodeText(encodedText, images);
|
||||
this.data.content = encodedText;
|
||||
this.data.images = images;
|
||||
this.data.url = $link.attr('href') || '';
|
||||
} else {
|
||||
this.data.content = this.data.content ? this.data.content.replace(/[ \t\r\n]+/g, ' ') : '';
|
||||
this.needLabel = true;
|
||||
}
|
||||
|
||||
var allBtnClassSuffixes = /(^|\s+)btn(-[a-z0-9_-]*)?/gi;
|
||||
var allBtnShapes = /\s*(rounded-circle|flat)\s*/gi;
|
||||
this.data.className = this.data.iniClassName
|
||||
.replace(allBtnClassSuffixes, ' ')
|
||||
.replace(allBtnShapes, ' ');
|
||||
// 'o_submit' class will force anchor to be handled as a button in linkdialog.
|
||||
if (/(?:s_website_form_send|o_submit)/.test(this.data.className)) {
|
||||
this.isButton = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.buttonOptsCollapseEl = this.el.querySelector('#o_link_dialog_button_opts_collapse');
|
||||
|
||||
this.$styleInputs = this.$('input.link-style');
|
||||
this.$styleInputs.prop('checked', false).filter('[value=""]').prop('checked', true);
|
||||
if (this.data.iniClassName) {
|
||||
_.each(this.$('input[name="link_style_color"], select[name="link_style_size"] > option, select[name="link_style_shape"] > option'), el => {
|
||||
var $option = $(el);
|
||||
if ($option.val() && this.data.iniClassName.match(new RegExp('(^|btn-| |btn-outline-)' + $option.val()))) {
|
||||
if ($option.is("input")) {
|
||||
$option.prop("checked", true);
|
||||
} else {
|
||||
$option.parent().find('option').removeAttr('selected').removeProp('selected');
|
||||
$option.parent().val($option.val());
|
||||
$option.attr('selected', 'selected').prop('selected', 'selected');
|
||||
}
|
||||
}
|
||||
});
|
||||
if (this.data.isNewWindow) {
|
||||
this.$('we-button.o_we_checkbox_wrapper').toggleClass('active', true);
|
||||
}
|
||||
if (this.data.url) {
|
||||
var match = /mailto:(.+)/.exec(this.data.url);
|
||||
this.$('input[name="url"]').val(match ? match[1] : this.data.url);
|
||||
this._onURLInput();
|
||||
}
|
||||
|
||||
this._updateOptionsUI();
|
||||
this._adaptPreview();
|
||||
|
||||
this.$('input:visible:first').focus();
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -168,7 +45,7 @@ var LinkDialog = Dialog.extend({
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
var allBtnTypes = /(^|[ ])(btn-secondary|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi;
|
||||
this.data.className = data.classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '');
|
||||
this.data.classes = data.classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '');
|
||||
if (data.classes.replace(allBtnTypes, ' ')) {
|
||||
this.data.style = {
|
||||
'background-color': '',
|
||||
@@ -177,11 +54,6 @@ var LinkDialog = Dialog.extend({
|
||||
}
|
||||
this.data.isNewWindow = data.isNewWindow;
|
||||
this.final_data = this.data;
|
||||
const returnValue = this._super.apply(this, arguments);
|
||||
if (this.insertedAnchors && this.insertedAnchors.length) {
|
||||
$(this.insertedAnchors[this.insertedAnchors.length - 1]).focus();
|
||||
}
|
||||
return returnValue;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -189,9 +61,7 @@ var LinkDialog = Dialog.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adapt the link preview to changes.
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_adaptPreview: function () {
|
||||
var data = this._getData();
|
||||
@@ -205,55 +75,40 @@ var LinkDialog = Dialog.extend({
|
||||
};
|
||||
this.$("#link-preview").attr(attrs).html((data.content && data.content.length) ? data.content : data.url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the link's data (url, content and styles).
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} {content: String, url: String, classes: String, isNewWindow: Boolean}
|
||||
* @override
|
||||
*/
|
||||
_getData: function () {
|
||||
var $url = this.$('input[name="url"]');
|
||||
var url = $url.val();
|
||||
var content = this.$('input[name="label"]').val() || url;
|
||||
content = wysiwygUtils.decodeText(content, this.data.images);
|
||||
|
||||
if (!this.isButton && $url.prop('required') && (!url || !$url[0].checkValidity())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = this.$('input[name="link_style_color"]:checked').val() || '';
|
||||
const size = this.$('select[name="link_style_size"]').val() || '';
|
||||
const shape = this.$('select[name="link_style_shape"]').val() || '';
|
||||
const shapes = shape ? shape.split(',') : [];
|
||||
const style = ['outline', 'fill'].includes(shapes[0]) ? `${shapes[0]}-` : '';
|
||||
const shapeClasses = shapes.slice(style ? 1 : 0).join(' ');
|
||||
const classes = (this.data.className || '') +
|
||||
(type ? (` btn btn-${style}${type}`) : '') +
|
||||
(shapeClasses ? (` ${shapeClasses}`) : '') +
|
||||
(size ? (' btn-' + size) : '');
|
||||
var isNewWindow = this.options.forceNewWindow ?
|
||||
this._isFromAnotherHostName(url) : this.$('input[name="is_new_window"]').prop('checked');
|
||||
if (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0 && !url.match(/^http[s]?/i)) {
|
||||
url = ('mailto:' + url);
|
||||
} else if (url.indexOf(location.origin) === 0 && this.$('#o_link_dialog_url_strip_domain').prop("checked")) {
|
||||
url = url.slice(location.origin.length);
|
||||
}
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
return {
|
||||
content: content,
|
||||
url: this._correctLink(url),
|
||||
classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, ''),
|
||||
isNewWindow: isNewWindow,
|
||||
};
|
||||
_doStripDomain: function () {
|
||||
return this.$('#o_link_dialog_url_strip_domain').prop('checked');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_updateOptionsUI: function () {
|
||||
const el = this.el.querySelector('[name="link_style_color"]:checked');
|
||||
$(this.buttonOptsCollapseEl).collapse(el && el.value ? 'show' : 'hide');
|
||||
_getLinkOptions: function () {
|
||||
const options = [
|
||||
'input[name="link_style_color"]',
|
||||
'select[name="link_style_size"] > option',
|
||||
'select[name="link_style_shape"] > option',
|
||||
];
|
||||
return this.$(options.join(','));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkShape: function () {
|
||||
return this.$('select[name="link_style_shape"]').val() || '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkSize: function () {
|
||||
return this.$('select[name="link_style_size"]').val() || '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkType: function () {
|
||||
return this.$('input[name="link_style_color"]:checked').val() || '';
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -271,30 +126,39 @@ var LinkDialog = Dialog.extend({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_correctLink: function (url) {
|
||||
if (url.indexOf('mailto:') === 0 || url.indexOf('tel:') === 0) {
|
||||
url = url.replace(/^tel:([0-9]+)$/, 'tel://$1');
|
||||
} else if (url.indexOf('@') !== -1 && url.indexOf(':') === -1) {
|
||||
url = 'mailto:' + url;
|
||||
} else if (url.indexOf('://') === -1 && url[0] !== '/'
|
||||
&& url[0] !== '#' && url.slice(0, 2) !== '${') {
|
||||
url = 'http://' + url;
|
||||
_isNewWindow: function (url) {
|
||||
if (this.options.forceNewWindow) {
|
||||
return this._isFromAnotherHostName(url);
|
||||
} else {
|
||||
return this.$('input[name="is_new_window"]').prop('checked');
|
||||
}
|
||||
return url;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_setSelectOption: function ($option, active) {
|
||||
if ($option.is("input")) {
|
||||
$option.prop("checked", active);
|
||||
} else if (active) {
|
||||
$option.parent().find('option').removeAttr('selected').removeProp('selected');
|
||||
$option.parent().val($option.val());
|
||||
$option.attr('selected', 'selected').prop('selected', 'selected');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_updateOptionsUI: function () {
|
||||
const el = this.el.querySelector('[name="link_style_color"]:checked');
|
||||
$(this.buttonOptsCollapseEl).collapse(el && el.value ? 'show' : 'hide');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAnyChange: function () {
|
||||
this._adaptPreview();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -302,15 +166,39 @@ var LinkDialog = Dialog.extend({
|
||||
this._updateOptionsUI();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_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');
|
||||
let value = $linkUrlInput.val();
|
||||
let isLink = value.indexOf('@') < 0;
|
||||
this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('d-none', !isLink);
|
||||
this.$('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0);
|
||||
this._super(...arguments);
|
||||
this.$('#o_link_dialog_url_input').closest('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
*/
|
||||
const LinkDialog = Dialog.extend({
|
||||
init: function (parent, ...args) {
|
||||
this._super(...arguments);
|
||||
this.linkWidget = new _DialogLinkWidget(this, ...args);
|
||||
},
|
||||
start: async function () {
|
||||
const res = await this._super(...arguments);
|
||||
this.linkWidget.appendTo(this.$el);
|
||||
return res;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
this.linkWidget.save();
|
||||
this.final_data = this.linkWidget.final_data;
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,129 +1,37 @@
|
||||
odoo.define('wysiwyg.widgets.LinkTools', function (require) {
|
||||
'use strict';
|
||||
|
||||
const core = require('web.core');
|
||||
const Link = require('wysiwyg.widgets.Link');
|
||||
const OdooEditorLib = require('web_editor.odoo-editor');
|
||||
const Widget = require('web.Widget');
|
||||
const wysiwygUtils = require('@web_editor/js/wysiwyg/wysiwyg_utils');
|
||||
|
||||
const getInSelection = OdooEditorLib.getInSelection;
|
||||
const getDeepRange = OdooEditorLib.getDeepRange;
|
||||
const setCursor = OdooEditorLib.setCursor;
|
||||
const nodeSize = OdooEditorLib.nodeSize;
|
||||
|
||||
const _t = core._t;
|
||||
const setCursor = OdooEditorLib.setCursor;
|
||||
|
||||
/**
|
||||
* Allows to customize link content and style.
|
||||
*/
|
||||
const LinkTools = Widget.extend({
|
||||
const LinkTools = Link.extend({
|
||||
template: 'wysiwyg.widgets.linkTools',
|
||||
xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],
|
||||
events: {
|
||||
'input': '_onAnyChange',
|
||||
'change': '_onAnyChange',
|
||||
'input input[name="url"]': '_onURLInput',
|
||||
events: _.extend({}, Link.prototype.events, {
|
||||
'click we-select we-button': '_onPickSelectOption',
|
||||
'click we-checkbox': '_onClickCheckbox',
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options, editable, $button, link) {
|
||||
this.options = options || {};
|
||||
this._super(parent, _.extend({
|
||||
title: _t("Link to"),
|
||||
}, this.options));
|
||||
|
||||
this.$button = $button;
|
||||
this.colorsData = [
|
||||
{type: '', label: _t("Link"), btnPreview: 'link'},
|
||||
{type: 'primary', label: _t("Primary"), btnPreview: 'primary'},
|
||||
{type: 'secondary', label: _t("Secondary"), btnPreview: 'secondary'},
|
||||
// Note: by compatibility the dialog should be able to remove old
|
||||
// colors that were suggested like the BS status colors or the
|
||||
// alpha -> epsilon classes. This is currently done by removing
|
||||
// all btn-* classes anyway.
|
||||
];
|
||||
|
||||
this.editable = editable;
|
||||
this.$editable = $(editable);
|
||||
this.data = {};
|
||||
|
||||
this.data.className = "";
|
||||
this.data.iniClassName = "";
|
||||
|
||||
this.data.range = editable.ownerDocument.getSelection().getRangeAt(0);
|
||||
|
||||
this.$link = this._getOrCreateLink(link);
|
||||
this.data.iniClassName = this.$link.attr("class") || "";
|
||||
this.colorCombinationClass = '';
|
||||
|
||||
const allBtnClassSuffixes = /(^|\s+)btn(-[a-z0-9_-]*)?/gi;
|
||||
this.data.className = this.data.iniClassName.replace(allBtnClassSuffixes, ' ');
|
||||
const [encodedText, images] = wysiwygUtils.encodeNodeToText(this.$link[0]);
|
||||
this.data.content = encodedText;
|
||||
this.data.originalText = wysiwygUtils.decodeText(encodedText, images);
|
||||
this.data.images = images;
|
||||
this.data.oldAttributes = this.$link.getAttributes();
|
||||
this.data.url = this.$link.attr('href');
|
||||
this.data.isNewWindow = this.$link.attr('target') === '_blank';
|
||||
|
||||
const allBtnShapes = /\s*(rounded-circle|flat)\s*/gi;
|
||||
this.data.className = this.data.iniClassName
|
||||
.replace(allBtnClassSuffixes, ' ')
|
||||
.replace(allBtnShapes, ' ');
|
||||
// 'o_submit' class will force anchor to be handled as a button in linkdialog.
|
||||
if (/(?:s_website_form_send|o_submit)/.test(this.data.className)) {
|
||||
this.isButton = true;
|
||||
}
|
||||
init: function (parent, options, editable, data, $button, link) {
|
||||
options.wysiwyg.odooEditor.automaticStepSkipStack();
|
||||
link = link || this.getOrCreateLink(editable);
|
||||
this._super(parent, options, editable, data, $button, link);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$link.addClass('oe_edited_link');
|
||||
this.$button.addClass('active');
|
||||
|
||||
if (this.data.iniClassName) {
|
||||
const options = [
|
||||
'we-selection-items[name="link_style_color"] > we-button',
|
||||
'we-selection-items[name="link_style_size"] > we-button',
|
||||
'we-selection-items[name="link_style_shape"] > we-button',
|
||||
];
|
||||
for (const option of this.$(options.join(','))) {
|
||||
const $option = $(option);
|
||||
const value = $option.data('value');
|
||||
let active = false;
|
||||
if (value) {
|
||||
const subValues = value.split(',');
|
||||
let subActive = true;
|
||||
for (let subValue of subValues) {
|
||||
const classPrefix = new RegExp('(^|btn-| |btn-outline-)' + subValue);
|
||||
subActive = subActive && classPrefix.test(this.data.iniClassName);
|
||||
}
|
||||
active = subActive;
|
||||
} else {
|
||||
active = !this.data.iniClassName.includes('btn-');
|
||||
}
|
||||
this._setSelectOption($option, active);
|
||||
}
|
||||
}
|
||||
if (this.data.url) {
|
||||
var match = /mailto:(.+)/.exec(this.data.url);
|
||||
this.$('input[name="url"]').val(match ? match[1] : this.data.url);
|
||||
this._onURLInput();
|
||||
}
|
||||
if (this.data.isNewWindow) {
|
||||
this.$('we-button.o_we_checkbox_wrapper').toggleClass('active', true);
|
||||
}
|
||||
|
||||
this._updateOptionsUI();
|
||||
this._adaptLink(false);
|
||||
|
||||
this.$('input:visible:first').focus();
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
return this._super(...arguments);
|
||||
},
|
||||
destroy: function () {
|
||||
this.options.wysiwyg.odooEditor.automaticStepSkipStack();
|
||||
@@ -144,34 +52,16 @@ const LinkTools = Widget.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adapt the link to changes.
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_adaptLink: function (createStep = true) {
|
||||
_adaptPreview: function (createStep = true) {
|
||||
var data = this._getData();
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
const attrs = Object.assign({}, this.data.oldAttributes, {
|
||||
target: data.isNewWindow ? '_blank' : '',
|
||||
href: data.url,
|
||||
class: `${data.classes}`,
|
||||
});
|
||||
const $links = $('.oe_edited_link');
|
||||
$links.removeClass('oe_edited_link');
|
||||
this.$link.attr(attrs);
|
||||
if (data.content !== this.data.originalText || data.url !== this.data.url) {
|
||||
const content = (data.content && data.content.length) ? data.content : data.url;
|
||||
// If there is a this.data.originalText, it means that we selected
|
||||
// the text and we could not change the content through the text input.
|
||||
// .html() in needed in case we selected images.
|
||||
if (this.data.originalText) {
|
||||
this.$link.html(content);
|
||||
} else {
|
||||
this.$link.text(content);
|
||||
}
|
||||
}
|
||||
this.applyLinkToDom(data);
|
||||
if (createStep) {
|
||||
this.options.wysiwyg.odooEditor.historyStep();
|
||||
}
|
||||
@@ -179,81 +69,49 @@ const LinkTools = Widget.extend({
|
||||
$links.addClass('oe_edited_link');
|
||||
},
|
||||
/**
|
||||
* Get the link's data (url, content and styles).
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} {content: String, url: String, classes: String, isNewWindow: Boolean}
|
||||
* @override
|
||||
*/
|
||||
_getData: function () {
|
||||
var $url = this.$('input[name="url"]');
|
||||
var url = $url.val();
|
||||
var content = this.$('input[name="label"]').val() || url;
|
||||
content = wysiwygUtils.decodeText(content, this.data.images);
|
||||
|
||||
if (!this.isButton && $url.prop('required') && (!url || !$url[0].checkValidity())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = this.$('we-selection-items[name="link_style_color"] we-button.active').data('value') || '';
|
||||
const size = this.$('we-selection-items[name="link_style_size"] we-button.active').data('value') || '';
|
||||
const shape = this.$('we-selection-items[name="link_style_shape"] we-button.active').data('value') || '';
|
||||
const shapes = shape ? shape.split(',') : [];
|
||||
const style = ['outline', 'fill'].includes(shapes[0]) ? `${shapes[0]}-` : '';
|
||||
const shapeClasses = shapes.slice(style ? 1 : 0).join(' ');
|
||||
const classes = (this.data.className || '') +
|
||||
(type ? (` btn btn-${style}${type}`) : '') +
|
||||
(type && shapeClasses ? (` ${shapeClasses}`) : '') +
|
||||
(type && size ? (' btn-' + size) : '');
|
||||
var isNewWindow = this.$('we-checkbox[name="is_new_window"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');
|
||||
var doStripDomain = this.$('we-checkbox[name="do_strip_domain"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');
|
||||
if (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0 && !url.match(/^http[s]?/i)) {
|
||||
url = ('mailto:' + url);
|
||||
} else if (url.indexOf(location.origin) === 0 && doStripDomain) {
|
||||
url = url.slice(location.origin.length);
|
||||
}
|
||||
var allWhitespace = /\s+/gi;
|
||||
var allStartAndEndSpace = /^\s+|\s+$/gi;
|
||||
return {
|
||||
content: content,
|
||||
url: this._correctLink(url),
|
||||
classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '').replace(/oe_edited_link/, ''),
|
||||
oldAttributes: this.data.oldAttributes,
|
||||
isNewWindow: isNewWindow,
|
||||
doStripDomain: doStripDomain,
|
||||
};
|
||||
_doStripDomain: function () {
|
||||
return this.$('we-checkbox[name="do_strip_domain"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');
|
||||
},
|
||||
_getOrCreateLink: function (linkToEdit) {
|
||||
this.options.wysiwyg.odooEditor.automaticStepSkipStack();
|
||||
const doc = this.editable.ownerDocument;
|
||||
const range = getDeepRange(this.editable, {splitText: true, select: true, correctTripleClick: true});
|
||||
this.needLabel = false;
|
||||
let link = linkToEdit || getInSelection(doc, 'a');
|
||||
const $link = $(link);
|
||||
if (link && !linkToEdit && (!$link.has(range.startContainer).length || !$link.has(range.endContainer).length)) {
|
||||
// Expand the current link to include the whole selection.
|
||||
let before = link.previousSibling;
|
||||
while (before !== null && range.intersectsNode(before)) {
|
||||
link.insertBefore(before, link.firstChild);
|
||||
before = link.previousSibling;
|
||||
}
|
||||
let after = link.nextSibling;
|
||||
while (after !== null && range.intersectsNode(after)) {
|
||||
link.appendChild(after);
|
||||
after = link.nextSibling;
|
||||
}
|
||||
} else if (!link) {
|
||||
link = document.createElement('a');
|
||||
if (range.collapsed) {
|
||||
range.insertNode(link);
|
||||
this.needLabel = true;
|
||||
} else {
|
||||
link.appendChild(range.extractContents());
|
||||
range.insertNode(link);
|
||||
}
|
||||
}
|
||||
link.classList.add('oe_edited_link');
|
||||
return $(link);
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkOptions: function () {
|
||||
const options = [
|
||||
'we-selection-items[name="link_style_color"] > we-button',
|
||||
'we-selection-items[name="link_style_size"] > we-button',
|
||||
'we-selection-items[name="link_style_shape"] > we-button',
|
||||
];
|
||||
return this.$(options.join(','));
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkShape: function () {
|
||||
return this.$('we-selection-items[name="link_style_shape"] we-button.active').data('value') || '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkSize: function () {
|
||||
return this.$('we-selection-items[name="link_style_size"] we-button.active').data('value') || '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getLinkType: function () {
|
||||
return this.$('we-selection-items[name="link_style_color"] we-button.active').data('value') || '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_isNewWindow: function () {
|
||||
return this.$('we-checkbox[name="is_new_window"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_setSelectOption: function ($option, active) {
|
||||
$option.toggleClass('active', active);
|
||||
if (active) {
|
||||
@@ -263,7 +121,7 @@ const LinkTools = Widget.extend({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_updateOptionsUI: function () {
|
||||
const el = this.el.querySelector('[name="link_style_color"] we-button.active');
|
||||
@@ -273,35 +131,15 @@ const LinkTools = Widget.extend({
|
||||
this.$('.link-size-row, .link-shape-row').toggleClass('d-none', !this.colorCombinationClass);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_correctLink: function (url) {
|
||||
if (url.indexOf('mailto:') === 0 || url.indexOf('tel:') === 0) {
|
||||
url = url.replace(/^tel:([0-9]+)$/, 'tel://$1');
|
||||
} else if (url.indexOf('@') !== -1 && url.indexOf(':') === -1) {
|
||||
url = 'mailto:' + url;
|
||||
} else if (url.indexOf('://') === -1 && url[0] !== '/'
|
||||
&& url[0] !== '#' && url.slice(0, 2) !== '${') {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAnyChange: function () {
|
||||
this._adaptLink();
|
||||
},
|
||||
_onClickCheckbox: function (ev) {
|
||||
const $target = $(ev.target);
|
||||
$target.closest('we-button.o_we_checkbox_wrapper').toggleClass('active');
|
||||
this._adaptLink();
|
||||
this._adaptPreview();
|
||||
},
|
||||
_onPickSelectOption: function (ev) {
|
||||
const $target = $(ev.target);
|
||||
@@ -309,17 +147,7 @@ const LinkTools = Widget.extend({
|
||||
$select.find('we-selection-items we-button').toggleClass('active', false);
|
||||
this._setSelectOption($target, true);
|
||||
this._updateOptionsUI();
|
||||
this._adaptLink();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onURLInput: function () {
|
||||
var $linkUrlInput = this.$('#o_link_dialog_url_input');
|
||||
let value = $linkUrlInput.val();
|
||||
let isLink = value.indexOf('@') < 0;
|
||||
this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('d-none', !isLink);
|
||||
this.$('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0);
|
||||
this._adaptPreview();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -540,7 +540,7 @@ const Wysiwyg = Widget.extend({
|
||||
}
|
||||
if (forceOpen || !this.linkTools) {
|
||||
const $btn = this.toolbar.$el.find('#create-link');
|
||||
this.linkTools = new weWidgets.LinkTools(this, {wysiwyg: this}, this.odooEditor.editable, $btn, link);
|
||||
this.linkTools = new weWidgets.LinkTools(this, {wysiwyg: this}, this.odooEditor.editable, {}, $btn, link);
|
||||
this.linkTools.appendTo(this.toolbar.$el);
|
||||
} else {
|
||||
this.linkTools = undefined;
|
||||
@@ -548,44 +548,16 @@ const Wysiwyg = Widget.extend({
|
||||
} else {
|
||||
const linkDialog = new weWidgets.LinkDialog(this, {
|
||||
forceNewWindow: this.options.linkForceNewWindow,
|
||||
}, this.$editable[0], undefined, link);
|
||||
wysiwyg: this,
|
||||
}, this.$editable[0], {}, undefined, link);
|
||||
linkDialog.open();
|
||||
linkDialog.on('save', this, (linkInfo) => {
|
||||
const linkUrl = linkInfo.url;
|
||||
const linkContent = linkInfo.content;
|
||||
const isNewWindow = linkInfo.isNewWindow;
|
||||
const hasTextChanged = linkInfo.originalText !== linkContent;
|
||||
|
||||
const range = linkInfo.range;
|
||||
|
||||
const ancestorAnchor = $(range.startContainer).closest('a')[0];
|
||||
let anchors = [];
|
||||
if (ancestorAnchor && ancestorAnchor === $(range.endContainer).closest('a')[0]) {
|
||||
anchors.push($(ancestorAnchor).html(linkContent).get(0));
|
||||
} else if (hasTextChanged) {
|
||||
const anchor = $('<A>' + linkContent + '</A>')[0];
|
||||
range.insertNode(anchor);
|
||||
anchors.push(anchor);
|
||||
} else {
|
||||
const anchor = $('<a>')[0];
|
||||
anchor.appendChild(range.extractContents());
|
||||
range.insertNode(anchor);
|
||||
anchors.push(anchor);
|
||||
linkDialog.on('save', this, data => {
|
||||
const linkWidget = linkDialog.linkWidget;
|
||||
getDeepRange(this.$editable[0], {range: data.range, select: true});
|
||||
if (!linkWidget.$link.length) {
|
||||
linkWidget.$link = $(linkWidget.getOrCreateLink(this.$editable[0]));
|
||||
}
|
||||
linkDialog.insertedAnchors = [...anchors];
|
||||
for (const anchor of anchors) {
|
||||
$(anchor).attr('href', linkUrl);
|
||||
$(anchor).attr('class', linkInfo.className || null);
|
||||
$(anchor).css(linkInfo.style || {});
|
||||
if (isNewWindow) {
|
||||
$(anchor).attr('target', '_blank');
|
||||
} else {
|
||||
$(anchor).removeAttr('target');
|
||||
}
|
||||
range.selectNode(anchor);
|
||||
range.collapse();
|
||||
}
|
||||
getDeepRange(this.odooEditor.editable, {range, select: true});
|
||||
linkWidget.applyLinkToDom(data);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,41 +3,3 @@
|
||||
export function isImg(node) {
|
||||
return (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)(media_iframe_video|o_image|fa)(\s|$)/i))));
|
||||
}
|
||||
|
||||
function getIntermediateNodes(rootNode) {
|
||||
const nodes = [];
|
||||
for (const node of rootNode.childNodes) {
|
||||
nodes.push(node);
|
||||
nodes.push(...getIntermediateNodes(node));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function encodeNodeToText(rootNode) {
|
||||
const fake = document.createElement('fake');
|
||||
for (const node of [...rootNode.cloneNode(true).childNodes]) {
|
||||
fake.appendChild(node);
|
||||
}
|
||||
|
||||
const nodes = getIntermediateNodes(fake);
|
||||
|
||||
const images = [];
|
||||
for (const node of nodes) {
|
||||
if (isImg(node)) {
|
||||
node.before(document.createTextNode('[IMG]'));
|
||||
node.remove();
|
||||
images.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const text = fake.innerText.replace(/[ \t\r\n]+/g, ' ');
|
||||
return [text, images];
|
||||
}
|
||||
|
||||
export function decodeText(text, images = []) {
|
||||
text = _.escape(text);
|
||||
for (const img of images) {
|
||||
text = text.replace(/\[IMG\]/, img.outerHTML);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user