aboutsummaryrefslogtreecommitdiffstats
path: root/resources/lib/ooui/oojs-ui-core.js
diff options
context:
space:
mode:
Diffstat (limited to 'resources/lib/ooui/oojs-ui-core.js')
-rw-r--r--resources/lib/ooui/oojs-ui-core.js446
1 files changed, 380 insertions, 66 deletions
diff --git a/resources/lib/ooui/oojs-ui-core.js b/resources/lib/ooui/oojs-ui-core.js
index 21ad1d552afc..66a8a1806c39 100644
--- a/resources/lib/ooui/oojs-ui-core.js
+++ b/resources/lib/ooui/oojs-ui-core.js
@@ -1,12 +1,12 @@
/*!
- * OOUI v0.48.2
+ * OOUI v0.48.3
* https://www.mediawiki.org/wiki/OOUI
*
* Copyright 2011–2023 OOUI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2023-10-24T20:22:08Z
+ * Date: 2023-12-06T23:10:27Z
*/
( function ( OO ) {
@@ -559,7 +559,6 @@ OO.ui.msg.messages = {
"ooui-combobox-button-label": "Toggle options",
"ooui-selectfile-button-select": "Select a file",
"ooui-selectfile-button-select-multiple": "Select files",
- "ooui-selectfile-not-supported": "File selection is not supported",
"ooui-selectfile-placeholder": "No file is selected",
"ooui-selectfile-dragdrop-placeholder": "Drop file here",
"ooui-selectfile-dragdrop-placeholder-multiple": "Drop files here",
@@ -6711,12 +6710,11 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
} );
// Initialization
- this.$element
- .addClass( 'oo-ui-popupButtonWidget' )
- .attr( {
- 'aria-haspopup': 'dialog',
- 'aria-owns': this.popup.getElementId()
- } );
+ this.$element.addClass( 'oo-ui-popupButtonWidget' );
+ this.$button.attr( {
+ 'aria-haspopup': 'dialog',
+ 'aria-owns': this.popup.getElementId()
+ } );
this.popup.$element
.addClass( 'oo-ui-popupButtonWidget-popup' )
.attr( {
@@ -7621,15 +7619,15 @@ OO.ui.SelectWidget.prototype.getItemMatcher = function ( query, mode ) {
mode = 'exact';
}
- return function ( item ) {
- var matchText = normalizeForMatching( item.getMatchText() );
-
- if ( normalizedQuery === '' ) {
- // Empty string matches all, except if we are in 'exact'
- // mode, where it doesn't match at all
+ // Empty string matches everything, except in "exact" mode where it matches nothing
+ if ( !normalizedQuery ) {
+ return function () {
return mode !== 'exact';
- }
+ };
+ }
+ return function ( item ) {
+ var matchText = normalizeForMatching( item.getMatchText() );
switch ( mode ) {
case 'exact':
return matchText === normalizedQuery;
@@ -14151,6 +14149,8 @@ OO.ui.NumberInputWidget.prototype.setReadOnly = function () {
*
* @class
* @extends OO.ui.InputWidget
+ * @mixins OO.ui.mixin.RequiredElement
+ * @mixins OO.ui.mixin.PendingElement
*
* @constructor
* @param {Object} [config] Configuration options
@@ -14159,10 +14159,15 @@ OO.ui.NumberInputWidget.prototype.setReadOnly = function () {
* @cfg {string} [placeholder] Text to display when no file is selected.
* @cfg {Object} [button] Config to pass to select file button.
* @cfg {Object|string|null} [icon=null] Icon to show next to file info
+ * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
+ * @cfg {boolean} [buttonOnly=false] Show only the select file button, no info field. Requires
+ * showDropTarget to be false.
+ * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be
+ * true.
+ * @cfg {number} [thumbnailSizeLimit=20] File size limit in MiB above which to not try and show a
+ * preview (for performance).
*/
OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
- var widget = this;
-
config = config || {};
// Construct buttons before parent method is called (calling setDisabled)
@@ -14180,9 +14185,23 @@ OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
config = $.extend( {
accept: null,
placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
- $tabIndexed: this.selectButton.$tabIndexed
+ $tabIndexed: this.selectButton.$tabIndexed,
+ droppable: true,
+ buttonOnly: false,
+ showDropTarget: false,
+ thumbnailSizeLimit: 20
}, config );
+ this.canSetFiles = true;
+ // Support: Safari < 14
+ try {
+ // eslint-disable-next-line no-new
+ new DataTransfer();
+ } catch ( e ) {
+ this.canSetFiles = false;
+ config.droppable = false;
+ }
+
this.info = new OO.ui.SearchInputWidget( {
classes: [ 'oo-ui-selectFileInputWidget-info' ],
placeholder: config.placeholder,
@@ -14204,6 +14223,7 @@ OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
// TODO: Display the required indicator somewhere
indicatorElement: null
}, config ) );
+ OO.ui.mixin.PendingElement.call( this );
// Properties
this.currentFiles = this.filterFiles( this.$input[ 0 ].files || [] );
@@ -14213,25 +14233,10 @@ OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
this.accept = null;
}
this.multiple = !!config.multiple;
+ this.showDropTarget = config.droppable && config.showDropTarget;
+ this.thumbnailSizeLimit = config.thumbnailSizeLimit;
- // Events
- this.info.connect( this, { change: 'onInfoChange' } );
- this.selectButton.$button.on( {
- keypress: this.onKeyPress.bind( this )
- } );
- this.$input.on( {
- change: this.onFileSelected.bind( this ),
- // Support: IE 11
- // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
- // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
- // Since this messes with our custom styling (the file input has large dimensions and this
- // causes the label to scroll out of view), scroll the button back to top. (T192131)
- focus: function () {
- widget.$input.parent().prop( 'scrollTop', 0 );
- }
- } );
- this.connect( this, { change: 'updateUI' } );
-
+ // Initialization
this.fieldLayout = new OO.ui.ActionFieldLayout( this.info, this.selectButton, { align: 'top' } );
this.$input
@@ -14253,9 +14258,70 @@ OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
this.selectButton.$button.append( this.$input );
this.$element
- .addClass( 'oo-ui-selectFileInputWidget' )
+ .addClass( 'oo-ui-selectFileInputWidget oo-ui-selectFileWidget' )
.append( this.fieldLayout.$element );
+ if ( this.showDropTarget ) {
+ this.selectButton.setIcon( 'upload' );
+ this.$element
+ .addClass( 'oo-ui-selectFileInputWidget-dropTarget oo-ui-selectFileWidget-dropTarget' )
+ .on( {
+ click: this.onDropTargetClick.bind( this )
+ } )
+ .append(
+ this.info.$element,
+ this.selectButton.$element,
+ $( '<span>' )
+ .addClass( 'oo-ui-selectFileInputWidget-dropLabel oo-ui-selectFileWidget-dropLabel' )
+ .text( OO.ui.msg(
+ this.multiple ?
+ 'ooui-selectfile-dragdrop-placeholder-multiple' :
+ 'ooui-selectfile-dragdrop-placeholder'
+ ) )
+ );
+ if ( !this.multiple ) {
+ this.$thumbnail = $( '<div>' ).addClass( 'oo-ui-selectFileInputWidget-thumbnail oo-ui-selectFileWidget-thumbnail' );
+ this.setPendingElement( this.$thumbnail );
+ this.$element
+ .addClass( 'oo-ui-selectFileInputWidget-withThumbnail oo-ui-selectFileWidget-withThumbnail' )
+ .prepend( this.$thumbnail );
+ }
+ this.fieldLayout.$element.remove();
+ } else if ( config.buttonOnly ) {
+ // Copy over any classes that may have been added already.
+ // Ensure no events are bound to this.$element before here.
+ this.selectButton.$element
+ .addClass( this.$element.attr( 'class' ) )
+ .addClass( 'oo-ui-selectFileInputWidget-buttonOnly oo-ui-selectFileWidget-buttonOnly' );
+ // Set this.$element to just be the button
+ this.$element = this.selectButton.$element;
+ }
+
+ // Events
+ this.info.connect( this, { change: 'onInfoChange' } );
+ this.selectButton.$button.on( {
+ keypress: this.onKeyPress.bind( this )
+ } );
+ this.$input.on( {
+ change: this.onFileSelected.bind( this ),
+ click: function ( e ) {
+ // Prevents dropTarget getting clicked which calls
+ // a click on this input
+ e.stopPropagation();
+ }
+ } );
+
+ this.connect( this, { change: 'updateUI' } );
+ if ( config.droppable ) {
+ var dragHandler = this.onDragEnterOrOver.bind( this );
+ this.$element.on( {
+ dragenter: dragHandler,
+ dragover: dragHandler,
+ dragleave: this.onDragLeave.bind( this ),
+ drop: this.onDrop.bind( this )
+ } );
+ }
+
this.updateUI();
};
@@ -14263,53 +14329,102 @@ OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
OO.inheritClass( OO.ui.SelectFileInputWidget, OO.ui.InputWidget );
OO.mixinClass( OO.ui.SelectFileInputWidget, OO.ui.mixin.RequiredElement );
+OO.mixinClass( OO.ui.SelectFileInputWidget, OO.ui.mixin.PendingElement );
+
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the currently selected files change
+ *
+ * @param {File[]} currentFiles Current file list
+ */
/* Static Properties */
// Set empty title so that browser default tooltips like "No file chosen" don't appear.
-// On SelectFileWidget this tooltip will often be incorrect, so create a consistent
-// experience on SelectFileInputWidget.
OO.ui.SelectFileInputWidget.static.title = '';
/* Methods */
/**
- * Get the filename of the currently selected file.
+ * Get the current value of the field
*
- * @return {string} Filename
+ * For single file widgets returns a File or null.
+ * For multiple file widgets returns a list of Files.
+ *
+ * @return {File|File[]|null}
*/
-OO.ui.SelectFileInputWidget.prototype.getFilename = function () {
- if ( this.currentFiles.length ) {
- return this.currentFiles.map( function ( file ) {
- return file.name;
- } ).join( ', ' );
- } else {
- // Try to strip leading fakepath.
- return this.getValue().split( '\\' ).pop();
- }
+OO.ui.SelectFileInputWidget.prototype.getValue = function () {
+ return this.multiple ? this.currentFiles : this.currentFiles[ 0 ];
};
/**
- * @inheritdoc
+ * Set the current file list
+ *
+ * Can only be set to a non-null/non-empty value if this.canSetFiles is true,
+ * or if the widget has been set natively and we are just updating the internal
+ * state.
+ *
+ * @param {File[]|null} files Files to select
+ * @chainable
+ * @return {OO.ui.SelectFileInputWidget} The widget, for chaining
*/
-OO.ui.SelectFileInputWidget.prototype.setValue = function ( value ) {
- if ( value === undefined ) {
+OO.ui.SelectFileInputWidget.prototype.setValue = function ( files ) {
+ if ( files === undefined || typeof files === 'string' ) {
// Called during init, don't replace value if just infusing.
return this;
}
- if ( value ) {
- // We need to update this.value, but without trying to modify
- // the DOM value, which would throw an exception.
- if ( this.value !== value ) {
- this.value = value;
- this.emit( 'change', this.value );
- }
- return this;
+
+ if ( files && !this.multiple ) {
+ files = files.slice( 0, 1 );
+ }
+
+ function comparableFile( file ) {
+ // Use extend to convert to plain objects so they can be compared.
+ // File objects contains name, size, timestamp and mime type which
+ // should be unique.
+ return $.extend( {}, file );
+ }
+
+ if ( !OO.compare(
+ files && files.map( comparableFile ),
+ this.currentFiles && this.currentFiles.map( comparableFile )
+ ) ) {
+ this.currentFiles = files || [];
+ this.emit( 'change', this.currentFiles );
+ }
+
+ if ( this.canSetFiles ) {
+ // Convert File[] array back to FileList for setting DOM value
+ var dataTransfer = new DataTransfer();
+ Array.prototype.forEach.call( this.currentFiles || [], function ( file ) {
+ dataTransfer.items.add( file );
+ } );
+ this.$input[ 0 ].files = dataTransfer.files;
} else {
- this.currentFiles = [];
- // Parent method
- return OO.ui.SelectFileInputWidget.super.prototype.setValue.call( this, '' );
+ if ( !files || !files.length ) {
+ // We're allowed to set the input value to empty string
+ // to clear.
+ OO.ui.SelectFileInputWidget.super.prototype.setValue.call( this, '' );
+ }
+ // Otherwise we assume the caller was just calling setValue with the
+ // current state of .files in the DOM.
}
+
+ return this;
+};
+
+/**
+ * Get the filename of the currently selected file.
+ *
+ * @return {string} Filename
+ */
+OO.ui.SelectFileInputWidget.prototype.getFilename = function () {
+ return this.currentFiles.map( function ( file ) {
+ return file.name;
+ } ).join( ', ' );
};
/**
@@ -14319,16 +14434,102 @@ OO.ui.SelectFileInputWidget.prototype.setValue = function ( value ) {
* @param {jQuery.Event} e
*/
OO.ui.SelectFileInputWidget.prototype.onFileSelected = function ( e ) {
- this.currentFiles = this.filterFiles( e.target.files || [] );
+ var files = this.filterFiles( e.target.files || [] );
+ this.setValue( files );
};
/**
+ * Disable InputWidget#onEdit listener, onFileSelected is used instead.
+ *
+ * @inheritdoc
+ */
+OO.ui.SelectFileInputWidget.prototype.onEdit = function () {};
+
+/**
* Update the user interface when a file is selected or unselected.
*
* @protected
*/
OO.ui.SelectFileInputWidget.prototype.updateUI = function () {
+ // Too early
+ if ( !this.selectButton ) {
+ return;
+ }
+
this.info.setValue( this.getFilename() );
+
+ if ( this.currentFiles.length ) {
+ this.$element.removeClass( 'oo-ui-selectFileInputWidget-empty' );
+
+ if ( this.showDropTarget ) {
+ if ( !this.multiple ) {
+ this.pushPending();
+ this.loadAndGetImageUrl( this.currentFiles[ 0 ] ).done( function ( url ) {
+ this.$thumbnail.css( 'background-image', 'url( ' + url + ' )' );
+ }.bind( this ) ).fail( function () {
+ this.$thumbnail.append(
+ new OO.ui.IconWidget( {
+ icon: 'attachment',
+ classes: [ 'oo-ui-selectFileInputWidget-noThumbnail-icon oo-ui-selectFileWidget-noThumbnail-icon' ]
+ } ).$element
+ );
+ }.bind( this ) ).always( function () {
+ this.popPending();
+ }.bind( this ) );
+ }
+ this.$element.off( 'click' );
+ }
+ } else {
+ if ( this.showDropTarget ) {
+ this.$element.off( 'click' );
+ this.$element.on( {
+ click: this.onDropTargetClick.bind( this )
+ } );
+ if ( !this.multiple ) {
+ this.$thumbnail
+ .empty()
+ .css( 'background-image', '' );
+ }
+ }
+ this.$element.addClass( 'oo-ui-selectFileInputWidget-empty' );
+ }
+};
+
+/**
+ * If the selected file is an image, get its URL and load it.
+ *
+ * @param {File} file File
+ * @return {jQuery.Promise} Promise resolves with the image URL after it has loaded
+ */
+OO.ui.SelectFileInputWidget.prototype.loadAndGetImageUrl = function ( file ) {
+ var deferred = $.Deferred(),
+ reader = new FileReader();
+
+ if (
+ ( OO.getProp( file, 'type' ) || '' ).indexOf( 'image/' ) === 0 &&
+ file.size < this.thumbnailSizeLimit * 1024 * 1024
+ ) {
+ reader.onload = function ( event ) {
+ var img = document.createElement( 'img' );
+ img.addEventListener( 'load', function () {
+ if (
+ img.naturalWidth === 0 ||
+ img.naturalHeight === 0 ||
+ img.complete === false
+ ) {
+ deferred.reject();
+ } else {
+ deferred.resolve( event.target.result );
+ }
+ } );
+ img.src = event.target.result;
+ };
+ reader.readAsDataURL( file );
+ } else {
+ deferred.reject();
+ }
+
+ return deferred.promise();
};
/**
@@ -14417,6 +14618,119 @@ OO.ui.SelectFileInputWidget.prototype.setDisabled = function ( disabled ) {
return this;
};
+/**
+ * Handle drop target click events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ * @return {undefined|boolean} False to prevent default if event is handled
+ */
+OO.ui.SelectFileInputWidget.prototype.onDropTargetClick = function () {
+ if ( !this.isDisabled() && this.$input ) {
+ this.$input.trigger( 'click' );
+ return false;
+ }
+};
+
+/**
+ * Handle drag enter and over events
+ *
+ * @private
+ * @param {jQuery.Event} e Drag event
+ * @return {undefined|boolean} False to prevent default if event is handled
+ */
+OO.ui.SelectFileInputWidget.prototype.onDragEnterOrOver = function ( e ) {
+ var hasDroppableFile = false,
+ dt = e.originalEvent.dataTransfer;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ( this.isDisabled() ) {
+ this.$element.removeClass( [
+ 'oo-ui-selectFileInputWidget-canDrop',
+ 'oo-ui-selectFileWidget-canDrop',
+ 'oo-ui-selectFileInputWidget-cantDrop'
+ ] );
+ dt.dropEffect = 'none';
+ return false;
+ }
+
+ // DataTransferItem and File both have a type property, but in Chrome files
+ // have no information at this point.
+ var itemsOrFiles = dt.items || dt.files;
+ var hasFiles = !!( itemsOrFiles && itemsOrFiles.length );
+ if ( hasFiles ) {
+ if ( this.filterFiles( itemsOrFiles ).length ) {
+ hasDroppableFile = true;
+ }
+ // dt.types is Array-like, but not an Array
+ } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
+ // File information is not available at this point for security so just assume
+ // it is acceptable for now.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
+ hasDroppableFile = true;
+ }
+
+ this.$element.toggleClass( 'oo-ui-selectFileInputWidget-canDrop oo-ui-selectFileWidget-canDrop', hasDroppableFile );
+ this.$element.toggleClass( 'oo-ui-selectFileInputWidget-cantDrop', !hasDroppableFile && hasFiles );
+ if ( !hasDroppableFile ) {
+ dt.dropEffect = 'none';
+ }
+
+ return false;
+};
+
+/**
+ * Handle drag leave events
+ *
+ * @private
+ * @param {jQuery.Event} e Drag event
+ */
+OO.ui.SelectFileInputWidget.prototype.onDragLeave = function () {
+ this.$element.removeClass( [
+ 'oo-ui-selectFileInputWidget-canDrop',
+ 'oo-ui-selectFileWidget-canDrop',
+ 'oo-ui-selectFileInputWidget-cantDrop'
+ ] );
+};
+
+/**
+ * Handle drop events
+ *
+ * @private
+ * @param {jQuery.Event} e Drop event
+ * @return {undefined|boolean} False to prevent default if event is handled
+ */
+OO.ui.SelectFileInputWidget.prototype.onDrop = function ( e ) {
+ var dt = e.originalEvent.dataTransfer;
+
+ e.preventDefault();
+ e.stopPropagation();
+ this.$element.removeClass( [
+ 'oo-ui-selectFileInputWidget-canDrop',
+ 'oo-ui-selectFileWidget-canDrop',
+ 'oo-ui-selectFileInputWidget-cantDrop'
+ ] );
+
+ if ( this.isDisabled() ) {
+ return false;
+ }
+
+ var files = this.filterFiles( dt.files || [] );
+ this.setValue( files );
+
+ return false;
+};
+
+// Deprecated alias
+OO.ui.SelectFileWidget = function OoUiSelectFileWidget() {
+ OO.ui.warnDeprecation( 'SelectFileWidget: Deprecated alias, use SelectFileInputWidget instead.' );
+ OO.ui.SelectFileWidget.super.apply( this, arguments );
+};
+
+OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.SelectFileInputWidget );
+
}( OO ) );
//# sourceMappingURL=oojs-ui-core.js.map.json \ No newline at end of file