[IMP] website, *: rework snippet anchor system
* web_editor Previously, the user could choose their anchor names using a modal window, anchor names also have weird rules, this was not a great user experience. This commit makes the anchor button generate anchors automatically based on the first title in a section, or the snippet name if there are none. If the anchor name is already taken, it is suffixed by a number. After it has been generated, it is also copied to the clipboard and the user is shown a toast to tell them the anchor has been generated and copied. The anchor is displayed in the toast along with an edit button that will open the same edition modal as previously, but it is now less restrictive. Part of https://github.com/odoo/odoo/pull/38959 maintask-2066614 task-2088270
This commit is contained in:
committed by
qsm-odoo
parent
bf5b4b6933
commit
f615cfda89
@@ -453,7 +453,7 @@ var SnippetEditor = Widget.extend({
|
||||
this,
|
||||
val.base_target ? this.$target.find(val.base_target).eq(0) : this.$target,
|
||||
this.$el,
|
||||
val.data,
|
||||
_.extend({snippetName: this._getName()}, val.data),
|
||||
this.options
|
||||
);
|
||||
var key = optionName || _.uniqueId('option');
|
||||
@@ -813,7 +813,7 @@ var SnippetsMenu = Widget.extend({
|
||||
if (!$target.closest('body > *').length) {
|
||||
return;
|
||||
}
|
||||
if ($target.closest('#web_editor-top-edit, #oe_snippets, #oe_manipulators, .o_technical_modal, .oe_drop_zone').length) {
|
||||
if ($target.closest('#web_editor-top-edit, #oe_snippets, #oe_manipulators, .o_technical_modal, .oe_drop_zone, .o_notification_manager').length) {
|
||||
return;
|
||||
}
|
||||
this._activateSnippet($target);
|
||||
|
||||
@@ -1547,13 +1547,31 @@ options.registry.topMenuColor = options.registry.colorpicker.extend({
|
||||
/**
|
||||
* Handles the edition of snippet's anchor name.
|
||||
*/
|
||||
options.registry.anchorName = options.Class.extend({
|
||||
options.registry.anchor = options.Class.extend({
|
||||
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
// Generate anchor and copy it to clipboard on click, show the tooltip on success
|
||||
this.$button = this.$el.find('we-button');
|
||||
const clipboard = new ClipboardJS(this.$button[0], {text: () => this._getAnchorLink()});
|
||||
clipboard.on('success', () => {
|
||||
const anchor = decodeURIComponent(this._getAnchorLink());
|
||||
this.displayNotification({
|
||||
title: _t("Copied !"),
|
||||
message: _.str.sprintf(_t("The anchor has been copied to your clipboard.<br>Link: %s"), anchor),
|
||||
buttons: [{text: _t("edit"), click: () => this.openAnchorDialog()}],
|
||||
});
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -1568,20 +1586,19 @@ options.registry.anchorName = options.Class.extend({
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
openAnchorDialog: function (previewMode, value, $opt) {
|
||||
var self = this;
|
||||
var buttons = [{
|
||||
text: _t("Save"),
|
||||
text: _t("Save & copy"),
|
||||
classes: 'btn-primary',
|
||||
click: function () {
|
||||
var $input = this.$('.o_input_anchor_name');
|
||||
var anchorName = $input.val().trim().replace(/\s/g, '_');
|
||||
var anchorName = self._text2Anchor($input.val());
|
||||
if (self.$target[0].id === anchorName) {
|
||||
// If the chosen anchor name is already the one used by the
|
||||
// element, close the dialog and do nothing else
|
||||
@@ -1589,15 +1606,13 @@ options.registry.anchorName = options.Class.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
var isValid = /^[\w-]+$/.test(anchorName);
|
||||
var alreadyExists = isValid && $('#' + anchorName).length > 0;
|
||||
var anchorOK = isValid && !alreadyExists;
|
||||
this.$('.o_anchor_not_valid').toggleClass('d-none', isValid);
|
||||
const alreadyExists = !!document.getElementById(anchorName);
|
||||
this.$('.o_anchor_already_exists').toggleClass('d-none', !alreadyExists);
|
||||
$input.toggleClass('is-invalid', !anchorOK);
|
||||
if (anchorOK) {
|
||||
$input.toggleClass('is-invalid', alreadyExists);
|
||||
if (!alreadyExists) {
|
||||
self._setAnchorName(anchorName);
|
||||
this.close();
|
||||
self.$button[0].click();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
@@ -1618,16 +1633,11 @@ options.registry.anchorName = options.Class.extend({
|
||||
new Dialog(this, {
|
||||
title: _t("Link Anchor"),
|
||||
$content: $(qweb.render('website.dialog.anchorName', {
|
||||
currentAnchor: this.$target.attr('id'),
|
||||
currentAnchor: decodeURIComponent(this.$target.attr('id')),
|
||||
})),
|
||||
buttons: buttons,
|
||||
}).open();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {String} value
|
||||
@@ -1643,6 +1653,35 @@ options.registry.anchorName = options.Class.extend({
|
||||
}
|
||||
this.$target.trigger('content_changed');
|
||||
},
|
||||
/**
|
||||
* Returns anchor text.
|
||||
*
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_getAnchorLink: function () {
|
||||
if (!this.$target[0].id) {
|
||||
const $titles = this.$target.find('h1, h2, h3, h4, h5, h6');
|
||||
const title = $titles.length > 0 ? $titles[0].innerText : this.data.snippetName;
|
||||
const anchorName = this._text2Anchor(title);
|
||||
let n = '';
|
||||
while (document.getElementById(anchorName + n)) {
|
||||
n = (n || 1) + 1;
|
||||
}
|
||||
this._setAnchorName(anchorName + n);
|
||||
}
|
||||
return `#${this.$target[0].id}`;
|
||||
},
|
||||
/**
|
||||
* Creates a safe id/anchor from text.
|
||||
*
|
||||
* @private
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
_text2Anchor: function (text) {
|
||||
return encodeURIComponent(text.trim().replace(/\s+/g, '-'));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -115,7 +115,6 @@
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control o_input_anchor_name" id="anchorName" t-attf-value="#{currentAnchor}" placeholder="Anchor name"/>
|
||||
<div class="invalid-feedback">
|
||||
<p class="d-none o_anchor_not_valid">The chosen name is not valid (use only a-Z A-Z 0-9 - _)</p>
|
||||
<p class="d-none o_anchor_already_exists">The chosen name already exists</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1352,13 +1352,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Anchor Name -->
|
||||
<div data-js="anchorName"
|
||||
<div data-js="anchor"
|
||||
data-selector=":not(p).oe_structure > *, :not(p)[data-oe-type=html] > *"
|
||||
data-exclude=".modal *">
|
||||
<we-button class="fa fa-fw fa-link"
|
||||
title="Create a link to target this section"
|
||||
data-no-preview="true"
|
||||
data-open-anchor-dialog=""/>
|
||||
data-no-preview="true"/>
|
||||
</div>
|
||||
|
||||
<!-- Mega Menu settings -->
|
||||
|
||||
Reference in New Issue
Block a user