User:Mooeypoo/wikiArticleWizard.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*!
* 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(
$( '<span>' )
.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 $( '<input>' )
.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 = $( '<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 = $( '<div>' )
.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: '_self'
} );
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' ) :
$( '<span>' )
.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: '_self'
} ).$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 = $( '<div>' )
.addClass( 'articlesandbox-page-articles' );
this.$createPageForm = $( '<div>' )
.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: $( '<div>' )
.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: '_self'
} ),
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': '{{GENDER:|צור|צרי}} ערך',
'articlesandbox-create-article-for': '{{GENDER:|צור|צרי}} ערך עבור $1',
'articlesandbox-sections': 'רשימת ערכים',
'articlesandbox-create-button': '{{GENDER:|צור|צרי}} ערך',
'articlesandbox-create-input-placeholder': 'כותרת הערך',
'articlesandbox-create-articlesummary': 'נוצר באמצעות [https://www.mediawiki.org/wiki/User:Mooeypoo/articlesandbox.js|אשף הערכים]',
'articlesandbox-error-badtitle': 'כותרת הערך שבחרת אינה תקינה. אנא {{GENDER:|נסה|נסי}} שוב.'
},
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 [https://www.mediawiki.org/wiki/User:Mooeypoo/articlesandbox.js|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 << */