[FIX] web_editor, website: permit the user to set link on images

This restores a (basic) feature that was lost in 15.0 by mistake.

Allow the user to set a link on an image. The user can access this link
tool in the editor panel or via the CTRL+K shortcut. This tool is very
similar to the existing link tool for texts. This tool uses the
suggestion system already existing on the text link tool.

task-2666467

closes odoo/odoo#82512

X-original-commit: bfcd25c5d66765b7436114dba3316ac7331a4d97
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
Guillaume (gdi)
2022-01-11 14:27:00 +00:00
co-authored by qsm-odoo
parent 5957cad422
commit 7c8323e816
6 changed files with 305 additions and 1 deletions
+15
View File
@@ -1676,6 +1676,13 @@ msgstr ""
msgid "Readonly field"
msgstr ""
#. module: web_editor
#. openerp-web
#: code:addons/web_editor/static/src/xml/image_link_tools.xml:0
#, python-format
msgid "Redirect the user elsewhere when he clicks on the media."
msgstr ""
#. module: web_editor
#. openerp-web
#: code:addons/web_editor/static/src/xml/editor.xml:0
@@ -2768,6 +2775,7 @@ msgstr ""
#. module: web_editor
#. openerp-web
#: code:addons/web_editor/static/src/xml/image_link_tools.xml:0
#: code:addons/web_editor/static/src/xml/wysiwyg.xml:0
#, python-format
msgid "⌙ Open in new window"
@@ -2811,3 +2819,10 @@ msgstr ""
#: model_terms:ir.ui.view,arch_db:web_editor.snippet_options_background_options
msgid "⌙ Width"
msgstr ""
#. module: web_editor
#. openerp-web
#: code:addons/web_editor/static/src/xml/image_link_tools.xml:0
#, python-format
msgid "⌙ Your URL"
msgstr ""
@@ -338,6 +338,15 @@ const UserValueWidget = Widget.extend({
}
return null;
},
/**
* Focus the main focusable element of the widget.
*/
focus() {
const el = this._getFocusableElement();
if (el) {
el.focus();
}
},
/**
* Returns the value that the widget would hold if it was active, by default
* the internal value it holds.
@@ -575,13 +584,32 @@ const UserValueWidget = Widget.extend({
* @param {boolean} show
*/
toggleVisibility: function (show) {
let doFocus = false;
if (show) {
const wasInvisible = this.el.classList.contains('d-none');
doFocus = wasInvisible && this.el.dataset.requestFocus === "true";
}
this.el.classList.toggle('d-none', !show);
if (doFocus) {
this.focus();
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns the main focusable element of the widget. By default supposes
* nothing is focusable.
*
* @todo review all specific widget's method
* @private
* @returns {HTMLElement}
*/
_getFocusableElement: function () {
return null;
},
/**
* @private
* @param {OdooEvent|Event}
@@ -1203,6 +1231,17 @@ const InputUserValueWidget = UnitUserValueWidget.extend({
this.inputEl.value = this._value;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_getFocusableElement() {
return this.inputEl;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -4686,6 +4725,23 @@ registry.SnippetMove = SnippetOptionWidget.extend({
* Allows for media to be replaced.
*/
registry.ReplaceMedia = SnippetOptionWidget.extend({
xmlDependencies: ['/web_editor/static/src/xml/image_link_tools.xml'],
/**
* @override
*/
async start() {
core.bus.on('activate_image_link_tool', this, this._activateLinkTool);
return this._super(...arguments);
},
/**
* @override
*/
onFocus() {
// When we start editing an image, rerender the UI to ensure the
// we-select that suggests the anchors is in a consistent state.
this.rerender = true;
},
//--------------------------------------------------------------------------
// Options
@@ -4701,6 +4757,122 @@ registry.ReplaceMedia = SnippetOptionWidget.extend({
// to be refactored when the new editor is merged
this.$target.dblclick();
},
/**
* Makes the image a clickable link by wrapping it in an <a>.
* This function is also called for the opposite operation.
*
* @see this.selectClass for parameters
*/
setLink(previewMode, widgetValue, params) {
const parentEl = this.$target[0].parentNode;
if (parentEl.tagName !== 'A') {
const wrapperEl = document.createElement('a');
this.$target[0].after(wrapperEl);
wrapperEl.appendChild(this.$target[0]);
} else {
parentEl.replaceWith(this.$target[0]);
}
},
/**
* Changes the image link so that the URL is opened on another tab or not
* when it is clicked.
*
* @see this.selectClass for parameters
*/
setNewWindow(previewMode, widgetValue, params) {
const linkEl = this.$target[0].parentElement;
if (widgetValue) {
linkEl.setAttribute('target', '_blank');
} else {
linkEl.removeAttribute('target');
}
},
/**
* Records the target url of the hyperlink.
*
* @see this.selectClass for parameters
*/
setUrl(previewMode, widgetValue, params) {
const linkEl = this.$target[0].parentElement;
let url = widgetValue;
if (!url) {
// As long as there is no URL, the image is not considered a link.
linkEl.removeAttribute('href');
return;
}
if (!url.startsWith('/') && !url.startsWith('#')
&& !/^([a-zA-Z]*.):.+$/gm.test(url)) {
// We permit every protocol (http:, https:, ftp:, mailto:,...).
// If none is explicitly specified, we assume it is a http.
url = 'http://' + url;
}
linkEl.setAttribute('href', url);
this.rerender = true;
},
/**
* @override
*/
async updateUI() {
if (this.rerender) {
this.rerender = false;
await this._rerenderXML();
return;
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_activateLinkTool() {
if (this.$target[0].parentElement.tagName === 'A') {
this._requestUserValueWidgets('media_url_opt')[0].focus();
} else {
this._requestUserValueWidgets('media_link_opt')[0].enable();
}
},
/**
* @override
*/
_computeWidgetState(methodName, params) {
const parentEl = this.$target[0].parentElement;
const linkEl = parentEl.tagName === 'A' ? parentEl : null;
switch (methodName) {
case 'setLink': {
return linkEl ? 'true' : '';
}
case 'setUrl': {
let href = linkEl ? linkEl.getAttribute('href') : '';
return href || '';
}
case 'setNewWindow': {
const target = linkEl ? linkEl.getAttribute('target') : '';
return target && target === '_blank' ? 'true' : '';
}
}
return this._super(...arguments);
},
/**
* @override
*/
async _computeWidgetVisibility(widgetName, params) {
if (widgetName === 'media_link_opt') {
return !this.$target[0].classList.contains('media_iframe_video');
}
return this._super(...arguments);
},
/**
* @override
*/
async _renderCustomXML(uiFragment) {
const rowEl = uiFragment.querySelector('we-row');
rowEl.insertAdjacentHTML('beforeend', qweb.render('web_editor.media_link_tools_button'));
rowEl.insertAdjacentHTML('afterend', qweb.render('web.editor.media_link_tools_fields'));
},
});
/*
@@ -1304,9 +1304,16 @@ const Wysiwyg = Widget.extend({
* Handle custom keyboard shortcuts.
*/
_handleShortcuts: function (e) {
// Open the link modal / tool when CTRL+K is pressed.
// Open the link tool when CTRL+K is pressed.
if (e && e.key === 'k' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
const targetEl = this.odooEditor.document.getSelection().baseNode; // FIXME this is undefined on Firefox after clicking on an image and hitting CTRL-K
// Link tool is different if the selection is an image or a text.
if (targetEl instanceof HTMLElement
&& (targetEl.tagName === 'IMG' || targetEl.querySelectorAll('img').length === 1)) {
core.bus.trigger('activate_image_link_tool');
return;
}
this.toggleLinkTools();
}
// Override selectAll (CTRL+A) to restrict it to the editable zone / current snippet and prevent traceback.
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="web_editor.media_link_tools_button">
<we-button class="fa fa-link"
data-name="media_link_opt"
data-set-link="true" data-no-preview="true"
title="Redirect the user elsewhere when he clicks on the media."/>
</t>
<t t-name="web.editor.media_link_tools_fields">
<t t-set="url_input_label">⌙ Your URL</t>
<we-input t-att-string="url_input_label"
data-name="media_url_opt" data-dependencies="media_link_opt"
data-set-url="" data-no-preview="true"
class="o_we_large" placeholder="www.example.com"
data-request-focus="true"/>
<t t-set="new_window_checkbox_label">⌙ Open in new window</t>
<we-checkbox t-att-string="new_window_checkbox_label"
data-dependencies="media_url_opt"
data-set-new-window="true" data-no-preview="true"/>
</t>
</templates>
+2
View File
@@ -5006,6 +5006,7 @@ msgstr ""
#. module: website
#. openerp-web
#: code:addons/website/static/src/js/editor/snippets.options.js:0
#: code:addons/website/static/src/xml/website.editor.xml:0
#, python-format
msgid ""
@@ -11402,6 +11403,7 @@ msgstr ""
#. module: website
#. openerp-web
#: code:addons/website/static/src/js/editor/snippets.options.js:0
#: code:addons/website/static/src/xml/website.editor.xml:0
#, python-format
msgid "⌙ Page Anchor"
@@ -807,6 +807,90 @@ options.registry.BackgroundShape.include({
}
});
options.registry.ReplaceMedia.include({
/**
* Adds an anchor to the url.
* Here "anchor" means a specific section of a page.
*
* @see this.selectClass for parameters
*/
setAnchor(previewMode, widgetValue, params) {
const linkEl = this.$target[0].parentElement;
let url = linkEl.getAttribute('href');
url = url.split('#')[0];
linkEl.setAttribute('href', url + widgetValue);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_computeWidgetState(methodName, params) {
if (methodName === 'setAnchor') {
const parentEl = this.$target[0].parentElement;
if (parentEl.tagName === 'A') {
const href = parentEl.getAttribute('href') || '';
return href ? `#${href.split('#')[1]}` : '';
}
return '';
}
return this._super(...arguments);
},
/**
* @override
*/
async _computeWidgetVisibility(widgetName, params) {
if (widgetName === 'media_link_anchor_opt') {
const parentEl = this.$target[0].parentElement;
const linkEl = parentEl.tagName === 'A' ? parentEl : null;
const href = linkEl ? linkEl.getAttribute('href') : false;
return href && href.startsWith('/');
}
return this._super(...arguments);
},
/**
* Fills the dropdown with the available anchors for the page referenced in
* the href.
*
* @override
*/
async _renderCustomXML(uiFragment) {
await this._super(...arguments);
const oldURLWidgetEl = uiFragment.querySelector('[data-name="media_url_opt"]');
const URLWidgetEl = document.createElement('we-urlpicker');
// Copy attributes
for (const {name, value} of oldURLWidgetEl.attributes) {
URLWidgetEl.setAttribute(name, value);
}
URLWidgetEl.title = _t("Hint: Type '/' to search an existing page and '#' to link to an anchor.");
oldURLWidgetEl.replaceWith(URLWidgetEl);
const hrefValue = this.$target[0].parentElement.getAttribute('href');
if (!hrefValue || !hrefValue.startsWith('/')) {
return;
}
const urlWithoutAnchor = hrefValue.split('#')[0];
const selectEl = document.createElement('we-select');
selectEl.dataset.name = 'media_link_anchor_opt';
selectEl.dataset.dependencies = 'media_url_opt';
selectEl.dataset.noPreview = 'true';
selectEl.setAttribute('string', _t("⌙ Page Anchor"));
const anchors = await wUtils.loadAnchors(urlWithoutAnchor);
for (const anchor of anchors) {
const weButtonEl = document.createElement('we-button');
weButtonEl.dataset.setAnchor = anchor;
weButtonEl.textContent = anchor;
selectEl.append(weButtonEl);
}
URLWidgetEl.after(selectEl);
},
});
options.registry.BackgroundVideo = options.Class.extend({
//--------------------------------------------------------------------------