summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorAndrea Ulliana <aul@odoo.com>2019-01-03 08:40:45 +0000
committerqsm-odoo <qsm@odoo.com>2019-01-09 16:16:01 +0000
commit59cc261448b67c424051a623c5c8d76ddd20aa71 (patch)
treebd09682296a5193ab9e41a1f6d4750d76adc0aa3 /addons
parent2ee21d9938c14a47e33b36446a03f284cfa67ffe (diff)
[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
Diffstat (limited to 'addons')
-rw-r--r--addons/portal/views/portal_templates.xml4
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js9
-rw-r--r--addons/web_editor/static/src/xml/editor.xml2
-rw-r--r--addons/website/static/src/js/content/snippets.animation.js25
-rw-r--r--addons/website/static/src/js/editor/editor.js68
-rw-r--r--addons/website/static/src/js/editor/snippets.options.js76
-rw-r--r--addons/website/static/src/js/utils.js54
-rw-r--r--addons/website/static/src/xml/website.editor.xml24
-rw-r--r--addons/website/views/snippets.xml6
9 files changed, 250 insertions, 18 deletions
diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml
index 6827424deea..3d6a0e0f7e4 100644
--- a/addons/portal/views/portal_templates.xml
+++ b/addons/portal/views/portal_templates.xml
@@ -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>
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js
index 194b7e15d4d..9dad13a0b2b 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/widgets/widgets.js
@@ -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);
},
});
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index 5fcfac2f4aa..05e7d584337 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -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"/>
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index b3e5f2898f3..587cbca61d4 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -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,
diff --git a/addons/website/static/src/js/editor/editor.js b/addons/website/static/src/js/editor/editor.js
index ca26c09246d..12dc17daba6 100644
--- a/addons/website/static/src/js/editor/editor.js
+++ b/addons/website/static/src/js/editor/editor.js
@@ -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();
+ },
});
});
diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js
index 3d79951cf70..44ad7e92e11 100644
--- a/addons/website/static/src/js/editor/snippets.options.js
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -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');
+ },
+});
});
diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js
index 63c44f67d78..9cf511fe160 100644
--- a/addons/website/static/src/js/utils.js
+++ b/addons/website/static/src/js/utils.js
@@ -7,6 +7,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.
*
* @param {ServicesMixin|Widget} self - an element capable to trigger an RPC
@@ -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,
};
diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml
index 5018d28d1d9..a9ed7b66601 100644
--- a/addons/website/static/src/xml/website.editor.xml
+++ b/addons/website/static/src/xml/website.editor.xml
@@ -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>
diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml
index c9a76741fad..9595a51a572 100644
--- a/addons/website/views/snippets.xml
+++ b/addons/website/views/snippets.xml
@@ -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>