mediawiki-extensions-Math/modules/ve-math/ve.ui.MWLatexDialog.js
Steph Toyofuku 17ac038ae4 Invert mathematical formulas in visual editor
Add the skin-invert class to latex symbols (insert chemical formula and
math formula dialogues) so that they appear legible in night mode.  This
is likely the only way we can accomplish this until mathml is fully
rolled out, at which I believe this will no longer be necessary

Bug: T366737
Change-Id: Ia0dfa4ab684f5205d109da7f9aefad927be70eb0
2024-06-25 14:30:30 -07:00

346 lines
10 KiB
JavaScript

/*!
* VisualEditor user interface MWLatexDialog class.
*
* @copyright See AUTHORS.txt
* @license MIT
*/
/**
* Abstract dialog for inserting and editing different formulas
* provided by the Math extension.
*
* @abstract
* @class
* @extends ve.ui.MWExtensionPreviewDialog
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.MWLatexDialog = function VeUiMWLatexDialog( config ) {
// Parent constructor
ve.ui.MWLatexDialog.super.call( this, config );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWLatexDialog, ve.ui.MWExtensionPreviewDialog );
/* Static properties */
ve.ui.MWLatexDialog.static.size = 'larger';
ve.ui.MWLatexDialog.static.dir = 'ltr';
ve.ui.MWLatexDialog.static.symbolsModule = null;
/* Methods */
/**
* @inheritdoc
*/
ve.ui.MWLatexDialog.prototype.initialize = function () {
// Parent method
ve.ui.MWLatexDialog.super.prototype.initialize.call( this );
// Layout for the formula inserter (formula tab panel) and options form (options tab panel)
this.indexLayout = new OO.ui.IndexLayout();
const formulaTabPanel = new OO.ui.TabPanelLayout( 'formula', {
label: ve.msg( 'math-visualeditor-mwlatexdialog-card-formula' ),
padded: true,
classes: [ 'latex-dialog-formula-panel' ]
} );
const optionsTabPanel = new OO.ui.TabPanelLayout( 'options', {
label: ve.msg( 'math-visualeditor-mwlatexdialog-card-options' ),
padded: true,
classes: [ 'latex-dialog-options-panel' ]
} );
this.indexLayout.addTabPanels( [
formulaTabPanel,
optionsTabPanel
] );
// Layout for symbol picker (menu) and input and preview (content)
this.menuLayout = new OO.ui.MenuLayout( {
menuPosition: 'bottom',
classes: [ 've-ui-mwLatexDialog-menuLayout' ]
} );
this.previewElement.$element.addClass(
've-ui-mwLatexDialog-preview'
);
this.input = new ve.ui.MWAceEditorWidget( {
rows: 1, // This will be recalculated later in onWindowManagerResize
autocomplete: 'live',
autocompleteWordList: this.constructor.static.autocompleteWordList
} ).setLanguage( 'latex' );
this.input.togglePrintMargin( false );
this.displaySelect = new OO.ui.ButtonSelectWidget( {
items: [
new OO.ui.ButtonOptionWidget( {
data: 'default',
icon: 'mathematicsDisplayDefault',
label: ve.msg( 'math-visualeditor-mwlatexinspector-display-default' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'inline',
icon: 'mathematicsDisplayInline',
label: ve.msg( 'math-visualeditor-mwlatexinspector-display-inline' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'block',
icon: 'mathematicsDisplayBlock',
label: ve.msg( 'math-visualeditor-mwlatexinspector-display-block' )
} )
]
} );
this.idInput = new OO.ui.TextInputWidget();
this.qidInput = new mw.widgets.MathWbEntitySelector();
const inputField = new OO.ui.FieldLayout( this.input, {
align: 'top',
classes: [ 'latex-dialog-formula-field' ],
label: ve.msg( 'math-visualeditor-mwlatexdialog-card-formula' )
} );
const displayField = new OO.ui.FieldLayout( this.displaySelect, {
align: 'top',
classes: [ 'latex-dialog-display-field' ],
label: ve.msg( 'math-visualeditor-mwlatexinspector-display' )
} );
const idField = new OO.ui.FieldLayout( this.idInput, {
align: 'top',
classes: [ 'latex-dialog-id-field' ],
label: ve.msg( 'math-visualeditor-mwlatexinspector-id' )
} );
const qidField = new OO.ui.FieldLayout( this.qidInput, {
align: 'top',
classes: [ 'latex-dialog-qid-field' ],
label: ve.msg( 'math-visualeditor-mwlatexinspector-qid' )
} );
const formulaPanel = new OO.ui.PanelLayout( {
scrollable: true,
padded: true
} );
// Layout for the symbol picker
this.bookletLayout = new ve.ui.SymbolListBookletLayout( {
classes: [ 've-ui-mwLatexDialog-symbols' ]
} );
this.pages = [];
this.symbolsPromise = mw.loader.using( this.constructor.static.symbolsModule ).done( ( require ) => {
// eslint-disable-next-line security/detect-non-literal-require
const symbols = require( this.constructor.static.symbolsModule );
const symbolData = {};
for ( const category in symbols ) {
const symbolList = symbols[ category ].filter( ( symbol ) => {
if ( symbol.notWorking || symbol.duplicate ) {
return false;
}
const tex = symbol.tex || symbol.insert;
const classes = [ 've-ui-mwLatexDialog-symbol' ];
classes.push(
've-ui-mwLatexSymbol-' + tex.replace( /[^\w]/g, ( c ) => '_' + c.charCodeAt( 0 ) + '_' )
);
if ( symbol.width ) {
// The following classes are used here:
// * ve-ui-mwLatexDialog-symbol-wide
// * ve-ui-mwLatexDialog-symbol-wider
// * ve-ui-mwLatexDialog-symbol-widest
classes.push( 've-ui-mwLatexDialog-symbol-' + symbol.width );
}
if ( symbol.contain ) {
classes.push( 've-ui-mwLatexDialog-symbol-contain' );
}
if ( symbol.largeLayout ) {
classes.push( 've-ui-mwLatexDialog-symbol-largeLayout' );
}
// T366737 - make sure the symbols appear in night mode
classes.push( 'skin-invert' );
symbol.label = '';
symbol.classes = classes;
return true;
} );
symbolData[ category ] = {
// eslint-disable-next-line mediawiki/msg-doc
label: ve.msg( category ),
symbols: symbolList
};
}
this.bookletLayout.setSymbolData( symbolData );
this.bookletLayout.connect( this, {
choose: 'onSymbolChoose'
} );
// Append everything
formulaPanel.$element.append(
this.previewElement.$element,
inputField.$element
);
this.menuLayout.setMenuPanel( this.bookletLayout );
this.menuLayout.setContentPanel( formulaPanel );
formulaTabPanel.$element.append(
this.menuLayout.$element
);
optionsTabPanel.$element.append(
displayField.$element,
idField.$element,
qidField.$element
);
this.$body
.addClass( 've-ui-mwLatexDialog-content' )
.append( this.indexLayout.$element );
} );
};
/**
* @inheritdoc
*/
ve.ui.MWLatexDialog.prototype.getSetupProcess = function ( data ) {
return ve.ui.MWLatexDialog.super.prototype.getSetupProcess.call( this, data )
.next( () => {
const attributes = this.selectedNode && this.selectedNode.getAttribute( 'mw' ).attrs,
display = attributes && attributes.display || 'default',
id = attributes && attributes.id || '',
qid = attributes && attributes.qid || '',
isReadOnly = this.isReadOnly();
// Populate form
// TODO: This widget is not readable when disabled
this.displaySelect.selectItemByData( display ).setDisabled( isReadOnly );
this.idInput.setValue( id ).setReadOnly( isReadOnly );
this.qidInput.setValue( qid ).setReadOnly( isReadOnly );
// Add event handlers
this.input.on( 'change', this.onChangeHandler );
this.displaySelect.on( 'choose', this.onChangeHandler );
this.idInput.on( 'change', this.onChangeHandler );
this.qidInput.on( 'change', this.onChangeHandler );
} );
};
/**
* @inheritdoc
*/
ve.ui.MWLatexDialog.prototype.getReadyProcess = function ( data ) {
mw.hook( 've.ui.MwLatexDialogReadyProcess' ).fire();
return ve.ui.MWLatexDialog.super.prototype.getReadyProcess.call( this, data )
.next( () => this.symbolsPromise )
.next( () => {
// Resize the input once the dialog has been appended
this.input.adjustSize( true ).focus().moveCursorToEnd();
this.getManager().connect( this, { resize: 'onWindowManagerResize' } );
this.onWindowManagerResize();
} );
};
/**
* @inheritdoc
*/
ve.ui.MWLatexDialog.prototype.getTeardownProcess = function ( data ) {
return ve.ui.MWLatexDialog.super.prototype.getTeardownProcess.call( this, data )
.first( () => {
this.input.off( 'change', this.onChangeHandler );
this.displaySelect.off( 'choose', this.onChangeHandler );
this.idInput.off( 'change', this.onChangeHandler );
this.qidInput.off( 'change', this.onChangeHandler );
this.getManager().disconnect( this );
this.indexLayout.setTabPanel( 'formula' );
this.indexLayout.resetScroll();
this.menuLayout.resetScroll();
this.bookletLayout.resetScroll();
} );
};
/**
* @inheritdoc
*/
ve.ui.MWLatexDialog.prototype.updateMwData = function ( mwData ) {
// Parent method
ve.ui.MWLatexDialog.super.prototype.updateMwData.call( this, mwData );
// Get data from dialog
const display = this.displaySelect.findSelectedItem().getData();
const id = this.idInput.getValue();
const qid = this.qidInput.getValue();
// Update attributes
mwData.attrs.display = display !== 'default' ? display : undefined;
mwData.attrs.id = id || undefined;
mwData.attrs.qid = qid || undefined;
};
/**
* @inheritdoc
*/
ve.ui.MWLatexDialog.prototype.getBodyHeight = function () {
return 600;
};
/**
* Handle the window resize event
*/
ve.ui.MWLatexDialog.prototype.onWindowManagerResize = function () {
this.input.loadingPromise.always( () => {
// Toggle short mode as necessary
// NB a change of mode triggers a transition...
this.menuLayout.$element.toggleClass(
've-ui-mwLatexDialog-menuLayout-short', this.menuLayout.$element.height() < 450
);
// ...So wait for the possible menuLayout transition to finish
setTimeout( () => {
// Give the input the right number of rows to fit the space
const availableSpace = this.menuLayout.$content.height() - this.input.$element.position().top;
// TODO: Compute this line height from the skin
const singleLineHeight = 21;
const border = 1;
const padding = 3;
const borderAndPadding = 2 * ( border + padding );
const maxInputHeight = availableSpace - borderAndPadding;
const minRows = Math.floor( maxInputHeight / singleLineHeight );
this.input.loadingPromise.done( () => {
this.input.setMinRows( minRows );
} ).fail( () => {
this.input.$input.attr( 'rows', minRows );
} );
}, OO.ui.theme.getDialogTransitionDuration() );
} );
};
/**
* Handle a symbol being chosen from the list
*
* @param {Object} symbol
*/
ve.ui.MWLatexDialog.prototype.onSymbolChoose = function ( symbol ) {
if ( this.isReadOnly() ) {
return;
}
const encapsulate = symbol.encapsulate;
if ( encapsulate ) {
const range = this.input.getRange();
if ( range.from === range.to ) {
this.input.insertContent( encapsulate.placeholder );
this.input.selectRange( range.from, range.from + encapsulate.placeholder.length );
}
this.input.encapsulateContent( encapsulate.pre, encapsulate.post );
} else {
const insert = symbol.insert;
this.input.insertContent( insert );
}
};