odoo.define('wysiwyg.widgets.LinkDialog', function (require) {
'use strict';
var core = require('web.core');
var Dialog = require('wysiwyg.widgets.Dialog');
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var _t = core._t;
/**
* Allows to customize link content and style.
*/
var LinkDialog = Dialog.extend({
template: 'wysiwyg.widgets.link',
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat([
'/web_editor/static/src/xml/wysiwyg.xml'
]),
events: _.extend({}, Dialog.prototype.events || {}, {
'input': '_onAnyChange',
'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, linkInfo) {
this.options = options || {};
this._super(parent, _.extend({
title: _t("Link to"),
}, this.options));
this.trigger_up('getRecordInfo', {
recordInfo: this.options,
callback: recordInfo => {
_.defaults(this.options, recordInfo);
},
});
this.data = linkInfo || {};
this.isButton = this.data.isButton;
// Using explicit type 'link' to preserve style when the target is .
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.data.className = "";
this.data.iniClassName = "";
var r = this.data.range;
this.needLabel = !r || (r.sc === r.ec && r.so === r.eo);
if (this.data.range) {
const $el = $(this.data.range.sc).filter(this.isButton ? "button" : "a");
this.data.iniClassName = $el.attr("class") || "";
this.colorCombinationClass = false;
let $node = $el;
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, ' ');
var is_link = this.data.range.isOnAnchor();
var sc = r.sc;
var so = r.so;
var ec = r.ec;
var eo = r.eo;
var nodes;
if (!is_link) {
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);
$(editable).data("range", this.data.range);
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 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.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);
},
//--------------------------------------------------------------------------
// 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 Promise.reject();
}
this.data.text = data.label;
this.data.url = data.url;
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, '');
if (data.classes.replace(allBtnTypes, ' ')) {
this.data.style = {
'background-color': '',
'color': '',
};
}
this.data.isNewWindow = data.isNewWindow;
this.final_data = this.data;
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Adapt the link preview to changes.
*
* @private
*/
_adaptPreview: function () {
var data = this._getData();
if (data === null) {
return;
}
const attrs = {
target: data.isNewWindow ? '_blank' : '',
href: data.url && data.url.length ? data.url : '#',
class: `${data.classes.replace(/float-\w+/, '')} o_btn_preview`,
};
this.$("#link-preview").attr(attrs).html((data.label && data.label.length) ? data.label : data.url);
},
/**
* Get the link's data (url, label and styles).
*
* @private
* @returns {Object} {label: String, url: String, classes: String, isNewWindow: Boolean}
*/
_getData: function () {
var $url = this.$('input[name="url"]');
var url = $url.val();
var label = _.escape(this.$('input[name="label"]').val() || url);
if (label && this.data.images) {
for (var i = 0; i < this.data.images.length; i++) {
label = label.replace(/\[IMG\]/, this.data.images[i].outerHTML);
}
}
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.$('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 {
label: label,
url: url,
classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, ''),
isNewWindow: isNewWindow,
};
},
/**
* @private
*/
_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
*/
_onTypeChange() {
this._updateOptionsUI();
},
/**
* @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');
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 LinkDialog;
});