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.
570 lines
19 KiB
JavaScript
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,
|
|
};
|
|
|
|
});
|