[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:
co-authored by
qsm-odoo
parent
5957cad422
commit
7c8323e816
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user