diff --git a/extension.json b/extension.json index 90fdbe8cc..dfcd21adb 100644 --- a/extension.json +++ b/extension.json @@ -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": { "class": "MediaWiki\\ResourceLoader\\CodexModule", "codexStyleOnly": true, diff --git a/includes/PopupsHooks.php b/includes/PopupsHooks.php index cfa20d538..c13979221 100644 --- a/includes/PopupsHooks.php +++ b/includes/PopupsHooks.php @@ -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 ); + ); } /** diff --git a/resources/ext.popups.referencePreviews/OWNERS.md b/resources/ext.popups.referencePreviews/OWNERS.md deleted file mode 100644 index ebf9c0bb0..000000000 --- a/resources/ext.popups.referencePreviews/OWNERS.md +++ /dev/null @@ -1 +0,0 @@ -Code in this folder and subfolders is maintained by WMDE. diff --git a/resources/ext.popups.referencePreviews/constants.js b/resources/ext.popups.referencePreviews/constants.js deleted file mode 100644 index 06be9a89f..000000000 --- a/resources/ext.popups.referencePreviews/constants.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - TYPE_REFERENCE: 'reference', - FETCH_DELAY_REFERENCE_TYPE: 150 -}; diff --git a/resources/ext.popups.referencePreviews/createReferenceGateway.js b/resources/ext.popups.referencePreviews/createReferenceGateway.js deleted file mode 100644 index 48dc4a759..000000000 --- a/resources/ext.popups.referencePreviews/createReferenceGateway.js +++ /dev/null @@ -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 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} - */ - 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   - ( !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 - }; -} diff --git a/resources/ext.popups.referencePreviews/createReferencePreview.js b/resources/ext.popups.referencePreviews/createReferencePreview.js deleted file mode 100644 index 46b99750e..000000000 --- a/resources/ext.popups.referencePreviews/createReferencePreview.js +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @module referencePreview - */ -const { isTrackingEnabled, LOGGING_SCHEMA } = require( './referencePreviews.js' ); - -const TEMPLATE = document.createElement( 'template' ); -TEMPLATE.innerHTML = ` -
-
-
-
- - - - -
-
-
-
-
-
-
-
-
`; - -/** - * @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; diff --git a/resources/ext.popups.referencePreviews/index.js b/resources/ext.popups.referencePreviews/index.js deleted file mode 100644 index 05a85e697..000000000 --- a/resources/ext.popups.referencePreviews/index.js +++ /dev/null @@ -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; diff --git a/resources/ext.popups.referencePreviews/isReferencePreviewsEnabled.js b/resources/ext.popups.referencePreviews/isReferencePreviewsEnabled.js deleted file mode 100644 index e1dff0819..000000000 --- a/resources/ext.popups.referencePreviews/isReferencePreviewsEnabled.js +++ /dev/null @@ -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; diff --git a/resources/ext.popups.referencePreviews/referencePreview.less b/resources/ext.popups.referencePreviews/referencePreview.less deleted file mode 100644 index bc81b0b84..000000000 --- a/resources/ext.popups.referencePreviews/referencePreview.less +++ /dev/null @@ -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 [2] -// 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; - } -} diff --git a/resources/ext.popups.referencePreviews/referencePreviews.js b/resources/ext.popups.referencePreviews/referencePreviews.js deleted file mode 100644 index ab62159d7..000000000 --- a/resources/ext.popups.referencePreviews/referencePreviews.js +++ /dev/null @@ -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 -}; diff --git a/resources/ext.popups.referencePreviews/setUserConfigFlags.js b/resources/ext.popups.referencePreviews/setUserConfigFlags.js deleted file mode 100644 index 25d67215c..000000000 --- a/resources/ext.popups.referencePreviews/setUserConfigFlags.js +++ /dev/null @@ -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 */ -}