Page MenuHomeWickedGov Phorge

ext.centralNotice.bannerSequence.js
No OneTemporary

Size
9 KB
Referenced Files
None
Subscribers
None

ext.centralNotice.bannerSequence.js

/**
* Banner sequence mixin. Displays banners in a sequence in which banners may repeat
* for a set number of page views. Sequence steps may also include no banner display.
* Allows flags in the user's browser to skip a step if it was seen before, or if a
* different step that uses the same identifier (in this or another sequence) was seen
* before.
*
* Page view counter may be reset with the URL parameter reset=1.
*
* For an explanation of the data structure for the sequences mixin parameter, please
* see ext.centralNotice.adminUi.bannerSequence.js.
*/
( function () {
'use strict';
var multiStorageOption, days, SequenceManager, sequenceManager,
preBannerHandler, postBannerOrFailHandler,
cn = mw.centralNotice,
mixin = new cn.Mixin( 'bannerSequence' ),
// Flag to indicate that the current step shows no banner
showingEmptyStep = false,
// We also check identifiers from the large banner limit mixin.
// Coordinate both storage keys with ext.centralNotice.largeBannerLimit.js.
// TODO Maybe find a better way to do this?
FLAG_STORAGE_KEY = 'banner_sequence_step_seen',
LARGE_BANNER_LIMIT_STORAGE_KEY = 'large_banner_limit',
PAGE_VIEW_STORAGE_KEY = 'banner_sequence_page_view',
// TTL for sequence page view, in days
PAGE_VIEW_STORAGE_TTL = 365;
/**
* A class to manage banner sequence data. Encapsulates counting page views,
* determining the current and next steps, steps to skip, etc.
*
* @class SequenceManager
* @constructor
* @param {Array} sequence The sequence to follow
* @param {number} currentPageView The current page view in the sequence (before a
* banner has been shown)
*/
SequenceManager = function ( sequence, currentPageView ) {
var i;
this.sequence = sequence;
// The page view in the sequence that we will display. (This will be updated if
// we have to skip this step or if the sequence length has been reduced and
// we're beyond the new limit.)
this.currentPageView = currentPageView;
// Track how many steps have been skipped due to stored flags
this.stepsSkipped = 0;
// Total number of page views in the sequence
this.seqTotalPageViews = 0;
// Array with starting page view indexes for each step
this.stepStarts = [];
// These will be set as needed in processPageView()
this.nextPageView = null;
this.identifierToSet = null;
// Find total page views in the sequence and the starting page views
for ( i = 0; i < this.sequence.length; i++ ) {
this.stepStarts[ i ] = this.seqTotalPageViews;
this.seqTotalPageViews += this.sequence[ i ].numPageViews;
}
// If the current page view is beyond the sequence length, that means the sequence
// has been shortened since last time we were here. In that case, start
// over.
this.currentPageView = ( this.currentPageView < this.seqTotalPageViews ) ?
this.currentPageView : 0;
// Find the current step
for ( i = 0; i < this.sequence.length; i++ ) {
if ( i === this.sequence.length - 1 ) {
this.currentStep = i;
break;
}
if ( ( this.currentPageView >= this.stepStarts[ i ] ) &&
( this.currentPageView < this.stepStarts[ i + 1 ] ) ) {
this.currentStep = i;
break;
}
}
};
/**
* Get the identifier to check for the current step.
*
* @return {string}
*/
SequenceManager.prototype.identifierToCheck = function () {
return this.sequence[ this.currentStep ].skipWithIdentifier;
};
/**
* Get the name of the banner to show on the current step, or null if this step has no
* banner.
*
* @return {string|null}
*/
SequenceManager.prototype.banner = function () {
return this.sequence[ this.currentStep ].banner;
};
/**
* Skip to the next step and re-calculate current step and current page view. (Called
* when a flag to skip the current step has been found).
*
* @return {boolean} true if we successfully skipped to the next step, false if we've
* already skipped all the steps in the sequence and can no longer show a step.
*/
SequenceManager.prototype.skipToNextStep = function () {
this.stepsSkipped++;
// Return false if we're out of steps
if ( this.stepsSkipped === this.sequence.length ) {
return false;
}
// Move one step ahead, wrapping around as needed
this.currentStep = ( this.currentStep + 1 ) % this.sequence.length;
this.currentPageView = this.stepStarts[ this.currentStep ];
return true;
};
/**
* Process data as needed following a successful display of a banner (or no banner)
* within the sequence. Set this.nextPageView and, if needed, this.identifierToSet.
*/
SequenceManager.prototype.processPageView = function () {
var nextStep = ( this.currentStep + 1 ) % this.sequence.length;
this.nextPageView = ( this.currentPageView + 1 ) % this.seqTotalPageViews;
// If we just finished a step, there might be an identifier to set
if ( this.nextPageView === this.stepStarts[ nextStep ] ) {
// If there is no identifier for this step, this will be null
this.identifierToSet = this.sequence[ this.currentStep ].skipWithIdentifier;
}
};
/**
* Get the current page view for this sequence, as stored in the browser.
* Note: The page view skips ahead if we skip a step because of an identifier, and
* rolls over when we re-start a sequence, so it's not an accurate count of page views
* in the campaign.
*
* @return {number}
*/
function getPageView() {
return cn.kvStore.getItem(
PAGE_VIEW_STORAGE_KEY,
cn.kvStore.contexts.CAMPAIGN,
multiStorageOption
) || 0;
}
/**
* Store in the browser a new value for current page view in the sequence.
*
* @param {number} pageView
*/
function setPageView( pageView ) {
cn.kvStore.setItem(
PAGE_VIEW_STORAGE_KEY,
pageView,
cn.kvStore.contexts.CAMPAIGN,
PAGE_VIEW_STORAGE_TTL,
multiStorageOption
);
}
/**
* Check if there is a flag with this identifier stored in the browser.
*
* @param {string} identifier
* @return {boolean}
*/
function getFlag( identifier ) {
// Also check flags set by the large banner limit mixin...
// TODO Refactor to unify code for setting flags for banners seen?
return Boolean( cn.kvStore.getItem(
FLAG_STORAGE_KEY + '_' + identifier,
cn.kvStore.contexts.GLOBAL,
multiStorageOption
) ) || Boolean( cn.kvStore.getItem(
LARGE_BANNER_LIMIT_STORAGE_KEY + '_' + identifier,
cn.kvStore.contexts.GLOBAL,
multiStorageOption
) );
}
/**
* Store a flag with this identifier in the browser.
*
* @param {string} identifier
*/
function setFlag( identifier ) {
// Value is timestamp for now. Flag expires as per the days mixin param.
cn.kvStore.setItem(
FLAG_STORAGE_KEY + '_' + identifier,
Math.round( Date.now() / 1000 ),
cn.kvStore.contexts.GLOBAL,
days,
multiStorageOption
);
}
preBannerHandler = function ( mixinParams ) {
var identifier, sequence, banner, pageView;
// Campaign was already failed
if ( cn.isCampaignFailed() ) {
return;
}
// json parameters will be null if there was a decoding error server-side. In that
// case, warn and bow out.
// TODO Handle this on the server?
if ( !mixinParams.sequences ) {
mw.log.warn( 'Invalid sequences parameter received for banner sequence' );
cn.failCampaign( 'jsonParamError' );
return;
}
// Determine if, and if so, how we can store data in the browser. This will try
// to use localStorage and will fall back to cookies if the campaign's category
// allows it.
multiStorageOption = cn.kvStore.getMultiStorageOption(
cn.getDataProperty( 'campaignCategoryUsesLegacy' ) );
// If there are no options for storing stuff, hide banner and bow out
if ( multiStorageOption === cn.kvStore.multiStorageOptions.NO_STORAGE ) {
cn.failCampaign( 'noStorage' );
return;
}
days = mixinParams.days;
sequence = mixinParams.sequences[ cn.getDataProperty( 'reducedBucket' ) ];
// Get current page view, resetting if requested (for testing)
// TODO Include a way to clear or ignore identifiers, too?
pageView = mw.util.getParamValue( 'reset' ) === '1' ? 0 : getPageView();
// Set up the sequence manager with the sequence for this bucket and the current
// page view in the sequence
sequenceManager = new SequenceManager( sequence, pageView );
// Check for identifiers and skip steps, if necessary. Make no assumptions about
// how many steps to skip.
while ( true ) {
identifier = sequenceManager.identifierToCheck();
// If this step has no identifier, or there's no flag set with this
// identifier, all good! Show the banner.
if ( !identifier || !getFlag( identifier ) ) {
break;
}
// Tell the sequence manager to skip to the next step. If we've already gone
// through all the steps, don't show anything.
if ( !sequenceManager.skipToNextStep() ) {
cn.failCampaign( 'bannerSequenceAllStepsSkipped' );
return;
}
}
// See if there is a banner for this step
banner = sequenceManager.banner();
// banner is null if this is an empty step
if ( banner === null ) {
showingEmptyStep = true;
cn.failCampaign( 'bannerSequenceEmptyStep' );
return;
}
// Request the banner
cn.requestBanner( banner );
};
postBannerOrFailHandler = function () {
// If a banner was shown, or we showed no banner as part of an empty step, move to
// the next page view in the sequence. If necessary, set a flag.
if ( cn.isBannerShown() || showingEmptyStep ) {
sequenceManager.processPageView();
setPageView( sequenceManager.nextPageView );
if ( sequenceManager.identifierToSet ) {
setFlag( sequenceManager.identifierToSet );
}
}
};
// Register the handlers and mixin
mixin.setPreBannerHandler( preBannerHandler );
mixin.setPostBannerOrFailHandler( postBannerOrFailHandler );
cn.registerCampaignMixin( mixin );
// Exports are for use in unit tests only
module.exports = { private: {
SequenceManager: SequenceManager,
PAGE_VIEW_STORAGE_KEY: PAGE_VIEW_STORAGE_KEY,
FLAG_STORAGE_KEY: FLAG_STORAGE_KEY,
LARGE_BANNER_LIMIT_STORAGE_KEY: LARGE_BANNER_LIMIT_STORAGE_KEY,
preBannerHandler: preBannerHandler,
postBannerOrFailHandler: postBannerOrFailHandler
} };
}() );

File Metadata

Mime Type
text/plain
Expires
Aug 17 2026, 11:07 (11 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
b3/9d/199964e2cf2f73736638c02b9a9d
Default Alt Text
ext.centralNotice.bannerSequence.js (9 KB)

Event Timeline