| Current File : /home/jvzmxxx/wiki/extensions/Flow/modules/engine/misc/mw-ui.enhance.js |
/*!
* Enhances mediawiki-ui style elements with JavaScript.
*/
// Expose for the sake of tests
mw.flow.ui.enhance = {};
/** @class mw.ui.enhance */
( function ( mw, $ ) {
/*
* Reduce eye-wandering due to adjacent colorful buttons
* This will make unhovered and unfocused sibling buttons become faded and blurred
* Usage: Buttons must be in a form, or in a parent with mw-ui-button-container, or they must be siblings
*/
$( document ).ready( function () {
function onMwUiButtonFocus( event ) {
var $el, $form, $siblings;
if ( event.target.className.indexOf( 'mw-ui-button' ) === -1 ) {
// Not a button event
return;
}
$el = $( event.target );
if ( event.type !== 'keyup' || $el.is( ':focus' ) ) {
// Reset style
$el.removeClass( 'mw-ui-button-althover' );
$form = $el.closest( 'form, .mw-ui-button-container' );
if ( $form.length ) {
// If this button is in a form, apply this to all the form's buttons.
$siblings = $form.find( '.mw-ui-button' );
} else {
// Otherwise, try to find neighboring buttons
$siblings = $el.siblings( '.mw-ui-button' );
}
// Add fade/blur to unfocused sibling buttons
$siblings.not( $el ).filter( ':not(:focus)' )
.addClass( 'mw-ui-button-althover' );
}
}
function onMwUiButtonBlur( event ) {
if ( event.target.className.indexOf( 'mw-ui-button' ) === -1 ) {
// Not a button event
return;
}
var $el = $( event.target ),
$form, $siblings, $focused;
$form = $el.closest( 'form, .mw-ui-button-container' );
if ( $form.length ) {
// If this button is in a form, apply this to all the form's buttons.
$siblings = $form.find( '.mw-ui-button' );
} else {
// Otherwise, try to find neighboring buttons
$siblings = $el.siblings( '.mw-ui-button' );
}
// Add fade/blur to unfocused sibling buttons
$focused = $siblings.not( $el ).filter( ':focus' );
if ( event.type === 'mouseleave' && $el.is( ':focus' ) ) {
// If this button is still focused, but the mouse left it, keep siblings faded
return;
} else if ( $focused.length ) {
// A sibling has focus; have it trigger the restyling
$focused.trigger( 'mouseenter.mw-ui-enhance' );
} else {
// No other siblings are focused; removing button fading
$siblings.removeClass( 'mw-ui-button-althover' );
}
}
// Attach the mouseenter and mouseleave handlers on document
$( document )
.on( 'mouseenter.mw-ui-enhance', '.mw-ui-button', onMwUiButtonFocus )
.on( 'mouseleave.mw-ui-enhance', '.mw-ui-button', onMwUiButtonBlur );
// Attach these independently, because jQuery doesn't support useCapture mode (focus propagation)
if ( document.attachEvent ) {
document.attachEvent( 'focusin', onMwUiButtonFocus );
document.attachEvent( 'focusout', onMwUiButtonBlur );
} else {
document.body.addEventListener( 'focus', onMwUiButtonFocus, true );
document.body.addEventListener( 'blur', onMwUiButtonBlur, true );
}
} );
/**
* Disables action and submit buttons when a form has required fields
* @param {jQuery} $form jQuery object corresponding to a form element.
*/
function enableFormWithRequiredFields( $form ) {
var
$fields = $form.find( 'input, textarea' ).filter( '[required]' ),
ready = true;
$fields.each( function () {
var $this = $( this );
if ( mw.flow.editor.exists( $this ) ) {
if ( mw.flow.editor.getEditor( $this ).isEmpty() ) {
ready = false;
}
} else if ( this.value === '' ) {
ready = false;
}
} );
// @todo scrap data-role? use submit types? or a single role=action?
$form.find( '.mw-ui-button' ).filter( '[data-role=action], [data-role=submit]' )
.prop( 'disabled', !ready );
}
// Expose for the sake of tests. This will be unnecessary and removed when all
// instances of the editor are migrated to ooui
mw.flow.ui.enhance.enableFormWithRequiredFields = enableFormWithRequiredFields;
/*
* Disable / enable submit buttons without/with text in field.
* Usage: field needs required attribute
*/
$( document ).ready( function () {
// We should probably not use this change detection method for VE
//
// Also, consider using the input event (which I think can replace all of these
// and paste) when we drop IE 8.
// https://developer.mozilla.org/en-US/docs/Web/Events/input
// setTimeout works around paste firing before the field is modified.
$( document ).on(
// @todo change this to listen to emitted change event once/if everything is OOUI thingies
'keyup.flow-actions-disabler cut.flow-actions-disabler paste.flow-actions-disabler',
'.mw-ui-input, .oo-ui-textInputWidget input, .oo-ui-textInputWidget textarea',
function () {
var $el = $( this );
setTimeout( function () {
enableFormWithRequiredFields( $el.closest( 'form' ) );
} );
}
);
} );
/*
* mw-ui-tooltip
* Renders tooltips on over, and also via mw.tooltip.
*/
$( document ).ready( function () {
var _$tooltip = $(
'<span class="flow-ui-tooltip flow-ui-tooltip-left">' +
'<span class="flow-ui-tooltip-content"></span>' +
'<span class="flow-ui-tooltip-triangle"></span>' +
'<span class="flow-ui-tooltip-close"></span>' +
'</span>'
),
$activeTooltips = $(),
_mwUiTooltipExpireTimer;
/**
* Renders a tooltip at target.
* Options (either given as param, or fetched from target as data-tooltip-x params):
* tooltipSize=String (small,large,block)
* tooltipContext=String (progressive,destructive)
* tooltipPointing=String (up,down,left,right)
* tooltipClosable=Boolean
* tooltipContentCallback=Function
*
* @param {jQuery|HTMLElement} target
* @param {jQuery|HTMLElement|string} [content] A jQuery set, an element, or a string of
* HTML. If omitted, first tries tooltipContentCallback, then target.title
* @param {Object} [options]
*/
function mwUiTooltipShow( target, content, options ) {
var $target = $( target ),
// Find previous tooltip for this el
$tooltip = $target.data( '$tooltip' ),
// Get window size and scroll details
windowWidth = $( window ).width(),
windowHeight = $( window ).height(),
scrollX = Math.max( window.pageXOffset, document.documentElement.scrollLeft, document.body.scrollLeft ),
scrollY = Math.max( window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop ),
// Store target and tooltip details
tooltipWidth, tooltipHeight,
targetPosition,
locationOrder, tooltipLocation = {},
insertFn = 'append',
// Options, no longer by objet reference
optionsUnreferenced = {},
i = 0;
options = options || {};
// Do this so that we don't alter the data object by reference
optionsUnreferenced.tooltipSize = options.tooltipSize || $target.data( 'tooltipSize' );
optionsUnreferenced.tooltipContext = options.tooltipContext || $target.data( 'tooltipContext' );
optionsUnreferenced.tooltipPointing = options.tooltipPointing || $target.data( 'tooltipPointing' );
optionsUnreferenced.tooltipContentCallback = options.tooltipContentCallback || $target.data( 'tooltipContentCallback' );
// @todo closable
optionsUnreferenced.tooltipClosable = options.tooltipClosable || $target.data( 'tooltipClosable' );
// Support passing jQuery as argument
target = $target[ 0 ];
if ( !content ) {
if ( optionsUnreferenced.tooltipContentCallback ) {
// Use content callback to get the content for this element
content = optionsUnreferenced.tooltipContentCallback( target, optionsUnreferenced );
if ( !content ) {
return false;
}
} else {
// Check to see if we're simply using target.title as the content
if ( !target.title ) {
return false;
}
content = target.title;
$target.data( 'tooltipTitle', content ); // store title
target.title = ''; // and hide it so it doesn't appear
insertFn = 'text';
if ( !optionsUnreferenced.tooltipSize ) {
// Default size for title tooltip is small
optionsUnreferenced.tooltipSize = 'small';
}
}
}
// No previous tooltip
if ( !$tooltip ) {
// See if content itself is a tooltip
try {
if ( $.type( content ) === 'string' ) {
$tooltip = $( $.parseHTML( content ) );
} else {
$tooltip = $( content );
}
} catch ( e ) {}
if ( !$tooltip || !$tooltip.is( '.flow-ui-tooltip' ) && !$tooltip.find( '.flow-ui-tooltip' ).length ) {
// Content is not and does not contain a tooltip, so instead, put content inside a new tooltip wrapper
$tooltip = _$tooltip.clone();
}
}
// Try to inherit tooltipContext from the target's classes
if ( !optionsUnreferenced.tooltipContext ) {
if ( $target.hasClass( 'mw-ui-progressive' ) ) {
optionsUnreferenced.tooltipContext = 'progressive';
} else if ( $target.hasClass( 'mw-ui-destructive' ) ) {
optionsUnreferenced.tooltipContext = 'destructive';
}
}
$tooltip
// Add the content to it
.find( '.flow-ui-tooltip-content' )
.empty()
[ insertFn ]( content )
.end()
// Move this off-page before rendering it, so that we can calculate its real dimensions
// @todo use .parent() loop to check for z-index and + that to this if needed
.css( { position: 'absolute', zIndex: 1000, top: 0, left: '-999em' } )
// Render
// @todo inject at #bodyContent to inherit (font-)styling
.appendTo( 'body' );
// Tooltip style context
if ( optionsUnreferenced.tooltipContext ) {
$tooltip.removeClass( 'mw-ui-progressive mw-ui-destructive' );
$tooltip.addClass( 'mw-ui-' + optionsUnreferenced.tooltipContext );
}
// Tooltip size (small, large)
if ( optionsUnreferenced.tooltipSize ) {
$tooltip.removeClass( 'flow-ui-tooltip-sm flow-ui-tooltip-lg' );
$tooltip.addClass( 'flow-ui-tooltip-' + optionsUnreferenced.tooltipSize );
}
// Remove the old pointing direction
$tooltip.removeClass( 'flow-ui-tooltip-up flow-ui-tooltip-down flow-ui-tooltip-left flow-ui-tooltip-right' );
// tooltip width and height with the new content
tooltipWidth = $tooltip.outerWidth( true );
tooltipHeight = $tooltip.outerHeight( true );
// target positioning info
targetPosition = $target.offset();
targetPosition.width = $target.outerWidth( true );
targetPosition.height = $target.outerHeight( true );
targetPosition.leftEnd = targetPosition.left + targetPosition.width;
targetPosition.topEnd = targetPosition.top + targetPosition.height;
targetPosition.leftMiddle = targetPosition.left + targetPosition.width / 2;
targetPosition.topMiddle = targetPosition.top + targetPosition.height / 2;
// Use the preferred pointing direction first
switch ( optionsUnreferenced.tooltipPointing ) {
case 'left': locationOrder = [ 'left', 'right', 'left' ]; break;
case 'right': locationOrder = [ 'right', 'left', 'right' ]; break;
case 'down': locationOrder = [ 'down', 'up', 'down' ]; break;
default: locationOrder = [ 'up', 'down', 'up' ];
}
do {
// Position of the POINTER, not the tooltip itself
switch ( locationOrder[ i ] ) {
case 'left':
tooltipLocation.left = targetPosition.leftEnd;
tooltipLocation.top = targetPosition.topMiddle - tooltipHeight / 2;
break;
case 'right':
tooltipLocation.left = targetPosition.left - tooltipWidth;
tooltipLocation.top = targetPosition.topMiddle - tooltipHeight / 2;
break;
case 'down':
tooltipLocation.left = targetPosition.leftMiddle - tooltipWidth / 2;
tooltipLocation.top = targetPosition.top - tooltipHeight;
break;
case 'up':
tooltipLocation.left = targetPosition.leftMiddle - tooltipWidth / 2;
tooltipLocation.top = targetPosition.topEnd;
break;
}
// Verify tooltip will be mostly visible in viewport
if (
tooltipLocation.left > scrollX - 5 &&
tooltipLocation.top > scrollY - 5 &&
tooltipLocation.left + tooltipWidth < windowWidth + scrollX + 5 &&
tooltipLocation.top + tooltipHeight < windowHeight + scrollY + 5
) {
break;
}
if ( i + 1 === locationOrder.length ) {
break;
}
} while ( ++i <= locationOrder.length );
// Add the pointing direction class from the loop
$tooltip.addClass( 'flow-ui-tooltip-' + locationOrder[ i ] );
// Apply the new location CSS
$tooltip.css( tooltipLocation );
// Store this tooltip onto target
$target.data( '$tooltip', $tooltip );
// Store this target onto tooltip
$tooltip.data( '$target', $target );
// Add this tooltip to our set of active tooltips
$activeTooltips = $activeTooltips.add( $tooltip );
// Start the expiry timer
_mwUiTooltipExpire();
return $tooltip;
}
/**
* Hides the tooltip associated with target instantly.
* @param {HTMLElement|jQuery} target
*/
function mwUiTooltipHide( target ) {
var $target = $( target ),
$tooltip = $target.data( '$tooltip' ),
tooltipTitle = $target.data( 'tooltipTitle' );
// Remove tooltip from DOM
if ( $tooltip ) {
$target.removeData( '$tooltip' );
$activeTooltips = $activeTooltips.not( $tooltip );
$tooltip.remove();
}
// Restore old title; was used for tooltip
if ( tooltipTitle ) {
$target[ 0 ].title = tooltipTitle;
$target.removeData( 'tooltipTitle' );
}
}
/**
* Runs on a timer to expire tooltips. This is useful in scenarios where a tooltip's target
* node has disappeared (removed from page), and didn't trigger a mouseout event. We detect
* the target disappearing, and as such remove the tooltip node.
*/
function _mwUiTooltipExpire() {
clearTimeout( _mwUiTooltipExpireTimer );
$activeTooltips.each( function () {
var $this = $( this ),
$target = $this.data( '$target' );
// Remove the tooltip if this tooltip has been removed,
// or if target is not visible (hidden or removed from DOM)
if ( !this.parentNode || !$target.is( ':visible' ) ) {
// Remove the tooltip from the DOM
$this.remove();
// Unset tooltip from target
$target.removeData( '$tooltip' );
// Remove the tooltip from our active tooltips list
$activeTooltips = $activeTooltips.not( $this );
}
} );
if ( $activeTooltips.length ) {
// Check again in 500ms if we still have active tooltips
_mwUiTooltipExpireTimer = setTimeout( _mwUiTooltipExpire, 500 );
}
}
/**
* MW UI Tooltip access through JS API.
*/
mw.tooltip = {
show: mwUiTooltipShow,
hide: mwUiTooltipHide
};
/**
* Event handler for mouse entering on a .flow-ui-tooltip-target
* @param {Event} event
*/
function onMwUiTooltipFocus( event ) {
mw.tooltip.show( this );
}
/**
* Event handler for mouse leaving a .flow-ui-tooltip-target
* @param {Event} event
*/
function onMwUiTooltipBlur( event ) {
mw.tooltip.hide( this );
}
// Attach the mouseenter and mouseleave handlers on document
$( document )
.on( 'mouseenter.mw-ui-enhance focus.mw-ui-enhance', '.flow-ui-tooltip-target', onMwUiTooltipFocus )
.on( 'mouseleave.mw-ui-enhance blur.mw-ui-enhance click.mw-ui-enhance', '.flow-ui-tooltip-target', onMwUiTooltipBlur );
} );
}( mw, jQuery ) );