Commit 6e02fe26 authored by Andriy Khomych's avatar Andriy Khomych Committed by Robert Ragas

Issue #2755181 by Andriy Khomych, ozin: Update js with the latest version of...

Issue #2755181 by Andriy Khomych, ozin: Update js with the latest version of the YoastSEO.js for Drupal 8
parent 1d576dc4
score_to_status_rules:
0: na
3: bad
5: poor
7: ok
10: good
na:
equal: 0
bad:
min: 0
max: 4
ok:
min: 4
max: 7
good:
min: 7
max: 10
default: bad
This diff is collapsed.
.js-form-item-field-yoast-seo-0-yoast-seo-focus-keyword {
width: 554px;
}
.yoast-seo-score-widget {
margin: 20px 0px;
padding: 0px 0px 0px 20px;
}
.yoast-seo-score-widget ul {
list-style: none;
margin: 0;
padding: 0;
}
.yoast-seo-score-widget ul li {
display: block;
}
.yoast-seo-score-widget ul li label {
font-weight: normal;
display: inline-block;
width: 115px;
}
.yoast-seo-score-widget ul li a.help {
position: relative;
margin: 0px 20px 0px 0px;
}
.yoast-seo-score-widget ul li a.help:after {
position: absolute;
top: 0px;
right: -20px;
content: "";
display: block;
width: 16px;
height: 16px;
background: transparent url("../misc/icons/help.png") repeat scroll 0% 0%;
}
.views-field-field-yoast-seo .overallScore {
margin: 0;
padding: 0;
}
.overallScore {
display: inline-block;
}
.overallScore .score {
display: inline-block;
padding: 0.1em 0 0 0;
}
.overallScore .score_circle {
display: inline-block;
width: 12px;
height: 12px;
margin: 2px 5px 0 3px;
vertical-align: top;
border-radius: 50%;
background: #888;
}
.overallScore.good .score_circle {
background: #7ad03a;
}
.overallScore.ok .score_circle {
background: #ffba00;
}
.overallScore.poor .score_circle {
background: #ee7c1b;
}
.overallScore.bad .score_circle {
background: #dd3d36;
}
.overallScore .score_title {
font-weight: normal;
}
.overallScore .score_title strong {
font-weight: bold;
}
\ No newline at end of file
var YoastSeo = YoastSeo || {};
YoastSeo.form = YoastSeo.form || {};
/**
* @file
* Drupal Yoast SEO form snippet element Backbone view.
*
* This widget as form aim to handle snippet preview field which are content
* editable element.
*
* @ignore
*/
(function ($, Drupal) {
'use strict';
/**
* FormItem view that has for aim to control snippet element which are content editable form item.
*
* @type {YoastSeo.form.SnippetElement}
*/
YoastSeo.form.SnippetElement = Drupal.BackboneForm.views.ContentEditableHtmlElement.extend({
/**
* {@inheritdoc}
*/
events: {
'focus': '_onFocus',
'blur': '_onBlur',
'keyup': '_onKeyup',
'keypress': '_onKeypress',
'paste': '_onPaste'
},
/**
* {@inheritdoc}
*/
_onKeypress: function (evt) {
// The user can't press enter on the snippet fields.
if (evt.keyCode == 13) {
evt.preventDefault();
evt.stopImmediatePropagation();
return;
}
}
}, {
// Can be any editable HTMLElement.
tag: 'span'
});
})(jQuery, Drupal);
var YoastSeo = YoastSeo || {};
YoastSeo.model = YoastSeo.model || {};
/**
* @file
* Drupal Yoast SEO analyser model class.
*
* @ignore
*/
(function ($, Drupal) {
"use strict";
YoastSeo.model.Status = Backbone.Model.extend({}, {
/**
* Returns a string that is used as a CSS class, based on the numeric score.
*
* @param score
* @returns output
*/
scoreRating: function (score) {
var rules = YoastSeo.model.Status.score_status,
def = rules['default'];
delete rules['default'];
for (var i in rules) {
if (score <= parseInt(i)) {
return rules[i];
}
}
return def;
}
});
})(jQuery, Drupal);
var YoastSeo = YoastSeo || {};
/**
* @file
* Drupal Yoast SEO focus keyword field handler.
*
* @ignore
*/
(function ($, Drupal) {
'use strict';
/**
* This component takes care of handling the focus keyword field.
*
*
* @type {YoastSeo.FocusKeyword}
*/
YoastSeo.FocusKeyword = Backbone.View.extend({
/**
* {@inheritdoc}
*/
initialize: function (options) {
this.options = options || {};
var language = options.language || null,
self = this;
// Autocomplete for focus keyword field.
// We use the google autocomplete api.
$(this.el).autocomplete({
source: function(request, response) {
$.getJSON("http://suggestqueries.google.com/complete/search?callback=?", {
hl: language,
q: request.term,
client: "youtube"
}).done(function(data) {
var suggestions = [];
$.each(data[1], function(key, val) {
suggestions.push({"value":val[0]});
});
suggestions.length = 5;
response(suggestions);
});
},
// When an item is selected, as no change event is triggered, do it manually.
close: function() {
var formItemView = Drupal.BackboneForm._formItemViews[self.$el.attr('id')];
formItemView._change();
}
});
}
}, {});
})(jQuery, Drupal);
var YoastSeo = YoastSeo || {};
/**
* @file
* Drupal Yoast SEO.
*
* @ignore
*/
(function ($, Drupal) {
'use strict';
/**
* This component takes care of displaying the Yoast SEO score computed for
* a content.
*
* @type {YoastSeo.Status}
*/
YoastSeo.Status = Backbone.View.extend({
/**
* {@inheritdoc}
*/
initialize: function (options) {
var options = options || {};
this.options = options;
// Initialize the tooltips.
$('#yoast-overall-score a.help').tooltip();
},
/**
* Sets the SEO score in both the hidden input and the rating element.
*
* @param score
*/
setScore: function (score) {
this.score = score;
var rate = YoastSeo.model.Status.scoreRating(score),
yoast_settings = drupalSettings.yoast_seo;
// Update score text in the score box.
$('.score_value', this.$el).text(rate);
// Update score in the score field.
$('[data-drupal-selector="' + yoast_settings.fields.seo_status + '"]')
.attr('value', score)
.val(score);
}
}, {});
})(jQuery, Drupal);
var YoastSeo = YoastSeo || {};
/**
* @file
* Drupal Yoast SEO analyser model class.
*
* @ignore
*/
(function ($, Drupal) {
"use strict";
YoastSeo.Analyser = Backbone.Model.extend({
/**
* The instance of the Yoast analyser.
*/
yoast_analyser: null,
/**
* The model default options.
*/
default_options: {
analyser: {
snippetPreview: null,
elementTarget: [],
typeDelay: 300,
typeDelayStep: 100,
maxTypeDelay: 1500,
dynamicDelay: true,
multiKeyword: false,
snippetFields: {
title: "snippet_title",
url: "snippet_cite",
meta: "snippet_meta"
},
targets: {
output: null,
overall: null,
snippet: null
},
sampleText: {
url: '',
title: '',
keyword: '',
meta: '',
text: ''
}
},
baseRoot: '/'
},
// Analyser constructor.
initialize: function (attributes, options) {
this.options = $.extend(true, {}, this.default_options, options);
// Declaring the callback functions required by the Yoast SEO analyser.
this.options.analyser.callbacks = {
getData: this.getData.bind(this),
getAnalyzerInput: this.getAnalyzerInput.bind(this),
bindElementEvents: this.bindElementEvents.bind(this),
updateSnippetValues: this.updateSnippetValues.bind(this),
saveScores: this.saveScores.bind(this)
};
// Make it global.
this.yoast_analyser = new YoastSEO.App(this.options.analyser);
},
/**
* Destroy the analyser
*/
destroy: function() {
delete this.yoast_analyser;
},
/**
* Return an object fulfilling the Yoast SEO library getData callback requirements.
*
* @callback YoastSEO.App~getData
*
* @returns {Object} data
* @returns {String} data.keyword The keyword that should be used
* @returns {String} data.meta
* @returns {String} data.text The text to analyze
* @returns {String} data.pageTitle The text in the HTML title tag
* @returns {String} data.title The title to analyze
* @returns {String} data.url The URL for the given page
* @returns {String} data.excerpt Excerpt for the pages
*/
getData: function () {
var data = {
keyword: '',
meta: '',
text: '',
pageTitle: '',
title: '',
url: '',
excerpt: '',
snippetMeta: '',
snippetCite: '',
snippetTitle: '',
baseUrl: ''
};
return data;
},
/**
* @callback YoastSEO.App~getAnalyzerInput
*/
getAnalyzerInput: function () {
// If needed implement your logic in an inherited class.
},
/**
* Calls the eventbinders.
* We don't need it.
*
* @callback YoastSEO.App~bindElementEvents
*/
bindElementEvents: function () {
// If needed implement your logic in an inherited class.
},
/**
* Updates the snippet values.
* We don't need it.
*
* @callback YoastSEO.App~updateSnippetValues
*
* @param {Object} ev
*/
updateSnippetValues: function () {
// If needed implement your logic in an inherited class.
},
/**
* Score has been calculated callback.
*
* @callback YoastSEO.App~saveScores
*/
saveScores: function (score) {
if (this.options.callback.saveScores != null) {
this.options.callback.saveScores(score);
}
}
});
})(jQuery, Drupal);
var YoastSeo = YoastSeo || {};
/**
* @file
* Drupal Yoast SEO analyser model class for the node edit page.
*
* @ignore
*/
(function ($, Drupal) {
"use strict";
YoastSeo.AnalyserEditNode = YoastSeo.Analyser.extend({
/**
* Map the field of the node edit page with the attributes of the array
* returned by the getData callback function.
*/
fieldsMapping: {
meta: 'meta_description',
text: 'body',
pageTitle: 'meta_title',
title: 'title',
url: 'path',
snippetCite: 'path',
snippetMeta: 'meta_description',
snippetTitle: 'meta_title',
keyword: 'focus_keyword'
},
/**
* Tokens already resolved remotely.
*/
tokensRemote: {},
/**
* Extract the form node edit page fields values.
* Resolve tokens if there are and they can be solved, either locally or remotely.
*
* @param data
* @returns {*}
*/
extractFieldsValues: function(data) {
// For all data required by the Yoast SEO snippet.
// If their is a field extract the data from the fields if these fields
// have been mapped.
for (var fieldName in data) {
var formItemView = Drupal.BackboneForm._formItemViews[this.options.fields[this.fieldsMapping[fieldName]]];
if (typeof formItemView !== 'undefined') {
var fieldValue = formItemView.value();
// If the field hasn't been filled already.
// Use the default value if provided.
if (fieldValue == '') {
if (typeof this.options.default_text[this.fieldsMapping[fieldName]] !== 'undefined'
&& this.options.default_text[this.fieldsMapping[fieldName]] != '') {
data[fieldName] = this.tokenReplace(this.options.default_text[this.fieldsMapping[fieldName]]);
}
}
// If the field has been filled.
// Extract the value from the field and replace the tokens if any by their values.
else {
data[fieldName] = this.tokenReplace(formItemView.value());
}
}
// If the data is empty and a place holder has been defined, use the placeholder as value.
if ((typeof this.options.placeholder_text[fieldName] !== 'undefined'
&& this.options.placeholder_text[fieldName] != '') && data[fieldName] == '') {
data[fieldName] = this.options.placeholder_text[fieldName];
}
}
return data;
},
/**
* {@inheritdoc}
*/
getData: function () {
var data = {
keyword: '',
meta: '',
text: '',
pageTitle: '',
title: '',
url: '',
excerpt: '',
snippetMeta: '',
snippetCite: '',
snippetTitle: '',
baseUrl: this.options.base_root
};
// Extract form values regarding the required getData fields.
data = this.extractFieldsValues(data);
// The get data function can be called before the yoast analyser
// has been instantiated (while instantiating this class by instance).
if (this.yoast_analyser != null) {
this.yoast_analyser.rawData = data;
}
return data;
},
/**
* Replace tokens contained in a string by their values.
* The token can be solved following two ways :
* * Either on the page, if the token is relative to a page field ;
* * Or remotely, by requesting the server ;
*
* @param value The string to process.
* @returns {string}
* @todo Can be moved in an util library.
*/
tokenReplace: function (value) {
var self = this,
tokenRegex = /(\[[^\]]*:[^\]]*\])/g,
match = value.match(tokenRegex),
tokensNotFound = [];
// If the value contains tokens.
if (match != null) {
// Replace all the tokens by their relative value.
for (var i in match) {
var tokenRelativeField = null,
tokenRawValue = false;
// Check if the token is relative to a field present on the page.
if (typeof this.options.tokens[match[i]] != 'undefined') {
var fieldName = this.options.tokens[match[i]],
isRelativeField = this.options.fields[fieldName] != undefined;
// If no field exist with the same token value, we consider it's a raw value.
if (!isRelativeField) {
tokenRawValue = true;
}
// Else, we know it's related to a field content.
else {
tokenRelativeField = this.options.tokens[match[i]];
}
}
// If the token can be solved locally.
if (tokenRelativeField != null) {
// Replace the token with the relative field token value.
var formItemView = Drupal.BackboneForm._formItemViews[this.options.fields[tokenRelativeField]];
if (typeof formItemView !== 'undefined') {
var tokenValue = this.tokenReplace(formItemView.value());
value = value.replace(match[i], tokenValue);
}
}
else if (tokenRawValue == true) {
value = value.replace(match[i], this.options.tokens[match[i]]);
}
// The token value has to be found remotely.
else {
// If the token value has already been resolved and stored locally.
if (typeof this.tokensRemote[match[i]] != 'undefined') {
value = value.replace(match[i], this.tokensRemote[match[i]]);
}
else {
tokensNotFound.push(match[i]);
}
}
}
// If some tokens hasn't been resolved locally.
// Try to solve them remotely.
if (tokensNotFound.length) {
jQuery.ajax({
async: false,
url: Drupal.url('yoast_seo/tokens'),
type: 'POST',
data: {'tokens[]': tokensNotFound},
dataType: 'json'
}).then(function (data) {
// Store their value locally.
// It will avoid an unnecessary call to the server.
for (var token in data) {
self.tokensRemote[token] = data[token];
value = value.replace(token, self.tokensRemote[token]);
}
});
}
}
return value;
},
/**
* refresh the snippet.
*/
refreshSnippet: function () {
this.yoast_analyser.reloadSnippetText();
},
/**
* Refresh the anlysis
*/
refreshAnalysis: function () {
//this.yoast_analyser.refresh(); // If the external Yoast lib is switched to 1.0.4
this.yoast_analyser.runAnalyzerCallback();
},
/**
* Save Meta title and description in a cookies.
* This is for reusing in the preview page.
*/
saveCookie: function() {
var data = this.getData(),
dataToStore = {
pageTitle: data.pageTitle,
meta: data.meta,
keyword: data.keyword,
url: data.url
};
// Store meta title and meta description in a cookie.
$.cookie.json = true;
$.cookie(this.options.cookie_data_key, dataToStore, {json: true, path: '/' });
}
});
})(jQuery, Drupal);
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
* @file
* Drupal Yoast SEO form utility.
*
* This library will help developers to interacts with drupal form
* on client side.
*
* @ignore
*/
(function ($, Drupal) {
'use strict';
/**
* @namespace
*/
var BackboneForm = {
/**
* Form item views store.
*/
_formItemViews: {},
/**
* Based on a form item HTMLElement wrapper, get the FormItem view class
* to use to control the form item HTMLElement field.
*
* @param el
*
* @returns {function}
*/
getFormItemClass: function (el_wrapper) {
var field_item_class = BackboneForm.views.Textfield;
var field_types_map = {
'js-form-type-textfield': 'Textfield',
'js-form-type-textarea': 'Textarea'
};
// If the element carries a CKEDITOR.
var $textarea = $('textarea', $(el_wrapper));
if ($textarea.length && CKEDITOR.dom.element.get($textarea[0]).getEditor()) {
field_item_class = BackboneForm.views.Ckeditor;
}
else {
// Else define the FormItem class regarding the element wrapper classes.
for (var field_type in field_types_map) {
if ($(el_wrapper).hasClass(field_type)) {
field_item_class = BackboneForm.views[field_types_map[field_type]];
}
}