diff options
| author | Andrea Ulliana <aul@odoo.com> | 2019-01-03 08:40:45 +0000 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2019-01-09 16:16:01 +0000 |
| commit | 59cc261448b67c424051a623c5c8d76ddd20aa71 (patch) | |
| tree | bd09682296a5193ab9e41a1f6d4750d76adc0aa3 /addons | |
| parent | 2ee21d9938c14a47e33b36446a03f284cfa67ffe (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.xml | 4 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/widgets/widgets.js | 9 | ||||
| -rw-r--r-- | addons/web_editor/static/src/xml/editor.xml | 2 | ||||
| -rw-r--r-- | addons/website/static/src/js/content/snippets.animation.js | 25 | ||||
| -rw-r--r-- | addons/website/static/src/js/editor/editor.js | 68 | ||||
| -rw-r--r-- | addons/website/static/src/js/editor/snippets.options.js | 76 | ||||
| -rw-r--r-- | addons/website/static/src/js/utils.js | 54 | ||||
| -rw-r--r-- | addons/website/static/src/xml/website.editor.xml | 24 | ||||
| -rw-r--r-- | addons/website/views/snippets.xml | 6 |
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&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> |
