resources/js/ext.translate.messagetable.js
( function () {
'use strict';
var itemsClass = {
proofread: '.tux-message-proofread',
page: '.tux-message-pagemode',
translate: '.tux-message'
};
mw.translate = mw.translate || {};
mw.translate = $.extend( mw.translate, {
/** @ignore */
getMessages: function ( messageGroup, language, offset, limit, filter ) {
var api = new mw.Api();
return api.get( {
action: 'query',
list: 'messagecollection',
mcgroup: messageGroup,
mclanguage: language,
mcoffset: offset,
mclimit: limit,
mcfilter: filter,
mcprop: 'definition|translation|tags|properties',
rawcontinue: 1,
errorformat: 'html',
formatversion: 2,
uselang: mw.config.get( 'wgUserLanguage' )
} );
}
} );
function MessageTable( container, options, settings ) {
this.$container = $( container );
this.options = options;
this.options = $.extend( {}, $.fn.messagetable.defaults, options );
this.settings = settings;
// mode can be proofread, page or translate
this.mode = this.options.mode;
this.firstProofreadTipShown = false;
this.initialized = false;
this.$header = this.$container.siblings( '.tux-messagetable-header' );
// Container is between these in the dom.
this.$loader = this.$container.siblings( '.tux-messagetable-loader' );
this.$loaderIcon = this.$loader.find( '.tux-loading-indicator' );
this.$loaderInfo = this.$loader.find( '.tux-messagetable-loader-info' );
this.$actionBar = this.$container.siblings( '.tux-action-bar' );
this.$statsBar = this.$actionBar.find( '.tux-message-list-statsbar' );
this.$proofreadOwnTranslations = this.$actionBar.find( '.tux-proofread-own-translations-button' );
this.messages = [];
this.loading = false;
this.init();
this.listen();
}
MessageTable.prototype = {
/** @private */
init: function () {
this.$actionBar.removeClass( 'hide' );
},
/** @private */
listen: function () {
var messageTable = this,
$filterInput = this.$container.parent().find( '.tux-message-filter-box' );
// Vector has transitions of 250ms which affect layout. Let those finish.
$( window ).on( 'scroll', mw.util.debounce( function () {
if ( isLoaderVisible( messageTable.$loader ) ) {
messageTable.load();
}
}, 250 ) ).on( 'resize', mw.util.throttle( function () {
messageTable.resize();
}, 250 ) );
$filterInput.on( 'input', mw.util.debounce( function () {
messageTable.search( $filterInput.val() );
}, 250 ) );
this.$actionBar.find( 'button.proofread-mode-button' ).on( 'click', function () {
messageTable.switchMode( 'proofread' );
} );
this.$actionBar.find( 'button.translate-mode-button' ).on( 'click', function () {
messageTable.switchMode( 'translate' );
} );
this.$actionBar.find( 'button.page-mode-button' ).on( 'click', function () {
messageTable.switchMode( 'page' );
} );
this.$proofreadOwnTranslations.on( 'click', function () {
var $this = $( this ),
enable = !$this.hasClass( 'down' );
messageTable.$container.toggleClass( 'tux-hide-own', enable );
$this.toggleClass( 'down', enable ).text( mw.msg( enable ?
'tux-editor-proofreading-show-own-translations' :
'tux-editor-proofreading-hide-own-translations' ) );
} );
},
/**
* Clear the message table
*
* @private
*/
clear: function () {
this.$container.empty();
$( '.translate-tooltip' ).remove();
this.messages = [];
// Any ongoing loading process will notice this and will reject results.
this.loading = false;
},
/**
* Adds a new message using current mode.
*
* @private
* @param {Object} message
*/
add: function ( message ) {
// Prepare the message for display
mw.hook( 'mw.translate.messagetable.formatMessageBeforeTable' ).fire( message );
if ( this.mode === 'translate' ) {
this.addTranslate( message );
} else if ( this.mode === 'proofread' ) {
this.addProofread( message );
} else if ( this.mode === 'page' ) {
this.addPageModeMessage( message );
}
},
/**
* Add a message to the message table for translation.
*
* @private
* @param {Object} message
*/
addTranslate: function ( message ) {
var targetLangCode = this.$container.data( 'targetlangcode' ),
sourceLangCode = this.$container.data( 'sourcelangcode' ),
sourceLangDir = $.uls.data.getDir( sourceLangCode ),
status = message.properties.status,
statusClass = 'tux-status-' + status,
$messageWrapper = $( '<div>' ).addClass( 'row tux-message' ),
statusMsg = '';
message.proofreadable = false;
if ( message.tags.length &&
message.tags.indexOf( 'optional' ) >= 0 &&
status === 'untranslated'
) {
status = 'optional';
statusClass = 'tux-status-optional';
}
// Fuzzy translations need warning class
if ( status === 'fuzzy' ) {
statusClass = statusClass + ' tux-notice';
}
// Label the status if it is not untranslated
if ( status !== 'untranslated' ) {
// Give grep a chance to find the usages:
// tux-status-optional, tux-status-fuzzy, tux-status-proofread,
// tux-status-translated, tux-status-saving, tux-status-unsaved
statusMsg = 'tux-status-' + status;
}
var targetLangDir, targetLangAttrib;
if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) {
targetLangAttrib = mw.config.get( 'wgContentLanguage' );
targetLangDir = $.uls.data.getDir( targetLangAttrib );
} else {
targetLangAttrib = targetLangCode;
targetLangDir = this.$container.data( 'targetlangdir' );
}
var $message = $( '<div>' )
.addClass( 'row message tux-message-item ' + status )
.append(
$( '<div>' )
.addClass( 'eight columns tux-list-message' )
.append(
$( '<span>' )
.addClass( 'tux-list-source' )
.attr( {
lang: sourceLangCode,
dir: sourceLangDir
} )
.text( message.definition ),
// Bidirectional isolation.
// This should be removed some day when proper
// unicode-bidi: isolate
// is supported everywhere
$( '<span>' )
.html( $( document.body ).hasClass( 'rtl' ) ? '‏' : '‎' ),
$( '<span>' )
.addClass( 'tux-list-translation' )
.attr( {
lang: targetLangAttrib,
dir: targetLangDir
} )
.text( message.translation || '' )
),
$( '<div>' )
.addClass( 'two columns tux-list-status text-center' )
.append(
$( '<span>' )
.addClass( statusClass )
// The following messages are used here:
// * tux-status-optional
// * tux-status-fuzzy
// * tux-status-proofread
// * tux-status-translated
// * tux-status-saving
// * tux-status-unsaved
.text( statusMsg ? mw.msg( statusMsg ) : '' )
),
$( '<div>' )
.addClass( 'two column tux-list-edit text-right' )
.append(
$( '<a>' )
.attr( {
title: mw.msg( 'translate-edit-title', message.key ),
href: mw.util.getUrl( message.title, { action: 'edit' } )
} )
.text( mw.msg( 'tux-edit' ) )
)
);
$messageWrapper.append( $message );
this.$container.append( $messageWrapper );
// Attach translate editor to the message
$messageWrapper.translateeditor( {
message: message
} );
},
/**
* Add a message to the message table for proofreading.
*
* @private
* @param {Object} message
*/
addProofread: function ( message ) {
var $message = $( '<div>' )
.addClass( 'row tux-message tux-message-proofread' );
this.$container.append( $message );
$message.proofread( {
message: message,
sourcelangcode: this.$container.data( 'sourcelangcode' ),
targetlangcode: this.$container.data( 'targetlangcode' )
} );
var $icon = $message.find( '.tux-proofread-action' );
if ( $icon.length === 0 ) {
return;
}
// Add autotooltip to first available proofread action icon
if ( this.firstProofreadTipShown ) {
return;
}
this.firstProofreadTipShown = true;
$icon.addClass( 'autotooltip' );
mw.loader.using( 'oojs-ui-core' ).done( function () {
var tooltip = new OO.ui.PopupWidget( {
padded: true,
align: 'center',
width: 250,
classes: [ 'translate-tooltip' ],
$content: $( '<p>' ).text( $icon.prop( 'title' ) )
} );
setTimeout( function () {
var $visibleIcon = $( '.autotooltip:visible' );
if ( !$visibleIcon.length ) {
return;
}
var offset = $visibleIcon.offset();
tooltip.$element.appendTo( document.body );
tooltip
.toggle( true )
.toggleClipping( false )
.togglePositioning( false )
.setAnchorEdge( 'top' );
tooltip.$element.css( {
top: offset.top + $visibleIcon.outerHeight() + 5,
left: offset.left + $visibleIcon.outerWidth() - tooltip.$element.width() / 2 - 15
} );
setTimeout( function () {
tooltip.$element.remove();
}, 4000 );
}, 1000 );
} );
},
/**
* Add a message to the message table for wiki page mode.
*
* @private
* @param {Object} message
*/
addPageModeMessage: function ( message ) {
var $message = $( '<div>' )
.addClass( 'row tux-message tux-message-pagemode' );
this.$container.append( $message );
$message.pagemode( {
message: message,
sourcelangcode: this.$container.data( 'sourcelangcode' ),
targetlangcode: this.$container.data( 'targetlangcode' )
} );
},
/**
* Search the message filter
*
* @private
* @param {string} query
*/
search: function ( query ) {
var resultCount = 0,
matcher = new RegExp( '(^|\\s|\\b)' + escapeRegex( query ), 'i' );
this.$container.find( itemsClass[ this.mode ] ).each( function () {
var $message = $( this ),
message = ( $message.data( 'translateeditor' ) ||
$message.data( 'pagemode' ) ||
$message.data( 'proofread' ) ).message;
if (
matcher.test( message.definition ) ||
matcher.test( message.translation ) ||
matcher.test( message.key )
) {
$message.removeClass( 'hide' );
resultCount++;
} else {
$message.addClass( 'hide' );
}
} );
var $result = this.$container.find( '.tux-message-filter-result' );
if ( !$result.length ) {
var $note = $( '<div>' )
.addClass( 'advanced-search' );
var $button = $( '<button>' )
.addClass( 'mw-ui-button' )
.text( mw.msg( 'tux-message-filter-advanced-button' ) );
$result = $( '<div>' )
.addClass( 'tux-message-filter-result' )
.append( $note, $button );
this.$container.prepend( $result );
}
if ( !query ) {
$result.addClass( 'hide' );
} else {
$result.removeClass( 'hide' )
.find( '.advanced-search' )
.text( mw.msg( 'tux-message-filter-result', resultCount, query ) );
$result.find( 'button' ).on( 'click', function () {
window.location.href = mw.util.getUrl( 'Special:SearchTranslations', { query: query } );
} );
}
this.updateLastMessage();
// Trigger a scroll event to make sure enough messages are loaded
$( window ).trigger( 'scroll' );
},
/** @private */
resize: function () {
var $messageSelector = $( '.row.tux-message-selector' );
if ( $messageSelector.is( ':hidden' ) ) {
return;
}
var actualWidth = 0;
// Calculate the total width required for the filters
$messageSelector.children( 'li' ).each( function () {
actualWidth += $( this ).outerWidth( true );
} );
// Grid row has a min width. After that scrollbars will appear.
// We are checking whether the message table is wider than the current grid row width.
if ( actualWidth >= parseInt( $( '.nine.columns' ).width(), 10 ) ) {
$( '.tux-message-selector .more ul' ) // Overflow menu
.prepend( $( '.row.tux-message-selector > li.column:last' ).prev() );
// See if more items to be pushed to the overflow menu
this.resize();
}
},
/**
* Start loading messages again with new settings.
*
* @internal
* @param {Object} changes
*/
changeSettings: function ( changes ) {
// Clear current messages
this.clear();
this.settings = $.extend( this.settings, changes );
if ( this.initialized === false ) {
this.switchMode( this.mode );
}
// Reset the number of messages remaining
this.$loaderInfo.text(
mw.msg( 'tux-messagetable-loading-messages', this.$loader.data( 'pagesize' ) )
);
// Update the target language
var languageDetails = mw.translate.getLanguageDetailsForHtml( this.settings.language );
this.$container.data( 'targetlangcode', languageDetails.code );
this.$container.data( 'targetlangdir', languageDetails.direction );
// Reset the statsbar
this.$statsBar
.empty()
.removeData()
.languagestatsbar( {
language: this.settings.language,
group: this.settings.group,
onlyLoadCurrentGroupData: true
} );
this.initialized = true;
// Reset other info and make visible
this.$loader
.removeData( 'offset' )
.removeAttr( 'data-offset' )
.removeClass( 'hide' );
if ( changes.offset ) {
this.$loader.data( 'offset', changes.offset );
}
this.$header.removeClass( 'hide' );
this.$actionBar.removeClass( 'hide' );
// Start loading messages
this.load( changes.limit );
},
/**
* @private
* @param {number} [limit] Only load this many messages and then stop even if there is more.
*/
load: function ( limit ) {
var self = this,
offset = this.$loader.data( 'offset' ),
pageSize = limit || this.$loader.data( 'pagesize' );
if ( offset === -1 ) {
return;
}
if ( this.loading ) {
// Avoid duplicate loading - the offset will be wrong and it will result
// in duplicate messages shown in the page
return;
}
this.loading = true;
this.$loaderIcon.removeClass( 'tux-loading-indicator--stopped' );
mw.translate.getMessages(
this.settings.group,
this.settings.language,
offset,
pageSize,
this.settings.filter
).done( function ( result ) {
var messages = result.query.messagecollection;
if ( !self.loading ) {
// reject. This was cancelled.
return;
}
self.clearLoadErrors();
if ( result.warnings ) {
for ( var i = 0; i !== result.warnings.length; i++ ) {
var currentWarning = result.warnings[ i ];
if ( currentWarning.code === 'translate-language-disabled-source' ) {
self.handleLoadErrors( [ currentWarning ] );
// Since translation to source language is disabled, do not display any messages
return;
}
if ( currentWarning.code === 'translate-language-targetlang-variant-of-source' ) {
self.displayLoadErrors( [ currentWarning ] );
break;
}
}
}
if ( messages.length === 0 ) {
// And this is the first load for the filter...
if ( self.$container.children().length === 0 ) {
self.displayEmptyListHelp();
}
}
messages.forEach( function ( message, index ) {
message.group = self.settings.group;
self.add( message );
self.messages.push( message );
if ( index === 0 && self.mode === 'translate' ) {
$( '.tux-message:first' ).data( 'translateeditor' ).init();
}
} );
var state = result.query.metadata && result.query.metadata.state;
$( '.tux-workflow' ).workflowselector(
self.settings.group,
self.settings.language,
state
).removeClass( 'hide' );
// Dynamically loaded messages should pass the search filter if present.
var query = $( '.tux-message-filter-box' ).val();
if ( query ) {
self.search( query );
}
if ( result[ 'query-continue' ] === undefined || limit ) {
// End of messages
self.$loader.data( 'offset', -1 )
.addClass( 'hide' );
} else {
self.$loader.data( 'offset', result[ 'query-continue' ].messagecollection.mcoffset );
var remaining = result.query.metadata.remaining;
self.$loaderInfo.text(
mw.msg( 'tux-messagetable-more-messages', remaining )
);
}
// Helpfully open the first message in show mode on page load
// But do not open it if we are at the bottom of the page waiting for more translation units
if ( self.messages.length <= pageSize ) {
// TODO: Refactor to avoid direct DOM access
$( '.tux-message-item' ).first().trigger( 'click' );
}
self.updateHideOwnInProofreadingToggleVisibility();
self.updateLastMessage();
} ).fail( function ( errorCode, response ) {
self.handleLoadErrors( response.errors, errorCode );
} ).always( function () {
self.$loaderIcon.addClass( 'tux-loading-indicator--stopped' );
self.loading = false;
} );
},
updateLastMessage: function () {
var $messages = this.$container.find( itemsClass[ this.mode ] );
// If a message was previously marked as "last", restore it to normal state
$messages.filter( '.last-message' ).removeClass( 'last-message' );
// At the class to the current last shown message
$messages
.not( '.hide' )
.last()
.addClass( 'last-message' );
},
/**
* Creates a uniformly styled button for different actions,
* shown when there are no messages to display.
*
* @private
* @param {string} labelMsg A message key for the button label.
* @param {Function} callback A callback for clicking the button.
* @return {jQuery} A button element.
*/
otherActionButton: function ( labelMsg, callback ) {
return $( '<button>' )
.addClass( 'mw-ui-button mw-ui-progressive mw-ui-big' )
.text( mw.msg( labelMsg ) )
.on( 'click', callback );
},
updateHideOwnInProofreadingToggleVisibility: function () {
var ownTranslations = this.$container.find( '.tux-message-proofread.own-translation' ).length;
this.$proofreadOwnTranslations.toggleClass( 'hide', !ownTranslations );
},
/**
* If the user selection doesn't show anything,
* give some pointers to other things to do.
*/
displayEmptyListHelp: function () {
var messageTable = this,
// @todo Ugly! This should be provided somehow
selectedTab = $( '.tux-message-selector .selected' ).data( 'title' ),
$wrap = $( '<div>' ).addClass( 'tux-empty-list' ),
$emptyListHeader = $( '<div>' ).addClass( 'tux-empty-list-header' ),
$guide = $( '<div>' ).addClass( 'tux-empty-list-guide' ),
$actions = $( '<div>' ).addClass( 'tux-empty-list-actions' );
if ( messageTable.mode === 'proofread' ) {
if ( selectedTab === 'all' ) {
$emptyListHeader.text( mw.msg( 'tux-empty-no-messages-to-display' ) );
$guide.append(
$( '<p>' )
.text( mw.msg( 'tux-empty-there-are-optional' ) ),
$( '<a>' )
.attr( 'href', '#' )
.text( mw.msg( 'tux-empty-show-optional-messages' ) )
.on( 'click', function ( e ) {
$( '#tux-option-optional' ).trigger( 'click' );
e.preventDefault();
} )
);
} else if ( selectedTab === 'outdated' ) {
$emptyListHeader.text( mw.msg( 'tux-empty-no-outdated-messages' ) );
$guide.text( mw.msg( 'tux-empty-list-other-guide' ) );
$actions.append( messageTable.otherActionButton(
'tux-empty-list-other-action',
function () {
$( '.tux-tab-unproofread' ).trigger( 'click' );
// @todo untranslated
} )
);
// @todo View all
} else if ( selectedTab === 'translated' ) {
$emptyListHeader.text( mw.msg( 'tux-empty-nothing-to-proofread' ) );
$guide.text( mw.msg( 'tux-empty-you-can-help-providing' ) );
$actions.append( messageTable.otherActionButton(
'tux-empty-list-translated-action',
function () {
messageTable.switchMode( 'translate' );
} )
);
} else if ( selectedTab === 'unproofread' ) {
$emptyListHeader.text( mw.msg( 'tux-empty-nothing-new-to-proofread' ) );
$guide.text( mw.msg( 'tux-empty-you-can-help-providing' ) );
$actions.append( messageTable.otherActionButton(
'tux-empty-you-can-review-already-proofread',
function () {
$( '.tux-tab-translated' ).trigger( 'click' );
} )
);
}
} else {
if ( selectedTab === 'all' ) {
$emptyListHeader.text( mw.msg( 'tux-empty-list-all' ) );
$guide.text( mw.msg( 'tux-empty-list-all-guide' ) );
} else if ( selectedTab === 'translated' ) {
$emptyListHeader.text( mw.msg( 'tux-empty-list-translated' ) );
$guide.text( mw.msg( 'tux-empty-list-translated-guide' ) );
$actions.append( messageTable.otherActionButton(
'tux-empty-list-translated-action',
function () {
$( '.tux-tab-untranslated' ).trigger( 'click' );
} )
);
} else {
$emptyListHeader.text( mw.msg( 'tux-empty-list-other' ) );
if ( mw.translate.canProofread() ) {
$guide.text( mw.msg( 'tux-empty-list-other-guide' ) );
$actions.append( messageTable.otherActionButton(
'tux-empty-list-other-action',
function () {
messageTable.switchMode( 'proofread' );
} )
);
}
$actions.append( $( '<a>' )
.text( mw.msg( 'tux-empty-list-other-link' ) )
.on( 'click', function () {
$( '.tux-tab-all' ).trigger( 'click' );
} )
);
}
}
$wrap.append( $emptyListHeader, $guide, $actions );
this.$container.append( $wrap );
},
/**
* Switch the message table mode
*
* @private
* @param {string} mode The message table mode to switch to: translate, page or proofread
*/
switchMode: function ( mode ) {
var messageTable = this,
filter = this.settings.filter,
userId = mw.config.get( 'wgUserId' );
messageTable.$actionBar.find( '.tux-view-switcher .down' ).removeClass( 'down' );
if ( mode === 'translate' ) {
messageTable.$actionBar.find( '.translate-mode-button' ).addClass( 'down' );
}
if ( mode === 'proofread' ) {
messageTable.$actionBar.find( '.proofread-mode-button' ).addClass( 'down' );
}
if ( mode === 'page' ) {
messageTable.$actionBar.find( '.page-mode-button' ).addClass( 'down' );
}
messageTable.firstProofreadTipShown = false;
messageTable.mode = mode;
mw.translate.changeUrl( { action: messageTable.mode } );
// Emulate clear without clearing loaded messages
messageTable.$container.empty();
$( '.translate-tooltip' ).remove();
var $tuxTabUntranslated = $( '.tux-message-selector > .tux-tab-untranslated' );
var $tuxTabUnproofread = $( '.tux-message-selector > .tux-tab-unproofread' );
var $hideTranslatedButton = messageTable.$actionBar.find( '.tux-editor-clear-translated' );
if ( messageTable.mode === 'proofread' ) {
$tuxTabUntranslated.addClass( 'hide' );
$tuxTabUnproofread.removeClass( 'hide' );
// Fix the filter if it is untranslated. Untranslated does not make sense
// for proofread mode. Keep the filter if it is not 'untranslated'
if ( !filter || filter.indexOf( '!translated' ) >= 0 ) {
messageTable.messages = [];
// default filter for proofread mode
mw.translate.changeFilter( 'translated|!reviewer:' + userId +
'|!last-translator:' + userId );
$tuxTabUnproofread.addClass( 'selected' );
// Own translations are not present in proofread + unreviewed mode
}
$hideTranslatedButton.addClass( 'hide' );
} else {
$tuxTabUntranslated.removeClass( 'hide' );
$tuxTabUnproofread.addClass( 'hide' );
if ( filter.indexOf( '!translated' ) > -1 ) {
$hideTranslatedButton.removeClass( 'hide' );
}
if ( filter && filter.indexOf( '!last-translator' ) >= 0 ) {
messageTable.messages = [];
// default filter for translate mode
mw.translate.changeFilter( '!translated' );
$tuxTabUntranslated.addClass( 'selected' );
}
}
if ( messageTable.messages.length ) {
messageTable.messages.forEach( function ( message ) {
messageTable.add( message );
} );
} else if ( messageTable.initialized && !messageTable.loading ) {
messageTable.displayEmptyListHelp();
}
this.$loaderInfo.text(
mw.msg( 'tux-messagetable-loading-messages', this.$loader.data( 'pagesize' ) )
);
messageTable.updateHideOwnInProofreadingToggleVisibility();
messageTable.updateLastMessage();
},
/**
* Clear errors encountered during the loading state
* @private
*/
clearLoadErrors: function () {
$( '.tux-editor-header .tux-group-warning .tux-api-load-error' ).remove();
},
/**
* Display errors encountered during the loading state.
*
* @private
* @param {Array} errors
* @param {string} errorCode
*/
displayLoadErrors: function ( errors, errorCode ) {
var $warningContainer = $( '.tux-editor-header .tux-group-warning' );
if ( errors ) {
errors.forEach( function ( error ) {
$warningContainer.append(
$( '<p>' )
.addClass( 'tux-api-load-error' )
.html( error.html )
);
} );
} else {
$warningContainer.append(
$( '<p>' )
.addClass( 'tux-api-load-error' )
.text( mw.msg( 'api-error-unknownerror', errorCode ) )
);
}
},
/**
* Displays the errors and updates the state of the table.
*
* @private
* @param {Array} errors
* @param {string} errorCode
*/
handleLoadErrors: function ( errors, errorCode ) {
this.displayLoadErrors( errors, errorCode );
$( '.tux-workflow' ).addClass( 'hide' );
this.$loader.data( 'offset', -1 ).addClass( 'hide' );
this.$actionBar.addClass( 'hide' );
this.$header.addClass( 'hide' );
}
};
/*
* messagetable PLUGIN DEFINITION
*/
$.fn.messagetable = function ( options ) {
return this.each( function () {
var $this = $( this ),
data = $this.data( 'messagetable' );
if ( !data ) {
$this.data( 'messagetable', ( data = new MessageTable( this, options ) ) );
}
if ( typeof options === 'string' ) {
data[ options ].call( $this );
}
} );
};
$.fn.messagetable.Constructor = MessageTable;
$.fn.messagetable.defaults = {
mode: new mw.Uri().query.action || 'translate'
};
/**
* Escape the search query for regex match.
*
* @param {string} value A search string to be escaped.
* @return {string} Escaped string that is safe to use for a search.
*/
function escapeRegex( value ) {
return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' );
}
function isLoaderVisible( $loader ) {
var $window = $( window );
var viewportBottom = ( window.innerHeight ? window.innerHeight : $window.height() ) +
$window.scrollTop();
var elementTop = $loader.offset().top;
// Start already if user is reaching close to the bottom
return elementTop - viewportBottom < 200;
}
}() );