MediaWiki:InfoboxEditorPreview/main.js

/* * Name: InfoboxEditorPreview * Description: Adds a preview dialog for trying out your infobox markup with existing articles. * Author: Pogodaanton */ require(['wikia.window', 'jquery', 'mw', 'wikia.mustache', 'wikia.ace.editor', 'wikia.nirvana'], function (window, $, mw, mustache, editarea, nirvana) { window.dev = window.dev || {};

if (   window.dev.infoboxEditorPreview ||    mw.config.get('wgNamespaceNumber') !== 10 ||    !mw.config.get('wgIsEditPage') ||    $('.template-classification-type-text').attr('data-type') !== 'infobox'  ) { return; }

function InfoboxEditorPreview { this.api = new mw.Api; this.wg = mw.config.get(['wgIsDarkTheme', 'wgEditedTitle', 'wgArticlePath']); this.infoboxTitle = this.wg.wgEditedTitle; this.embedTitle = this.infoboxTitle; this.previewEditor = null; this.embeddedInCache = []; this.isDraftBypassEnabled = false; this.$ipInner = null; this.$throbber = null; this.infoboxArgs = {}; this.mwmsg = {};

mw.hook('dev.fetch').add(this.cacheMWMessages.bind(this)); mw.hook('dev.i18n').add(this.loadi18nMessages.bind(this)); mw.hook('dev.wds').add(this.libraries.$handle.bind({ referrer: this, library: 'wds' })); mw.hook('dev.colors').add(this.libraries.$handle.bind({ referrer: this, library: 'colors' }));

$.when(this.libraries.$wdsPromise, this.libraries.$fetchPromise, this.libraries.$colorsPromise, this.libraries.$i18nPromise) .then(this.cacheEmbeddedInList.bind(this)) .then(this.addEditorUI.bind(this));

window.importArticle({     type: 'script',      articles: [        'u:dev:WDSIcons/code.js',        'u:dev:Colors/code.js',        'u:dev:MediaWiki:Fetch.js',        'u:dev:MediaWiki:I18n-js/code.js'      ]    },    {      type: 'style',      articles: [        'u:dev:MediaWiki:InfoboxEditorPreview/style.css'      ]    }); }

// Library cache, promises & hook handler InfoboxEditorPreview.prototype.libraries = { $handle: function (data) { this.referrer[this.library] = data; this.referrer.libraries['$' + this.library + 'Promise'].resolve; },   $wdsPromise: $.Deferred, $fetchPromise: $.Deferred, $colorsPromise: $.Deferred, $i18nPromise: $.Deferred };

/**  * Caches needed MediaWiki messages and fires fetch library handler afterwards. * @param {Function} fetch Function that comes with the 'dev.fetch' hook. */ InfoboxEditorPreview.prototype.cacheMWMessages = function (fetch) { var self = this; fetch(['templatedraft-subpage', 'template-classification-type-infobox'], function (msg) {     var messages = msg;      self.mwmsg.draft = messages[0];      self.mwmsg.infobox = messages[1];      self.isInfoboxDraft = self.infoboxTitle.indexOf('/' + self.mwmsg.draft) > -1;      self.libraries.$handle.call({ referrer: self, library: 'fetch' }, fetch);    }); };

/**  * Loads all custom messages via i18n-js. * @param {Function} i18n Function that comes with the 'dev.i18n' hook. */ InfoboxEditorPreview.prototype.loadi18nMessages = function (i18n) { i18n.loadMessages('InfoboxEditorPreview').done(this.libraries.$handle.bind({ referrer: this, library: 'i18n' })); };

/**  * Displays an error in the infobox preview. * This should only be called when the dialog is already opened. * @param {String} msg Error message. */ InfoboxEditorPreview.prototype.setInfoboxError = function (msg) { if (typeof this.$ipInner === 'undefined' || typeof this.$ipInner.html === 'undefined') { console.error('[InfoboxEditorPreview] An error happened while not in previewing mode:', msg); }

this.$ipInner.html('' + this.i18n.msg('errorpi').plain + ' ' + msg + ' '); this.$throbber.stopThrobbing; };

/**  * Initial UI creation; Inserts the preview button and custom CSS */ InfoboxEditorPreview.prototype.addEditorUI = function  { var self = this; var previewIcon = this.wds.icon('pages').outerHTML; var $editPage = $('#EditPage');

// Adding CSS with custom parameters var cssVariables = [ '--wdsBackground:' + this.colors.wikia.menu, '--wdsHoverBackground:' + this.colors.parse(this.colors.wikia.menu).lighten(20), '--wdsColor:' + this.colors.wikia.contrast ];   mw.util.addCSS(':root {' + cssVariables.join(';') + '}');

// Removing Mobile preview and adding custom button $('#wpPreviewMobile').remove; $('') .attr({       id: 'wpPreviewInfobox',        class: 'preview_infobox preview_icon',        href: '#'      }) .html(previewIcon + ' ' + this.mwmsg.infobox + ' ') .on('click', this.createDialog.bind(this)) .popover({       placement: 'top',        trigger: 'manual'      }) .on('mouseenter', function (e) {       var modifiers = e.ctrlKey || e.metaKey;        if (modifiers && self.isInfoboxDraft) {          $(this).attr('data-original-title', self.i18n.msg('draftbypasstooltip').plain).popover('show');        } else if ($editPage.hasClass('editpage-sourcewidemode-on')) {          $(this).attr('data-original-title', self.mwmsg.infobox).popover('show');        }      }) .on('mouseleave', function { $(this).popover('hide'); }) .insertBefore('#wpPreview'); };

/**  * Checks whether the provided click event also triggered draftBypass. * In such cases the embeddedInCache needs to be rebuilt. * @param {Object} e The click event from #wpPreviewInfobox. * @returns Deferred promise object. */ InfoboxEditorPreview.prototype.checkCache = function (e) { if (e.ctrlKey || e.metaKey) { this.isDraftBypassEnabled = true; return this.cacheEmbeddedInList; } else if (this.isDraftBypassEnabled) { this.isDraftBypassEnabled = false; return this.cacheEmbeddedInList; } else if (this.cacheEmbeddedInList.length <= 0) { return this.cacheEmbeddedInList; }

return $.Deferred.resolve; };

/**  * Caches a list of articles transcluding the previewed infobox. * If the previewed infobox is a draft, the script will automatically request the linklist of its parent template. */ InfoboxEditorPreview.prototype.cacheEmbeddedInList = function  { var promise = $.Deferred; var self = this;

// Removing "/draft" for better results if (     !this.isDraftBypassEnabled &&      this.isInfoboxDraft    ) { this.embedTitle = this.embedTitle.replace('/' + this.mwmsg.draft, ''); } else { this.embedTitle = this.infoboxTitle; }

// Requesting linklist and pushing page names to an array this.api.get({     action: 'query',      list: 'embeddedin',      eititle: this.embedTitle,      einamespace: 0,      eilimit: 15,      format: 'json'    }).done(function (data) {      if (typeof data.query.embeddedin !== 'undefined') {        var embeddedin = data.query.embeddedin;        self.embeddedInCache = [];

for (var i = 0; i < embeddedin.length; i++) self.embeddedInCache.push({ val: embeddedin[i].title }); self.embeddedInCache.push({ break: true }); self.embeddedInCache.push({ val: self.infoboxTitle }); if (self.infoboxTitle !== self.embedTitle) self.embeddedInCache.push({ val: self.embedTitle });

promise.resolve; }   }).fail(promise.reject);

return promise; };

