mirror of
https://github.com/Universal-Omega/PortableInfobox.git
synced 2024-11-13 18:36:54 +00:00
Add infobox builder (#37)
This commit is contained in:
parent
4a0bac951a
commit
25c6c90bae
|
@ -7,7 +7,7 @@
|
|||
],
|
||||
"url": "https://github.com/Universal-Omega/PortableInfobox",
|
||||
"descriptionmsg": "portable-infobox-desc",
|
||||
"version": "0.5",
|
||||
"version": "0.6",
|
||||
"type": "parserhook",
|
||||
"license-name": "GPL-3.0-or-later",
|
||||
"requires": {
|
||||
|
@ -53,6 +53,42 @@
|
|||
"desktop",
|
||||
"mobile"
|
||||
]
|
||||
},
|
||||
"ext.PortableInfoboxBuilder": {
|
||||
"scripts": [
|
||||
"resources/PortableInfoboxBuilderNodes.js",
|
||||
"resources/PortableInfoboxBuilder.js"
|
||||
],
|
||||
"styles": "resources/PortableInfoboxBuilder.less",
|
||||
"dependencies": [
|
||||
"mediawiki.jqueryMsg",
|
||||
"jquery.ui",
|
||||
"oojs-ui-core",
|
||||
"oojs-ui-widgets",
|
||||
"oojs-ui-windows"
|
||||
],
|
||||
"messages": [
|
||||
"confirmable-confirm",
|
||||
"infoboxbuilder-action-addnode",
|
||||
"infoboxbuilder-action-clear",
|
||||
"infoboxbuilder-action-deletenode",
|
||||
"infoboxbuilder-action-publish",
|
||||
"infoboxbuilder-editerror",
|
||||
"infoboxbuilder-editerrorunknown",
|
||||
"infoboxbuilder-editsummary",
|
||||
"infoboxbuilder-node-title",
|
||||
"infoboxbuilder-node-title-value",
|
||||
"infoboxbuilder-node-title-value-pagename",
|
||||
"infoboxbuilder-node-data",
|
||||
"infoboxbuilder-node-data-value-source",
|
||||
"infoboxbuilder-node-media",
|
||||
"infoboxbuilder-nodeerror-invalidsource",
|
||||
"infoboxbuilder-nodeerror-nosourceordefault",
|
||||
"infoboxbuilder-nodeparam-default",
|
||||
"infoboxbuilder-nodeparam-label",
|
||||
"infoboxbuilder-nodeparam-source",
|
||||
"infoboxbuilder-templatename"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
|
@ -73,7 +109,8 @@
|
|||
"ApiPortableInfobox": "includes/controllers/ApiPortableInfobox.php",
|
||||
"ApiQueryPortableInfobox": "includes/controllers/ApiQueryPortableInfobox.php",
|
||||
"ApiQueryAllInfoboxes": "includes/controllers/ApiQueryAllInfoboxes.php",
|
||||
"AllInfoboxesQueryPage": "includes/querypage/AllInfoboxesQueryPage.php"
|
||||
"AllInfoboxesQueryPage": "includes/querypage/AllInfoboxesQueryPage.php",
|
||||
"SpecialPortableInfoboxBuilder": "includes/specials/SpecialPortableInfoboxBuilder.php"
|
||||
},
|
||||
"Hooks": {
|
||||
"ParserFirstCallInit": "PortableInfoboxParserTagController::parserTagInit",
|
||||
|
@ -85,7 +122,8 @@
|
|||
"ResourceLoaderRegisterModules": "PortableInfoboxHooks::onResourceLoaderRegisterModules"
|
||||
},
|
||||
"SpecialPages": {
|
||||
"AllInfoboxes": "AllInfoboxesQueryPage"
|
||||
"AllInfoboxes": "AllInfoboxesQueryPage",
|
||||
"InfoboxBuilder": "SpecialPortableInfoboxBuilder"
|
||||
},
|
||||
"APIModules": {
|
||||
"infobox": "ApiPortableInfobox"
|
||||
|
|
22
i18n/en.json
22
i18n/en.json
|
@ -20,5 +20,25 @@
|
|||
"apihelp-infobox-param-args": "Variable list to use during parse (json format)",
|
||||
"apihelp-query+allinfoboxes-summary": "List all infoboxes",
|
||||
"apihelp-query+infobox-summary": "Get infobox metadata",
|
||||
"apiwarn-infobox-invalidargs": "Args param format is incorrect"
|
||||
"apiwarn-infobox-invalidargs": "Args param format is incorrect",
|
||||
"infoboxbuilder": "Infobox builder",
|
||||
"infoboxbuilder-action-addnode": "Add infobox elements",
|
||||
"infoboxbuilder-action-clear": "Clear infobox",
|
||||
"infoboxbuilder-action-deletenode": "Delete element",
|
||||
"infoboxbuilder-action-publish": "Publish infobox",
|
||||
"infoboxbuilder-editerror": "An error occured during infobox publishing: $1",
|
||||
"infoboxbuilder-editerrorunknown": "An unknown error occured during infobox publishing.",
|
||||
"infoboxbuilder-editsummary": "Infobox created with infobox builder.",
|
||||
"infoboxbuilder-node-title": "Title",
|
||||
"infoboxbuilder-node-title-value": "Infobox title",
|
||||
"infoboxbuilder-node-title-value-pagename": "Page name",
|
||||
"infoboxbuilder-node-data": "Data",
|
||||
"infoboxbuilder-node-data-value-source": "Value of $1",
|
||||
"infoboxbuilder-node-media": "Image",
|
||||
"infoboxbuilder-nodeerror-invalidsource": "Source parameter is invalid.",
|
||||
"infoboxbuilder-nodeerror-nosourceordefault": "Element without source parameter or default value won't be displayed.",
|
||||
"infoboxbuilder-nodeparam-default": "Default value",
|
||||
"infoboxbuilder-nodeparam-label": "Label",
|
||||
"infoboxbuilder-nodeparam-source": "Source parameter",
|
||||
"infoboxbuilder-templatename": "Template name"
|
||||
}
|
||||
|
|
22
i18n/pl.json
22
i18n/pl.json
|
@ -20,5 +20,25 @@
|
|||
"apihelp-infobox-param-args": "Lista parametrów do użycia przy parsowaniu (w formacie JSON)",
|
||||
"apihelp-query+allinfoboxes-summary": "Wymień wszystkie infoboksy",
|
||||
"apihelp-query+infobox-summary": "Pobierz metadane infoboksu",
|
||||
"apiwarn-infobox-invalidargs": "Nieprawidłowy format parametru args"
|
||||
"apiwarn-infobox-invalidargs": "Nieprawidłowy format parametru args",
|
||||
"infoboxbuilder": "Kreator infoboksów",
|
||||
"infoboxbuilder-action-addnode": "Dodaj element infoboksu",
|
||||
"infoboxbuilder-action-clear": "Wyczyść infoboks",
|
||||
"infoboxbuilder-action-deletenode": "Usuń element",
|
||||
"infoboxbuilder-action-publish": "Opublikuj infoboks",
|
||||
"infoboxbuilder-editerror": "Wystąpił błąd podczas publikowania infoboksu: $1",
|
||||
"infoboxbuilder-editerrorunknown": "Wystąpił nieznany błąd podczas publikowania infoboksu.",
|
||||
"infoboxbuilder-editsummary": "Infoboks utworzony przy użyciu kreatora infoboksów.",
|
||||
"infoboxbuilder-node-title": "Tytuł",
|
||||
"infoboxbuilder-node-title-value": "Tytuł infoboksu",
|
||||
"infoboxbuilder-node-title-value-pagename": "Tytuł strony",
|
||||
"infoboxbuilder-node-data": "Wiersz",
|
||||
"infoboxbuilder-node-data-value-source": "Wartość $1",
|
||||
"infoboxbuilder-node-media": "Obraz",
|
||||
"infoboxbuilder-nodeerror-invalidsource": "Parametr zawiera niedozwolone znaki.",
|
||||
"infoboxbuilder-nodeerror-nosourceordefault": "Element bez parametru lub domyślnej wartości nie będzie się wyświetlał.",
|
||||
"infoboxbuilder-nodeparam-default": "Domyślna wartosć",
|
||||
"infoboxbuilder-nodeparam-label": "Etykieta",
|
||||
"infoboxbuilder-nodeparam-source": "Parametr",
|
||||
"infoboxbuilder-templatename": "Nazwa szablonu"
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ $specialPageAliases = [];
|
|||
|
||||
/** English **/
|
||||
$specialPageAliases['en'] = [
|
||||
'AllInfoboxes' => [ 'AllInfoboxes' ]
|
||||
'AllInfoboxes' => [ 'AllInfoboxes' ],
|
||||
'InfoboxBuilder' => [ 'InfoboxBuilder' ]
|
||||
];
|
||||
|
||||
/** Polish */
|
||||
$specialPageAliases['pl'] = [
|
||||
'AllInfoboxes' => [ 'Wszystkie infoboksy' ]
|
||||
'AllInfoboxes' => [ 'Wszystkie infoboksy' ],
|
||||
'InfoboxBuilder' => [ 'Kreator infoboksów' ]
|
||||
];
|
||||
|
|
22
includes/specials/SpecialPortableInfoboxBuilder.php
Normal file
22
includes/specials/SpecialPortableInfoboxBuilder.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
class SpecialPortableInfoboxBuilder extends SpecialPage {
|
||||
public function __construct() {
|
||||
parent::__construct( 'InfoboxBuilder' );
|
||||
$this->mRestriction = $this->getConfig()->get( 'NamespaceProtection' )[NS_TEMPLATE] ?? '';
|
||||
}
|
||||
|
||||
public function execute( $par ) {
|
||||
$out = $this->getOutput();
|
||||
|
||||
$this->setHeaders();
|
||||
$out->enableOOUI();
|
||||
|
||||
$out->addModules( [ 'ext.PortableInfobox.styles', 'ext.PortableInfoboxBuilder' ] );
|
||||
$out->addHTML(
|
||||
'<div id="mw-infoboxbuilder" data-title="' . str_replace( '"', '"', $par ) . '">' .
|
||||
new OOUI\ProgressBarWidget( [ 'progress' => false ] ) .
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
}
|
284
resources/PortableInfoboxBuilder.js
Normal file
284
resources/PortableInfoboxBuilder.js
Normal file
|
@ -0,0 +1,284 @@
|
|||
(function (window, $) {
|
||||
'use strict';
|
||||
|
||||
const CLASS_ITEMSELECTED = 'pi-ib-itemselected';
|
||||
const MSG_PREFIX = 'infoboxbuilder-';
|
||||
|
||||
const XSL_STYLESHEET = new DOMParser().parseFromString(
|
||||
'<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">\
|
||||
<xsl:output indent="yes" omit-xml-declaration="yes"/>\
|
||||
<xsl:template match="node()|@*">\
|
||||
<xsl:copy>\
|
||||
<xsl:apply-templates select="node()|@*"/>\
|
||||
</xsl:copy>\
|
||||
</xsl:template>\
|
||||
</xsl:stylesheet>',
|
||||
'text/xml'
|
||||
);
|
||||
|
||||
window.mediaWiki.PortableInfoboxBuilder = window.mediaWiki.PortableInfoboxBuilder || {};
|
||||
var Nodes = window.mediaWiki.PortableInfoboxBuilder.Nodes;
|
||||
|
||||
class PortableInfoboxBuilder {
|
||||
constructor( config ) {
|
||||
this.config = config;
|
||||
|
||||
this.api = new mw.Api();
|
||||
this.xmlSerializer = new XMLSerializer();
|
||||
|
||||
if ( window.XSLTProcessor ) {
|
||||
this.xsltProcessor = new XSLTProcessor();
|
||||
this.xsltProcessor.importStylesheet( XSL_STYLESHEET );
|
||||
}
|
||||
|
||||
this.infobox = new Nodes.NodeInfobox( this );
|
||||
this.infobox.children = [
|
||||
Nodes.Node.factory( this.infobox.markupDoc, 'title' ),
|
||||
Nodes.Node.factory( this.infobox.markupDoc, 'media' ),
|
||||
Nodes.Node.factory( this.infobox.markupDoc, 'data' ),
|
||||
Nodes.Node.factory( this.infobox.markupDoc, 'data' )
|
||||
];
|
||||
|
||||
mw.hook( 'portableinfoboxbuilder.nodeselect' ).add( ( node ) => this.select( node ) );
|
||||
this.$element = this.buildUI();
|
||||
}
|
||||
|
||||
msg( key, params = [], optional = false ) {
|
||||
if ( !( params instanceof Array ) ) {
|
||||
params = [ params ];
|
||||
}
|
||||
params.unshift( MSG_PREFIX + key );
|
||||
|
||||
let msg = mw.message.apply( mw, params );
|
||||
|
||||
if ( optional && !msg.exists() ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return msg.text()
|
||||
}
|
||||
|
||||
buildUI() {
|
||||
var menuLayout = new OO.ui.MenuLayout( { menuPosition: 'after' } ),
|
||||
self = this;
|
||||
|
||||
menuLayout.$menu.append(
|
||||
this.buildNewNodesMenu(),
|
||||
this.buildNodeMenu(),
|
||||
this.buildActionsMenu()
|
||||
);
|
||||
menuLayout.$content.append(
|
||||
new OO.ui.PanelLayout( { padded: true } ).$element.append(
|
||||
this.infobox.html()
|
||||
).click( function( e ) {
|
||||
if( e.target != this ) { return; }
|
||||
self.deselect()
|
||||
} )
|
||||
);
|
||||
|
||||
this.$element = new OO.ui.PanelLayout( {
|
||||
framed: true,
|
||||
expanded: false,
|
||||
id: 'mw-infoboxbuilder'
|
||||
} ).$element.append( menuLayout.$element );
|
||||
|
||||
return this.$element;
|
||||
}
|
||||
|
||||
buildActionsMenu() {
|
||||
return new OO.ui.PanelLayout( { padded: true, expanded: false } ).$element.append(
|
||||
new OO.ui.ButtonWidget( {
|
||||
label: this.msg( 'action-clear' ),
|
||||
icon: 'trash',
|
||||
flags: [ 'primary', 'destructive' ]
|
||||
} ).on( 'click', () => { this.clearInfobox() } ).$element,
|
||||
new OO.ui.ButtonWidget( {
|
||||
label: this.msg( 'action-publish' ),
|
||||
icon: 'check',
|
||||
flags: [ 'primary', 'progressive' ]
|
||||
} ).on( 'click', () => { this.publishInfobox() } ).$element
|
||||
)
|
||||
}
|
||||
|
||||
buildNewNodesMenu() {
|
||||
let menu = new OO.ui.PanelLayout( { padded: true, expanded: false } ),
|
||||
$menu = menu.$element.append(
|
||||
new OO.ui.LabelWidget( { label: this.msg( 'action-addnode' ) } ).$element
|
||||
);
|
||||
|
||||
Nodes.NODE_LIST.forEach( ( e ) => {
|
||||
$menu.append(
|
||||
new OO.ui.ButtonWidget( { label: this.msg( 'node-' + e ) } )
|
||||
.on( 'click', () => this.addInfoboxElem( e ) )
|
||||
.$element
|
||||
)
|
||||
} );
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
buildNodeMenu() {
|
||||
this.nodeInputSource = new OO.ui.TextInputWidget( {
|
||||
placeholder: this.msg( 'nodeparam-source' ),
|
||||
disabled: true
|
||||
} );
|
||||
this.nodeInputLabel = new OO.ui.TextInputWidget( {
|
||||
placeholder: this.msg( 'nodeparam-label' ),
|
||||
disabled: true
|
||||
} );
|
||||
this.nodeInputDefault = new OO.ui.TextInputWidget( {
|
||||
placeholder: this.msg( 'nodeparam-default' ),
|
||||
disabled: true
|
||||
} );
|
||||
this.deleteNodeButton = new OO.ui.ButtonWidget( {
|
||||
label: this.msg( 'action-deletenode' ),
|
||||
icon: 'trash',
|
||||
disabled: true
|
||||
} ).on( 'click', () => this.deleteSelectedNode() );
|
||||
|
||||
return new OO.ui.PanelLayout( { padded: true, expanded: false } ).$element.append(
|
||||
new OO.ui.FieldLayout( this.nodeInputSource, {
|
||||
label: this.msg( 'nodeparam-source' ),
|
||||
align: 'top',
|
||||
help: this.msg( 'nodeparamhelp-source', [], true ),
|
||||
disabled: true
|
||||
} ).$element,
|
||||
new OO.ui.FieldLayout( this.nodeInputLabel, {
|
||||
label: this.msg( 'nodeparam-label' ),
|
||||
align: 'top',
|
||||
help: this.msg( 'nodeparamhelp-label', [], true ),
|
||||
disabled: true
|
||||
} ).$element,
|
||||
new OO.ui.FieldLayout( this.nodeInputDefault, {
|
||||
label: this.msg( 'nodeparam-default' ),
|
||||
align: 'top',
|
||||
help: this.msg( 'nodeparamhelp-default', [], true ),
|
||||
disabled: true
|
||||
} ).$element,
|
||||
this.deleteNodeButton.$element
|
||||
)
|
||||
}
|
||||
|
||||
toggleNodeMenu( supports = false ) {
|
||||
if ( supports ) {
|
||||
this.deleteNodeButton.setDisabled( false );
|
||||
} else {
|
||||
this.deleteNodeButton.setDisabled( true );
|
||||
supports = {};
|
||||
}
|
||||
|
||||
this.toggleNodeMenuWidget( this.nodeInputSource, supports.source, 'source' );
|
||||
this.toggleNodeMenuWidget( this.nodeInputLabel, supports.label, 'label' );
|
||||
this.toggleNodeMenuWidget( this.nodeInputDefault, supports.default, 'default' );
|
||||
}
|
||||
|
||||
toggleNodeMenuWidget( widget, enabled, param ) {
|
||||
widget.off( 'change' ).setDisabled( !enabled );
|
||||
|
||||
if ( enabled ) {
|
||||
widget
|
||||
.setValue( this.selectedNode.params[param] )
|
||||
.on( 'change', ( value ) => this.selectedNode.changeParam( param, value ) );
|
||||
}
|
||||
}
|
||||
|
||||
addInfoboxElem( type ) {
|
||||
this.deselect();
|
||||
this.infobox.createChildren( type );
|
||||
}
|
||||
|
||||
select( node ) {
|
||||
if( this.selectedNode !== undefined ) {
|
||||
this.selectedNode.deselect();
|
||||
}
|
||||
this.selectedNode = node;
|
||||
this.toggleNodeMenu( node.supports() );
|
||||
this.infobox.element.classList.add( CLASS_ITEMSELECTED );
|
||||
}
|
||||
|
||||
deselect() {
|
||||
if( this.selectedNode === undefined ) {
|
||||
return;
|
||||
}
|
||||
this.selectedNode.deselect();
|
||||
this.selectedNode = undefined;
|
||||
this.toggleNodeMenu();
|
||||
this.infobox.element.classList.remove( CLASS_ITEMSELECTED );
|
||||
}
|
||||
|
||||
deleteSelectedNode() {
|
||||
this.infobox.removeChildren( this.selectedNode );
|
||||
this.deselect();
|
||||
}
|
||||
|
||||
clearInfobox() {
|
||||
OO.ui.confirm( mw.message( 'confirmable-confirm', mw.user.getName() ).text() )
|
||||
.done( ( confirmed ) => {
|
||||
if ( confirmed ) {
|
||||
this.deselect();
|
||||
this.infobox.clearChildren();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getInfoboxMarkup() {
|
||||
let markup = this.infobox.markup();
|
||||
|
||||
if ( this.xsltProcessor ) {
|
||||
let transformed = this.xsltProcessor.transformToDocument( markup );
|
||||
if ( transformed ) {
|
||||
return this.xmlSerializer.serializeToString( transformed )
|
||||
.replace( /(?<=^ *) /mg, '\t' );
|
||||
}
|
||||
}
|
||||
|
||||
console.log( this.xmlSerializer.serializeToString( markup ) );
|
||||
return this.xmlSerializer.serializeToString( markup );
|
||||
}
|
||||
|
||||
publishInfobox() {
|
||||
console.log( this.getInfoboxMarkup() );
|
||||
OO.ui.prompt( this.msg( 'templatename' ), {
|
||||
size: 'large',
|
||||
textInput: {
|
||||
placeholder: this.msg( 'templatename' ),
|
||||
value: this.config.title,
|
||||
required: true
|
||||
}
|
||||
} ).done( ( title ) => {
|
||||
if ( title === null || title.trim() === "" ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.api.postWithToken( 'csrf', {
|
||||
action: 'edit',
|
||||
title: title,
|
||||
text: this.getInfoboxMarkup(),
|
||||
summary: this.msg( 'editsummary' ),
|
||||
notminor: true
|
||||
} ).done( () => {
|
||||
window.location.assign( mw.config.get( 'wgArticlePath' ).replace( '$1', title ) )
|
||||
} ).fail( ( code, err ) => {
|
||||
OO.ui.alert(
|
||||
err.error && err.error.info ? this.msg( 'editerror', err.error.info ) :
|
||||
this.msg( 'editerrorunknown' ),
|
||||
{ size: 'large' }
|
||||
);
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
window.mediaWiki.PortableInfoboxBuilder.Builder = PortableInfoboxBuilder;
|
||||
|
||||
mw.loader.using( 'oojs-ui-core' ).done( function() {
|
||||
let content = document.getElementById( 'mw-infoboxbuilder' );
|
||||
if ( content ) {
|
||||
let builder = new PortableInfoboxBuilder( {
|
||||
title: content.dataset.title
|
||||
} );
|
||||
content.replaceWith( builder.$element[0] );
|
||||
}
|
||||
} );
|
||||
})(window, jQuery);
|
170
resources/PortableInfoboxBuilder.less
Normal file
170
resources/PortableInfoboxBuilder.less
Normal file
|
@ -0,0 +1,170 @@
|
|||
#mw-infoboxbuilder {
|
||||
min-height: 700px;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.oo-ui-menuLayout-content {
|
||||
/* dirty fix for node warnings/errors */
|
||||
left: -200px !important;
|
||||
overflow-y: auto;
|
||||
|
||||
.oo-ui-panelLayout {
|
||||
left: 200px;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.oo-ui-menuLayout-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid #c8ccd1;
|
||||
|
||||
> :nth-child(2) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .oo-ui-buttonElement {
|
||||
margin-top: auto;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #c8ccd1;
|
||||
}
|
||||
|
||||
&:not(.oo-ui-popupButtonWidget) > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oo-ui-buttonElement-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .oo-ui-buttonElement:not(:last-child) .oo-ui-buttonElement-button {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
> .oo-ui-buttonElement + .oo-ui-buttonElement .oo-ui-buttonElement-button {
|
||||
border-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.oo-ui-fieldLayout-disabled,
|
||||
.oo-ui-widget-disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.portable-infobox {
|
||||
float: none;
|
||||
margin: auto;
|
||||
user-select: none;
|
||||
|
||||
.pi-item {
|
||||
background: inherit;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -1px;
|
||||
position: relative;
|
||||
transition: outline-color .2s;
|
||||
z-index: 0;
|
||||
|
||||
&:hover {
|
||||
outline-color: #ccc;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.pi-ib-selected {
|
||||
box-shadow: 0 0 20px #aaa;
|
||||
outline: 2px solid #36c;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&.pi-ib-warning,
|
||||
&.pi-ib-error {
|
||||
overflow: visible;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.pi-ib-error {
|
||||
outline-color: #f00;
|
||||
}
|
||||
|
||||
.pi-ib-error-message {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
position: absolute;
|
||||
top: ~"calc( 50% - 14px )";
|
||||
right: ~"calc( 100% + 18px )";
|
||||
min-height: 20px;
|
||||
filter: drop-shadow( 0 2px 2px rgba( 0, 0, 0, 0.25 ) );
|
||||
text-align: left;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
border-left: 14px solid #f00;
|
||||
border-top: 14px solid transparent;
|
||||
border-bottom: 14px solid transparent;
|
||||
}
|
||||
|
||||
> .oo-ui-iconElement {
|
||||
background-color: #f00;
|
||||
background-size: 20px;
|
||||
padding: 4px 6px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
transition: padding-left .1s;
|
||||
}
|
||||
|
||||
> .oo-ui-labelElement {
|
||||
flex: 1;
|
||||
background: #f00;
|
||||
color: #fff;
|
||||
font-size: 0;
|
||||
line-height: 22px;
|
||||
width: 0;
|
||||
min-height: 20px;
|
||||
max-height: 28px;
|
||||
transition: font-size, width, max-height .1s;
|
||||
}
|
||||
}
|
||||
|
||||
&.pi-ib-warning .pi-ib-error-message {
|
||||
> * {
|
||||
background-color: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-left-color: #ff0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.pi-ib-itemselected) .pi-item:hover .pi-ib-error-message,
|
||||
& .pi-item.pi-ib-selected .pi-ib-error-message {
|
||||
> .oo-ui-iconElement {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
> .oo-ui-labelElement {
|
||||
padding: 4px 6px;
|
||||
max-height: 175px;
|
||||
font-size: 14px;
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
435
resources/PortableInfoboxBuilderNodes.js
Normal file
435
resources/PortableInfoboxBuilderNodes.js
Normal file
|
@ -0,0 +1,435 @@
|
|||
(function (window, $) {
|
||||
'use strict';
|
||||
|
||||
const MSG_PREFIX = 'infoboxbuilder-';
|
||||
const NODE_ATTRIBUTES = [
|
||||
'accent-color-default',
|
||||
'accent-color-source',
|
||||
'accent-color-text-default',
|
||||
'accent-color-text-source',
|
||||
'audio',
|
||||
'image',
|
||||
'layout',
|
||||
'source',
|
||||
'span',
|
||||
'theme',
|
||||
'theme-source',
|
||||
'video'
|
||||
];
|
||||
const NODE_CONTENTNODES = [
|
||||
'label',
|
||||
'format',
|
||||
'default'
|
||||
];
|
||||
const NODE_IDPREFIX = 'pi-ib-node-';
|
||||
const NODE_CLASSSELECTED = 'pi-ib-selected';
|
||||
|
||||
var nodeId = 0,
|
||||
dataId = 0;
|
||||
|
||||
class NodeValidationError {
|
||||
constructor( message ) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
render() {
|
||||
let msg = document.createElement( 'div' );
|
||||
|
||||
msg.className = 'pi-ib-error-message';
|
||||
msg.appendChild( new OO.ui.LabelWidget( { label: this.message } ).$element[0] );
|
||||
msg.appendChild( this.getIcon() );
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
let icon = new OO.ui.IconWidget( { icon: 'alert', flags: 'primary' } );
|
||||
// trigger inverted icon variant
|
||||
icon.isFramed = () => true;
|
||||
|
||||
return icon.$element[0];
|
||||
}
|
||||
|
||||
getClass() {
|
||||
return 'pi-ib-error';
|
||||
}
|
||||
}
|
||||
|
||||
class NodeValidationWarning extends NodeValidationError {
|
||||
getIcon() {
|
||||
return new OO.ui.IconWidget( { icon: 'alert' } ).$element[0];
|
||||
}
|
||||
getClass() {
|
||||
return 'pi-ib-warning';
|
||||
}
|
||||
}
|
||||
|
||||
class PINode {
|
||||
constructor( markupDoc, params ) {
|
||||
if ( this.constructor === PINode ) {
|
||||
throw new TypeError( 'Use Node.factory() instead, "Node" is an abstract class.' );
|
||||
}
|
||||
|
||||
this.elementClasses = 'pi-item ';
|
||||
this.elementSelectable = true;
|
||||
this.elementTag = 'div';
|
||||
this.markupContentTag = false;
|
||||
this.markupDoc = markupDoc;
|
||||
this.markupTag = undefined;
|
||||
this.id = nodeId++;
|
||||
this.params = params || this.getDefaultParams();
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
static factory( markupDoc, type, params ) {
|
||||
switch ( type ) {
|
||||
case 'data':
|
||||
return new NodeData( markupDoc, params );
|
||||
case 'title':
|
||||
return new NodeTitle( markupDoc, params );
|
||||
case 'media':
|
||||
return new NodeMedia( markupDoc, params );
|
||||
case 'infobox':
|
||||
throw new TypeError( 'Use new NodeInfobox() instead.' );
|
||||
default:
|
||||
throw new TypeError( 'Unknown node type "' + type + '"' );
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultParams() {
|
||||
return {};
|
||||
}
|
||||
|
||||
html() {
|
||||
if( this.element ) {
|
||||
this.element.innerHTML = '';
|
||||
} else {
|
||||
let element = document.createElement( this.elementTag );
|
||||
element.id = NODE_IDPREFIX + this.id;
|
||||
|
||||
if ( this.elementSelectable ) {
|
||||
element.onmousedown = () => this.select();
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
this.element.className = this.elementClasses;
|
||||
|
||||
if ( this.selected && this.elementSelectable ) {
|
||||
this.element.classList.add( NODE_CLASSSELECTED );
|
||||
}
|
||||
|
||||
try {
|
||||
this.validate();
|
||||
} catch ( error ) {
|
||||
if ( error instanceof NodeValidationError ) {
|
||||
this.element.classList.add( error.getClass() );
|
||||
this.element.appendChild( error.render() );
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
markup() {
|
||||
let node = this.markupDoc.createElement( this.markupTag ),
|
||||
supports = this.supports();
|
||||
|
||||
NODE_ATTRIBUTES.forEach( ( a ) => {
|
||||
if ( supports[a] && this.params[a] ) {
|
||||
node.setAttribute( a, this.params[a] );
|
||||
}
|
||||
} );
|
||||
|
||||
if ( this.markupContentTag && this.params.value ) {
|
||||
node.appendChild( this.markupDoc.createTextNode( this.params.value ) );
|
||||
} else {
|
||||
NODE_CONTENTNODES.forEach( ( n ) => {
|
||||
if ( supports[n] && this.params[n] ) {
|
||||
let subnode = this.markupDoc.createElement( n );
|
||||
subnode.appendChild( this.markupDoc.createTextNode( this.params[n] ) );
|
||||
node.appendChild( subnode );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
select() {
|
||||
if ( this.elementSelectable ) {
|
||||
mw.hook( 'portableinfoboxbuilder.nodeselect' ).fire( this );
|
||||
this.selected = true;
|
||||
this.element.classList.add( NODE_CLASSSELECTED );
|
||||
}
|
||||
}
|
||||
|
||||
deselect() {
|
||||
if ( this.elementSelectable ) {
|
||||
this.selected = false;
|
||||
this.element.classList.remove( NODE_CLASSSELECTED );
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
$( this.element ).remove();
|
||||
}
|
||||
|
||||
supports() {
|
||||
return {};
|
||||
}
|
||||
|
||||
validate() {
|
||||
if ( this.params.source.match( /["|={}]/ ) ) {
|
||||
throw new NodeValidationError( this.msg( 'nodeerror-invalidsource' ) );
|
||||
}
|
||||
|
||||
if ( !this.params.source && !this.params.default ) {
|
||||
throw new NodeValidationWarning( this.msg( 'nodeerror-nosourceordefault' ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
changeParam( param, value ) {
|
||||
if ( this.supports()[param] ) {
|
||||
this.params[param] = value;
|
||||
this.html();
|
||||
}
|
||||
}
|
||||
|
||||
msg( key, params ) {
|
||||
if ( !( params instanceof Array ) ) {
|
||||
params = [ params ];
|
||||
}
|
||||
params.unshift( MSG_PREFIX + key );
|
||||
|
||||
return mw.message.apply( mw, params ).text();
|
||||
}
|
||||
}
|
||||
|
||||
class NodeData extends PINode {
|
||||
constructor( markupDoc, params ) {
|
||||
super( markupDoc, params );
|
||||
this.elementClasses += 'pi-data pi-item-spacing pi-border-color';
|
||||
this.markupTag = 'data';
|
||||
}
|
||||
|
||||
getDefaultParams() {
|
||||
return {
|
||||
label: this.msg( 'nodeparam-label' ),
|
||||
source: this.msg( 'node-data' ).toLocaleLowerCase() + ( ++dataId )
|
||||
};
|
||||
}
|
||||
|
||||
html() {
|
||||
super.html();
|
||||
|
||||
if ( this.params.label ) {
|
||||
let label = document.createElement( 'h3' );
|
||||
label.className = 'pi-data-label pi-secondary-font';
|
||||
label.textContent = this.params.label;
|
||||
this.element.appendChild( label );
|
||||
}
|
||||
|
||||
let value = document.createElement( 'div' );
|
||||
value.className = 'pi-data-value pi-font';
|
||||
|
||||
// '{{{$1}}}' in msg throws an error with jqueryMsg enabled
|
||||
value.textContent = this.params.source ?
|
||||
this.msg( 'node-data-value-source', '{{{' + this.params.source + '}}}' ) :
|
||||
this.params.default ? this.params.default : '';
|
||||
|
||||
this.element.appendChild( value );
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
supports() {
|
||||
return {
|
||||
default: true,
|
||||
format: true,
|
||||
label: true,
|
||||
layout: [ 'default' ],
|
||||
source: true,
|
||||
span: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NodeTitle extends PINode {
|
||||
constructor( markupDoc, params ) {
|
||||
super( markupDoc, params );
|
||||
this.elementTag = 'h2';
|
||||
this.elementClasses += 'pi-item-spacing pi-title';
|
||||
this.markupTag = 'title';
|
||||
}
|
||||
|
||||
getDefaultParams() {
|
||||
return {
|
||||
source: this.msg( 'node-title' ).toLowerCase(),
|
||||
default: '{{PAGENAME}}'
|
||||
};
|
||||
}
|
||||
|
||||
html() {
|
||||
super.html();
|
||||
|
||||
this.element.textContent = this.params.default === '{{PAGENAME}}' ?
|
||||
this.msg( 'node-title-value-pagename' ) : this.msg( 'node-title-value' );
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
supports() {
|
||||
return {
|
||||
source: true,
|
||||
format: true,
|
||||
default: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NodeMedia extends PINode {
|
||||
constructor( markupDoc, params ) {
|
||||
super( markupDoc, params );
|
||||
this.elementTag = 'figure';
|
||||
this.elementClasses += 'pi-media pi-image';
|
||||
this.markupTag = 'image';
|
||||
}
|
||||
|
||||
getDefaultParams() {
|
||||
return {
|
||||
source: this.msg( 'node-media' ).toLowerCase()
|
||||
};
|
||||
}
|
||||
|
||||
html() {
|
||||
super.html();
|
||||
|
||||
let a = document.createElement( 'a' ),
|
||||
img = document.createElement( 'img' );
|
||||
|
||||
a.className = 'image image-thumbnail';
|
||||
a.appendChild( img );
|
||||
img.className = 'pi-image-thumbnail';
|
||||
img.src = mw.config.get( 'wgScriptPath' ) + '/resources/assets/mediawiki.png';
|
||||
this.element.appendChild( a );
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
supports() {
|
||||
return {
|
||||
source: true,
|
||||
audio: true,
|
||||
image: true,
|
||||
video: true,
|
||||
alt: true,
|
||||
caption: true,
|
||||
default: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NodeInfobox extends PINode {
|
||||
constructor( params ) {
|
||||
super( document.implementation.createDocument( '', '' ), params );
|
||||
|
||||
this.children = [];
|
||||
this.elementClasses = 'portable-infobox pi-background';
|
||||
this.elementSelectable = false;
|
||||
this.elementTag = 'aside';
|
||||
this.markupTag = 'infobox';
|
||||
}
|
||||
|
||||
addChildren( children ) {
|
||||
if ( children instanceof PINode ) {
|
||||
this.children.push( children );
|
||||
this.element.append( children.html() );
|
||||
}
|
||||
}
|
||||
|
||||
createChildren( type, params ) {
|
||||
this.addChildren( PINode.factory( this.markupDoc, type, params ) );
|
||||
}
|
||||
|
||||
clearChildren() {
|
||||
this.children = [];
|
||||
$( this.element ).empty();
|
||||
dataId = 0;
|
||||
}
|
||||
|
||||
removeChildren( children ) {
|
||||
let i = this.children.indexOf( children );
|
||||
if ( i >= 0 ) {
|
||||
this.children.splice( i, 1 );
|
||||
$( children.element ).remove();
|
||||
}
|
||||
}
|
||||
|
||||
reorderChildren( order, prefixed = false ) {
|
||||
let newChildren = [];
|
||||
this.children.forEach( c => {
|
||||
let i = order.indexOf( ( prefixed ? NODE_IDPREFIX : 0 ) + c.id );
|
||||
if ( i >= 0 ) {
|
||||
newChildren[i] = c;
|
||||
}
|
||||
} );
|
||||
this.children = newChildren;
|
||||
}
|
||||
|
||||
html() {
|
||||
super.html();
|
||||
|
||||
this.children.forEach( c => { this.element.appendChild( c.html() ) } );
|
||||
$( this.element ).sortable( {
|
||||
axis: 'y',
|
||||
containment: 'parent',
|
||||
cursor: 'move',
|
||||
scroll: false,
|
||||
tolerance: 'pointer',
|
||||
deactivate: ( e, ui ) => {
|
||||
ui.item.attr( 'style', '' );
|
||||
this.reorderChildren( $( this.element ).sortable( 'toArray' ), true );
|
||||
}
|
||||
} );
|
||||
|
||||
return this.element;
|
||||
}
|
||||
|
||||
markup() {
|
||||
let node = super.markup();
|
||||
this.children.forEach( c => { node.appendChild( c.markup() ) } );
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
supports() {
|
||||
return {
|
||||
'accent-color-default': true,
|
||||
'accent-color-source': true,
|
||||
'accent-color-text-default': true,
|
||||
'accent-color-text-source': true,
|
||||
'layout': [ 'default', 'stacked' ],
|
||||
'theme': true,
|
||||
'theme-source': true
|
||||
};
|
||||
}
|
||||
|
||||
validate() {}
|
||||
}
|
||||
|
||||
window.mediaWiki.PortableInfoboxBuilder = window.mediaWiki.PortableInfoboxBuilder || {};
|
||||
window.mediaWiki.PortableInfoboxBuilder.Nodes = {
|
||||
Node: PINode,
|
||||
NodeData: NodeData,
|
||||
NodeMedia: NodeMedia,
|
||||
NodeTitle: NodeTitle,
|
||||
NodeInfobox: NodeInfobox,
|
||||
NODE_LIST: [ 'data', 'title', 'media' ]
|
||||
};
|
||||
})(window, jQuery);
|
Loading…
Reference in a new issue