diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/js/menu/seo.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website/static/src/js/menu/seo.js')
| -rw-r--r-- | addons/website/static/src/js/menu/seo.js | 902 |
1 files changed, 902 insertions, 0 deletions
diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js new file mode 100644 index 00000000..b724bc1a --- /dev/null +++ b/addons/website/static/src/js/menu/seo.js @@ -0,0 +1,902 @@ +odoo.define('website.seo', function (require) { +'use strict'; + +var core = require('web.core'); +var Class = require('web.Class'); +var Dialog = require('web.Dialog'); +var mixins = require('web.mixins'); +var rpc = require('web.rpc'); +var Widget = require('web.Widget'); +var weWidgets = require('wysiwyg.widgets'); +var websiteNavbarData = require('website.navbar'); + +var _t = core._t; + +// This replaces \b, because accents(e.g. à, é) are not seen as word boundaries. +// Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b +var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)'; + +var Suggestion = Widget.extend({ + template: 'website.seo_suggestion', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click .o_seo_suggestion': 'select', + }, + + init: function (parent, options) { + this.keyword = options.keyword; + this._super(parent); + }, + select: function () { + this.trigger('selected', this.keyword); + }, +}); + +var SuggestionList = Widget.extend({ + template: 'website.seo_suggestion_list', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + + init: function (parent, options) { + this.root = options.root; + this.language = options.language; + this.htmlPage = options.htmlPage; + this._super(parent); + }, + start: function () { + this.refresh(); + }, + refresh: function () { + var self = this; + self.$el.append(_t("Loading...")); + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + var language = self.language || context.lang.toLowerCase(); + this._rpc({ + route: '/website/seo_suggest', + params: { + keywords: self.root, + lang: language, + }, + }).then(function (keyword_list) { + self.addSuggestions(JSON.parse(keyword_list)); + }); + }, + addSuggestions: function (keywords) { + var self = this; + self.$el.empty(); + // TODO Improve algorithm + Ajust based on custom user keywords + var regex = new RegExp(WORD_SEPARATORS_REGEX + self.root + WORD_SEPARATORS_REGEX, 'gi'); + keywords = _.map(_.uniq(keywords), function (word) { + return word.replace(regex, '').trim(); + }); + // TODO Order properly ? + _.each(keywords, function (keyword) { + if (keyword) { + var suggestion = new Suggestion(self, { + keyword: keyword, + }); + suggestion.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + suggestion.appendTo(self.$el); + } + }); + }, +}); + +var Keyword = Widget.extend({ + template: 'website.seo_keyword', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click a[data-action=remove-keyword]': 'destroy', + }, + + init: function (parent, options) { + this.keyword = options.word; + this.language = options.language; + this.htmlPage = options.htmlPage; + this.used_h1 = this.htmlPage.isInHeading1(this.keyword); + this.used_h2 = this.htmlPage.isInHeading2(this.keyword); + this.used_content = this.htmlPage.isInBody(this.keyword); + this._super(parent); + }, + start: function () { + var self = this; + this.$('.o_seo_keyword_suggestion').empty(); + this.suggestionList = new SuggestionList(this, { + root: this.keyword, + language: this.language, + htmlPage: this.htmlPage, + }); + this.suggestionList.on('selected', this, function (word, language) { + this.trigger('selected', word, language); + }); + return this.suggestionList.appendTo(this.$('.o_seo_keyword_suggestion')).then(function() { + self.htmlPage.on('title-changed', self, self._updateTitle); + self.htmlPage.on('description-changed', self, self._updateDescription); + self._updateTitle(); + self._updateDescription(); + }); + }, + destroy: function () { + this.trigger('removed'); + this._super(); + }, + _updateTitle: function () { + var $title = this.$('.js_seo_keyword_title'); + if (this.htmlPage.isInTitle(this.keyword)) { + $title.css('visibility', 'visible'); + } else { + $title.css('visibility', 'hidden'); + } + }, + _updateDescription: function () { + var $description = this.$('.js_seo_keyword_description'); + if (this.htmlPage.isInDescription(this.keyword)) { + $description.css('visibility', 'visible'); + } else { + $description.css('visibility', 'hidden'); + } + }, +}); + +var KeywordList = Widget.extend({ + template: 'website.seo_list', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + maxKeywords: 10, + + init: function (parent, options) { + this.htmlPage = options.htmlPage; + this._super(parent); + }, + start: function () { + var self = this; + var existingKeywords = self.htmlPage.keywords(); + if (existingKeywords.length > 0) { + _.each(existingKeywords, function (word) { + self.add.call(self, word); + }); + } + }, + keywords: function () { + var result = []; + this.$('.js_seo_keyword').each(function () { + result.push($(this).data('keyword')); + }); + return result; + }, + isFull: function () { + return this.keywords().length >= this.maxKeywords; + }, + exists: function (word) { + return _.contains(this.keywords(), word); + }, + add: async function (candidate, language) { + var self = this; + // TODO Refine + var word = candidate ? candidate.replace(/[,;.:<>]+/g, ' ').replace(/ +/g, ' ').trim().toLowerCase() : ''; + if (word && !self.isFull() && !self.exists(word)) { + var keyword = new Keyword(self, { + word: word, + language: language, + htmlPage: this.htmlPage, + }); + keyword.on('removed', self, function () { + self.trigger('list-not-full'); + self.trigger('content-updated', true); + }); + keyword.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + await keyword.appendTo(self.$el); + } + if (self.isFull()) { + self.trigger('list-full'); + } + self.trigger('content-updated'); + }, +}); + +var Preview = Widget.extend({ + template: 'website.seo_preview', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + + init: function (parent, options) { + this.title = options.title; + this.url = options.url; + this.description = options.description; + if (this.description.length > 160) { + this.description = this.description.substring(0, 159) + '…'; + } + this._super(parent); + }, +}); + +var HtmlPage = Class.extend(mixins.PropertiesMixin, { + init: function () { + mixins.PropertiesMixin.init.call(this); + this.initTitle = this.title(); + this.defaultTitle = $('meta[name="default_title"]').attr('content'); + this.initDescription = this.description(); + }, + url: function () { + return window.location.origin + window.location.pathname; + }, + title: function () { + return $('title').text().trim(); + }, + changeTitle: function (title) { + // TODO create tag if missing + $('title').text(title.trim() || this.defaultTitle); + this.trigger('title-changed', title); + }, + description: function () { + return ($('meta[name=description]').attr('content') || '').trim(); + }, + changeDescription: function (description) { + // TODO create tag if missing + $('meta[name=description]').attr('content', description); + this.trigger('description-changed', description); + }, + keywords: function () { + var $keywords = $('meta[name=keywords]'); + var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(','); + return (parsed && parsed[0]) ? parsed: []; + }, + changeKeywords: function (keywords) { + // TODO create tag if missing + $('meta[name=keywords]').attr('content', keywords.join(',')); + }, + headers: function (tag) { + return $('#wrap '+tag).map(function () { + return $(this).text(); + }); + }, + getOgMeta: function () { + var ogImageUrl = $('meta[property="og:image"]').attr('content'); + var title = $('meta[property="og:title"]').attr('content'); + var description = $('meta[property="og:description"]').attr('content'); + return { + ogImageUrl: ogImageUrl && ogImageUrl.replace(window.location.origin, ''), + metaTitle: title, + metaDescription: description, + }; + }, + images: function () { + return $('#wrap img').filter(function () { + return this.naturalHeight >= 200 && this.naturalWidth >= 200; + }).map(function () { + return { + src: this.getAttribute('src'), + alt: this.getAttribute('alt'), + }; + }); + }, + company: function () { + return $('html').attr('data-oe-company-name'); + }, + bodyText: function () { + return $('body').children().not('.oe_seo_configuration').text(); + }, + heading1: function () { + return $('body').children().not('.oe_seo_configuration').find('h1').text(); + }, + heading2: function () { + return $('body').children().not('.oe_seo_configuration').find('h2').text(); + }, + isInBody: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.bodyText()); + }, + isInTitle: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.title()); + }, + isInDescription: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.description()); + }, + isInHeading1: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.heading1()); + }, + isInHeading2: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.heading2()); + }, +}); + +var MetaTitleDescription = Widget.extend({ + // Form and preview for SEO meta title and meta description + // + // We only want to show an alert for "description too small" on those cases + // - at init and the description is not empty + // - we reached past the minimum and went back to it + // - focus out of the field + // Basically we don't want the too small alert when the field is empty and + // we start typing on it. + template: 'website.seo_meta_title_description', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'input input[name=website_meta_title]': '_titleChanged', + 'input input[name=website_seo_name]': '_seoNameChanged', + 'input textarea[name=website_meta_description]': '_descriptionOnInput', + 'change textarea[name=website_meta_description]': '_descriptionOnChange', + }, + maxRecommendedDescriptionSize: 300, + minRecommendedDescriptionSize: 50, + showDescriptionTooSmall: false, + + /** + * @override + */ + init: function (parent, options) { + this.htmlPage = options.htmlPage; + this.canEditTitle = !!options.canEditTitle; + this.canEditDescription = !!options.canEditDescription; + this.canEditUrl = !!options.canEditUrl; + this.isIndexed = !!options.isIndexed; + this.seoName = options.seoName; + this.seoNameDefault = options.seoNameDefault; + this.seoNameHelp = options.seoNameHelp; + this.previewDescription = options.previewDescription; + this._super(parent, options); + }, + /** + * @override + */ + start: function () { + this.$title = this.$('input[name=website_meta_title]'); + this.$seoName = this.$('input[name=website_seo_name]'); + this.$seoNamePre = this.$('span.seo_name_pre'); + this.$seoNamePost = this.$('span.seo_name_post'); + this.$description = this.$('textarea[name=website_meta_description]'); + this.$warning = this.$('div#website_meta_description_warning'); + this.$preview = this.$('.js_seo_preview'); + + if (!this.canEditTitle) { + this.$title.attr('disabled', true); + } + if (!this.canEditDescription) { + this.$description.attr('disabled', true); + } + if (this.htmlPage.title().trim() !== this.htmlPage.defaultTitle.trim()) { + this.$title.val(this.htmlPage.title()); + } + if (this.htmlPage.description().trim() !== this.previewDescription) { + this.$description.val(this.htmlPage.description()); + } + + if (this.canEditUrl) { + this.previousSeoName = this.seoName; + this.$seoName.val(this.seoName); + this.$seoName.attr('placeholder', this.seoNameDefault); + // make slug editable with input group for static text + const splitsUrl = window.location.pathname.split(this.previousSeoName || this.seoNameDefault); + this.$seoNamePre.text(splitsUrl[0]); + this.$seoNamePost.text(splitsUrl.slice(-1)[0]); // at least the -id theorically + } + this._descriptionOnChange(); + }, + /** + * Get the current title + */ + getTitle: function () { + return this.$title.val().trim() || this.htmlPage.defaultTitle; + }, + /** + * Get the potential new url with custom seoName as slug. + I can differ after save if slug JS != slug Python, but it provide an idea for the preview + */ + getUrl: function () { + const path = window.location.pathname.replace( + this.previousSeoName || this.seoNameDefault, + (this.$seoName.length && this.$seoName.val() ? this.$seoName.val().trim() : this.$seoName.attr('placeholder')) + ); + return window.location.origin + path + }, + /** + * Get the current description + */ + getDescription: function () { + return this.getRealDescription() || this.previewDescription; + }, + /** + * Get the current description chosen by the user + */ + getRealDescription: function () { + return this.$description.val() || ''; + }, + /** + * @private + */ + _titleChanged: function () { + var self = this; + self._renderPreview(); + self.trigger('title-changed'); + }, + /** + * @private + */ + _seoNameChanged: function () { + var self = this; + // don't use _, because we need to keep trailing whitespace during edition + const slugified = this.$seoName.val().toString().toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '-') // Remove all non-word chars + .replace(/\-\-+/g, '-'); // Replace multiple - with single - + this.$seoName.val(slugified); + self._renderPreview(); + }, + /** + * @private + */ + _descriptionOnChange: function () { + this.showDescriptionTooSmall = true; + this._descriptionOnInput(); + }, + /** + * @private + */ + _descriptionOnInput: function () { + var length = this.getDescription().length; + + if (length >= this.minRecommendedDescriptionSize) { + this.showDescriptionTooSmall = true; + } else if (length === 0) { + this.showDescriptionTooSmall = false; + } + + if (length > this.maxRecommendedDescriptionSize) { + this.$warning.text(_t('Your description looks too long.')).show(); + } else if (this.showDescriptionTooSmall && length < this.minRecommendedDescriptionSize) { + this.$warning.text(_t('Your description looks too short.')).show(); + } else { + this.$warning.hide(); + } + + this._renderPreview(); + this.trigger('description-changed'); + }, + /** + * @private + */ + _renderPreview: function () { + var indexed = this.isIndexed; + var preview = ""; + if (indexed) { + preview = new Preview(this, { + title: this.getTitle(), + description: this.getDescription(), + url: this.getUrl(), + }); + } else { + preview = new Preview(this, { + description: _t("You have hidden this page from search results. It won't be indexed by search engines."), + }); + } + this.$preview.empty(); + preview.appendTo(this.$preview); + }, +}); + +var MetaKeywords = Widget.extend({ + // Form and table for SEO meta keywords + template: 'website.seo_meta_keywords', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'keyup input[name=website_meta_keywords]': '_confirmKeyword', + 'click button[data-action=add]': '_addKeyword', + }, + + init: function (parent, options) { + this.htmlPage = options.htmlPage; + this._super(parent, options); + }, + start: function () { + var self = this; + this.$input = this.$('input[name=website_meta_keywords]'); + this.keywordList = new KeywordList(this, {htmlPage: this.htmlPage}); + this.keywordList.on('list-full', this, function () { + self.$input.attr({ + readonly: 'readonly', + placeholder: "Remove a keyword first" + }); + self.$('button[data-action=add]').prop('disabled', true).addClass('disabled'); + }); + this.keywordList.on('list-not-full', this, function () { + self.$input.removeAttr('readonly').attr('placeholder', ""); + self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled'); + }); + this.keywordList.on('selected', this, function (word, language) { + self.keywordList.add(word, language); + }); + this.keywordList.on('content-updated', this, function (removed) { + self._updateTable(removed); + }); + return this.keywordList.insertAfter(this.$('.table thead')).then(function() { + self._getLanguages(); + self._updateTable(); + }); + }, + _addKeyword: function () { + var $language = this.$('select[name=seo_page_language]'); + var keyword = this.$input.val(); + var language = $language.val().toLowerCase(); + this.keywordList.add(keyword, language); + this.$input.val('').focus(); + }, + _confirmKeyword: function (e) { + if (e.keyCode === 13) { + this._addKeyword(); + } + }, + _getLanguages: function () { + var self = this; + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + this._rpc({ + route: '/website/get_languages', + }).then(function (data) { + self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { + 'language': data, + 'def_lang': context.lang + })); + }); + }, + /* + * Show the table if there is at least one keyword. Hide it otherwise. + * + * @private + * @param {boolean} removed: a keyword is about to be removed, + * we need to exclude it from the count + */ + _updateTable: function (removed) { + var min = removed ? 1 : 0; + if (this.keywordList.keywords().length > min) { + this.$('table').show(); + } else { + this.$('table').hide(); + } + }, +}); + +var MetaImageSelector = Widget.extend({ + template: 'website.seo_meta_image_selector', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click .o_meta_img_upload': '_onClickUploadImg', + 'click .o_meta_img': '_onClickSelectImg', + }, + /** + * @override + * @param {widget} parent + * @param {Object} data + */ + init: function (parent, data) { + this.metaTitle = data.title || ''; + this.activeMetaImg = data.metaImg; + this.serverUrl = data.htmlpage.url(); + const imgField = data.hasSocialDefaultImage ? 'social_default_image' : 'logo'; + data.pageImages.unshift(_.str.sprintf('/web/image/website/%s/%s', odoo.session_info.website_id, imgField)); + this.images = _.uniq(data.pageImages); + this.customImgUrl = _.contains( + data.pageImages.map((img)=> new URL(img, window.location.origin).pathname), + new URL(data.metaImg, window.location.origin).pathname) + ? false : data.metaImg; + this.previewDescription = data.previewDescription; + this._setDescription(this.previewDescription); + this._super(parent); + }, + setTitle: function (title) { + this.metaTitle = title; + this._updateTemplateBody(); + }, + setDescription: function (description) { + this._setDescription(description); + this._updateTemplateBody(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Set the description, applying ellipsis if too long. + * + * @private + */ + _setDescription: function (description) { + this.metaDescription = description || this.previewDescription; + if (this.metaDescription.length > 160) { + this.metaDescription = this.metaDescription.substring(0, 159) + '…'; + } + }, + + /** + * Update template. + * + * @private + */ + _updateTemplateBody: function () { + this.$el.empty(); + this.images = _.uniq(this.images); + this.$el.append(core.qweb.render('website.og_image_body', {widget: this})); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a select image from list -> change the preview accordingly. + * + * @private + * @param {MouseEvent} ev + */ + _onClickSelectImg: function (ev) { + var $img = $(ev.currentTarget); + this.activeMetaImg = $img.find('img').attr('src'); + this._updateTemplateBody(); + }, + /** + * Open a mediaDialog to select/upload image. + * + * @private + * @param {MouseEvent} ev + */ + _onClickUploadImg: function (ev) { + var self = this; + var $image = $('<img/>'); + var mediaDialog = new weWidgets.MediaDialog(this, { + onlyImages: true, + res_model: 'ir.ui.view', + }, $image[0]); + mediaDialog.open(); + mediaDialog.on('save', this, function (image) { + self.activeMetaImg = image.src; + self.customImgUrl = image.src; + self._updateTemplateBody(); + }); + }, +}); + +var SeoConfigurator = Dialog.extend({ + template: 'website.seo_configuration', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.seo.xml'] + ), + canEditTitle: false, + canEditDescription: false, + canEditKeywords: false, + canEditLanguage: false, + canEditUrl: false, + + init: function (parent, options) { + options = options || {}; + _.defaults(options, { + title: _t('Optimize SEO'), + buttons: [ + {text: _t('Save'), classes: 'btn-primary', click: this.update}, + {text: _t('Discard'), close: true}, + ], + }); + + this._super(parent, options); + }, + start: function () { + var self = this; + + this.$modal.addClass('oe_seo_configuration'); + + this.htmlPage = new HtmlPage(); + + this.disableUnsavableFields().then(function () { + // Image selector + self.metaImageSelector = new MetaImageSelector(self, { + htmlpage: self.htmlPage, + hasSocialDefaultImage: self.hasSocialDefaultImage, + title: self.htmlPage.getOgMeta().metaTitle, + metaImg: self.metaImg || self.htmlPage.getOgMeta().ogImageUrl, + pageImages: _.pluck(self.htmlPage.images().get(), 'src'), + previewDescription: _t('The description will be generated by social media based on page content unless you specify one.'), + }); + self.metaImageSelector.appendTo(self.$('.js_seo_image')); + + // title and description + self.metaTitleDescription = new MetaTitleDescription(self, { + htmlPage: self.htmlPage, + canEditTitle: self.canEditTitle, + canEditDescription: self.canEditDescription, + canEditUrl: self.canEditUrl, + isIndexed: self.isIndexed, + previewDescription: _t('The description will be generated by search engines based on page content unless you specify one.'), + seoNameHelp: _t('This value will be escaped to be compliant with all major browsers and used in url. Keep it empty to use the default name of the record.'), + seoName: self.seoName, // 'my-custom-display-name' or '' + seoNameDefault: self.seoNameDefault, // 'display-name' + }); + self.metaTitleDescription.on('title-changed', self, self.titleChanged); + self.metaTitleDescription.on('description-changed', self, self.descriptionChanged); + self.metaTitleDescription.appendTo(self.$('.js_seo_meta_title_description')); + + // keywords + self.metaKeywords = new MetaKeywords(self, {htmlPage: self.htmlPage}); + self.metaKeywords.appendTo(self.$('.js_seo_meta_keywords')); + }); + }, + /* + * Reset meta tags to their initial value if not saved. + * + * @private + */ + destroy: function () { + if (!this.savedData) { + this.htmlPage.changeTitle(this.htmlPage.initTitle); + this.htmlPage.changeDescription(this.htmlPage.initDescription); + } + this._super.apply(this, arguments); + }, + disableUnsavableFields: function () { + var self = this; + return this.loadMetaData().then(function (data) { + // We only need a reload for COW when the copy is happening, therefore: + // - no reload if we are not editing a view (condition: website_id === undefined) + // - reload if generic page (condition: website_id === false) + self.reloadOnSave = data.website_id === undefined ? false : !data.website_id; + //If website.page, hide the google preview & tell user his page is currently unindexed + self.isIndexed = (data && ('website_indexed' in data)) ? data.website_indexed : true; + self.canEditTitle = data && ('website_meta_title' in data); + self.canEditDescription = data && ('website_meta_description' in data); + self.canEditKeywords = data && ('website_meta_keywords' in data); + self.metaImg = data.website_meta_og_img; + self.hasSocialDefaultImage = data.has_social_default_image; + self.canEditUrl = data && ('seo_name' in data); + self.seoName = self.canEditUrl && data.seo_name; + self.seoNameDefault = self.canEditUrl && data.seo_name_default; + if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) { + // disable the button to prevent an error if the current page doesn't use the mixin + // we make the check here instead of on the view because we don't need to check + // at every page load, just when the rare case someone clicks on this link + // TODO don't show the modal but just an alert in this case + self.$footer.find('button[data-action=update]').attr('disabled', true); + } + }); + }, + update: function () { + var self = this; + var data = {}; + if (this.canEditTitle) { + data.website_meta_title = this.metaTitleDescription.$title.val(); + } + if (this.canEditDescription) { + data.website_meta_description = this.metaTitleDescription.$description.val(); + } + if (this.canEditKeywords) { + data.website_meta_keywords = this.metaKeywords.keywordList.keywords().join(', '); + } + if (this.canEditUrl) { + if (this.metaTitleDescription.$seoName.val() != this.metaTitleDescription.previousSeoName) { + data.seo_name = this.metaTitleDescription.$seoName.val(); + self.reloadOnSave = true; // will force a refresh on old url and redirect to new slug + } + } + data.website_meta_og_img = this.metaImageSelector.activeMetaImg; + this.saveMetaData(data).then(function () { + // We want to reload if we are editing a generic page + // because it will become a specific page after this change (COW) + // and we want the user to be on the page he just created. + if (self.reloadOnSave) { + window.location.href = self.htmlPage.url(); + } else { + self.htmlPage.changeKeywords(self.metaKeywords.keywordList.keywords()); + self.savedData = true; + self.close(); + } + }); + }, + getMainObject: function () { + var mainObject; + this.trigger_up('main_object_request', { + callback: function (value) { + mainObject = value; + }, + }); + return mainObject; + }, + getSeoObject: function () { + var seoObject; + this.trigger_up('seo_object_request', { + callback: function (value) { + seoObject = value; + }, + }); + return seoObject; + }, + loadMetaData: function () { + var obj = this.getSeoObject() || this.getMainObject(); + return new Promise(function (resolve, reject) { + if (!obj) { + // return Promise.reject(new Error("No main_object was found.")); + resolve(null); + } else { + rpc.query({ + route: "/website/get_seo_data", + params: { + 'res_id': obj.id, + 'res_model': obj.model, + }, + }).then(function (data) { + var meta = data; + meta.model = obj.model; + resolve(meta); + }).guardedCatch(reject); + } + }); + }, + saveMetaData: function (data) { + var obj = this.getSeoObject() || this.getMainObject(); + if (!obj) { + return Promise.reject(); + } else { + return this._rpc({ + model: obj.model, + method: 'write', + args: [[obj.id], data], + }); + } + }, + titleChanged: function () { + var self = this; + _.defer(function () { + var title = self.metaTitleDescription.getTitle(); + self.htmlPage.changeTitle(title); + self.metaImageSelector.setTitle(title); + }); + }, + descriptionChanged: function () { + var self = this; + _.defer(function () { + var description = self.metaTitleDescription.getRealDescription(); + self.htmlPage.changeDescription(description); + self.metaImageSelector.setDescription(description); + }); + }, +}); + +var SeoMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { + 'promote-current-page': '_promoteCurrentPage', + }), + + init: function (parent, options) { + this._super(parent, options); + + if ($.deparam.querystring().enable_seo !== undefined) { + this._promoteCurrentPage(); + } + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Opens the SEO configurator dialog. + * + * @private + */ + _promoteCurrentPage: function () { + new SeoConfigurator(this).open(); + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(SeoMenu, '#promote-menu'); + +return { + SeoConfigurator: SeoConfigurator, + SeoMenu: SeoMenu, +}; +}); |
