Merge "Add scroll event + init A/B test logging to sticky header, AB js"
This commit is contained in:
commit
e95e48ce51
|
@ -206,6 +206,14 @@ final class Constants {
|
||||||
*/
|
*/
|
||||||
public const FEATURE_STICKY_HEADER_EDIT = 'StickyHeaderEdit';
|
public const FEATURE_STICKY_HEADER_EDIT = 'StickyHeaderEdit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether the Sticky Header A/B test is running. See
|
||||||
|
* https://phabricator.wikimedia.org/T292587 for additional detail about the test.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const CONFIG_STICKY_HEADER_TREATMENT_AB_TEST_ENROLLMENT = 'VectorWebABTestEnrollment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `mediawiki.searchSuggest` protocol piece of the SearchSatisfaction instrumention reads
|
* The `mediawiki.searchSuggest` protocol piece of the SearchSatisfaction instrumention reads
|
||||||
* the value of an element with the "data-search-loc" attribute and set the event's
|
* the value of an element with the "data-search-loc" attribute and set the event's
|
||||||
|
|
|
@ -35,6 +35,9 @@ class Hooks {
|
||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
'wgVectorSearchHost' => $config->get( 'VectorSearchHost' ),
|
'wgVectorSearchHost' => $config->get( 'VectorSearchHost' ),
|
||||||
|
'wgVectorWebABTestEnrollment' => $config->get(
|
||||||
|
Constants::CONFIG_STICKY_HEADER_TREATMENT_AB_TEST_ENROLLMENT
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"Event": "https://developer.mozilla.org/docs/Web/API/Event",
|
"Event": "https://developer.mozilla.org/docs/Web/API/Event",
|
||||||
"EventTarget": "https://developer.mozilla.org/docs/Web/API/EventTarget",
|
"EventTarget": "https://developer.mozilla.org/docs/Web/API/EventTarget",
|
||||||
"HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement",
|
"HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement",
|
||||||
|
"IntersectionObserver": "https://developer.mozilla.org/docs/Web/API/IntersectionObserver",
|
||||||
"Node": "https://developer.mozilla.org/docs/Web/API/Node",
|
"Node": "https://developer.mozilla.org/docs/Web/API/Node",
|
||||||
"NodeList": "https://developer.mozilla.org/docs/Web/API/NodeList",
|
"NodeList": "https://developer.mozilla.org/docs/Web/API/NodeList",
|
||||||
"HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement",
|
"HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement",
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* Example A/B test configuration for sticky header:
|
||||||
|
*
|
||||||
|
* $wgVectorABTestEnrollment = [
|
||||||
|
* 'name' => 'vector.sticky_header_2021_11',
|
||||||
|
* 'enabled' => true,
|
||||||
|
* 'buckets' => [
|
||||||
|
* 'unsampled' => [
|
||||||
|
* 'samplingRate' => 0.1,
|
||||||
|
* ],
|
||||||
|
* 'control' => [
|
||||||
|
* 'samplingRate' => 0.3,
|
||||||
|
* ],
|
||||||
|
* 'stickyHeaderDisabled' => [
|
||||||
|
* 'samplingRate' => 0.3,
|
||||||
|
* ],
|
||||||
|
* 'stickyHeaderEnabled' => [
|
||||||
|
* 'samplingRate' => 0.3,
|
||||||
|
* ],
|
||||||
|
* ],
|
||||||
|
* ];
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions and variables to implement A/B testing.
|
||||||
|
*/
|
||||||
|
const ABTestConfig = require( /** @type {string} */ ( './config.json' ) ).wgVectorWebABTestEnrollment || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the bucket the user is assigned to for A/B testing.
|
||||||
|
*
|
||||||
|
* @return {string} the name of the bucket the user is assigned.
|
||||||
|
*/
|
||||||
|
function getBucketName() {
|
||||||
|
/**
|
||||||
|
* Provided config should contain the keys:
|
||||||
|
* name: the name of the experiment prefixed with the skin name.
|
||||||
|
* enabled: must be true or all users are assigned to control.
|
||||||
|
* buckets: dict with bucket name as key and test config as value.
|
||||||
|
*
|
||||||
|
* Bucket test config can contain the keys:
|
||||||
|
* samplingRate: sampling rates will be summed up and each bucket will receive a proportion
|
||||||
|
* equal to its value.
|
||||||
|
*/
|
||||||
|
return mw.experiments.getBucket( {
|
||||||
|
name: ABTestConfig.name,
|
||||||
|
enabled: ABTestConfig.enabled,
|
||||||
|
buckets: {
|
||||||
|
// @ts-ignore
|
||||||
|
unsampled: ABTestConfig.buckets.unsampled.samplingRate,
|
||||||
|
control: ABTestConfig.buckets.control.samplingRate,
|
||||||
|
stickyHeaderDisabled: ABTestConfig.buckets.stickyHeaderDisabled.samplingRate,
|
||||||
|
stickyHeaderEnabled: ABTestConfig.buckets.stickyHeaderEnabled.samplingRate
|
||||||
|
}
|
||||||
|
}, mw.user.getId().toString() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the group and experiment name for an A/B test.
|
||||||
|
*
|
||||||
|
* @return {Object} data to pass to event logging
|
||||||
|
*/
|
||||||
|
function getABTestGroupExperimentName() {
|
||||||
|
return {
|
||||||
|
group: getBucketName(),
|
||||||
|
experimentName: ABTestConfig.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides A/B test config for the current user.
|
||||||
|
*
|
||||||
|
* @return {Object} A/B test config data
|
||||||
|
*/
|
||||||
|
function getEnabledExperiment() {
|
||||||
|
const mergedConfig = {};
|
||||||
|
|
||||||
|
if ( ABTestConfig.enabled ) {
|
||||||
|
// Merge all the A/B config to return.
|
||||||
|
Object.assign( mergedConfig, getABTestGroupExperimentName(), ABTestConfig );
|
||||||
|
}
|
||||||
|
return mergedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire hook to register A/B test enrollment.
|
||||||
|
*
|
||||||
|
* @param {string} bucket the bucket user is assigned to
|
||||||
|
*/
|
||||||
|
function initAB( bucket ) {
|
||||||
|
// Send data to WikimediaEvents to log A/B test initialization if experiment is enabled
|
||||||
|
// and if the user is logged in.
|
||||||
|
if ( ABTestConfig.enabled && !mw.user.isAnon() ) {
|
||||||
|
// @ts-ignore
|
||||||
|
mw.hook( 'mediawiki.web_AB_test_enrollment' ).fire( getABTestGroupExperimentName() );
|
||||||
|
|
||||||
|
// Remove class if present on the html element so that scroll padding isn't undesirably
|
||||||
|
// applied to users who don't experience the new treatment.
|
||||||
|
if ( bucket !== 'stickyHeaderEnabled' ) {
|
||||||
|
document.documentElement.classList.remove( 'vector-sticky-header-enabled' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getEnabledExperiment,
|
||||||
|
initAB
|
||||||
|
};
|
|
@ -1,6 +1,9 @@
|
||||||
// Enable Vector features limited to ES6 browse
|
// Enable Vector features limited to ES6 browse
|
||||||
const stickyHeader = require( './stickyHeader.js' ),
|
const
|
||||||
searchToggle = require( './searchToggle.js' );
|
searchToggle = require( './searchToggle.js' ),
|
||||||
|
stickyHeader = require( './stickyHeader.js' ),
|
||||||
|
scrollObserver = require( './scrollObserver.js' ),
|
||||||
|
AB = require( './AB.js' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {void}
|
* @return {void}
|
||||||
|
@ -12,7 +15,45 @@ const main = () => {
|
||||||
if ( searchToggleElement ) {
|
if ( searchToggleElement ) {
|
||||||
searchToggle( searchToggleElement );
|
searchToggle( searchToggleElement );
|
||||||
}
|
}
|
||||||
stickyHeader();
|
|
||||||
|
// Get the A/B test config for sticky header if enabled.
|
||||||
|
const
|
||||||
|
testConfig = AB.getEnabledExperiment(),
|
||||||
|
stickyConfig = testConfig &&
|
||||||
|
// @ts-ignore
|
||||||
|
testConfig.experimentName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ?
|
||||||
|
testConfig : null,
|
||||||
|
// Note that the default test group is set to experience the feature by default.
|
||||||
|
// @ts-ignore
|
||||||
|
testGroup = stickyConfig ? stickyConfig.group : scrollObserver.FEATURE_TEST_GROUP,
|
||||||
|
targetElement = stickyHeader.header;
|
||||||
|
|
||||||
|
// Check for target html, sticky header conditionals, and test group to continue.
|
||||||
|
if ( !( targetElement &&
|
||||||
|
stickyHeader.isStickyHeaderAllowed() &&
|
||||||
|
testGroup !== 'unsampled' )
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire the A/B test enrollment hook.
|
||||||
|
AB.initAB( testGroup );
|
||||||
|
|
||||||
|
// Set up intersection observer for sticky header functionality and firing scroll event hooks
|
||||||
|
// for event logging if AB test is enabled.
|
||||||
|
const observer = scrollObserver.initScrollObserver(
|
||||||
|
() => {
|
||||||
|
scrollObserver.onShowFeature( targetElement, testGroup );
|
||||||
|
scrollObserver.logScrollEvent( 'down' );
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
scrollObserver.onHideFeature( targetElement, testGroup );
|
||||||
|
scrollObserver.logScrollEvent( 'up' );
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
stickyHeader.initStickyHeader( observer );
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
const
|
||||||
|
FEATURE_VISIBLE_CLASS = 'vector-sticky-header-visible',
|
||||||
|
FEATURE_TEST_GROUP = 'stickyHeaderEnabled',
|
||||||
|
SCROLL_HOOK = 'vector.page_title_scroll',
|
||||||
|
SCROLL_CONTEXT_ABOVE = 'scrolled-above-page-title',
|
||||||
|
SCROLL_CONTEXT_BELOW = 'scrolled-below-page-title',
|
||||||
|
SCROLL_ACTION = 'scroll-to-top';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if user is in test group to experience feature.
|
||||||
|
*
|
||||||
|
* @param {string} bucket the bucket name the user is assigned
|
||||||
|
* @param {string} targetGroup the target test group to experience feature
|
||||||
|
* @return {boolean} true if the user should experience feature
|
||||||
|
*/
|
||||||
|
function isInTestGroup( bucket, targetGroup ) {
|
||||||
|
return bucket === targetGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the feature based on test group.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element target feature
|
||||||
|
* @param {string} group A/B test bucket of the user
|
||||||
|
*/
|
||||||
|
function onShowFeature( element, group ) {
|
||||||
|
if ( isInTestGroup( group, FEATURE_TEST_GROUP ) ) {
|
||||||
|
// eslint-disable-next-line mediawiki/class-doc
|
||||||
|
element.classList.add( FEATURE_VISIBLE_CLASS );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the feature based on test group.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element target feature
|
||||||
|
* @param {string} group A/B test bucket of the user
|
||||||
|
*/
|
||||||
|
function onHideFeature( element, group ) {
|
||||||
|
if ( isInTestGroup( group, FEATURE_TEST_GROUP ) ) {
|
||||||
|
// eslint-disable-next-line mediawiki/class-doc
|
||||||
|
element.classList.remove( FEATURE_VISIBLE_CLASS );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a hook to be captured by WikimediaEvents for scroll event logging.
|
||||||
|
*
|
||||||
|
* @param {string} direction the scroll direction
|
||||||
|
*/
|
||||||
|
function logScrollEvent( direction ) {
|
||||||
|
if ( direction === 'down' ) {
|
||||||
|
// @ts-ignore
|
||||||
|
mw.hook( SCROLL_HOOK ).fire( { context: SCROLL_CONTEXT_BELOW } );
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
mw.hook( SCROLL_HOOK ).fire( {
|
||||||
|
context: SCROLL_CONTEXT_ABOVE,
|
||||||
|
action: SCROLL_ACTION
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observer for showing/hiding feature and for firing scroll event hooks.
|
||||||
|
*
|
||||||
|
* @param {Function} show functionality for when feature is visible
|
||||||
|
* @param {Function} hide functionality for when feature is hidden
|
||||||
|
* @return {IntersectionObserver}
|
||||||
|
*/
|
||||||
|
function initScrollObserver( show, hide ) {
|
||||||
|
/* eslint-disable-next-line compat/compat */
|
||||||
|
return new IntersectionObserver( function ( entries ) {
|
||||||
|
if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) {
|
||||||
|
// Viewport has crossed the bottom edge of the target element.
|
||||||
|
show();
|
||||||
|
} else {
|
||||||
|
// Viewport is above the bottom edge of the target element.
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initScrollObserver,
|
||||||
|
onShowFeature,
|
||||||
|
onHideFeature,
|
||||||
|
logScrollEvent,
|
||||||
|
FEATURE_TEST_GROUP
|
||||||
|
};
|
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Functions and variables to implement sticky header.
|
||||||
|
*/
|
||||||
const
|
const
|
||||||
STICKY_HEADER_ID = 'vector-sticky-header',
|
STICKY_HEADER_ID = 'vector-sticky-header',
|
||||||
initSearchToggle = require( './searchToggle.js' ),
|
initSearchToggle = require( './searchToggle.js' ),
|
||||||
|
@ -7,7 +10,8 @@ const
|
||||||
FIRST_HEADING_ID = 'firstHeading',
|
FIRST_HEADING_ID = 'firstHeading',
|
||||||
USER_MENU_ID = 'p-personal',
|
USER_MENU_ID = 'p-personal',
|
||||||
VECTOR_USER_LINKS_SELECTOR = '.vector-user-links',
|
VECTOR_USER_LINKS_SELECTOR = '.vector-user-links',
|
||||||
SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle';
|
SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle',
|
||||||
|
STICKY_HEADER_EXPERIMENT_NAME = 'vector.sticky_header_2021_11';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies attribute from an element to another.
|
* Copies attribute from an element to another.
|
||||||
|
@ -222,33 +226,54 @@ function isInViewport( element ) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add hooks for sticky header when Visual Editor is used.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element target feature
|
||||||
|
* @param {HTMLElement} targetIntersection intersection element
|
||||||
|
* @param {IntersectionObserver} observer
|
||||||
|
*/
|
||||||
|
function addVisualEditorHooks( element, targetIntersection, observer ) {
|
||||||
|
// When Visual Editor is activated, hide the sticky header.
|
||||||
|
mw.hook( 've.activate' ).add( () => {
|
||||||
|
// eslint-disable-next-line mediawiki/class-doc
|
||||||
|
element.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
|
||||||
|
observer.unobserve( targetIntersection );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// When Visual Editor is deactivated (by clicking "Read" tab at top of page), show sticky header
|
||||||
|
// by re-triggering the observer.
|
||||||
|
mw.hook( 've.deactivationComplete' ).add( () => {
|
||||||
|
observer.observe( targetIntersection );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// After saving edits, re-apply the sticky header if the target is not in the viewport.
|
||||||
|
mw.hook( 'postEdit.afterRemoval' ).add( () => {
|
||||||
|
if ( !isInViewport( targetIntersection ) ) {
|
||||||
|
// eslint-disable-next-line mediawiki/class-doc
|
||||||
|
element.classList.add( STICKY_HEADER_VISIBLE_CLASS );
|
||||||
|
observer.observe( targetIntersection );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes sticky header functional for modern Vector.
|
* Makes sticky header functional for modern Vector.
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} header
|
* @param {HTMLElement} header
|
||||||
* @param {HTMLElement} stickyIntersection
|
|
||||||
* @param {HTMLElement} userMenu
|
* @param {HTMLElement} userMenu
|
||||||
* @param {Element} userMenuStickyContainer
|
* @param {Element} userMenuStickyContainer
|
||||||
|
* @param {IntersectionObserver} stickyObserver
|
||||||
|
* @param {HTMLElement} stickyIntersection
|
||||||
*/
|
*/
|
||||||
function makeStickyHeaderFunctional(
|
function makeStickyHeaderFunctional(
|
||||||
header,
|
header,
|
||||||
stickyIntersection,
|
|
||||||
userMenu,
|
userMenu,
|
||||||
userMenuStickyContainer
|
userMenuStickyContainer,
|
||||||
|
stickyObserver,
|
||||||
|
stickyIntersection
|
||||||
) {
|
) {
|
||||||
const
|
const
|
||||||
/* eslint-disable-next-line compat/compat */
|
|
||||||
stickyObserver = new IntersectionObserver( function ( entries ) {
|
|
||||||
if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) {
|
|
||||||
// Viewport has crossed the bottom edge of firstHeading so show sticky header.
|
|
||||||
// eslint-disable-next-line mediawiki/class-doc
|
|
||||||
header.classList.add( STICKY_HEADER_VISIBLE_CLASS );
|
|
||||||
} else {
|
|
||||||
// Viewport is above the bottom edge of firstHeading so hide sticky header.
|
|
||||||
// eslint-disable-next-line mediawiki/class-doc
|
|
||||||
header.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
|
|
||||||
}
|
|
||||||
} ),
|
|
||||||
// Type declaration needed because of https://github.com/Microsoft/TypeScript/issues/3734#issuecomment-118934518
|
// Type declaration needed because of https://github.com/Microsoft/TypeScript/issues/3734#issuecomment-118934518
|
||||||
userMenuClone = /** @type {HTMLElement} */( userMenu.cloneNode( true ) ),
|
userMenuClone = /** @type {HTMLElement} */( userMenu.cloneNode( true ) ),
|
||||||
userMenuStickyElementsWithIds = userMenuClone.querySelectorAll( '[ id ], [ data-event-name ]' ),
|
userMenuStickyElementsWithIds = userMenuClone.querySelectorAll( '[ id ], [ data-event-name ]' ),
|
||||||
|
@ -306,23 +331,6 @@ function makeStickyHeaderFunctional(
|
||||||
);
|
);
|
||||||
|
|
||||||
stickyObserver.observe( stickyIntersection );
|
stickyObserver.observe( stickyIntersection );
|
||||||
|
|
||||||
// When Visual Editor is activated, hide sticky header.
|
|
||||||
mw.hook( 've.activate' ).add( disableStickyHeader );
|
|
||||||
|
|
||||||
// When Visual Editor is deactivated, by cliking "read" tab at top of page, show sticky header.
|
|
||||||
mw.hook( 've.deactivationComplete' ).add( () => {
|
|
||||||
stickyObserver.observe( stickyIntersection );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// After saving edits, re-apply the sticky header if the target is not in the viewport.
|
|
||||||
mw.hook( 'postEdit.afterRemoval' ).add( () => {
|
|
||||||
if ( !isInViewport( stickyIntersection ) ) {
|
|
||||||
// eslint-disable-next-line mediawiki/class-doc
|
|
||||||
header.classList.add( STICKY_HEADER_VISIBLE_CLASS );
|
|
||||||
stickyObserver.observe( stickyIntersection );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -365,8 +373,8 @@ function isAllowedAction( action ) {
|
||||||
return disallowedActions.indexOf( action ) < 0 && !hasDiffId;
|
return disallowedActions.indexOf( action ) < 0 && !hasDiffId;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function initStickyHeader() {
|
const
|
||||||
const header = document.getElementById( STICKY_HEADER_ID ),
|
header = document.getElementById( STICKY_HEADER_ID ),
|
||||||
stickyIntersection = document.getElementById(
|
stickyIntersection = document.getElementById(
|
||||||
FIRST_HEADING_ID
|
FIRST_HEADING_ID
|
||||||
),
|
),
|
||||||
|
@ -377,17 +385,47 @@ module.exports = function initStickyHeader() {
|
||||||
allowedNamespace = isAllowedNamespace( mw.config.get( 'wgNamespaceNumber' ) ),
|
allowedNamespace = isAllowedNamespace( mw.config.get( 'wgNamespaceNumber' ) ),
|
||||||
allowedAction = isAllowedAction( mw.config.get( 'wgAction' ) );
|
allowedAction = isAllowedAction( mw.config.get( 'wgAction' ) );
|
||||||
|
|
||||||
if ( !(
|
/**
|
||||||
header &&
|
* Check if all conditions are met to enable sticky header
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function isStickyHeaderAllowed() {
|
||||||
|
// @ts-ignore
|
||||||
|
return header &&
|
||||||
stickyIntersection &&
|
stickyIntersection &&
|
||||||
userMenu &&
|
userMenu &&
|
||||||
userMenuStickyContainer &&
|
userMenuStickyContainer &&
|
||||||
allowedNamespace &&
|
allowedNamespace &&
|
||||||
allowedAction &&
|
allowedAction &&
|
||||||
'IntersectionObserver' in window ) ) {
|
'IntersectionObserver' in window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {IntersectionObserver} observer
|
||||||
|
*/
|
||||||
|
function initStickyHeader( observer ) {
|
||||||
|
if ( !isStickyHeaderAllowed() ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
makeStickyHeaderFunctional( header, stickyIntersection, userMenu, userMenuStickyContainer );
|
makeStickyHeaderFunctional(
|
||||||
|
// @ts-ignore
|
||||||
|
header,
|
||||||
|
userMenu,
|
||||||
|
userMenuStickyContainer,
|
||||||
|
observer,
|
||||||
|
stickyIntersection
|
||||||
|
);
|
||||||
|
// @ts-ignore
|
||||||
setupSearchIfNeeded( header );
|
setupSearchIfNeeded( header );
|
||||||
|
// @ts-ignore
|
||||||
|
addVisualEditorHooks( header, stickyIntersection, observer );
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initStickyHeader,
|
||||||
|
isStickyHeaderAllowed,
|
||||||
|
header,
|
||||||
|
STICKY_HEADER_EXPERIMENT_NAME
|
||||||
};
|
};
|
||||||
|
|
35
skin.json
35
skin.json
|
@ -206,7 +206,19 @@
|
||||||
"packageFiles": [
|
"packageFiles": [
|
||||||
"resources/skins.vector.es6/main.js",
|
"resources/skins.vector.es6/main.js",
|
||||||
"resources/skins.vector.es6/searchToggle.js",
|
"resources/skins.vector.es6/searchToggle.js",
|
||||||
"resources/skins.vector.es6/stickyHeader.js"
|
"resources/skins.vector.es6/stickyHeader.js",
|
||||||
|
"resources/skins.vector.es6/scrollObserver.js",
|
||||||
|
"resources/skins.vector.es6/AB.js",
|
||||||
|
{
|
||||||
|
"name": "resources/skins.vector.es6/config.json",
|
||||||
|
"callback": "Vector\\Hooks::getVectorResourceLoaderConfig"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": [
|
||||||
|
"skins.vector.icons.js",
|
||||||
|
"mediawiki.page.ready",
|
||||||
|
"mediawiki.util",
|
||||||
|
"mediawiki.experiments"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"skins.vector.js": {
|
"skins.vector.js": {
|
||||||
|
@ -364,6 +376,27 @@
|
||||||
},
|
},
|
||||||
"description": "@var array Enables the edit icons if $wgVectorStickyHeader is true."
|
"description": "@var array Enables the edit icons if $wgVectorStickyHeader is true."
|
||||||
},
|
},
|
||||||
|
"VectorWebABTestEnrollment": {
|
||||||
|
"value": {
|
||||||
|
"name": "vector.sticky_header_2021_11",
|
||||||
|
"enabled": false,
|
||||||
|
"buckets": {
|
||||||
|
"unsampled": {
|
||||||
|
"samplingRate": "0.1"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"samplingRate": "0.3"
|
||||||
|
},
|
||||||
|
"stickyHeaderDisabled": {
|
||||||
|
"samplingRate": "0.3"
|
||||||
|
},
|
||||||
|
"stickyHeaderEnabled": {
|
||||||
|
"samplingRate": "0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "An associative array of A/B test configs keyed by parameters noted in mediawiki.experiments.js."
|
||||||
|
},
|
||||||
"VectorDisableSidebarPersistence": {
|
"VectorDisableSidebarPersistence": {
|
||||||
"value": false,
|
"value": false,
|
||||||
"description": "@var boolean Temporary feature flag that disables saving the sidebar expanded/collapsed state as a user-preference (triggered via clicking the main menu icon). This is intended as a temporary kill-switch in the event that the DB is overloaded with writes to the user_options table."
|
"description": "@var boolean Temporary feature flag that disables saving the sidebar expanded/collapsed state as a user-preference (triggered via clicking the main menu icon). This is intended as a temporary kill-switch in the event that the DB is overloaded with writes to the user_options table."
|
||||||
|
|
Loading…
Reference in New Issue