diff options
Diffstat (limited to 'resources/lib/ooui/oojs-ui-core.js')
-rw-r--r-- | resources/lib/ooui/oojs-ui-core.js | 446 |
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 |