Files
jquery.uls/src/jquery.uls.core.js
Santhosh Thottingal d4de09ffb9 Improve the hide-show logic of ULS
* The events are suppressed on click of ULS trigger, it is an anti
  pattern. It can cause other overlay dialogs, if any, to stay with
  ULS and causes UI glitch. This patch just propagates the events and not
  eats up.
* The show method was hiding all other ULS dialoges open using a
  global $('.uls-menu').hide(). This is again not a good pattern.
  A plugin instance should not interfere with other instance's state.
  More over, calling jQuery hide() method on menu instead of plugin's
  hide method leaves the other plugin instance in a corrupted state.
  The plugin hide method does more things than just hiding the menu.
  It has a 'shown' book keeping property to update. This kind of
  corrupted state was causing bugs like https://phabricator.wikimedia.org/T114123
* While avoiding the above two antipatterns, the way ULS was hidden when
  clicked on any 'other' part of body was improved. It now uses event.target
  to correctly handle the 'click-outside-hide' logic

All these above changes does not change any existing UX.

Change-Id: I40b355115cbda54a68e8d58d3750fb9f1c3b6920
2015-10-06 10:35:05 +05:30

473 lines
12 KiB
JavaScript

/**
* Universal Language Selector
* ULS core component.
*
* Copyright (C) 2012 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon Harris,
* Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland and other
* contributors. See CREDITS for a list.
*
* UniversalLanguageSelector is dual licensed GPLv2 or later and MIT. You don't
* have to do anything special to choose one license or the other and you don't
* have to notify anyone which license you are using. You are free to use
* UniversalLanguageSelector in commercial projects as long as the copyright
* header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
*
* @file
* @ingroup Extensions
* @licence GNU General Public Licence 2.0 or later
* @licence MIT License
*/
( function ( $ ) {
'use strict';
var template, ULS;
// Region numbers in id attributes also appear in the langdb.
/*jshint multistr:true */
template = '<div class="grid uls-menu"> \
<div class="row"> \
<span id="uls-close" class="uls-icon-close"></span> \
<div class="uls-title-region seven columns">\
<h1 data-i18n="uls-select-language" class="uls-title">Select Language</h1>\
</div>\
</div>\
<div id="search" class="row uls-search"> \
<div class="one column">\
<label class="uls-search-label" for="uls-languagefilter"></label>\
</div>\
<div class="ten columns">\
<div id="uls-search-input-block" class="uls-search-input-block">\
<input type="text" class="uls-filterinput uls-filtersuggestion" id="uls-filtersuggestion" disabled="true"\
autocomplete="off" /> <input type="text" class="uls-filterinput uls-languagefilter" id="uls-languagefilter"\
data-clear="uls-languagefilter-clear" data-suggestion="uls-filtersuggestion"\
placeholder="Language search" autocomplete="off" />\
</div>\
</div>\
<div class="one column">\
<span id="uls-languagefilter-clear" class="uls-languagefilter-clear"></span>\
</div>\
</div>\
<div class="row uls-language-list"></div>\
<div class="row" id="uls-settings-block"></div>\
</div>';
/*jshint multistr:false */
/**
* Count the number of keys in an object.
* Works in a cross-browser way.
* @param {Object} The object.
*/
function objectLength ( obj ) {
var count, key;
// Some old browsers don't support Object.keys
if ( Object.keys ) {
return Object.keys( obj ).length;
}
count = 0;
for ( key in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, key ) ) {
count++;
}
}
return count;
}
/**
* ULS Public class definition
*/
ULS = function ( element, options ) {
this.$element = $( element );
this.options = $.extend( {}, $.fn.uls.defaults, options );
this.$menu = $( template );
this.languages = this.options.languages;
for ( var code in this.languages ) {
if ( $.uls.data.languages[ code ] === undefined ) {
// Language is unknown to ULS.
delete this.languages[ code ];
}
}
this.left = this.options.left;
this.top = this.options.top;
this.shown = false;
this.initialized = false;
this.$languageFilter = this.$menu.find( '#uls-languagefilter' );
this.$resultsView = this.$menu.find( 'div.uls-language-list' );
this.render();
this.listen();
this.ready();
};
ULS.prototype = {
constructor: ULS,
/**
* A "hook" that runs after the ULS constructor.
* At this point it is not guaranteed that the ULS has its dimensions
* and that the languages lists are initialized.
*
* To use it, pass a function as the onReady parameter
* in the options when initializing ULS.
*/
ready: function () {
if ( this.options.onReady ) {
this.options.onReady.call( this );
}
},
/**
* A "hook" that runs after the ULS panel becomes visible
* by using the show method.
*
* To use it, pass a function as the onVisible parameter
* in the options when initializing ULS.
*/
visible: function () {
if ( this.options.onVisible ) {
this.options.onVisible.call( this );
}
},
/**
* Calculate the position of ULS
* Returns an object with top and left properties.
* @returns {Object}
*/
position: function () {
var pos;
pos = $.extend( {}, this.$element.offset(), {
height: this.$element[ 0 ].offsetHeight
} );
return {
top: this.top !== undefined ? this.top : pos.top + pos.height,
left: this.left !== undefined ? this.left : '25%'
};
},
/**
* Show the ULS window
*/
show: function () {
var widthClasses = {
wide: 'uls-wide',
medium: 'uls-medium',
narrow: 'uls-narrow'
};
this.$menu.addClass( widthClasses[this.getMenuWidth()] );
this.$menu.css( this.position() );
if ( this.options.compact ) {
this.$menu.addClass( 'uls-compact' );
}
if ( !this.initialized ) {
$( 'body' ).prepend( this.$menu );
this.i18n();
this.initialized = true;
}
this.$menu.show();
this.$menu.scrollIntoView();
this.shown = true;
if ( !this.isMobile() ) {
this.$languageFilter.focus();
}
this.visible();
},
i18n: function () {
if ( $.i18n ) {
this.$menu.find( '[data-i18n]' ).i18n();
this.$languageFilter.prop( 'placeholder', $.i18n( 'uls-search-placeholder' ) );
}
},
/**
* Hide the ULS window
*/
hide: function () {
this.$menu.hide();
this.shown = false;
},
/**
* Render the UI elements.
* Does nothing by default. Can be used for customization.
*/
render: function () {
// Rendering stuff here
},
/**
* Callback for no results found context.
*/
noresults: function () {
this.$resultsView.lcd( 'noResults' );
},
/**
* callback for results found context.
*/
success: function () {
this.$resultsView.show();
},
/**
* Bind the UI elements with their event listeners
*/
listen: function () {
var lcd, columnsOptions, languagesCount,
uls = this;
columnsOptions = {
wide: 4,
medium: 2,
narrow: 1
};
// Register all event listeners to the ULS here.
this.$element.on( 'click', $.proxy( this.click, this ) );
// Close when clicking on the close button
this.$menu.find( '#uls-close' ).on( 'click', $.proxy( this.cancel, this ) );
// Don't do anything if pressing on empty space in the ULS
this.$menu.on( 'click', function ( e ) {
e.stopPropagation();
} );
// Handle key press events on the menu
this.$menu.on( 'keypress', $.proxy( this.keypress, this ) )
.on( 'keyup', $.proxy( this.keyup, this ) );
if ( this.eventSupported( 'keydown' ) ) {
this.$menu.on( 'keydown', $.proxy( this.keypress, this ) );
}
languagesCount = objectLength( this.options.languages );
lcd = this.$resultsView.lcd( {
languages: this.languages,
columns: columnsOptions[ this.getMenuWidth() ],
quickList: languagesCount > 12 ? this.options.quickList : false,
clickhandler: $.proxy( this.select, this ),
source: this.$languageFilter,
showRegions: this.options.showRegions,
languageDecorator: this.options.languageDecorator
} ).data( 'lcd' );
this.$languageFilter.languagefilter( {
$target: lcd,
languages: this.languages,
searchAPI: this.options.searchAPI,
onSelect: $.proxy( this.select, this )
} );
// Create region selectors, one per region
this.regionFilter = new $.uls.RegionSelector( {
$target: lcd,
languages: this.languages,
success: function ( regionfilter ) {
// Deactivate search filtering
uls.$languageFilter.languagefilter( 'deactivate' );
// If it is the WW region, show the quicklist
if ( regionfilter.regionGroup === 1 ) {
lcd.quicklist();
}
// Show 'results view' if we are in no results mode
uls.success();
},
noresults: function () {
uls.$languageFilter.languagefilter( 'clear' );
}
} );
this.$languageFilter.on( 'searchclear.uls', $.proxy( this.regionFilter.show, this.regionFilter ) );
this.$languageFilter.on( 'noresults.uls', $.proxy( this.noresults, this ) );
this.$languageFilter.on( 'resultsfound.uls', $.proxy( this.success, this ) );
$( 'html' ).click( $.proxy( this.cancel, this ) );
},
/**
* On select handler for search results
* @param langCode
*/
select: function ( langCode ) {
this.hide();
this.$languageFilter.trigger( 'searchclear' );
if ( this.options.onSelect ) {
this.options.onSelect.call( this, langCode );
}
},
/**
* On cancel handler for the uls menu
*/
cancel: function ( e ) {
if ( e && this.$element.is( e.target ) ) {
return;
}
this.hide();
if ( this.options.onCancel ) {
this.options.onCancel.call( this );
}
},
keyup: function ( e ) {
if ( !this.shown ) {
return;
}
if ( e.keyCode === 27 ) { // escape
this.cancel();
e.preventDefault();
e.stopPropagation();
}
},
keypress: function ( e ) {
if ( !this.shown ) {
return;
}
if ( e.keyCode === 27 ) { // escape
this.cancel();
e.preventDefault();
e.stopPropagation();
}
},
click: function () {
if ( this.shown ) {
this.hide();
} else {
this.show();
}
},
eventSupported: function ( eventName ) {
var isSupported = eventName in this.$menu;
if ( !isSupported ) {
this.$element.setAttribute( eventName, 'return;' );
isSupported = typeof this.$element[ eventName ] === 'function';
}
return isSupported;
},
/**
* Get the panel menu width parameter
* @return string
*/
getMenuWidth: function () {
var languagesCount;
if ( this.options.menuWidth ) {
return this.options.menuWidth;
}
languagesCount = objectLength( this.options.languages );
if ( languagesCount < 12 ) {
return 'narrow';
}
if ( languagesCount < 100 ) {
return 'medium';
}
return 'wide';
},
isMobile: function () {
return navigator.userAgent.match( /(iPhone|iPod|iPad|Android|BlackBerry)/ );
}
};
/* ULS PLUGIN DEFINITION
* =========================== */
$.fn.uls = function ( option ) {
return this.each( function () {
var $this = $( this ),
data = $this.data( 'uls' ),
options = typeof option === 'object' && option;
if ( !data ) {
$this.data( 'uls', ( data = new ULS( this, options ) ) );
}
if ( typeof option === 'string' ) {
data[ option ]();
}
} );
};
$.fn.uls.defaults = {
onSelect: null, // Callback function to be called when a language is selected
searchAPI: null, // Language search API
languages: $.uls.data.getAutonyms(), // Languages to be used for ULS, default is all languages
quickList: null, // Array of language codes or function that returns such
compact: false, // Show ULS in compact mode
// The options are wide (4 columns), medium (2 columns), and narrow (1 column).
// If not specified, it will be set automatically.
menuWidth: null,
showRegions: [ 'WW', 'AM', 'EU', 'ME', 'AF', 'AS', 'PA' ],
languageDecorator: null // Callback function to be called when a language link is prepared - for custom decoration.
};
// Define a dummy i18n function, if jquery.i18n not integrated.
if ( !$.fn.i18n ) {
$.fn.i18n = function () {};
}
/*
* Simple scrollIntoView plugin.
* Scrolls the element to the viewport smoothly if it is not already.
*/
$.fn.scrollIntoView = function () {
return this.each( function () {
var scrollPosition,
$window = $( window ),
windowHeight = $window.height(),
windowTop = $window.scrollTop(),
windowBottom = windowTop + windowHeight,
$element = $( this ),
panelHeight = $element.height(),
panelTop = $element.offset().top,
panelBottom = panelTop + panelHeight;
if ( ( panelTop < windowTop ) || ( panelBottom > windowBottom ) ) {
if ( windowTop > panelTop ) {
scrollPosition = panelTop;
} else {
scrollPosition = panelBottom - windowHeight;
}
$( 'html, body' ).stop().animate( {
scrollTop: scrollPosition
}, 500 );
}
} );
};
$.fn.uls.Constructor = ULS;
}( jQuery ) );