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.
( function () {
if ( !window.ext || !ext.contentdroplets || !ext.contentdroplets.object ) { return; }
var api = new mw.Api();
var restBase = mw.util.wikiScript( 'rest' ) + '/neowiki/v0/';
var dataPromise = null;
var valueCache = {};
var EMPTY_OPERATORS = [ 'IS NULL', 'IS NOT NULL' ];
var DATE_TYPES = [ 'date', 'dateTime' ];
function parseText( text ) {
return api.post( { action: 'parse', text: text, contentmodel: 'wikitext', prop: 'text', disablelimitreport: 1, formatversion: 2 } )
.then( function ( r ) { return $( '<div>' ).html( r.parse.text ).text().trim(); } );
}
function splitByMarker( text ) {
var parts = text.split( /%%%(\d+)%%%/ ), out = {}, i;
for ( i = 1; i < parts.length; i += 2 ) { out[ parts[ i ] ] = ( parts[ i + 1 ] || '' ).trim(); }
return out;
}
// All schemas with their property definitions, and which pages use which schema. Loaded once per page view.
function loadData() {
if ( dataPromise ) { return dataPromise; }
dataPromise = api.get( { action: 'query', list: 'allpages', apnamespace: 7474, aplimit: 'max', formatversion: 2 } ).then( function ( r ) {
var names = r.query.allpages.map( function ( p ) { return p.title.slice( p.title.indexOf( ':' ) + 1 ); } );
var schemaRequests = names.map( function ( name ) {
return $.getJSON( restBase + 'schema/' + encodeURIComponent( name ) ).then(
function ( j ) { return ( j && j.schema && j.schema.propertyDefinitions ) || {}; },
function () { return $.Deferred().resolve( {} ).promise(); }
);
} );
var wikitext = names.map( function ( name, i ) {
return '%%%' + i + '%%%{{#neowiki_query:{"properties":{},"hasSubjects":["' + name + '"],"pageDisplayFormat":"prefixed","showPageLink":false,"limit":2000}|format=list|separator=@@}}';
} ).join( '' );
return $.when.apply( $, schemaRequests.concat( [ parseText( wikitext ) ] ) ).then( function () {
var args = Array.prototype.slice.call( arguments );
var pageLists = splitByMarker( args.pop() );
var data = { names: names, schemas: {}, schemaPages: {}, pageSchemas: {}, pages: [] };
names.forEach( function ( name, i ) {
data.schemas[ name ] = args[ i ] || {};
data.schemaPages[ name ] = ( pageLists[ i ] || '' ) ? pageLists[ i ].split( '@@' ).map( function ( t ) { return t.trim(); } ).filter( Boolean ) : [];
data.schemaPages[ name ].forEach( function ( title ) {
if ( !data.pageSchemas[ title ] ) { data.pageSchemas[ title ] = []; data.pages.push( title ); }
data.pageSchemas[ title ].push( name );
} );
} );
data.pages.sort();
return data;
} );
} );
dataPromise.fail( function () { dataPromise = null; } );
return dataPromise;
}
// "Schema.Property" -> { schema, property, def }
function resolveProperty( data, full ) {
var best = null;
full = ( full || '' ).trim();
data.names.forEach( function ( name ) {
if ( full.indexOf( name + '.' ) === 0 && ( !best || name.length > best.length ) ) { best = name; }
} );
if ( !best ) { return null; }
var prop = full.slice( best.length + 1 );
return { schema: best, property: prop, def: data.schemas[ best ][ prop ] || null };
}
function propertyOptions( data, schemaNames ) {
var out = [];
schemaNames.forEach( function ( name ) {
var defs = data.schemas[ name ] || {}, key;
for ( key in defs ) {
if ( Object.prototype.hasOwnProperty.call( defs, key ) ) {
out.push( { data: name + '.' + key, label: name + '.' + key + ' (' + defs[ key ].type + ')' } );
}
}
} );
return out;
}
function toDate( value ) {
return /^-?\d{9,11}$/.test( value ) ? new Date( parseInt( value, 10 ) * 1000 ).toISOString().slice( 0, 10 ) : value;
}
// Possible filter values for one property: select options, true/false, or the values already stored on pages
function loadValues( data, full ) {
var info = resolveProperty( data, full );
if ( !info || !info.def ) { return $.Deferred().resolve( [] ).promise(); }
if ( info.def.type === 'select' ) {
return $.Deferred().resolve( ( info.def.options || [] ).map( function ( o ) { return o.label; } ) ).promise();
}
if ( info.def.type === 'boolean' ) {
return $.Deferred().resolve( [ { data: 'true', label: 'true (yes)' }, { data: 'false', label: 'false (no)' } ] ).promise();
}
if ( info.def.type === 'relation' ) {
return $.Deferred().resolve( [] ).promise();
}
if ( valueCache[ full ] ) { return valueCache[ full ]; }
var pages = ( data.schemaPages[ info.schema ] || [] ).slice( 0, 300 );
if ( !pages.length ) { return $.Deferred().resolve( [] ).promise(); }
var wikitext = pages.map( function ( title, i ) {
return '%%%' + i + '%%%{{#neowiki_show:' + title + '|' + full + '}}';
} ).join( '' );
valueCache[ full ] = parseText( wikitext ).then( function ( text ) {
var found = splitByMarker( text ), counts = {}, key, value;
for ( key in found ) {
value = found[ key ];
if ( !value ) { continue; }
if ( DATE_TYPES.indexOf( info.def.type ) !== -1 ) { value = toDate( value ); }
counts[ value ] = ( counts[ value ] || 0 ) + 1;
}
return Object.keys( counts ).sort( function ( a, b ) { return counts[ b ] - counts[ a ] || a.localeCompare( b ); } ).slice( 0, 100 )
.map( function ( v ) { return { data: v, label: v + ' (' + counts[ v ] + ')' }; } );
}, function () { delete valueCache[ full ]; return []; } );
return valueCache[ full ];
}
function setOptions( widget, list ) {
if ( !widget || typeof widget.setOptions !== 'function' ) { return; }
if ( typeof widget.getMenu === 'function' ) {
widget.getMenu().filterFromInput = true;
widget.getMenu().filterMode = 'substring';
}
widget.setOptions( list.map( function ( x ) { return typeof x === 'object' ? x : { data: x, label: x }; } ) );
}
function setEnabled( widget, enabled ) {
if ( widget && typeof widget.setDisabled === 'function' ) { widget.setDisabled( !enabled ); }
}
function val( widget ) {
return widget && typeof widget.getValue === 'function' ? String( widget.getValue() || '' ).trim() : '';
}
function setupValueForm( inputs ) {
loadData().then( function ( data ) {
setOptions( inputs.page, data.pages );
function updateProperties() {
var page = val( inputs.page ) || mw.config.get( 'wgPageName' ).replace( /_/g, ' ' );
var schemas = data.pageSchemas[ page ] || data.names;
setOptions( inputs.property, propertyOptions( data, schemas ) );
}
function updateDateFormat() {
var info = resolveProperty( data, val( inputs.property ) );
setEnabled( inputs.dateformat, !info || !info.def || DATE_TYPES.indexOf( info.def.type ) !== -1 );
}
updateProperties();
updateDateFormat();
if ( inputs.page ) { inputs.page.on( 'change', OO.ui.debounce( updateProperties, 300 ) ); }
if ( inputs.property ) { inputs.property.on( 'change', OO.ui.debounce( updateDateFormat, 300 ) ); }
} );
}
function setupQueryForm( inputs ) {
function updateFormatFields() {
var isList = val( inputs.format ) === 'list';
setEnabled( inputs.separator, isList );
setEnabled( inputs.display, isList );
}
function updateOperator() {
var empty = EMPTY_OPERATORS.indexOf( val( inputs.operator ) ) !== -1;
if ( empty && inputs.value ) { inputs.value.setValue( '' ); }
setEnabled( inputs.value, !empty );
}
updateFormatFields();
updateOperator();
if ( inputs.format ) { inputs.format.on( 'change', updateFormatFields ); }
if ( inputs.operator ) { inputs.operator.on( 'change', updateOperator ); }
loadData().then( function ( data ) {
setOptions( inputs.schema, data.names.map( function ( name ) {
return { data: name, label: name + ' (' + data.schemaPages[ name ].length + ' pages)' };
} ) );
function updateProperties( initial ) {
var schema = val( inputs.schema ), info;
setOptions( inputs.property, propertyOptions( data, data.schemas[ schema ] ? [ schema ] : data.names ) );
if ( initial !== true && data.schemas[ schema ] && val( inputs.property ) ) {
info = resolveProperty( data, val( inputs.property ) );
if ( !info || info.schema !== schema ) { inputs.property.setValue( '' ); }
}
}
function updateValues( initial ) {
var property = val( inputs.property );
if ( initial !== true && inputs.value ) { inputs.value.setValue( '' ); }
setOptions( inputs.value, [] );
loadValues( data, property ).then( function ( list ) {
if ( val( inputs.property ) === property ) { setOptions( inputs.value, list ); }
} );
}
updateProperties( true );
updateValues( true );
if ( inputs.schema ) { inputs.schema.on( 'change', OO.ui.debounce( updateProperties, 300 ) ); }
if ( inputs.property ) { inputs.property.on( 'change', OO.ui.debounce( updateValues, 400 ) ); }
} );
}
function createDroplet( fields, formItems, setup ) {
var Droplet = function ( cfg ) { Droplet.parent.call( this, cfg ); };
OO.inheritClass( Droplet, ext.contentdroplets.object.TransclusionDroplet );
Droplet.prototype.getFormItems = function () { return formItems; };
Droplet.prototype.getForm = function ( data ) {
var form = Droplet.parent.prototype.getForm.call( this, data );
form.connect( this, { initComplete: function ( f ) { setup( f.getItems().inputs || {} ); } } );
return form;
};
Droplet.prototype.updateMWData = function ( newData, mwData ) {
var template = mwData.parts && mwData.parts[ 0 ] && mwData.parts[ 0 ].template;
newData = newData || {};
if ( !template ) { return mwData; }
template.params = template.params || {};
fields.forEach( function ( key ) { var value = newData[ key ]; if ( value === undefined || value === null || String( value ).trim() === '' ) { delete template.params[ key ]; return; } template.params[ key ] = { wt: String( value ) }; } );
return mwData;
};
Droplet.prototype.convertDataForForm = function ( data ) {
var out = {}, key;
for ( key in data ) { if ( Object.prototype.hasOwnProperty.call( data, key ) ) { out[ key ] = ( data[ key ] && typeof data[ key ] === 'object' ) ? data[ key ].wt : data[ key ]; } }
return out;
};
return Droplet;
}
ext.contentdroplets.registry.register( 'metadata-value', createDroplet( [ 'page', 'property', 'dateformat' ], [
{ name: 'page', type: 'combobox', options: [], label: 'Page (leave empty for the current page)' },
{ name: 'property', type: 'combobox', options: [], required: true, label: 'Property (Schema.Property)' },
{ name: 'dateformat', type: 'combobox', label: 'Date format (dates only)', options: [
{ data: 'd.m.Y', label: 'd.m.Y (31.12.2027)' },
{ data: 'j. F Y', label: 'j. F Y (31. December 2027)' },
{ data: 'Y-m-d', label: 'Y-m-d (2027-12-31)' },
{ data: 'd/m/Y', label: 'd/m/Y (31/12/2027)' },
{ data: 'm/d/Y', label: 'm/d/Y (12/31/2027)' },
{ data: 'F j, Y', label: 'F j, Y (December 31, 2027)' },
{ data: 'M Y', label: 'M Y (Dec 2027)' }
] }
], setupValueForm ) );
ext.contentdroplets.registry.register( 'metadata-query', createDroplet(
[ 'schema', 'property', 'operator', 'value', 'format', 'separator', 'display', 'limit' ], [
{ name: 'schema', type: 'combobox', options: [], required: true, label: 'Schema' },
{ name: 'property', type: 'combobox', options: [], label: 'Filter property (optional)' },
{ name: 'operator', type: 'dropdown', value: '=', label: 'Operator', options: [
{ data: '=', label: '= (equals)' }, { data: '!=', label: '!= (not equal)' },
{ data: '>', label: '> (greater than)' }, { data: '>=', label: '>= (greater or equal)' },
{ data: '<', label: '< (less than)' }, { data: '<=', label: '<= (less or equal)' },
{ data: '~', label: '~ (contains)' }, { data: '^', label: '^ (starts with)' },
{ data: 'IS NOT NULL', label: 'has a value' }, { data: 'IS NULL', label: 'has no value' } ] },
{ name: 'value', type: 'combobox', options: [], label: 'Value' },
{ name: 'format', type: 'dropdown', value: 'count', label: 'Format', options: [ { data: 'count', label: 'Count' }, { data: 'list', label: 'List' } ] },
{ name: 'separator', type: 'combobox', label: 'Separator (list only, default: comma)', options: [
{ data: ', ', label: 'Comma' }, { data: '; ', label: 'Semicolon' },
{ data: ' · ', label: 'Middle dot' }, { data: ' {{!}} ', label: 'Vertical bar' },
{ data: '<br />', label: 'New line' } ] },
{ name: 'display', type: 'dropdown', value: 'subpage', label: 'Page name display (list only)', options: [ { data: 'subpage', label: 'Subpage name only' }, { data: 'text', label: 'Page name' }, { data: 'prefixed', label: 'Page name with namespace' } ] },
{ name: 'limit', type: 'number', value: 50, label: 'Limit' } ], setupQueryForm ) );
}() );