/**  * Creates the preview dialog. * @param {Object} e jQuery ClickEvent */ InfoboxEditorPreview.prototype.createDialog = function (e) { var self = this; this.infoboXML = editarea.getContent.replace(/\r?\n|\r/g, '').match(//g)[0]; var parseIcon = this.wds.icon('play', { class: 'wds-icon-small' }).outerHTML; var refreshIcon = this.wds.icon('refresh', { class: 'wds-icon-small' }).outerHTML;

if (typeof e !== 'undefined') e.preventDefault;

$.showCustomModal(this.i18n.msg('dialogtitle').plain, '   ', {      id: 'InfoboxPreviewDialog',      width: self.getPreviewDialogWidth,      buttons: [{        id: 'close',        message: 'Close',        handler: function  {          $('#InfoboxPreviewDialog').closeModal;        }      }],      callback: function  {        var $dialog = $('#InfoboxPreviewDialog');        self.$throbber = $dialog.find('.ip-container');        self.$ipInner = $dialog.find('.ip-inner');

// Rewriting throbber functions to disable dropdown while throbbing self.$throbber.startThrobbing = (function {          var cachedFunc = self.$throbber.startThrobbing;          return function  {            $('#embedPageDropdown').attr('disabled', true);            return cachedFunc.apply(this, arguments);          };        });

self.$throbber.stopThrobbing = (function {          var cachedFunc = self.$throbber.stopThrobbing;          return function  {            $('#embedPageDropdown').attr('disabled', false);            return cachedFunc.apply(this, arguments);          };        });

self.$throbber.startThrobbing;

// Rebuild cache if draftBypass has been toggled self.checkCache.call(self, e)         .done(function  {            // Resize dialog            $dialog.find('.ip-container, #ipEditor').css({ height: $(window).height - 280 });

// Render header dropdown var rendered = mustache.render('  ──────   ', { options: self.embeddedInCache }); $(rendered).insertBefore($dialog.find('.ip-container')); $('#embedPageDropdown').on('change', self.parseNirvanaToInfobox.bind(self)); $(' ')             .text('?') .attr({               class: 'tooltip-icon',                rel: 'tooltip',                title: self.i18n.msg('choosedescription').plain              }) .tooltip({ placement: 'bottom' }) .insertAfter('#embedPageDropdown');

// Adding refresh button $('') .attr({               id: 'parsePreviewButton',                class: 'wds-button',                href: '#',                title: self.i18n.msg('parsedisclaimer').plain              }) .tooltip({ placement: 'bottom' }) .html(parseIcon + ' ' + self.i18n.msg('parse').plain + ' ') .on('click', self.parseInvocationToInfobox.bind(self)) .appendTo('form .wds-button-group');

// Adding refresh button $('') .attr({               id: 'refreshPreviewButton',                class: 'wds-button wds-is-secondary',                href: '#'              }) .html(refreshIcon + ' ' + self.i18n.msg('reload').plain + ' ') .on('click', self.parseNirvanaToInfobox.bind(self)) .appendTo('form .wds-button-group');

// Initialise ace editor self.previewEditor = window.ace.edit('ipEditor'); self.previewEditor.setTheme('ace/theme/' + (self.wg.wgIsDarkTheme ? 'solarized_dark' : 'solarized_light')); self.previewEditor.session.setMode('ace/mode/' + window.codePageType);

// Starting first preview self.parseNirvanaToInfobox.apply(self); // self.parsePageWikitextToInfobox.apply(self); })         .fail(function (e) { console.log(e); self.setInfoboxError(self.i18n.msg('errorlinklist').plain); });     }    });  };

/**  * Returns a suitable width for the preview dialog according to window.innerWidth */ InfoboxEditorPreview.prototype.getPreviewDialogWidth = function  { var pageWidth = $(window).width; if (pageWidth < 1024) return pageWidth - 120; if (pageWidth < 1184) return pageWidth - 190; return pageWidth - 350; };

/**  * Retrieves the infobox parameters of a specified page via Nirvana and creates an infobox preview. */ InfoboxEditorPreview.prototype.parseNirvanaToInfobox = function  { var self = this; this.$throbber.startThrobbing; this.getArticleMetadata($('#embedPageDropdown').val, this.embedTitle, this.createInfobox) .done(function {        var embedding = self.getTemplateEmbedFromMetadata.apply(self, arguments);        if (embedding) {          var params = self.trimValues(embedding.parameters);          self.previewEditor.setValue(self.parseInfoboxParametersToWikitext.call(self, params));          self.createInfobox.call(self, params);        } else self.setInfoboxError(self.i18n.msg('errorinvocation').plain, embedding);      }) .fail(console.error); };

/**  * Retrieves metadata of an article from Nirvana. * @param {String} pageTitle Title of the article. * @returns {$.Deferred} A jQuery deferred object. */ InfoboxEditorPreview.prototype.getArticleMetadata = function (pageTitle) { var promise = $.Deferred; nirvana.getJson('TemplatesApi', 'getMetadata', { title: pageTitle }, promise.resolve, promise.reject); return promise; };

/**  * Trims the value of each object item. * @param {Object} obj An object with strings as values. * @returns {Object} */ InfoboxEditorPreview.prototype.trimValues = function (obj) { $.each(obj, function (key, value) { obj[key] = value.trim; }); return obj; };

/**  * Creates a wikitext invocation of the infobox * @param {Object} params An object with infobox parameters (key) and values. * @returns {String} Multiline wikitext string. */ InfoboxEditorPreview.prototype.parseInfoboxParametersToWikitext = function (params) { var rows = []; rows.push('');

return rows.join('\n'); };

/**  * Processes Nirvana metadata object. * @param {Object} res Nirvana response object. * @returns {$.Deferred} A jQuery deferred object. */ InfoboxEditorPreview.prototype.getTemplateEmbedFromMetadata = function (res) { var templates = res.templates; var embedTitleNoNamespace = this.embedTitle.replace(/^[^:]+:/g, '');

for (var i = 0; i < templates.length; i++) { if (templates[i].type === 'infobox' && templates[i].name === embedTitleNoNamespace) { return templates[i]; }   }

return null; };

/**  * Parses the custom invocation in the ace editor and creates an infobox out of its parameters. */ InfoboxEditorPreview.prototype.parseInvocationToInfobox = function  { var splitToNewline = this.previewEditor.getValue.trim.split('\n|').slice(1, -1); var embedObj = {};

this.$throbber.startThrobbing;

try { for (var i = 0; i < splitToNewline.length; i++) { var line = splitToNewline[i]; var splitAtEqualChar = line.split(/=(.+)/); var key = splitAtEqualChar[0].trim; embedObj[key] = (splitAtEqualChar[1] + ' ').trim; }   } catch (e) { this.setInfoboxError(this.i18n.msg('errorinvocation').plain); return; }

this.createInfobox.call(this, embedObj); };

/**  * Creates an infobox via api.php and passes the result to the preview dialog. * @param {Object} parameters An object with infobox parameters (key) and values. */ InfoboxEditorPreview.prototype.createInfobox = function (parameters) { if (typeof parameters !== 'object') return;

this.api.post({     action: 'infobox',      text: this.infoboXML,      args: JSON.stringify(parameters),      format: 'json'    }).done(this.handleCreateInfoboxRes.bind(this)).error(this.handleCreateInfoboxRes.bind(this)); };

/**  * Handles the result from this.createInfobox * @param {Object} data Response data from api.php * @param {Number} code Response code from api.php * @param {String} msg Response message from api.php */ InfoboxEditorPreview.prototype.handleCreateInfoboxRes = function (data, code, msg) { if (typeof data.error !== 'undefined') { this.setInfoboxError('Code: ' + data.error.code || msg); return; } else if (typeof data.infobox.text['*'] === 'undefined') { this.setInfoboxError(this.i18n.msg('errorresponse').plain); return; }

this.$ipInner.html(data.infobox.text['*']); mw.hook('wikipage.content').fire(this.$ipInner); this.$throbber.stopThrobbing; };

window.dev.infoboxEditorPreview = new InfoboxEditorPreview; });