"mw.cookie",
"mw.experiments",
"mw.viewport",
- "mw.htmlform.*"
+ "mw.htmlform.*",
+ "mw.visibleTimeout"
]
},
{
'scripts' => 'resources/src/mediawiki/mediawiki.notification.js',
'dependencies' => [
'mediawiki.util',
+ 'mediawiki.visibleTimeout',
],
'targets' => [ 'desktop', 'mobile' ],
],
'styles' => 'resources/src/mediawiki/mediawiki.editfont.css',
'targets' => [ 'desktop', 'mobile' ],
],
+ 'mediawiki.visibleTimeout' => [
+ 'scripts' => 'resources/src/mediawiki/mediawiki.visibleTimeout.js',
+ 'targets' => [ 'desktop', 'mobile' ],
+ ],
/* MediaWiki Action */
// to stop replacement of a tagged notification with another notification using the same message.
// options: The options passed to the notification with a little sanitization. Used by various methods.
// $notification: jQuery object containing the notification DOM node.
+ // timeout: Holds appropriate methods to set/clear timeouts
this.autoHideSeconds = options.autoHideSeconds &&
notification.autoHideSeconds[ options.autoHideSeconds ] ||
notification.autoHideSeconds.short;
this.message = message;
this.options = options;
this.$notification = $notification;
+ if ( options.visibleTimeout ) {
+ this.timeout = require( 'mediawiki.visibleTimeout' );
+ } else {
+ this.timeout = {
+ set: setTimeout,
+ clear: clearTimeout
+ };
+ }
}
/**
}
this.isPaused = true;
- if ( this.timeout ) {
- clearTimeout( this.timeout );
- delete this.timeout;
+ if ( this.timeoutId ) {
+ this.timeout.clear( this.timeoutId );
+ delete this.timeoutId;
}
};
*/
Notification.prototype.resume = function () {
var notif = this;
+
if ( !notif.isPaused ) {
return;
}
// Start any autoHide timeouts
if ( notif.options.autoHide ) {
notif.isPaused = false;
- notif.timeout = setTimeout( function () {
+ notif.timeoutId = notif.timeout.set( function () {
// Already finished, so don't try to re-clear it
- delete notif.timeout;
+ delete notif.timeoutId;
notif.close();
}, this.autoHideSeconds * 1000 );
}
* - type:
* An optional string for the type of the message used for styling:
* Examples: 'info', 'warn', 'error'.
+ *
+ * - visibleTimeout:
+ * A boolean indicating if the autoHide timeout should be based on
+ * time the page was visible to user. Or if it should use wall clock time.
*/
defaults: {
autoHide: true,
autoHideSeconds: 'short',
tag: null,
title: null,
- type: null
+ type: null,
+ visibleTimeout: true
},
/**
--- /dev/null
+( function ( mw, document ) {
+ var hidden, visibilityChange,
+ nextVisibleTimeoutId = 0,
+ activeTimeouts = {},
+ init = function ( overrideDoc ) {
+ if ( overrideDoc !== undefined ) {
+ document = overrideDoc;
+ }
+
+ if ( document.hidden !== undefined ) {
+ hidden = 'hidden';
+ visibilityChange = 'visibilitychange';
+ } else if ( document.mozHidden !== undefined ) {
+ hidden = 'mozHidden';
+ visibilityChange = 'mozvisibilitychange';
+ } else if ( document.msHidden !== undefined ) {
+ hidden = 'msHidden';
+ visibilityChange = 'msvisibilitychange';
+ } else if ( document.webkitHidden !== undefined ) {
+ hidden = 'webkitHidden';
+ visibilityChange = 'webkitvisibilitychange';
+ }
+ };
+
+ init();
+
+ /**
+ * @class mw.visibleTimeout
+ * @singleton
+ */
+ module.exports = {
+ /**
+ * Generally similar to setTimeout, but turns itself on/off on page
+ * visibility changes. The passed function fires after the page has been
+ * cumulatively visible for the specified number of ms.
+ *
+ * @param {Function} fn The action to execute after visible timeout has expired.
+ * @param {number} delay The number of ms the page should be visible before
+ * calling fn.
+ * @return {number} A positive integer value which identifies the timer. This
+ * value can be passed to clearVisibleTimeout() to cancel the timeout.
+ */
+ set: function ( fn, delay ) {
+ var handleVisibilityChange,
+ timeoutId = null,
+ visibleTimeoutId = nextVisibleTimeoutId++,
+ lastStartedAt = mw.now(),
+ clearVisibleTimeout = function () {
+ if ( timeoutId !== null ) {
+ clearTimeout( timeoutId );
+ timeoutId = null;
+ }
+ delete activeTimeouts[ visibleTimeoutId ];
+ if ( hidden !== undefined ) {
+ document.removeEventListener( visibilityChange, handleVisibilityChange, false );
+ }
+ },
+ onComplete = function () {
+ clearVisibleTimeout();
+ fn();
+ };
+
+ handleVisibilityChange = function () {
+ var now = mw.now();
+
+ if ( document[ hidden ] ) {
+ // pause timeout if running
+ if ( timeoutId !== null ) {
+ delay = Math.max( 0, delay - Math.max( 0, now - lastStartedAt ) );
+ if ( delay === 0 ) {
+ onComplete();
+ } else {
+ clearTimeout( timeoutId );
+ timeoutId = null;
+ }
+ }
+ } else {
+ // resume timeout if not running
+ if ( timeoutId === null ) {
+ lastStartedAt = now;
+ timeoutId = setTimeout( onComplete, delay );
+ }
+ }
+ };
+
+ activeTimeouts[ visibleTimeoutId ] = clearVisibleTimeout;
+ if ( hidden !== undefined ) {
+ document.addEventListener( visibilityChange, handleVisibilityChange, false );
+ }
+ handleVisibilityChange();
+
+ return visibleTimeoutId;
+ },
+
+ /**
+ * Cancel a visible timeout previously established by calling set.
+ * Passing an invalid ID silently does nothing.
+ *
+ * @param {number} visibleTimeoutId The identifier of the visible
+ * timeout you want to cancel. This ID was returned by the
+ * corresponding call to set().
+ */
+ clear: function ( visibleTimeoutId ) {
+ if ( activeTimeouts.hasOwnProperty( visibleTimeoutId ) ) {
+ activeTimeouts[ visibleTimeoutId ]();
+ }
+ }
+ };
+
+ if ( window.QUnit ) {
+ module.exports.setDocument = init;
+ }
+
+}( mediaWiki, document ) );
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.visibleTimeout.test.js',
],
'dependencies' => [
'jquery.accessKeyLabel',
'mediawiki.cookie',
'mediawiki.experiments',
'mediawiki.inspect',
+ 'mediawiki.visibleTimeout',
'test.mediawiki.qunit.testrunner',
],
]
--- /dev/null
+( function ( mw ) {
+
+ QUnit.module( 'mediawiki.visibleTimeout', QUnit.newMwEnvironment( {
+ setup: function () {
+ // Document with just enough stuff to make the tests work.
+ var listeners = [];
+ this.mockDocument = {
+ hidden: false,
+ addEventListener: function ( type, listener ) {
+ if ( type === 'visibilitychange' ) {
+ listeners.push( listener );
+ }
+ },
+ removeEventListener: function ( type, listener ) {
+ var i;
+ if ( type === 'visibilitychange' ) {
+ i = listeners.indexOf( listener );
+ if ( i >= 0 ) {
+ listeners.splice( i, 1 );
+ }
+ }
+ },
+ // Helper function to swap visibility and run listeners
+ toggleVisibility: function () {
+ var i;
+ this.hidden = !this.hidden;
+ for ( i = 0; i < listeners.length; i++ ) {
+ listeners[ i ]();
+ }
+ }
+ };
+ this.visibleTimeout = require( 'mediawiki.visibleTimeout' );
+ this.visibleTimeout.setDocument( this.mockDocument );
+
+ this.sandbox.useFakeTimers();
+ // mw.now() doesn't respect the fake clock injected by useFakeTimers
+ this.stub( mw, 'now', ( function () {
+ return this.sandbox.clock.now;
+ } ).bind( this ) );
+ }
+ } ) );
+
+ QUnit.test( 'basic usage', function ( assert ) {
+ var called = 0;
+
+ this.visibleTimeout.set( function () {
+ called++;
+ }, 0 );
+ assert.strictEqual( called, 0 );
+ this.sandbox.clock.tick( 1 );
+ assert.strictEqual( called, 1 );
+
+ this.sandbox.clock.tick( 100 );
+ assert.strictEqual( called, 1 );
+
+ this.visibleTimeout.set( function () {
+ called++;
+ }, 10 );
+ this.sandbox.clock.tick( 10 );
+ assert.strictEqual( called, 2 );
+ } );
+
+ QUnit.test( 'can cancel timeout', function ( assert ) {
+ var called = 0,
+ timeout = this.visibleTimeout.set( function () {
+ called++;
+ }, 0 );
+
+ this.visibleTimeout.clear( timeout );
+ this.sandbox.clock.tick( 10 );
+ assert.strictEqual( called, 0 );
+
+ timeout = this.visibleTimeout.set( function () {
+ called++;
+ }, 100 );
+ this.sandbox.clock.tick( 50 );
+ assert.strictEqual( called, 0 );
+ this.visibleTimeout.clear( timeout );
+ this.sandbox.clock.tick( 100 );
+ assert.strictEqual( called, 0 );
+ } );
+
+ QUnit.test( 'start hidden and become visible', function ( assert ) {
+ var called = 0;
+
+ this.mockDocument.hidden = true;
+ this.visibleTimeout.set( function () {
+ called++;
+ }, 0 );
+ this.sandbox.clock.tick( 10 );
+ assert.strictEqual( called, 0 );
+
+ this.mockDocument.toggleVisibility();
+ this.sandbox.clock.tick( 10 );
+ assert.strictEqual( called, 1 );
+ } );
+
+ QUnit.test( 'timeout is cumulative', function ( assert ) {
+ var called = 0;
+
+ this.visibleTimeout.set( function () {
+ called++;
+ }, 100 );
+ this.sandbox.clock.tick( 50 );
+ assert.strictEqual( called, 0 );
+
+ this.mockDocument.toggleVisibility();
+ this.sandbox.clock.tick( 1000 );
+ assert.strictEqual( called, 0 );
+
+ this.mockDocument.toggleVisibility();
+ this.sandbox.clock.tick( 50 );
+ assert.strictEqual( called, 1 );
+ } );
+}( mediaWiki ) );