[IMP] website, *: allow creating anchor links
* portal, web_editor
Anchor Link:
- Some visual improvements of the link dialog.
- New 'Page Anchor' field.
- System by default: top and bottom.
- Shows existing anchors.
Creation of the anchor:
- New option 'Anchor Name' to be able to create an anchor.
task-1913458
closes odoo/odoo#29150
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
|
||||
<t t-set="direction" t-value="request.env['res.lang'].search([('code', '=', request.env.lang)]).direction"/>
|
||||
<div id="wrapwrap" t-attf-class="#{pageName or ''} #{direction == 'rtl' and 'o_rtl' or ''} #{'o_portal' if is_portal else ''}">
|
||||
<header>
|
||||
<header id="top" data-anchor="true">
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a href="/" class="navbar-brand logo">
|
||||
@@ -55,7 +55,7 @@
|
||||
<main>
|
||||
<t t-raw="0"/>
|
||||
</main>
|
||||
<footer class="bg-light o_footer">
|
||||
<footer id="bottom" data-anchor="true" class="bg-light o_footer">
|
||||
<div class="container mt16 mb16" id="footer">
|
||||
<div class="float-right mb16" t-ignore="true" t-if="not editable">
|
||||
Powered by <a target="_blank" href="http://www.odoo.com?utm_source=db&utm_medium=portal">Odoo</a>
|
||||
|
||||
@@ -1428,6 +1428,8 @@ var LinkDialog = Dialog.extend({
|
||||
}
|
||||
|
||||
this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' ');
|
||||
|
||||
this._onURLInput = _.debounce(this._onURLInput, 400);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -1574,9 +1576,10 @@ var LinkDialog = Dialog.extend({
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onURLInput: function (ev) {
|
||||
$(ev.currentTarget).closest('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
|
||||
var isLink = $(ev.currentTarget).val().indexOf('@') < 0;
|
||||
_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');
|
||||
var isLink = $linkUrlInput.val().indexOf('@') < 0;
|
||||
this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('d-none', !isLink);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
<input type="text" name="label" class="form-control" id="o_link_dialog_label_input" required="required" t-att-value="widget.data.text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="form-group row o_url_input">
|
||||
<label class="col-form-label col-md-3" for="o_link_dialog_url_input">URL or Email</label>
|
||||
<div class="col-md-9">
|
||||
<input type="text" name="url" class="form-control" id="o_link_dialog_url_input" required="required"/>
|
||||
|
||||
@@ -1080,6 +1080,31 @@ registry._fixAppleCollapse = Animation.extend({
|
||||
},
|
||||
});
|
||||
|
||||
registry.anchorSlide = Animation.extend({
|
||||
selector: 'a[href^="/"][href*="#"], a[href^="#"]',
|
||||
read_events: {
|
||||
'click': '_onAnimateClick',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAnimateClick: function (ev) {
|
||||
var hash = ev.currentTarget.hash;
|
||||
var $anchor = $(hash);
|
||||
if ($anchor.length && $anchor.attr('data-anchor') && this.$target[0].pathname === window.location.pathname) {
|
||||
ev.preventDefault();
|
||||
$('html, body').animate({
|
||||
scrollTop: $anchor.offset().top,
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
Class: Animation,
|
||||
registry: registry,
|
||||
|
||||
@@ -5,6 +5,17 @@ var weWidgets = require('web_editor.widget');
|
||||
var wUtils = require('website.utils');
|
||||
|
||||
weWidgets.LinkDialog.include({
|
||||
xmlDependencies: (weWidgets.LinkDialog.prototype.xmlDependencies || []).concat(
|
||||
['/website/static/src/xml/website.editor.xml']
|
||||
),
|
||||
events: _.extend({}, weWidgets.LinkDialog.prototype.events || {}, {
|
||||
'change select[name="link_anchor"]': '_onAnchorChange',
|
||||
'input input[name="url"]': '_onURLInput',
|
||||
}),
|
||||
custom_events: _.extend({}, weWidgets.LinkDialog.prototype.custom_events || {}, {
|
||||
website_url_chosen: '_onAutocompleteClose',
|
||||
}),
|
||||
|
||||
/**
|
||||
* Allows the URL input to propose existing website pages.
|
||||
*
|
||||
@@ -14,5 +25,62 @@ weWidgets.LinkDialog.include({
|
||||
wUtils.autocompleteWithPages(this, this.$('input[name="url"]'));
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_adaptPageAnchor: function () {
|
||||
var urlInputValue = this.$('input[name="url"]').val();
|
||||
var $pageAnchor = this.$('.o_link_dialog_page_anchor');
|
||||
var isFromWebsite = urlInputValue[0] === '/';
|
||||
var $selectMenu = this.$('select[name="link_anchor"]');
|
||||
var $anchorsLoading = this.$('.o_anchors_loading');
|
||||
|
||||
$anchorsLoading.removeClass('d-none');
|
||||
$pageAnchor.toggleClass('d-none', !isFromWebsite);
|
||||
$selectMenu.empty();
|
||||
wUtils.loadAnchors(urlInputValue).then(function (anchors) {
|
||||
_.each(anchors, function (anchor) {
|
||||
$selectMenu.append($('<option>', {text: anchor}));
|
||||
});
|
||||
}).always(function () {
|
||||
$anchorsLoading.addClass('d-none');
|
||||
$selectMenu.prop("selectedIndex", -1);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAutocompleteClose: function () {
|
||||
this._onURLInput();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onAnchorChange: function () {
|
||||
var anchorValue = this.$('[name="link_anchor"]').val();
|
||||
var $urlInput = this.$('[name="url"]');
|
||||
var urlInputValue = $urlInput.val();
|
||||
if (urlInputValue.indexOf('#') > -1) {
|
||||
urlInputValue = urlInputValue.substr(0, urlInputValue.indexOf('#'));
|
||||
}
|
||||
$urlInput.val(urlInputValue + anchorValue);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onURLInput: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._adaptPageAnchor();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1430,4 +1430,80 @@ options.registry.topMenuColor = options.registry.colorpicker.extend({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the edition of snippet's anchor name.
|
||||
*/
|
||||
options.registry.anchorName = options.Class.extend({
|
||||
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
|
||||
preventChildPropagation: true,
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
onClone: function () {
|
||||
this.$target.removeAttr('id data-anchor');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
openAnchorDialog: function (previewMode, value, $opt) {
|
||||
var self = this;
|
||||
new Dialog(this, {
|
||||
title: _t("Anchor Name"),
|
||||
$content: $(qweb.render('website.dialog.anchorName', {
|
||||
currentAnchor: this.$target.attr('id'),
|
||||
})),
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Save"),
|
||||
classes: 'btn-primary',
|
||||
click: function () {
|
||||
var $input = this.$('.o_input_anchor_name');
|
||||
var anchorName = $input.val().trim().replace(/\s/g, '_');
|
||||
var alreadyExists = $('#' + anchorName).length > 0;
|
||||
$input.toggleClass('is-invalid', alreadyExists);
|
||||
if (!alreadyExists) {
|
||||
self._setAnchorName(anchorName);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text: _t("Discard"),
|
||||
close: true,
|
||||
},
|
||||
],
|
||||
}).open();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {String} value
|
||||
*/
|
||||
_setAnchorName: function (value) {
|
||||
if (value) {
|
||||
this.$target.attr({
|
||||
'id': value,
|
||||
'data-anchor': true,
|
||||
});
|
||||
} else {
|
||||
this.$target.removeAttr('id data-anchor');
|
||||
}
|
||||
this.$target.trigger('content_changed');
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,26 @@ var core = require('web.core');
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
/**
|
||||
* Allows to load anchors from a page.
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {Deferred<string[]>}
|
||||
*/
|
||||
function loadAnchors(url) {
|
||||
var def;
|
||||
if (url !== window.location.pathname && url[0] !== '#') {
|
||||
def = $.get(window.location.origin + url);
|
||||
} else {
|
||||
def = $.when(document.body.outerHTML);
|
||||
}
|
||||
return def.then(function (response) {
|
||||
return _.map($(response).find('[id][data-anchor=true]'), function (el) {
|
||||
return '#' + el.id;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the given input to propose existing website URLs.
|
||||
*
|
||||
@@ -15,19 +35,28 @@ var qweb = core.qweb;
|
||||
function autocompleteWithPages(self, $input) {
|
||||
$input.autocomplete({
|
||||
source: function (request, response) {
|
||||
return self._rpc({
|
||||
model: 'website',
|
||||
method: 'search_pages',
|
||||
args: [null, request.term],
|
||||
kwargs: {
|
||||
limit: 15,
|
||||
},
|
||||
}).then(function (exists) {
|
||||
var rs = _.map(exists, function (r) {
|
||||
return r.loc;
|
||||
if (request.term[0] === '#') {
|
||||
loadAnchors(request.term).then(function (anchors) {
|
||||
response(anchors);
|
||||
});
|
||||
response(rs);
|
||||
});
|
||||
} else {
|
||||
return self._rpc({
|
||||
model: 'website',
|
||||
method: 'search_pages',
|
||||
args: [null, request.term],
|
||||
kwargs: {
|
||||
limit: 15,
|
||||
},
|
||||
}).then(function (exists) {
|
||||
var rs = _.map(exists, function (r) {
|
||||
return r.loc;
|
||||
});
|
||||
response(rs.sort());
|
||||
});
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
self.trigger_up('website_url_chosen');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -137,6 +166,7 @@ function prompt(options, _qweb) {
|
||||
}
|
||||
|
||||
return {
|
||||
loadAnchors: loadAnchors,
|
||||
autocompleteWithPages: autocompleteWithPages,
|
||||
prompt: prompt,
|
||||
};
|
||||
|
||||
@@ -69,4 +69,28 @@
|
||||
<span t-attf-class="o_color_preview bg-#{color}"/>
|
||||
</button>
|
||||
</t>
|
||||
<t t-extend="web_editor.dialog.link">
|
||||
<t t-jquery="#o_link_dialog_url_input" t-operation="after">
|
||||
<small class="form-text">Hint: Type '/' to link to an existing page and '#' to an anchor.</small>
|
||||
</t>
|
||||
<t t-jquery="div.o_url_input" t-operation="after">
|
||||
<div class="form-group row o_link_dialog_page_anchor d-none">
|
||||
<label class="col-form-label col-md-3" for="o_link_dialog_anchor_input">Page Anchor</label>
|
||||
<div class="col-md-9">
|
||||
<select name="link_anchor" class="form-control link-style"></select>
|
||||
<small class="form-text font-weight-bold o_anchors_loading">Loading...</small>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Anchor Name option dialog -->
|
||||
<div t-name="website.dialog.anchorName">
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-3" for="anchorName">Choose an anchor name</label>
|
||||
<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">The chosen name already exists</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
@@ -1348,5 +1348,11 @@
|
||||
<div class="dropdown-menu"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anchor Name -->
|
||||
<div data-js="anchorName"
|
||||
data-selector=":not(p).oe_structure > *, :not(p)[data-oe-type=html] > *, .row > div">
|
||||
<a tabindex="-1" href="#" class="dropdown-item" data-no-preview="true" data-open-anchor-dialog=""><i class="fa fa-anchor"/>Anchor Name</a>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user