mediawiki-extensions-Popups/src/index.js

306 lines
9.7 KiB
JavaScript
Raw Normal View History

import * as Redux from 'redux';
import * as ReduxThunk from 'redux-thunk';
import createPagePreviewGateway from './gateway/page';
import createUserSettings from './userSettings';
import createPreviewBehavior from './previewBehavior';
import createSettingsDialogRenderer from './ui/settingsDialogRenderer';
import registerChangeListener from './changeListener';
import createIsPagePreviewsEnabled from './isPagePreviewsEnabled';
import { fromElement as titleFromElement } from './title';
import { init as rendererInit, registerPreviewUI, createPagePreview,
createDisambiguationPreview
} from './ui/renderer';
import createExperiments from './experiments';
import { isEnabled as isStatsvEnabled } from './instrumentation/statsv';
import changeListeners from './changeListeners';
import * as actions from './actions';
import reducers from './reducers';
import createMediaWikiPopupsObject from './integrations/mwpopups';
import { previewTypes, getPreviewType,
registerModel, findNearestEligibleTarget } from './preview/model';
import setUserConfigFlags from './setUserConfigFlags';
import { registerGatewayForPreviewType, getGatewayForPreviewType } from './gateway';
import { FETCH_START_DELAY, FETCH_COMPLETE_TARGET_DELAY } from './constants';
/**
* @module popups
* @private
*/
const EXCLUDED_LINK_SELECTORS = [
'.extiw',
// ignore links that point to the same article
'.mw-selflink',
'.image',
'.new',
'.internal',
'.external',
'.mw-cite-backlink a',
'.oo-ui-buttonElement-button',
'.ve-ce-surface a', // T259889
'.ext-discussiontools-init-timestamplink',
'.cancelLink a',
// T198652: lists to hash fragments are ignored.
// Note links that include the path will still trigger a hover,
// e.g. <a href="Foo#foo"> will trigger a preview but <a href="#foo"> will not.
// This is intentional behaviour that will not be handled by page previews, to avoid
// introducing complex behaviour. If a link must include the path it should make use of
// the .mw-selflink-fragment class.
'.mw-selflink-fragment',
'[href^="#"]'
];
/**
* @typedef {Function} EventTracker
*
* An analytics event tracker, i.e. `mw.track`.
*
* @param {string} topic
* @param {Object} data
*/
/**
* Gets the appropriate analytics event tracker for logging metrics to StatsD
* via [the "StatsD timers and counters" analytics event protocol][0].
*
* If logging metrics to StatsD is enabled for the duration of the user's
* session, then the appriopriate function is `mw.track`; otherwise it's
* `() => {}`.
*
* [0]: https://github.com/wikimedia/mediawiki-extensions-WikimediaEvents/blob/29c864a0/modules/ext.wikimediaEvents.statsd.js
*
* @param {Object} user
* @param {Object} config
* @param {Experiments} experiments
* @return {EventTracker}
*/
function getStatsvTracker( user, config, experiments ) {
return isStatsvEnabled( user, config, experiments ) ? mw.track : () => {};
}
/**
* Gets the appropriate analytics event tracker for logging virtual pageviews.
*
* @param {Object} config
* @return {EventTracker}
*/
function getPageviewTracker( config ) {
return config.get( 'wgPopupsVirtualPageViews' ) ? mw.track : () => {
// NOP
};
}
/**
* Subscribes the registered change listeners to the
* [store](http://redux.js.org/docs/api/Store.html#store).
*
* @param {Redux.Store} store
* @param {Object} registerActions
* @param {UserSettings} userSettings
* @param {Function} settingsDialog
* @param {PreviewBehavior} previewBehavior
* @param {EventTracker} statsvTracker
* @param {EventTracker} pageviewTracker
*/
function registerChangeListeners(
store, registerActions, userSettings, settingsDialog, previewBehavior,
statsvTracker, pageviewTracker
) {
registerChangeListener( store, changeListeners.footerLink( registerActions ) );
registerChangeListener( store, changeListeners.linkTitle() );
registerChangeListener( store, changeListeners.render( previewBehavior ) );
registerChangeListener(
store, changeListeners.statsv( registerActions, statsvTracker ) );
registerChangeListener(
store, changeListeners.syncUserSettings( userSettings ) );
registerChangeListener(
store, changeListeners.settings( registerActions, settingsDialog ) );
registerChangeListener( store,
changeListeners.pageviews( registerActions, pageviewTracker )
);
}
/**
* Creates an event handler that only executes if the current target
* is eligible for page previews and a title can be associated with the element.
*
* @param {Function} handler
* @return {Function}
*/
function handleDOMEventIfEligible( handler ) {
return function ( event ) {
let target = event && event.target;
if ( !target ) {
return;
}
// if the element is a text node, as events can be triggered on text nodes
// it won't have a closest method, so we get its parent element (T340081)
if ( target.nodeType === 3 ) {
target = target.parentNode;
}
// If the event bubbles up all the way,
// document does not have closest method, so exit early (T336650).
if ( target === document ) {
return;
}
// If the closest method is not defined, let's return early and
// understand this better by logging an error. (T340081)
if ( target && !target.closest ) {
const err = new Error( `T340081: Unexpected DOM element ${ target.tagName } with nodeType ${ target.nodeType }` );
mw.errorLogger.logError( err, 'error.web-team' );
return;
}
target = findNearestEligibleTarget( target );
if ( target === null ) {
return;
}
const mwTitle = titleFromElement( target, mw.config );
if ( mwTitle ) {
handler( target, mwTitle, event );
}
};
}
/*
* Initialize the application by:
* 1. Initializing side-effects and "services"
* 2. Creating the state store
* 3. Binding the actions to such store
* 4. Registering change listeners
* 5. Triggering the boot action to bootstrap the system
* 6. When the page content is ready:
* - Initializing the renderer
* - Binding hover and click events to the eligible links to trigger actions
*/
( function init() {
setUserConfigFlags( mw.config );
let compose = Redux.compose;
const
// So-called "services".
generateToken = mw.user.generateRandomSessionId,
pagePreviewGateway = createPagePreviewGateway( mw.config ),
userSettings = createUserSettings( mw.storage ),
Generalize settings code (attempt 2) This reverts commit a6a65204c69399b8332c1894ee7ad7cece0fbeb5. to restore custom preview types. -- Changes since revert The previous patch accidentally removed the syncUserSettings changeListener. This has now been restored with several modifications: * We have a migrate script which rewrites existing localStorage settings to the new system * The existing save functions are generalized. The changes since this patch are captured in Ia73467799a9b535f7a3cf7268727c9fab7af0d7e -- More information A new REGISTER_SETTING action replaces the BOOT action for registering settings. This allows custom preview types to be associated with a setting. They do this by adding the enabled property to the module they provide to mw.popups.register Every time the new action is called, we refresh the settings dialog UI with the new settings. Previously the settings dialog was hardcoded, but now it is generated from the registered preview types by deriving associated messages and checking they exist, so by default custom types will not show up in the settings. Benefits: * This change empowers us to add a setting for Math previews to allow them to be enabled or disabled. * Allows us to separate references as its own module Additional notes: * The syncUserSettings.js changeListener is no longer needed as the logic for this is handled inside the "userSettings" change listener in response to the "settings" reducer which is responding to SETTINGS_CHANGE and REGISTER_SETTING actions. Upon merging: * https://www.mediawiki.org/wiki/Extension:Popups#Extensibility will be updated to detail how a setting can be registered. Bug: T334261 Bug: T326692 Change-Id: Ie17d622870511ac9730fc9fa525698fc3aa0d5b6
2023-10-19 21:12:09 +00:00
settingsDialog = createSettingsDialogRenderer(),
experiments = createExperiments( mw.experiments ),
statsvTracker = getStatsvTracker( mw.user, mw.config, experiments ),
pageviewTracker = getPageviewTracker( mw.config ),
Generalize settings code (attempt 2) This reverts commit a6a65204c69399b8332c1894ee7ad7cece0fbeb5. to restore custom preview types. -- Changes since revert The previous patch accidentally removed the syncUserSettings changeListener. This has now been restored with several modifications: * We have a migrate script which rewrites existing localStorage settings to the new system * The existing save functions are generalized. The changes since this patch are captured in Ia73467799a9b535f7a3cf7268727c9fab7af0d7e -- More information A new REGISTER_SETTING action replaces the BOOT action for registering settings. This allows custom preview types to be associated with a setting. They do this by adding the enabled property to the module they provide to mw.popups.register Every time the new action is called, we refresh the settings dialog UI with the new settings. Previously the settings dialog was hardcoded, but now it is generated from the registered preview types by deriving associated messages and checking they exist, so by default custom types will not show up in the settings. Benefits: * This change empowers us to add a setting for Math previews to allow them to be enabled or disabled. * Allows us to separate references as its own module Additional notes: * The syncUserSettings.js changeListener is no longer needed as the logic for this is handled inside the "userSettings" change listener in response to the "settings" reducer which is responding to SETTINGS_CHANGE and REGISTER_SETTING actions. Upon merging: * https://www.mediawiki.org/wiki/Extension:Popups#Extensibility will be updated to detail how a setting can be registered. Bug: T334261 Bug: T326692 Change-Id: Ie17d622870511ac9730fc9fa525698fc3aa0d5b6
2023-10-19 21:12:09 +00:00
pagePreviewState = createIsPagePreviewsEnabled( mw.user, userSettings, mw.config );
// If debug mode is enabled, then enable Redux DevTools.
if ( mw.config.get( 'debug' ) ||
/* global process */
process.env.NODE_ENV !== 'production' ) {
// eslint-disable-next-line no-underscore-dangle
compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
}
const store = Redux.createStore(
Redux.combineReducers( reducers ),
compose( Redux.applyMiddleware(
ReduxThunk.default
) )
);
const boundActions = Redux.bindActionCreators( actions, store.dispatch );
const previewBehavior = createPreviewBehavior( mw.user, boundActions );
registerChangeListeners(
store, boundActions, userSettings, settingsDialog,
previewBehavior, statsvTracker, pageviewTracker
);
boundActions.boot(
Generalize settings code (attempt 2) This reverts commit a6a65204c69399b8332c1894ee7ad7cece0fbeb5. to restore custom preview types. -- Changes since revert The previous patch accidentally removed the syncUserSettings changeListener. This has now been restored with several modifications: * We have a migrate script which rewrites existing localStorage settings to the new system * The existing save functions are generalized. The changes since this patch are captured in Ia73467799a9b535f7a3cf7268727c9fab7af0d7e -- More information A new REGISTER_SETTING action replaces the BOOT action for registering settings. This allows custom preview types to be associated with a setting. They do this by adding the enabled property to the module they provide to mw.popups.register Every time the new action is called, we refresh the settings dialog UI with the new settings. Previously the settings dialog was hardcoded, but now it is generated from the registered preview types by deriving associated messages and checking they exist, so by default custom types will not show up in the settings. Benefits: * This change empowers us to add a setting for Math previews to allow them to be enabled or disabled. * Allows us to separate references as its own module Additional notes: * The syncUserSettings.js changeListener is no longer needed as the logic for this is handled inside the "userSettings" change listener in response to the "settings" reducer which is responding to SETTINGS_CHANGE and REGISTER_SETTING actions. Upon merging: * https://www.mediawiki.org/wiki/Extension:Popups#Extensibility will be updated to detail how a setting can be registered. Bug: T334261 Bug: T326692 Change-Id: Ie17d622870511ac9730fc9fa525698fc3aa0d5b6
2023-10-19 21:12:09 +00:00
{},
mw.user,
userSettings,
mw.config,
window.location.href
);
/*
* Register external interface exposing popups internals so that other
* extensions can query it (T171287)
*/
mw.popups = createMediaWikiPopupsObject(
Generalize settings code (attempt 2) This reverts commit a6a65204c69399b8332c1894ee7ad7cece0fbeb5. to restore custom preview types. -- Changes since revert The previous patch accidentally removed the syncUserSettings changeListener. This has now been restored with several modifications: * We have a migrate script which rewrites existing localStorage settings to the new system * The existing save functions are generalized. The changes since this patch are captured in Ia73467799a9b535f7a3cf7268727c9fab7af0d7e -- More information A new REGISTER_SETTING action replaces the BOOT action for registering settings. This allows custom preview types to be associated with a setting. They do this by adding the enabled property to the module they provide to mw.popups.register Every time the new action is called, we refresh the settings dialog UI with the new settings. Previously the settings dialog was hardcoded, but now it is generated from the registered preview types by deriving associated messages and checking they exist, so by default custom types will not show up in the settings. Benefits: * This change empowers us to add a setting for Math previews to allow them to be enabled or disabled. * Allows us to separate references as its own module Additional notes: * The syncUserSettings.js changeListener is no longer needed as the logic for this is handled inside the "userSettings" change listener in response to the "settings" reducer which is responding to SETTINGS_CHANGE and REGISTER_SETTING actions. Upon merging: * https://www.mediawiki.org/wiki/Extension:Popups#Extensibility will be updated to detail how a setting can be registered. Bug: T334261 Bug: T326692 Change-Id: Ie17d622870511ac9730fc9fa525698fc3aa0d5b6
2023-10-19 21:12:09 +00:00
store, registerModel, registerPreviewUI, registerGatewayForPreviewType,
boundActions.registerSetting, userSettings
);
Generalize settings code (attempt 2) This reverts commit a6a65204c69399b8332c1894ee7ad7cece0fbeb5. to restore custom preview types. -- Changes since revert The previous patch accidentally removed the syncUserSettings changeListener. This has now been restored with several modifications: * We have a migrate script which rewrites existing localStorage settings to the new system * The existing save functions are generalized. The changes since this patch are captured in Ia73467799a9b535f7a3cf7268727c9fab7af0d7e -- More information A new REGISTER_SETTING action replaces the BOOT action for registering settings. This allows custom preview types to be associated with a setting. They do this by adding the enabled property to the module they provide to mw.popups.register Every time the new action is called, we refresh the settings dialog UI with the new settings. Previously the settings dialog was hardcoded, but now it is generated from the registered preview types by deriving associated messages and checking they exist, so by default custom types will not show up in the settings. Benefits: * This change empowers us to add a setting for Math previews to allow them to be enabled or disabled. * Allows us to separate references as its own module Additional notes: * The syncUserSettings.js changeListener is no longer needed as the logic for this is handled inside the "userSettings" change listener in response to the "settings" reducer which is responding to SETTINGS_CHANGE and REGISTER_SETTING actions. Upon merging: * https://www.mediawiki.org/wiki/Extension:Popups#Extensibility will be updated to detail how a setting can be registered. Bug: T334261 Bug: T326692 Change-Id: Ie17d622870511ac9730fc9fa525698fc3aa0d5b6
2023-10-19 21:12:09 +00:00
// Migrate any old preferences to new system.
// FIXME: This can be removed in 4 weeks time.
userSettings.migrateOldPreferences();
if ( pagePreviewState !== null ) {
const excludedLinksSelector = EXCLUDED_LINK_SELECTORS.join( ', ' );
// Register default preview type
mw.popups.register( {
type: previewTypes.TYPE_PAGE,
selector: `#mw-content-text a[href][title]:not(${ excludedLinksSelector })`,
delay: FETCH_COMPLETE_TARGET_DELAY - FETCH_START_DELAY,
gateway: pagePreviewGateway,
renderFn: createPagePreview,
subTypes: [
{
type: previewTypes.TYPE_DISAMBIGUATION,
renderFn: createDisambiguationPreview,
doNotRequireSummary: true
}
]
} );
}
rendererInit();
/*
* Binding hover and click events to the eligible links to trigger actions
*/
function setupEventListeners() {
const onHover = handleDOMEventIfEligible( function ( target, mwTitle, event ) {
const type = getPreviewType( target );
const gateway = getGatewayForPreviewType( type );
if ( !gateway ) {
return;
}
const scrollTop = window.scrollY;
const bbox = target.getBoundingClientRect();
const offset = {
top: scrollTop + bbox.y,
left: window.scrollX + bbox.x
};
const measures = {
pageX: event.pageX,
pageY: event.pageY,
clientY: event.clientY,
width: target.offsetWidth,
height: target.offsetHeight,
offset,
clientRects: target.getClientRects(),
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
scrollTop
};
boundActions.linkDwell( mwTitle, target, measures, gateway, generateToken, type );
} );
const onHoverOut = handleDOMEventIfEligible( function () {
boundActions.abandon();
} );
const onClick = handleDOMEventIfEligible( function ( target ) {
if ( previewTypes.TYPE_PAGE === getPreviewType( target ) ) {
boundActions.linkClick( target );
}
} );
document.addEventListener( 'mouseover', onHover );
document.addEventListener( 'keyup', onHover );
document.addEventListener( 'mouseout', onHoverOut );
document.addEventListener( 'blur', onHoverOut );
document.addEventListener( 'click', onClick );
}
setupEventListeners();
}() );
window.Redux = Redux;
window.ReduxThunk = ReduxThunk;