4 var website = openerp.website;
5 website.add_template_file('/website/static/src/xml/website.seo.xml');
7 website.EditorBar.include({
8 events: _.extend({}, website.EditorBar.prototype.events, {
9 'click a[data-action=promote-current-page]': 'launchSeo',
11 launchSeo: function () {
12 (new website.seo.Configurator(this)).appendTo($(document.body));
18 function analyzeKeyword(htmlPage, keyword) {
19 return htmlPage.isInTitle(keyword) ? {
20 title: 'label label-primary',
21 description: "This keyword is used in the page title",
22 } : htmlPage.isInDescription(keyword) ? {
23 title: 'label label-info',
24 description: "This keyword is used in the page description",
25 } : htmlPage.isInBody(keyword) ? {
26 title: 'label label-info',
27 description: "This keyword is used in the page content."
29 title: 'label label-default',
30 description: "This keyword is not used anywhere on the page."
34 website.seo.Suggestion = openerp.Widget.extend({
35 template: 'website.seo_suggestion',
37 'click .js_seo_suggestion': 'select',
39 init: function (parent, options) {
40 this.root = options.root;
41 this.keyword = options.keyword;
42 this.htmlPage = options.page;
46 this.htmlPage.on('title-changed', this, this.renderElement);
47 this.htmlPage.on('description-changed', this, this.renderElement);
49 analyze: function () {
50 return analyzeKeyword(this.htmlPage, this.keyword);
52 highlight: function () {
53 return this.analyze().title;
55 tooltip: function () {
56 return this.analyze().description;
59 this.trigger('selected', this.keyword);
63 website.seo.SuggestionList = openerp.Widget.extend({
64 template: 'website.seo_suggestion_list',
65 init: function (parent, options) {
66 this.root = options.root;
67 this.htmlPage = options.page;
73 refresh: function () {
75 self.$el.append("Loading...");
76 function addSuggestions (list) {
78 // TODO Improve algorithm + Ajust based on custom user keywords
79 var regex = new RegExp(self.root, "gi");
80 var cleanList = _.map(list, function (word) {
81 return word.replace(regex, "").trim();
83 // TODO Order properly ?
84 _.each(_.uniq(cleanList), function (keyword) {
86 var suggestion = new website.seo.Suggestion(self, {
91 suggestion.on('selected', self, function (word) {
92 self.trigger('selected', word);
94 suggestion.appendTo(self.$el);
98 $.getJSON("http://suggest.hp.af.cm/suggest/"+encodeURIComponent(this.root + " "), addSuggestions);
102 website.seo.Keyword = openerp.Widget.extend({
103 template: 'website.seo_keyword',
105 'click a[data-action=remove-keyword]': 'destroy',
107 maxWordsPerKeyword: 4, // TODO Check
108 init: function (parent, options) {
109 this.keyword = options.word;
110 this.htmlPage = options.page;
114 this.htmlPage.on('title-changed', this, this.updateLabel);
115 this.htmlPage.on('description-changed', this, this.updateLabel);
116 this.suggestionList = new website.seo.SuggestionList(this, {
120 this.suggestionList.on('selected', this, function (word) {
121 this.trigger('selected', word);
123 this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion'));
125 analyze: function () {
126 return analyzeKeyword(this.htmlPage, this.keyword);
128 highlight: function () {
129 return this.analyze().title;
131 tooltip: function () {
132 return this.analyze().description;
134 updateLabel: function () {
135 var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight();
136 this.$(".js_seo_keyword").attr('class', cssClass);
137 this.$(".js_seo_keyword").attr('title', this.tooltip());
139 destroy: function () {
140 this.trigger('removed');
145 website.seo.KeywordList = openerp.Widget.extend({
146 template: 'website.seo_list',
148 init: function (parent, options) {
149 this.htmlPage = options.page;
154 var existingKeywords = self.htmlPage.keywords();
155 if (existingKeywords.length > 0) {
156 _.each(existingKeywords, function (word) {
157 self.add.call(self, word);
160 var companyName = self.htmlPage.company().toLowerCase();
161 if (companyName != 'yourcompany') {
162 self.add(companyName);
166 keywords: function () {
168 this.$('.js_seo_keyword').each(function () {
169 result.push($(this).data('keyword'));
173 isFull: function () {
174 return this.keywords().length >= this.maxKeywords;
176 exists: function (word) {
177 return _.contains(this.keywords(), word);
179 add: function (candidate) {
182 var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim().toLowerCase() : "";
183 if (word && !self.isFull() && !self.exists(word)) {
184 var keyword = new website.seo.Keyword(self, {
188 keyword.on('removed', self, function () {
189 self.trigger('list-not-full');
190 self.trigger('removed', word);
192 keyword.on('selected', self, function (word) {
193 self.trigger('selected', word);
195 keyword.appendTo(self.$el);
198 self.trigger('list-full');
203 website.seo.Image = openerp.Widget.extend({
204 template: 'website.seo_image',
205 init: function (parent, options) {
206 this.src = options.src;
207 this.alt = options.alt;
213 website.seo.ImageList = openerp.Widget.extend({
214 init: function (parent, options) {
215 this.htmlPage = options.page;
220 this.htmlPage.images().each(function (index, image) {
221 new website.seo.Image(self, image).appendTo(self.$el);
224 images: function () {
226 this.$('input').each(function () {
227 var $input = $(this);
229 src: $input.attr('src'),
235 add: function (image) {
236 new website.seo.Image(this, image).appendTo(this.$el);
240 website.seo.Preview = openerp.Widget.extend({
241 template: 'website.seo_preview',
242 init: function (parent, options) {
243 this.title = options.title;
244 this.url = options.url;
245 this.description = options.description || "[ The description will be generated by google unless you specify one ]";
250 website.seo.HtmlPage = openerp.Class.extend(openerp.PropertiesMixin, {
252 var url = window.location.href;
253 var hashIndex = url.indexOf('#');
254 return hashIndex >= 0 ? url.substring(0, hashIndex) : url;
257 var $title = $('title');
258 return ($title.length > 0) && $title.text() && $title.text().trim();
260 changeTitle: function (title) {
261 // TODO create tag if missing
262 $('title').text(title);
263 this.trigger('title-changed', title);
265 description: function () {
266 var $description = $('meta[name=description]');
267 return ($description.length > 0) && ($description.attr('content') && $description.attr('content').trim());
269 changeDescription: function (description) {
270 // TODO create tag if missing
271 $('meta[name=description]').attr('content', description);
272 this.trigger('description-changed', description);
274 keywords: function () {
275 var $keywords = $('meta[name=keywords]');
276 var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(",");
277 return (parsed && parsed[0]) ? parsed: [];
279 changeKeywords: function (keywords) {
280 // TODO create tag if missing
281 $('meta[name=keywords]').attr('content', keywords.join(","));
282 this.trigger('keywords-changed', keywords);
284 headers: function (tag) {
285 return $('#wrap '+tag).map(function () {
286 return $(this).text();
289 images: function () {
290 return $('#wrap img').map(function () {
293 src: $img.attr('src'),
294 alt: $img.attr('alt'),
298 company: function () {
299 return $('html').attr('data-oe-company-name');
301 bodyText: function () {
302 return $('body').children().not('.js_seo_configuration').text();
304 isInBody: function (text) {
305 return new RegExp("\\b"+text+"\\b", "gi").test(this.bodyText());
307 isInTitle: function (text) {
308 return new RegExp("\\b"+text+"\\b", "gi").test(this.title());
310 isInDescription: function (text) {
311 return new RegExp("\\b"+text+"\\b", "gi").test(this.description());
315 website.seo.Tip = openerp.Widget.extend({
316 template: 'website.seo_tip',
318 'closed.bs.alert': 'destroy',
320 init: function (parent, options) {
321 this.message = options.message;
322 // cf. http://getbootstrap.com/components/#alerts
323 // success, info, warning or danger
324 this.type = options.type || 'info';
329 website.seo.Configurator = openerp.Widget.extend({
330 template: 'website.seo_configuration',
332 'keyup input[name=seo_page_keywords]': 'confirmKeyword',
333 'keyup input[name=seo_page_title]': 'titleChanged',
334 'keyup textarea[name=seo_page_description]': 'descriptionChanged',
335 'click button[data-action=add]': 'addKeyword',
336 'click button[data-action=update]': 'update',
337 'hidden.bs.modal': 'destroy',
340 canEditDescription: false,
341 canEditKeywords: false,
343 maxDescriptionSize: 150,
346 var $modal = self.$el;
347 var htmlPage = this.htmlPage = new website.seo.HtmlPage();
348 $modal.find('.js_seo_page_url').text(htmlPage.url());
349 $modal.find('input[name=seo_page_title]').val(htmlPage.title());
350 $modal.find('textarea[name=seo_page_description]').val(htmlPage.description());
351 // self.suggestImprovements();
352 // self.imageList = new website.seo.ImageList(self, { page: htmlPage });
353 // if (htmlPage.images().length === 0) {
354 // $modal.find('.js_image_section').remove();
356 // self.imageList.appendTo($modal.find('.js_seo_image_list'));
358 self.keywordList = new website.seo.KeywordList(self, { page: htmlPage });
359 self.keywordList.on('list-full', self, function () {
360 $modal.find('input[name=seo_page_keywords]')
361 .attr('readonly', "readonly")
362 .attr('placeholder', "Remove a keyword first");
363 $modal.find('button[data-action=add]')
364 .prop('disabled', true).addClass('disabled');
366 self.keywordList.on('list-not-full', self, function () {
367 $modal.find('input[name=seo_page_keywords]')
368 .removeAttr('readonly').attr('placeholder', "");
369 $modal.find('button[data-action=add]')
370 .prop('disabled', false).removeClass('disabled');
372 self.keywordList.on('selected', self, function (word) {
373 self.keywordList.add(word);
375 self.keywordList.appendTo($modal.find('.js_seo_keywords_list'));
376 self.disableUnsavableFields();
377 self.renderPreview();
380 disableUnsavableFields: function () {
382 var $modal = self.$el;
383 self.loadMetaData().then(function (data) {
384 self.canEditTitle = data && ('website_meta_title' in data);
385 self.canEditDescription = data && ('website_meta_description' in data);
386 self.canEditKeywords = data && ('website_meta_keywords' in data);
387 if (!self.canEditTitle) {
388 $modal.find('input[name=seo_page_title]').attr('disabled', true);
390 if (!self.canEditDescription) {
391 $modal.find('textarea[name=seo_page_description]').attr('disabled', true);
393 if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) {
394 $modal.find('button[data-action=update]').attr('disabled', true);
398 suggestImprovements: function () {
401 function displayTip(message, type) {
402 new website.seo.Tip(self, {
405 }).appendTo(self.$('.js_seo_tips'));
407 var htmlPage = this.htmlPage;
409 // Add message suggestions at the top of the dialog
411 // if (htmlPage.headers('h1').length === 0) {
414 // message: "This page seems to be missing a title.",
418 if (tips.length > 0) {
419 _.each(tips, function (tip) {
420 displayTip(tip.message, tip.type);
424 confirmKeyword: function (e) {
425 if (e.keyCode == 13) {
429 addKeyword: function (word) {
430 var $input = this.$('input[name=seo_page_keywords]');
431 var keyword = _.isString(word) ? word : $input.val();
432 this.keywordList.add(keyword);
435 update: function () {
438 if (self.canEditTitle) {
439 data.website_meta_title = self.htmlPage.title();
441 if (self.canEditDescription) {
442 data.website_meta_description = self.htmlPage.description();
444 if (self.canEditKeywords) {
445 data.website_meta_keywords = self.keywordList.keywords().join(", ");
447 self.saveMetaData(data).then(function () {
448 self.$el.modal('hide');
451 getMainObject: function () {
452 var repr = $('html').data('main-object');
453 var m = repr.match(/.+\((.+), (\d+)\)/);
463 loadMetaData: function () {
465 var obj = this.getMainObject();
466 var def = $.Deferred();
468 // return $.Deferred().reject(new Error("No main_object was found."));
471 var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'];
472 var model = website.session.model(obj.model);
473 model.call('read', [[obj.id], fields, website.get_context()]).then(function (data) {
476 meta.model = obj.model;
481 }).fail(function () {
487 saveMetaData: function (data) {
488 var obj = this.getMainObject();
490 return $.Deferred().reject();
492 var model = website.session.model(obj.model);
493 return model.call('write', [[obj.id], data, website.get_context()]);
496 titleChanged: function () {
498 setTimeout(function () {
499 var title = self.$('input[name=seo_page_title]').val();
500 self.htmlPage.changeTitle(title);
501 self.renderPreview();
504 descriptionChanged: function () {
506 setTimeout(function () {
507 var description = self.$('textarea[name=seo_page_description]').attr('value');
508 self.htmlPage.changeDescription(description);
509 self.renderPreview();
512 renderPreview: function () {
513 var preview = new website.seo.Preview(this, {
514 title: this.htmlPage.title(),
515 description: this.htmlPage.description(),
516 url: this.htmlPage.url(),
518 var $preview = this.$('.js_seo_preview');
520 preview.appendTo($preview);
522 destroy: function () {
523 this.htmlPage.changeKeywords(this.keywordList.keywords());