Files
odoo_source/addons/website/static/src/js/website.seo.js
T
Géry Debongnie 2106e3dd2e [REF] web, *: simplify our RPC system (in JS)
The RPC system was not completely satisfactory, we decided to prepare
the future and do it properly.  This commit introduces the new rpc
system, which replace the previous new one.  We now simply have a
method, this._rpc, which takes a dictionary of parameters.  The idea is
that depending on the parameters, it is able to add correct default
value when necessary.

For situations where we don't have the this._rpc method, we can use the
rpc.query method, which takes the same arguments, but directly calls
ajax.rpc instead of triggering up some events.
2017-04-11 19:44:38 +02:00

570 lines
19 KiB
JavaScript

odoo.define('website.seo', function (require) {
'use strict';
var core = require('web.core');
var ajax = require('web.ajax');
var Class = require('web.Class');
var Dialog = require('web.Dialog');
var mixins = require('web.mixins');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var website = require('website.website');
var _t = core._t;
var qweb = core.qweb;
ajax.loadXML('/website/static/src/xml/website.seo.xml', qweb);
// 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]+|^|$)';
function analyzeKeyword(htmlPage, keyword) {
return htmlPage.isInTitle(keyword) ? {
title: 'label label-primary',
description: "This keyword is used in the page title",
} : htmlPage.isInDescription(keyword) ? {
title: 'label label-info',
description: "This keyword is used in the page description",
} : htmlPage.isInBody(keyword) ? {
title: 'label label-info',
description: "This keyword is used in the page content."
} : {
title: 'label label-default',
description: "This keyword is not used anywhere on the page."
};
}
var Suggestion = Widget.extend({
template: 'website.seo_suggestion',
events: {
'click .js_seo_suggestion': 'select',
},
init: function (parent, options) {
this.root = options.root;
this.keyword = options.keyword;
this.language = options.language;
this.htmlPage = options.page;
this._super(parent);
},
start: function () {
this.htmlPage.on('title-changed', this, this.renderElement);
this.htmlPage.on('description-changed', this, this.renderElement);
},
analyze: function () {
return analyzeKeyword(this.htmlPage, this.keyword);
},
highlight: function () {
return this.analyze().title;
},
tooltip: function () {
return this.analyze().description;
},
select: function () {
this.trigger('selected', this.keyword);
},
});
var SuggestionList = Widget.extend({
template: 'website.seo_suggestion_list',
init: function (parent, options) {
this.root = options.root;
this.language = options.language;
this.htmlPage = options.page;
this._super(parent);
},
start: function () {
this.refresh();
},
refresh: function () {
var self = this;
self.$el.append(_t("Loading..."));
var language = self.language || base.get_context().lang.toLowerCase();
ajax.jsonRpc('/website/seo_suggest', 'call', {
'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(self.root, "gi");
var 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, {
root: self.root,
language: self.language,
keyword: keyword,
page: self.htmlPage,
});
suggestion.on('selected', self, function (word, language) {
self.trigger('selected', word, language);
});
suggestion.appendTo(self.$el);
}
});
},
});
var Keyword = Widget.extend({
template: 'website.seo_keyword',
events: {
'click a[data-action=remove-keyword]': 'destroy',
},
maxWordsPerKeyword: 4, // TODO Check
init: function (parent, options) {
this.keyword = options.word;
this.language = options.language;
this.htmlPage = options.page;
this._super(parent);
},
start: function () {
this.htmlPage.on('title-changed', this, this.updateLabel);
this.htmlPage.on('description-changed', this, this.updateLabel);
this.suggestionList = new SuggestionList(this, {
root: this.keyword,
language: this.language,
page: this.htmlPage,
});
this.suggestionList.on('selected', this, function (word, language) {
this.trigger('selected', word, language);
});
this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion'));
},
analyze: function () {
return analyzeKeyword(this.htmlPage, this.keyword);
},
highlight: function () {
return this.analyze().title;
},
tooltip: function () {
return this.analyze().description;
},
updateLabel: function () {
var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight();
this.$(".js_seo_keyword").attr('class', cssClass);
this.$(".js_seo_keyword").attr('title', this.tooltip());
},
destroy: function () {
this.trigger('removed');
this._super();
},
});
var KeywordList = Widget.extend({
template: 'website.seo_list',
maxKeywords: 10,
init: function (parent, options) {
this.htmlPage = options.page;
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: 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,
page: this.htmlPage,
});
keyword.on('removed', self, function () {
self.trigger('list-not-full');
self.trigger('removed', word);
});
keyword.on('selected', self, function (word, language) {
self.trigger('selected', word, language);
});
keyword.appendTo(self.$el);
}
if (self.isFull()) {
self.trigger('list-full');
}
},
});
var Image = Widget.extend({
template: 'website.seo_image',
init: function (parent, options) {
this.src = options.src;
this.alt = options.alt;
this._super(parent);
},
});
var ImageList = Widget.extend({
init: function (parent, options) {
this.htmlPage = options.page;
this._super(parent);
},
start: function () {
var self = this;
this.htmlPage.images().each(function (index, image) {
new Image(self, image).appendTo(self.$el);
});
},
images: function () {
var result = [];
this.$('input').each(function () {
var $input = $(this);
result.push({
src: $input.attr('src'),
alt: $input.val(),
});
});
return result;
},
add: function (image) {
new Image(this, image).appendTo(this.$el);
},
});
var Preview = Widget.extend({
template: 'website.seo_preview',
init: function (parent, options) {
this.title = options.title;
this.url = options.url;
this.description = options.description || "[ The description will be generated by google unless you specify one ]";
this._super(parent);
},
});
var HtmlPage = Class.extend(mixins.PropertiesMixin, {
url: function () {
var url = window.location.href;
var hashIndex = url.indexOf('#');
return hashIndex >= 0 ? url.substring(0, hashIndex) : url;
},
title: function () {
var $title = $('title');
return ($title.length > 0) && $title.text() && $title.text().trim();
},
changeTitle: function (title) {
// TODO create tag if missing
$('title').text(title);
this.trigger('title-changed', title);
},
description: function () {
var $description = $('meta[name=description]');
return ($description.length > 0) && ($description.attr('content') && $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(","));
this.trigger('keywords-changed', keywords);
},
headers: function (tag) {
return $('#wrap '+tag).map(function () {
return $(this).text();
});
},
images: function () {
return $('#wrap img').map(function () {
var $img = $(this);
return {
src: $img.attr('src'),
alt: $img.attr('alt'),
};
});
},
company: function () {
return $('html').attr('data-oe-company-name');
},
bodyText: function () {
return $('body').children().not('.js_seo_configuration').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());
},
});
var Tip = Widget.extend({
template: 'website.seo_tip',
events: {
'closed.bs.alert': 'destroy',
},
init: function (parent, options) {
this.message = options.message;
// cf. http://getbootstrap.com/components/#alerts
// success, info, warning or danger
this.type = options.type || 'info';
this._super(parent);
},
});
var Configurator = Dialog.extend({
template: 'website.seo_configuration',
events: {
'keyup input[name=seo_page_keywords]': 'confirmKeyword',
'blur input[name=seo_page_title]': 'titleChanged',
'blur textarea[name=seo_page_description]': 'descriptionChanged',
'click button[data-action=add]': 'addKeyword',
},
canEditTitle: false,
canEditDescription: false,
canEditKeywords: false,
canEditLanguage: false,
maxTitleSize: 65,
maxDescriptionSize: 160, // TODO master: remove me and add warning
init: function (parent, options) {
options = options || {};
_.defaults(options, {
title: _t('Promote This Page'),
subtitle: _t('Get this page efficiently referenced in Google to attract more visitors.'),
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 js_seo_configuration');
this.htmlPage = new HtmlPage();
this.$('.js_seo_page_url').text(this.htmlPage.url());
this.$('input[name=seo_page_title]').val(this.htmlPage.title());
this.$('textarea[name=seo_page_description]').val(this.htmlPage.description());
this.keywordList = new KeywordList(self, { page: this.htmlPage });
this.keywordList.on('list-full', self, function () {
self.$('input[name=seo_page_keywords]').attr({
readonly: "readonly",
placeholder: "Remove a keyword first"
});
self.$('button[data-action=add]').prop('disabled', true).addClass('disabled');
});
this.keywordList.on('list-not-full', self, function () {
self.$('input[name=seo_page_keywords]').removeAttr('readonly').attr('placeholder', "");
self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled');
});
this.keywordList.on('selected', self, function (word, language) {
self.keywordList.add(word, language);
});
this.keywordList.appendTo(this.$('.js_seo_keywords_list'));
this.disableUnsavableFields();
this.renderPreview();
this.getLanguages();
},
getLanguages: function () {
var self = this;
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'get_languages',
args: [[base.get_context().website_id]],
kwargs: {context: base.get_context()}
}).then( function (data) {
self.$('#language-box').html(core.qweb.render('Configurator.language_promote', {
'language': data,
'def_lang': base.get_context().lang
}));
});
},
disableUnsavableFields: function () {
var self = this;
this.loadMetaData().then(function (data) {
self.canEditTitle = data && ('website_meta_title' in data);
self.canEditDescription = data && ('website_meta_description' in data);
self.canEditKeywords = data && ('website_meta_keywords' in data);
if (!self.canEditTitle) {
self.$('input[name=seo_page_title]').attr('disabled', true);
}
if (!self.canEditDescription) {
self.$('textarea[name=seo_page_description]').attr('disabled', true);
}
if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) {
self.$footer.find('button[data-action=update]').attr('disabled', true);
}
});
},
suggestImprovements: function () {
var self = this;
var tips = [];
_.each(tips, function (tip) {
displayTip(tip.message, tip.type);
});
function displayTip(message, type) {
new Tip(self, {
message: message,
type: type,
}).appendTo(self.$('.js_seo_tips'));
}
},
confirmKeyword: function (e) {
if (e.keyCode == 13) {
this.addKeyword();
}
},
addKeyword: function (word) {
var $input = this.$('input[name=seo_page_keywords]');
var $language = this.$('select[name=seo_page_language]');
var keyword = _.isString(word) ? word : $input.val();
var language = $language.val().toLowerCase();
this.keywordList.add(keyword, language);
$input.val("");
},
update: function () {
var self = this;
var data = {};
if (this.canEditTitle) {
data.website_meta_title = this.htmlPage.title();
}
if (this.canEditDescription) {
data.website_meta_description = this.htmlPage.description();
}
if (this.canEditKeywords) {
data.website_meta_keywords = this.keywordList.keywords().join(", ");
}
this.saveMetaData(data).then(function () {
self.close();
});
},
getMainObject: function () {
var repr = $('html').data('main-object');
var m = repr.match(/(.+)\((\d+),(.*)\)/);
if (!m) {
return null;
} else {
return {
model: m[1],
id: m[2]|0
};
}
},
loadMetaData: function () {
var obj = this.getMainObject();
var def = $.Deferred();
if (!obj) {
// return $.Deferred().reject(new Error("No main_object was found."));
def.resolve(null);
} else {
var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'];
this._rpc({
model: obj.model,
method: 'read',
args: [[obj.id], fields, base.get_context()],
})
.then(function (data) {
if (data.length) {
var meta = data[0];
meta.model = obj.model;
def.resolve(meta);
} else {
def.resolve(null);
}
}).fail(function () {
def.reject();
});
}
return def;
},
saveMetaData: function (data) {
var obj = this.getMainObject();
if (!obj) {
return $.Deferred().reject();
} else {
return this._rpc({
model: obj.model,
method: 'write',
args: [[obj.id], data, base.get_context()],
});
}
},
titleChanged: function () {
var self = this;
_.defer(function () {
var title = self.$('input[name=seo_page_title]').val();
self.htmlPage.changeTitle(title);
self.renderPreview();
});
},
descriptionChanged: function () {
var self = this;
_.defer(function () {
var description = self.$('textarea[name=seo_page_description]').val();
self.htmlPage.changeDescription(description);
self.renderPreview();
});
},
renderPreview: function () {
var preview = new Preview(this, {
title: this.htmlPage.title(),
description: this.htmlPage.description(),
url: this.htmlPage.url(),
});
var $preview = this.$('.js_seo_preview');
$preview.empty();
preview.appendTo($preview);
},
destroy: function () {
this.htmlPage.changeKeywords(this.keywordList.keywords());
this._super.apply(this, arguments);
},
});
website.TopBar.include({
start: function () {
this.$el.on('click', 'a[data-action=promote-current-page]', function () {
new Configurator(this).open();
});
return this._super();
}
});
return {
Configurator: Configurator,
};
});