User:Mooeypoo/wikiArticleWizard.js

/*! * wikiArticleWizard v0.0.1 * * This is a user script to display an article creation wizard * based on preset article skeletons dynamically created per wiki. * * The code for this user script is being maintained on Github: * https://github.com/mooeypoo/wikiArticleWizard * * ** Please do not change this local file directly ** * * Any updates should be created from the official repository * by running `grunt build` and using * `dist/wikiArticleWizard.userscript.js` and * `dist/wikiArticleWizard.userscript.css` * * Latest update: 2018-05-28T14:04:31Z * (Monday, May 28th, 2018, 5:04:31 PM +0300) */ /* >> Starting source: src/init.waw.js << */ // Set up a global variable for the rest of the code to use. var waw = { ui: {} }; // eslint-disable-line no-unused-vars /* >> End source: src/init.waw.js << */ /* >> Starting source: src/waw.Config.js << */ waw.Config = { ARTICLE_REPO: mw.config.get( 'articlesandbox-article-repo' ) || '', NAME_FOR_SANDBOX: mw.config.get( 'articlesandbox-article-nameforsandbox' ) || 'sandbox' }; /* >> End source: src/waw.Config.js << */ /* >> Starting source: src/init.loader.start.js << */ mw.loader.using( [	'oojs-ui',	'mediawiki.api',	'ext.visualEditor.targetLoader' ] ).then( function { /* >> End source: src/init.loader.start.js << */ /* >> Starting source: src/waw.Utils.js << */

