[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:
Andrea Ulliana
2019-01-09 16:16:01 +00:00
committed by qsm-odoo
parent 2ee21d9938
commit 59cc261448
9 changed files with 250 additions and 18 deletions
+2 -2
View File
@@ -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&amp;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);
},
});
+1 -1
View File
@@ -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');
},
});
});
+42 -12
View File
@@ -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>
+6
View File
@@ -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>