Remove deprecated copy of referencePreviews module

This has been provided by the Cite extension since I0dc47abb59a4 .

Bug: T362332
Change-Id: Ic2594d0638685326fd08198c691e18371e132ffc
This commit is contained in:
Adam Wight 2024-04-15 10:44:48 +02:00
parent 3cf45dd5ab
commit da29c0299b
11 changed files with 3 additions and 532 deletions

View file

@ -155,23 +155,6 @@
"resources/ext.popups/index.js"
]
},
"ext.popups.referencePreviews": {
"dependencies": [
"ext.popups.main"
],
"styles": [
"resources/ext.popups.referencePreviews/referencePreview.less"
],
"packageFiles": [
"resources/ext.popups.referencePreviews/index.js",
"resources/ext.popups.referencePreviews/constants.js",
"resources/ext.popups.referencePreviews/createReferenceGateway.js",
"resources/ext.popups.referencePreviews/createReferencePreview.js",
"resources/ext.popups.referencePreviews/isReferencePreviewsEnabled.js",
"resources/ext.popups.referencePreviews/referencePreviews.js",
"resources/ext.popups.referencePreviews/setUserConfigFlags.js"
]
},
"ext.popups.main": {
"scripts": [
"resources/dist/index.js"

View file

@ -27,7 +27,6 @@ use MediaWiki\Hook\BeforePageDisplayHook;
use MediaWiki\Hook\MakeGlobalVariablesScriptHook;
use MediaWiki\Output\OutputPage;
use MediaWiki\Preferences\Hook\GetPreferencesHook;
use MediaWiki\ResourceLoader\Context;
use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
use MediaWiki\User\Hook\UserGetDefaultOptionsHook;
use MediaWiki\User\Options\UserOptionsManager;
@ -83,18 +82,12 @@ class PopupsHooks implements
/**
* Get custom Popups types registered by extensions
* @param Context $context
* @return array
*/
public static function getCustomPopupTypes( Context $context ): array {
// FIXME: If the module ext.cite.referencePreviews does not exist register reference previews.
// This code can be removed once T355194 is complete.
$others = $context->getResourceLoader()->getModule( 'ext.cite.referencePreviews' ) ?
[] : [ 'ext.popups.referencePreviews' ];
return array_merge( ExtensionRegistry::getInstance()->getAttribute(
public static function getCustomPopupTypes(): array {
return ExtensionRegistry::getInstance()->getAttribute(
'PopupsPluginModules'
), $others );
);
}
/**

View file

@ -1 +0,0 @@
Code in this folder and subfolders is maintained by WMDE.

View file

@ -1,4 +0,0 @@
module.exports = {
TYPE_REFERENCE: 'reference',
FETCH_DELAY_REFERENCE_TYPE: 150
};

View file

@ -1,94 +0,0 @@
/**
* @module gateway/reference
*/
const { TYPE_REFERENCE } = require( './constants.js' );
/**
* @return {Gateway}
*/
module.exports = function createReferenceGateway() {
/**
* @param {string} id
* @return {HTMLElement}
*/
function scrapeReferenceText( id ) {
const idSelector = `#${ CSS.escape( id ) }`;
/**
* Same alternative selectors with and without mw- as in the RESTbased endpoint.
*
* @see https://phabricator.wikimedia.org/diffusion/GMOA/browse/master/lib/transformations/references/structureReferenceListContent.js$138
*/
return document.querySelector( `${ idSelector } .mw-reference-text, ${ idSelector } .reference-text` );
}
/**
* Attempts to find a single reference type identifier, limited to a list of known types.
* - When a `class="…"` attribute mentions multiple known types, the last one is used, following
* CSS semantics.
* - When there are multiple <cite> tags, the first with a known type is used.
*
* @param {HTMLElement} referenceText
* @return {string|null}
*/
function scrapeReferenceType( referenceText ) {
const KNOWN_TYPES = [ 'book', 'journal', 'news', 'note', 'web' ];
let type = null;
const citeTags = referenceText.querySelectorAll( 'cite[class]' );
Array.prototype.forEach.call( citeTags, ( element ) => {
// don't need to keep scanning if one is found.
if ( type ) {
return;
}
const classNames = element.className.split( /\s+/ );
for ( let i = classNames.length; i--; ) {
if ( KNOWN_TYPES.indexOf( classNames[ i ] ) !== -1 ) {
type = classNames[ i ];
return false;
}
}
} );
return type;
}
/**
* @param {mw.Title} title
* @param {HTMLAnchorElement} el
* @return {AbortPromise<ReferencePreviewModel>}
*/
function fetchPreviewForTitle( title, el ) {
// Need to encode the fragment again as mw.Title returns it as decoded text
const id = title.getFragment().replace( / /g, '_' ),
referenceNode = scrapeReferenceText( id );
if ( !referenceNode ||
// Skip references that don't contain anything but whitespace, e.g. a single &nbsp;
( !referenceNode.textContent.trim() && !referenceNode.children.length )
) {
return Promise.reject(
// Required to set `showNullPreview` to false and not open an error popup
{ textStatus: 'abort', textContext: 'Footnote not found or empty', xhr: { readyState: 0 } }
);
}
const model = {
url: `#${ id }`,
extract: referenceNode.innerHTML,
type: TYPE_REFERENCE,
referenceType: scrapeReferenceType( referenceNode ),
// Note: Even the top-most HTMLHtmlElement is guaranteed to have a parent.
sourceElementId: el.parentNode.id
};
// Make promise abortable.
const promise = Promise.resolve( model );
promise.abort = () => {};
return promise;
}
return {
fetchPreviewForTitle
};
}

View file

@ -1,191 +0,0 @@
/**
* @module referencePreview
*/
const { isTrackingEnabled, LOGGING_SCHEMA } = require( './referencePreviews.js' );
const TEMPLATE = document.createElement( 'template' );
TEMPLATE.innerHTML = `
<div class="mwe-popups mwe-popups mwe-popups-type-reference" aria-hidden>
<div class="mwe-popups-container">
<div class="mwe-popups-extract">
<div class="mwe-popups-scroll">
<strong class="mwe-popups-title">
<span class="popups-icon"></span>
<span class="mwe-popups-title-placeholder"></span>
</strong>
<bdi><div class="mw-parser-output"></div></bdi>
</div>
<div class="mwe-popups-fade"></div>
</div>
<footer>
<div class="mwe-popups-settings"></div>
</footer>
</div>
</div>`;
/**
* @param {HTMLElement} node
* @param {HTMLElement|string} htmlOrOtherNode
*/
const replaceWith = ( node, htmlOrOtherNode ) => {
if ( typeof htmlOrOtherNode === 'string' ) {
node.insertAdjacentHTML( 'afterend', htmlOrOtherNode );
} else {
node.parentNode.appendChild( htmlOrOtherNode );
}
node.remove();
};
/**
* @param {ext.popups.ReferencePreviewModel} model
* @return {jQuery}
*/
function renderReferencePreview(
model
) {
const type = model.referenceType || 'generic';
// The following messages are used here:
// * popups-refpreview-book
// * popups-refpreview-journal
// * popups-refpreview-news
// * popups-refpreview-note
// * popups-refpreview-web
let titleMsg = mw.message( `popups-refpreview-${ type }` );
if ( !titleMsg.exists() ) {
titleMsg = mw.message( 'popups-refpreview-reference' );
}
const el = TEMPLATE.content.cloneNode( true ).children[ 0 ];
replaceWith(
el.querySelector( '.mwe-popups-title-placeholder' ),
mw.html.escape( titleMsg.text() )
);
// The following classes are used here:
// * popups-icon--reference-generic
// * popups-icon--reference-book
// * popups-icon--reference-journal
// * popups-icon--reference-news
// * popups-icon--reference-note
// * popups-icon--reference-web
el.querySelector( '.mwe-popups-title .popups-icon' )
.classList.add( `popups-icon--reference-${ type }` );
el.querySelector( '.mw-parser-output' )
.innerHTML = model.extract;
// Make sure to not destroy existing targets, if any
Array.prototype.forEach.call(
el.querySelectorAll( '.mwe-popups-extract a[href][class~="external"]:not([target])' ),
( a ) => {
a.target = '_blank';
// Don't let the external site access and possibly manipulate window.opener.location
a.rel = `${ a.rel ? `${ a.rel } ` : '' }noopener`;
}
);
// We assume elements that benefit from being collapsible are to large for the popup
Array.prototype.forEach.call( el.querySelectorAll( '.mw-collapsible' ), ( node ) => {
const otherNode = document.createElement( 'div' );
otherNode.classList.add( 'mwe-collapsible-placeholder' );
const icon = document.createElement( 'span' );
icon.classList.add( 'popups-icon', 'popups-icon--infoFilled' );
const label = document.createElement( 'span' );
label.classList.add( 'mwe-collapsible-placeholder-label' );
label.textContent = mw.msg( 'popups-refpreview-collapsible-placeholder' );
otherNode.appendChild( icon );
otherNode.appendChild( label );
replaceWith( node, otherNode );
} );
// Undo remaining effects from the jquery.tablesorter.js plugin
const undoHeaderSort = ( headerSort ) => {
headerSort.classList.remove( 'headerSort' );
headerSort.removeAttribute( 'tabindex' );
headerSort.removeAttribute( 'title' );
};
Array.prototype.forEach.call( el.querySelectorAll( 'table.sortable' ), ( node ) => {
node.classList.remove( 'sortable', 'jquery-tablesorter' );
Array.prototype.forEach.call( node.querySelectorAll( '.headerSort' ), undoHeaderSort );
} );
// TODO: Do not remove this but move it up into the templateHTML constant!
const settingsButton = document.createElement( 'a' );
settingsButton.classList.add( 'cdx-button', 'cdx-button--fake-button', 'cdx-button--fake-button--enabled', 'cdx-button--weight-quiet', 'cdx-button--icon-only', 'mwe-popups-settings-button' );
const settingsIcon = document.createElement( 'span' );
settingsIcon.classList.add( 'popups-icon', 'popups-icon--size-small', 'popups-icon--settings' );
const settingsButtonLabel = document.createElement( 'span' );
settingsButtonLabel.textContent = mw.msg( 'popups-settings-icon-gear-title' );
settingsButton.append( settingsIcon );
settingsButton.append( settingsButtonLabel );
el.querySelector( '.mwe-popups-settings' ).appendChild( settingsButton );
if ( isTrackingEnabled() ) {
el.querySelector( '.mw-parser-output' ).addEventListener( 'click', ( ev ) => {
if ( !ev.target.matches( 'a' ) ) {
return;
}
mw.track( LOGGING_SCHEMA, {
action: 'clickedReferencePreviewsContentLink'
} );
} );
}
el.querySelector( '.mwe-popups-scroll' ).addEventListener( 'scroll', function ( e ) {
const element = e.target,
// We are dealing with floating point numbers here when the page is zoomed!
scrolledToBottom = element.scrollTop >= element.scrollHeight - element.clientHeight - 1;
if ( isTrackingEnabled() ) {
if ( !element.isOpenRecorded ) {
mw.track( LOGGING_SCHEMA, {
action: 'poppedOpen',
scrollbarsPresent: element.scrollHeight > element.clientHeight
} );
element.isOpenRecorded = true;
}
if (
element.scrollTop > 0 &&
!element.isScrollRecorded
) {
mw.track( LOGGING_SCHEMA, {
action: 'scrolled'
} );
element.isScrollRecorded = true;
}
}
if ( !scrolledToBottom && element.isScrolling ) {
return;
}
const extract = element.parentNode,
hasHorizontalScroll = element.scrollWidth > element.clientWidth,
scrollbarHeight = element.offsetHeight - element.clientHeight,
hasVerticalScroll = element.scrollHeight > element.clientHeight,
scrollbarWidth = element.offsetWidth - element.clientWidth;
const fade = extract.querySelector( '.mwe-popups-fade' );
fade.style.bottom = hasHorizontalScroll ? `${ scrollbarHeight }px` : 0;
fade.style.right = hasVerticalScroll ? `${ scrollbarWidth }px` : 0;
element.isScrolling = !scrolledToBottom;
extract.classList.toggle( 'mwe-popups-fade-out', element.isScrolling );
extract.setAttribute( 'lang', mw.config.get( 'wgPageContentLanguage' ) );
} );
return el;
}
/**
* @param {ext.popups.ReferencePreviewModel} model
* @return {ext.popups.Preview}
*/
function createReferencePreview( model ) {
return {
el: renderReferencePreview( model ),
hasThumbnail: false,
isTall: false
};
}
module.exports = createReferencePreview;

View file

@ -1,37 +0,0 @@
const isReferencePreviewsEnabled = require( './isReferencePreviewsEnabled.js' );
const { initReferencePreviewsInstrumentation } = require( './referencePreviews.js' )
const createReferenceGateway = require( './createReferenceGateway.js' )
const renderFn = require( './createReferencePreview.js' );
const { TYPE_REFERENCE, FETCH_DELAY_REFERENCE_TYPE } = require( './constants.js' )
const setUserConfigFlags = require( './setUserConfigFlags.js' )
const REFERENCE_PREVIEWS_LOGGING_SCHEMA = 'event.ReferencePreviewsPopups';
setUserConfigFlags( mw.config );
const referencePreviewsState = isReferencePreviewsEnabled(
mw.user, mw.popups.isEnabled,
mw.config
);
const gateway = createReferenceGateway();
// For tracking baseline stats in the Cite extension https://phabricator.wikimedia.org/T353798
// FIXME: This might be obsolete when the code moves to the Cite extension and the tracking there
// can check that state differently.
mw.config.set( 'wgPopupsReferencePreviewsVisible', !!referencePreviewsState );
mw.trackSubscribe( 'Popups.SettingChange', ( data ) => {
if ( data.previewType === TYPE_REFERENCE ) {
mw.track( REFERENCE_PREVIEWS_LOGGING_SCHEMA, data );
}
} );
module.exports = referencePreviewsState !== null ? {
type: TYPE_REFERENCE,
selector: '#mw-content-text .reference a[ href*="#" ]',
delay: FETCH_DELAY_REFERENCE_TYPE,
gateway,
renderFn,
init: () => {
initReferencePreviewsInstrumentation();
}
} : null;

View file

@ -1,41 +0,0 @@
const { TYPE_REFERENCE } = require( './constants.js' );
/**
* @module isReferencePreviewsEnabled
*/
/**
* Given the global state of the application, creates a function that gets
* whether or not the user should have Reference Previews enabled.
*
* @param {mw.User} user The `mw.user` singleton instance
* @param {Function} isPreviewTypeEnabled check whether preview has been disabled or enabled.
* @param {mw.Map} config
*
* @return {boolean|null} Null when there is no way the popup type can be enabled at run-time.
*/
function isReferencePreviewsEnabled( user, isPreviewTypeEnabled, config ) {
// TODO: This and the final `mw.user.options` check are currently redundant. Only this here
// should be removed when the wgPopupsReferencePreviews feature flag is not needed any more.
if ( !config.get( 'wgPopupsReferencePreviews' ) ) {
return null;
}
// T265872: Unavailable when in conflict with (one of the) reference tooltips gadgets.
if ( config.get( 'wgPopupsConflictsWithRefTooltipsGadget' ) ||
config.get( 'wgPopupsConflictsWithNavPopupGadget' ) ||
// T243822: Temporarily disabled in the mobile skin
config.get( 'skin' ) === 'minerva'
) {
return null;
}
if ( user.isAnon() ) {
return isPreviewTypeEnabled( TYPE_REFERENCE );
}
// Registered users never can enable popup types at run-time.
return user.options.get( 'popups-reference-previews' ) === '1' ? true : null;
}
module.exports = isReferencePreviewsEnabled;

View file

@ -1,81 +0,0 @@
@import 'mediawiki.skin.variables.less';
@import '../../src/ui/variables.less';
.popups-icon--reference-generic {
.cdx-mixin-css-icon( @cdx-icon-reference );
}
.popups-icon--reference-book {
.cdx-mixin-css-icon( @cdx-icon-book );
}
.popups-icon--reference-journal {
.cdx-mixin-css-icon( @cdx-icon-journal );
}
.popups-icon--reference-news {
.cdx-mixin-css-icon( @cdx-icon-newspaper );
}
.popups-icon--reference-web {
.cdx-mixin-css-icon( @cdx-icon-browser );
}
.popups-icon--preview-disambiguation {
.cdx-mixin-css-icon( @cdx-icon-articles );
}
// Don't do any event bubbling on childs like <a><span>[</span>2]</a>
// see https://phabricator.wikimedia.org/T214693
#mw-content-text .reference a[ href*='#' ] * {
pointer-events: none;
}
.mwe-popups.mwe-popups-type-reference .mwe-popups-container {
.mwe-popups-title .popups-icon--reference-note {
// There is currently no "reference-note" icon specified in extension.json
display: none;
}
.mwe-popups-extract {
margin-right: 0;
max-height: inherit;
.mwe-popups-scroll {
// This is how the @previewFooterHeight in popup.less is calculated
@marginBottom: @popupPadding + 34px;
// Same as @previewPointerHeight in popup.less
@pointerHeight: 8px;
max-height: 401px - @popupPadding - @marginBottom + @pointerHeight;
overflow: auto;
padding-right: @popupPadding;
}
.mw-parser-output {
overflow-wrap: break-word;
}
// Remove the default fade-out effect set by popup.less
&::after {
display: none;
}
.mwe-popups-fade {
position: absolute;
width: 100%;
height: @lineHeight;
background-color: transparent;
background-image: linear-gradient( rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) );
opacity: 0;
pointer-events: none; // Allows clicking "through" the element
transition: opacity 250ms ease;
}
&.mwe-popups-fade-out .mwe-popups-fade {
opacity: 1;
}
}
.mwe-collapsible-placeholder {
font-weight: bold;
margin: 1em 0;
position: relative;
}
}

View file

@ -1,27 +0,0 @@
let isTracking = false;
const LOGGING_SCHEMA = 'event.ReferencePreviewsPopups';
/**
* Run once the preview is initialized.
*/
function initReferencePreviewsInstrumentation() {
if ( mw.config.get( 'wgPopupsReferencePreviews' ) &&
navigator.sendBeacon &&
mw.config.get( 'wgIsArticle' ) &&
!isTracking
) {
isTracking = true;
mw.track( LOGGING_SCHEMA, { action: 'pageview' } );
}
}
function isTrackingEnabled() {
return isTracking;
}
module.exports = {
LOGGING_SCHEMA,
initReferencePreviewsInstrumentation,
isTrackingEnabled
};

View file

@ -1,29 +0,0 @@
/**
* @module setUserConfigFlags
*/
/**
* Same as in includes/PopupsContext.php
*/
const REF_TOOLTIPS_ENABLED = 2,
REFERENCE_PREVIEWS_ENABLED = 4;
/**
* Decodes the bitmask that represents preferences to the related config options.
*
* @param {mw.Map} config
*/
module.exports = function setUserConfigFlags( config ) {
const popupsFlags = parseInt( config.get( 'wgPopupsFlags' ), 10 );
/* eslint-disable no-bitwise */
config.set(
'wgPopupsConflictsWithRefTooltipsGadget',
!!( popupsFlags & REF_TOOLTIPS_ENABLED )
);
config.set(
'wgPopupsReferencePreviews',
!!( popupsFlags & REFERENCE_PREVIEWS_ENABLED )
);
/* eslint-enable no-bitwise */
}