/**	 * Define a set of utility functions and values *	 * @return {Objects} */	waw.Utils = ( function {		/**		 * Translate from the structure given by the prefixsearch API to		 * the required hierarchical article structure and paths that		 * the wikiArticleWizard expects		 *		 * @param  {Object} apiPrefixSearch API result		 * @param  {string} repository Name of the base repository		 * @return {Object} Article structure		 */		var getArticleStrctureFromPrefixSearch = function ( apiPrefixSearch, repository ) {				var path, flatPath,					articles = {};

apiPrefixSearch.forEach( function ( articleData ) {					var pieces = [],						title = articleData.title;

if ( title.indexOf( repository ) !== 0 ) { return; }

pieces = title.replace( repository, '' ).split( '/' ); pieces = pieces.filter( function ( piece ) {						return !!piece; // Get rid of empty strings					} );

// Build article structure path = articles; flatPath = ''; pieces.forEach( function ( piece ) {						flatPath += '/' + piece;

path[ piece ] = path[ piece ] || { _path: repository + flatPath, _articles: {} };

// Shift reference // eslint-disable-next-line no-underscore-dangle path = path[ piece ]._articles; } );				} );				return articles; },			/**			 * Fetch the details from the API and build the base article * structure the system is expecting, in the form of a hierarchical * object representing sections and their sub-pages. *			 * @param {string} repository Path to the article repository * @return {Object} Article structure */			buildArticleStructure = function ( repository ) { var utils = this;

if ( !repository ) { return $.Deferred.reject( 'noconfig' ); }

return ( new mw.Api ).get( {					action: 'query',					format: 'json',					list: 'prefixsearch',					pssearch: repository				} ).then(					// Success					function ( queryData ) {						var articles = utils.getArticleStrctureFromPrefixSearch( queryData.query.prefixsearch, repository );

if ( $.isEmptyObject( articles ) ) { return $.Deferred.reject( 'noarticles' ); }

return articles; },					function { return $.Deferred.reject( 'noarticles' ); }				);			},			/**			 * Fetch the HTML contents of a given wiki page from restbase			 *			 * @param {string} page Page name			 * @return {jQuery} HTML content of the page body			 */			getRestbasePage = function ( page ) {				return mw.libs.ve.targetLoader.requestParsoidData( page, {} )					.then( function ( response ) { var content = response.visualeditor.content, doc = new DOMParser.parseFromString( content, 'text/html' );

// TODO: Consider re-adding the body classes so the // inner classes will be represented properly; otherwise // images and some templates are not floated correctly // in the preview return $( doc.body.childNodes ); },						// Failure function { OO.ui.alert( mw.msg( 'articlesandbox-error-restbase-preview' ) );

// Resolve with an error message return $.Deferred.resolve(								$( ' ' )									.append( mw.msg( 'articlesandbox-error-restbase-preview-content' ) )							); }					);			},			/**			 * Create an array of article items preceeded by a section item per section			 * in the article structure.			 *			 * Recursively adds sections with the respective indent for representation			 * in the end result list.			 *			 * @param {string} title Section title			 * @param {Object} articles Article structure object			 * @param {string} sectionPath A full path for the parent section			 * @param {number} [indent=0] The indentation of this section, if it is a sub-section			 * @return {waw.ui.ArticleItemWidget[]} An array of items and section items representing			 * the section and its sub sections			 */			buildItemsSection = function ( title, articles, sectionPath, indent ) {				var utils = this,					items = [],					pages = articles ? Object.keys( articles ) : [],					sectionTitle = new waw.ui.ArticleSectionWidget( title, indent );

indent = indent || 0;

// Add title if ( title ) { items.push( sectionTitle ); }

// Look for sub sections and items pages.forEach( function ( page ) {					// eslint-disable-next-line no-underscore-dangle					if ( !$.isEmptyObject( articles[ page ]._articles ) ) {						// This is a sub-section, recurse						// eslint-disable-next-line no-underscore-dangle						items.concat( utils.buildItemsSection( page, articles[ page ]._articles, articles[ page ]._path, indent + 1 ) );					} else {						// This is a direct child						items.push( new waw.ui.ArticleItemWidget(								page,								// eslint-disable-next-line no-underscore-dangle								articles[ page ]._path,								sectionPath,								indent							) );					}				} );

return items; },			/**			 * Build the 'create page' form. * This will be updated with details from the item as it is chosen *			 * @param {string} itemPath Path to the article this item represents * @return {jQuery} Form jQuery object with the relevant data in the * fields. */			buildCreatePageForm = function ( itemPath ) { var makeHiddenInput = function ( name, value ) { return $( ' ' ) .attr( 'type', 'hidden' ) .attr( 'name', name ) .attr( 'value', value ); },					titleInput = new OO.ui.TextInputWidget( {						name: 'visibletitle',						// name: 'title',						placeholder: mw.msg( 'articlesandbox-create-input-placeholder' )					} ), errorLabel = new OO.ui.LabelWidget( {						classes: [ 'articlesandbox-create-titleerror' ],						label: mw.msg( 'articlesandbox-error-badtitle' )					} ), submit = new OO.ui.ButtonInputWidget( {						type: 'submit',						icon: 'add',						flags: [ 'progressive' ],						label: mw.msg( 'articlesandbox-create-button' )					} ), $hiddenTitle = makeHiddenInput( 'title' ), // Mock a inputbox process $form = $( ' ' ) .attr( 'action', '/w/index.php' ) .attr( 'method', 'get' ) .append(							makeHiddenInput( 'veaction', 'edit' ),							$hiddenTitle,							makeHiddenInput( 'preload', itemPath ),							makeHiddenInput( 'summary', mw.msg( 'articlesandbox-create-articlesummary' ) ),							makeHiddenInput( 'prefix', 'Special:MyPage/' + waw.Config.NAME_FOR_SANDBOX + '/' )						);

errorLabel.toggle( false ); submit.setDisabled( true );

titleInput.on( 'change', function ( val ) {					var valid = !!mw.Title.newFromText( val );

titleInput.setValidityFlag( valid ); submit.setDisabled( !valid ); errorLabel.toggle( !valid ); $hiddenTitle.attr( 'value', 'Special:MyPage/' + waw.Config.NAME_FOR_SANDBOX + '/' + titleInput.getValue ); } );

$form.append(					titleInput.$element,					errorLabel.$element,					submit.$element				);

return $form; };

// Public methods return { buildArticleStructure: buildArticleStructure, getRestbasePage: getRestbasePage, buildItemsSection: buildItemsSection, buildCreatePageForm: buildCreatePageForm, getArticleStrctureFromPrefixSearch: getArticleStrctureFromPrefixSearch };	} ); /* >> End source: src/waw.Utils.js << */ /* >> Starting source: src/waw.ui.DialogPageLayout.js << */

/**	 * Create a custom page layout for the wizard dialog *	 * @class * @extends OO.ui.PageLayout *	 * @constructor * @param {string} name Page name * @param {Object} [config] Optional config */	waw.ui.DialogPageLayout = function DialogPageLayout( name, config ) { waw.ui.DialogPageLayout.super.call( this, name, config );

config = config || {};

this.titleWidget = new OO.ui.LabelWidget( {			label: config.title,			classes: [ 'articlesandbox-page-title' ]		} ); this.$content = config.$content;

// Create a panel that holds the intro this.$intro = $( ' ' ) .addClass( 'articlesandbox-page-intro-content' ) .append( config.intro ); this.introPanel = new OO.ui.PanelLayout( {			$content: this.$intro,			classes: [ 'articlesandbox-page-intro' ],			framed: true,			expanded: false,			padded: true		} );

this.introLink = new OO.ui.ButtonWidget( {			label: mw.msg( 'articlesandbox-moreinfo' ),			icon: 'newWindow',			framed: false,			classes: [ 'articlesandbox-page-introlink' ],			target: '_blank'		} );

this.$element.append(			this.titleWidget.$element,			this.introPanel.$element,			this.introLink.$element,			this.$content		);

// Initialize this.titleWidget.toggle( config.title ); this.introPanel.toggle( config.intro ); this.introLink.toggle( false ); };	OO.inheritClass( waw.ui.DialogPageLayout, OO.ui.PageLayout );

/**	 * Set the title for the page *	 * @param {string} title Page title */	waw.ui.DialogPageLayout.prototype.setTitle = function ( title ) { this.titleWidget.setLabel( title ); this.titleWidget.toggle( title ); };

/**	 * Set or change the intro of this page. * Also include an optional button to 'read more' *	 * @param {jQuery|string} intro Page intro * @param {string} [linkToReadMore] Link to the 'read more' button. * If not given, the 'read more' button won't be displayed. */	waw.ui.DialogPageLayout.prototype.setIntro = function ( intro, linkToReadMore ) { this.$intro.empty.append( intro ); this.introLink.setHref( linkToReadMore );

this.introPanel.toggle( !!intro ); this.introLink.toggle( !!intro && !!linkToReadMore ); };

/* >> End source: src/waw.ui.DialogPageLayout.js << */ /* >> Starting source: src/waw.ui.ArticleItemWidget.js << */

/**	 * Create a special item option that represents an article item. *	 * @param {string} pageName Page name * @param {string} path Page path * @param {string} [parentPath=''] Page parent path * @param {Number} [indent=0] Indent; this dictates how many levels * of indent this page is under, in case it is in a sub-category. * @constructor */	waw.ui.ArticleItemWidget = function ArticleItemWidget( pageName, path, parentPath, indent ) { waw.ui.ArticleItemWidget.super.call( this, { label: pageName, data: { path: path, parentPath: parentPath }, icon: 'article' } );

this.$element .addClass( 'articlesandbox-articleItemWidget' ) .css( { left: ( indent ) + 'em' } ) .toggleClass( 'articlesandbox-articleItemWidget-indent', indent ); };	OO.inheritClass( waw.ui.ArticleItemWidget, OO.ui.DecoratedOptionWidget ); /* >> End source: src/waw.ui.ArticleItemWidget.js << */ /* >> Starting source: src/waw.ui.ArticleSectionWidget.js << */

/**	 * Create a section title inside the article list to represent hierarchical * and categorized article items *	 * @class * @extends OO.ui.MenuSectionOptionWidget *	 * @constructor * @param {string} pageName Page name * @param {Number} [indent=0] Indent; this dictates how many levels * of indent this page is under, in case it is in a sub-category. * @param {Object} [config] Optional configuration options */	waw.ui.ArticleSectionWidget = function ArticleSectionWidget( pageName, indent, config ) { waw.ui.ArticleSectionWidget.super.call( this, $.extend( { label: pageName }, config ) );

this.$element .addClass( 'articlesandbox-articleSectionWidget' ) .css( { left: ( indent ) + 'em' } ) .toggleClass( 'articlesandbox-articleSectionWidget-indent', indent ); };	OO.inheritClass( waw.ui.ArticleSectionWidget, OO.ui.MenuSectionOptionWidget ); /* >> End source: src/waw.ui.ArticleSectionWidget.js << */ /* >> Starting source: src/waw.ui.WizardDialog.js << */

/**	 * Create the wizard dialog *	 * @class * @extends OO.ui.ProcessDialog *	 * @constructor * @param {Object} [config] Configuration options */	waw.ui.WizardDialog = function WizardDialog( config ) { waw.ui.WizardDialog.super.call( this, config );

this.chosenItem = null; this.error = false; };	OO.inheritClass( waw.ui.WizardDialog, OO.ui.ProcessDialog ); waw.ui.WizardDialog.static.actions = [ { label: mw.msg( 'articlesandbox-dismiss' ), modes: [ 'articles', 'error', 'create' ], flags: 'safe' }, { action: 'back', label: mw.msg( 'articlesandbox-goback' ), modes: 'create', flags: 'safe', icon: 'arrowPrevious' } ];	waw.ui.WizardDialog.static.title = mw.msg( 'articlesandbox-title' ); waw.ui.WizardDialog.static.name = 'articlehelperdialog'; waw.ui.WizardDialog.static.size = 'large';

/**	 * Set error state for the dialog *	 * @param {string} [type='noarticles'] Error type; 'noconfig' or 'noarticles' */	waw.ui.WizardDialog.prototype.setError = function ( type ) { var label = type === 'noconfig' ? mw.msg( 'articlesandbox-error-missing-repoconfig' ) : $( ' ' )				.append(					mw.msg( 'articlesandbox-error-missing-articles' ),					new OO.ui.ButtonWidget( { flags: [ 'progressive' ], icon: 'newWindow', label: mw.msg( 'articlesandbox-error-missing-articles-gotorepo' ), href: mw.config.get( 'wgArticlePath' ).replace( '$1', waw.Config.ARTICLE_REPO ), target: '_blank' } ).$element				);

this.error = true; this.errorLabel.setLabel( label ); this.showPage( 'error' ); };

/**	 * @inheritdoc */	waw.ui.WizardDialog.prototype.initialize = function { waw.ui.WizardDialog.super.prototype.initialize.apply( this, arguments );

this.$articlesPage = $( ' ' ) .addClass( 'articlesandbox-page-articles' ); this.$createPageForm = $( ' ' ) .addClass( 'articlesandbox-page-create-form' );

this.errorLabel = new OO.ui.LabelWidget( {			classes: [ 'articlesandbox-error' ]		} );

this.bookletLayout = new OO.ui.BookletLayout; this.bookletLayout.addPages( [			new waw.ui.DialogPageLayout( 'error', { title: mw.msg( 'articlesandbox-title-error' ), $content: this.errorLabel.$element } ),			new waw.ui.DialogPageLayout( 'articles', { title: mw.msg( 'articlesandbox-create-article' ), $content: this.$articlesPage } ),			new waw.ui.DialogPageLayout( 'create', { $content: $( ' ' ) .addClass( 'articlesandbox-page-create' ) .append( this.$createPageForm ) } )		] );

this.$body.append( this.bookletLayout.$element ); };

/**	 * @inheritdoc */	waw.ui.WizardDialog.prototype.getActionProcess = function ( action ) { if ( action === 'back' ) { this.showPage( 'articles' ); }		return waw.ui.WizardDialog.super.prototype.getActionProcess.call( this, action ); };

/**	 * @inheritdoc */	waw.ui.WizardDialog.prototype.getSetupProcess = function ( data ) { return waw.ui.WizardDialog.super.prototype.getSetupProcess.call( this, data ) .next( function {				if ( this.error ) {					this.showPage( 'error' );				} else {					this.showPage( 'articles' );				}			}, this ); };

/**	 * Show a specific page in the dialog *	 * @param {string} [name='articles'] Page name; 'articles', 'create' or 'error' */	waw.ui.WizardDialog.prototype.showPage = function ( name ) { name = name || 'articles';

this.bookletLayout.setPage( name ); this.getActions.setMode( name ); };

/**	 * Respond to 'choose' event from any of the list widgets. *	 * @param {OO.ui.OptionWidget} item Chosen item */	waw.ui.WizardDialog.prototype.onArticleListChoose = function ( item ) { var promise, dialog = this, data = item.getData;

this.chosenItem = item;

// Get the data from parent section if ( data.parentPath ) { promise = waw.Utils.getRestbasePage( data.path ); } else { promise = $.Deferred.resolve( '' ).promise; }

this.pushPending; promise.then( function ( $intro ) {			var currPage;

dialog.$createPageForm.empty.append( waw.Utils.buildCreatePageForm( data.path ) ); dialog.showPage( 'create' );

currPage = dialog.bookletLayout.getCurrentPage; currPage.setTitle( mw.msg( 'articlesandbox-create-article-for', item.getLabel ) ); currPage.setIntro( $intro );

dialog.popPending; } );	};

/**	 * Build the content based on the article structure given *	 * @param {Object} articles Article structure with paths * @param {jQuery} [$introContent] A jQuery object for the intro */	waw.ui.WizardDialog.prototype.buildContent = function ( articles, $introContent ) { var introLink = new OO.ui.ButtonWidget( {				label: mw.msg( 'articlesandbox-moreinfo' ),				icon: 'newWindow',				framed: false,				href: mw.config.get( 'wgArticlePath' ).replace( '$1', waw.Config.ARTICLE_REPO ),				target: '_blank'			} ), list = new OO.ui.SelectWidget( {				classes: [ 'articlesandbox-articles-section-list' ]			} );

if ( $introContent ) { this.$articlesPage.append(				new OO.ui.PanelLayout( { $content: $introContent, classes: [ 'articlesandbox-page-intro' ], framed: true, expanded: false, padded: true } ).$element,				introLink.$element			); }

// Events list.on( 'choose', this.onArticleListChoose.bind( this ) );

// Create the list Object.keys( articles ).forEach( function ( page ) {			// eslint-disable-next-line no-underscore-dangle			var items = waw.Utils.buildItemsSection( page, articles[ page ]._articles, articles[ page ]._path, 0 );			list.addItems( items );		} );

// Append to articles page this.$articlesPage.append( list.$element ); };

/**	 * @inheritdoc */	waw.ui.WizardDialog.prototype.getBodyHeight = function { // 50% height of window // TODO: Figure out how to make the height variable // every time a page is switched return $( window ).height * 0.7; };

/* >> End source: src/waw.ui.WizardDialog.js << */ /* >> Starting source: src/waw.init.DOM.js << */

var mainDialog = new waw.ui.WizardDialog, windowManager = new OO.ui.WindowManager, mainButton = new OO.ui.ButtonWidget( {			label: mw.msg( 'articlesandbox-button' ),			icon: 'article',			flags: [ 'progressive' ]		} );

mainButton.setDisabled( true ); windowManager.addWindows( [ mainDialog ] );

// Attach events mainButton.on( 'click', function {		windowManager.openWindow( mainDialog );	} );

if ( waw.Config.ARTICLE_REPO ) { // Get the data $.when(			// Get the data for the articles			waw.Utils.buildArticleStructure( waw.Config.ARTICLE_REPO ),			// Get the content for the introduction page			waw.Utils.getRestbasePage( waw.Config.ARTICLE_REPO )		).then(			// Success			mainDialog.buildContent.bind( mainDialog ),			// Failure			mainDialog.setError.bind( mainDialog )		).always( function {			mainButton.setDisabled( false );		} ); } else { // If ARTICLE_REPO is undefined, show an error mainDialog.setError( 'noconfig' ); mainButton.setDisabled( false ); }

// Load the stylesheet and wait for document ready to attach the // main button to the DOM $.when(		// Stylesheet		mw.loader.load( 'https://www.mediawiki.org/w/index.php?title=User:Mooeypoo/articlesandbox.css&action=raw&ctype=text/css', 'text/css' ),		// Document ready		$.ready	).then( function {		// Attach to DOM		$( '#right-navigation' ).append( mainButton.$element );		$( 'body' ).append( windowManager.$element );	} ); /* >> End source: src/waw.init.DOM.js << */ /* >> Starting source: src/init.loader.end.js << */ } ); // End mw.loader.using /* >> End source: src/init.loader.end.js << */ /* >> Starting source: src/init.language.js << */ /**/( function { /**	 * Define translation messages and initialize by user language * This happens early, without waiting for modules to load. */	var userLang = mw.config.get( 'wgUserLanguage' ), messages = { he: { 'articlesandbox-title': 'אשף ערכים', 'articlesandbox-button': 'אשף ערכים', 'articlesandbox-dismiss': 'ביטול', 'articlesandbox-moreinfo': 'מידע נוסף', 'articlesandbox-goback': 'חזרה למסך הקודם', 'articlesandbox-create-article': ' ערך', 'articlesandbox-create-article-for': ' ערך עבור $1', 'articlesandbox-sections': 'רשימת ערכים', 'articlesandbox-create-button': ' ערך', 'articlesandbox-create-input-placeholder': 'כותרת הערך', 'articlesandbox-create-articlesummary': 'נוצר באמצעות |אשף הערכים', 'articlesandbox-error-badtitle': 'כותרת הערך שבחרת אינה תקינה. אנא שוב.' },			en: { 'articlesandbox-title': 'Article wizard', 'articlesandbox-button': 'Article wizard', 'articlesandbox-dismiss': 'Dismiss', 'articlesandbox-goback': 'Go back', 'articlesandbox-moreinfo': 'More info', 'articlesandbox-title-error': 'Error displaying contents', 'articlesandbox-create-article': 'Create article', 'articlesandbox-create-article-for': 'Create article for $1', 'articlesandbox-sections': 'Available article templates', 'articlesandbox-create-button': 'Create article', 'articlesandbox-create-input-placeholder': 'Article title', 'articlesandbox-create-articlesummary': 'Created by |ArticleSandbox user script', 'articlesandbox-error-missing-repoconfig': 'Cannot load article templates, since there is no repository configured. Please configure a base repository for all article templates through setting the mw.config entry \'articlesandbox-article-repo\' where you load this script, and try again.', 'articlesandbox-error-missing-articles': 'Cannot find any defined article templates. Please add articles to the repository, and try again.', 'articlesandbox-error-missing-articles-gotorepo': 'Go to the article repository', 'articlesandbox-error-badtitle': 'Invalid title: The title you chose contains invalid characters. Please retry.', 'articlesandbox-error-restbase-preview': 'There was a problem fetching the preview. You can still add the article!', 'articlesandbox-error-restbase-preview-content': 'Preview could not be displayed.' }		};

// Set language, with default 'English' mw.messages.set( messages.en ); if ( userLang && userLang !== 'en' && userLang in messages ) { mw.messages.set( messages[ userLang ] ); } } ); /* >> End source: src/init.language.js << */