Файловый менеджер - Редактировать - /var/www/html/skins.vector.js.zip
Ðазад
PK ! I��X� � watchstar.jsnu �Iw�� /** * @param {HTMLElement} watchIcon * @param {boolean} isWatched * @param {string} expiry */ const updateWatchIcon = ( watchIcon, isWatched, expiry ) => { watchIcon.classList.remove( // Vector attaches two icon classes to the element. // Remove the mw-ui-icon one rather than managing both. 'mw-ui-icon-star', 'mw-ui-icon-unStar', 'mw-ui-icon-wikimedia-unStar', 'mw-ui-icon-wikimedia-star', 'mw-ui-icon-wikimedia-halfStar' ); if ( isWatched ) { if ( mw.util.isInfinity( expiry ) ) { watchIcon.classList.add( 'mw-ui-icon-wikimedia-unStar' ); } else { watchIcon.classList.add( 'mw-ui-icon-wikimedia-halfStar' ); } } else { watchIcon.classList.add( 'mw-ui-icon-wikimedia-star' ); } }; const init = () => { mw.hook( 'wikipage.watchlistChange' ).add( ( /** @type {boolean} */ isWatched, /** @type {string} */ expiry ) => { const watchIcons = document.querySelectorAll( '.mw-watchlink .vector-icon' ); if ( !watchIcons ) { return; } Array.from( watchIcons ).forEach( ( watchIcon ) => { updateWatchIcon( /** @type {HTMLElement} */ ( watchIcon ), isWatched, expiry ); } ); } ); }; module.exports = { updateWatchIcon, init }; PK ! V�?5L 5L tableOfContents.jsnu �Iw�� /** @module TableOfContents */ /** * TableOfContents Mustache templates */ const templateTocContents = require( /** @type {string} */ ( './templates/TableOfContents__list.mustache' ) ); const templateTocLine = require( /** @type {string} */ ( './templates/TableOfContents__line.mustache' ) ); /** * TableOfContents Config object for filling mustache templates */ const tableOfContentsConfig = require( /** @type {string} */ ( './tableOfContentsConfig.json' ) ); const deferUntilFrame = require( './deferUntilFrame.js' ); const SECTION_ID_PREFIX = 'toc-'; const SECTION_CLASS = 'vector-toc-list-item'; const ACTIVE_SECTION_CLASS = 'vector-toc-list-item-active'; const EXPANDED_SECTION_CLASS = 'vector-toc-list-item-expanded'; const TOP_SECTION_CLASS = 'vector-toc-level-1'; const ACTIVE_TOP_SECTION_CLASS = 'vector-toc-level-1-active'; const LINK_CLASS = 'vector-toc-link'; const TOGGLE_CLASS = 'vector-toc-toggle'; const TOC_CONTENTS_ID = 'mw-panel-toc-list'; /** * Fired when the user clicks a toc link. Note that this callback takes * precedence over the onHashChange callback. The onHashChange callback will not * be called when the user clicks a toc link. * * @callback onHeadingClick * @param {string} id The id of the clicked list item. */ /** * Fired when the page's hash fragment has changed. Note that if the user clicks * a link inside the TOC, the `onHeadingClick` callback will fire instead of the * `onHashChange` callback to avoid redundant behavior. * * @callback onHashChange * @param {string} id The id of the list item that corresponds to the hash change event. */ /** * @callback onToggleClick * @param {string} id The id of the list item corresponding to the arrow. */ /** * @callback onTogglePinned */ /** * @callback tableOfContents * @param {TableOfContentsProps} props * @return {TableOfContents} */ /** * @typedef {Object} TableOfContentsProps * @property {HTMLElement} container The container element for the table of contents. * @property {onHeadingClick} onHeadingClick Called when an arrow is clicked. * @property {onHashChange} onHashChange Called when a hash change event * matches the id of a LINK_CLASS anchor element. * @property {onToggleClick} [onToggleClick] Called when an arrow is clicked. * @property {onTogglePinned} onTogglePinned Called when pinned toggle buttons are clicked. */ /** * @typedef {Object} Section * @property {number} toclevel * @property {string} anchor * @property {string} line * @property {string} number * @property {string} index * @property {number} byteoffset * @property {string} fromtitle * @property {boolean} is-parent-section * @property {boolean} is-top-level-section * @property {Section[]} array-sections * @property {string} level */ /** * @typedef {Object} SectionsListData * @property {Section[]} array-sections * @property {boolean} vector-is-collapse-sections-enabled * @property {string} msg-vector-toc-beginning */ /** * @typedef {Object} ArraySectionsData * @property {number} number-section-count * @property {Section[]} array-sections */ /** * Initializes the sidebar's Table of Contents. * * @param {TableOfContentsProps} props * @return {TableOfContents} */ module.exports = function tableOfContents( props ) { let /** @type {HTMLElement | undefined} */ activeTopSection; let /** @type {HTMLElement | undefined} */ activeSubSection; let /** @type {Array<HTMLElement>} */ expandedSections; /** * @typedef {Object} activeSectionIds * @property {string|undefined} parent - The active top level section ID * @property {string|undefined} child - The active subsection ID */ /** * Get the ids of the active sections. * * @return {activeSectionIds} */ function getActiveSectionIds() { return { parent: ( activeTopSection ) ? activeTopSection.id : undefined, child: ( activeSubSection ) ? activeSubSection.id : undefined }; } /** * Does the user prefer reduced motion? * * @return {boolean} */ const prefersReducedMotion = () => window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches; /** * Sets an `ACTIVE_SECTION_CLASS` on the element with an id that matches `id`. * Sets an `ACTIVE_TOP_SECTION_CLASS` on the top level heading (e.g. element with the * `TOP_SECTION_CLASS`). * If the element is a top level heading, the element will have both classes. * * @param {string} id The id of the element to be activated in the Table of Contents. */ function activateSection( id ) { const selectedTocSection = document.getElementById( id ); const { parent: previousActiveTopId, child: previousActiveSubSectionId } = getActiveSectionIds(); if ( !selectedTocSection || ( previousActiveTopId === id ) || ( previousActiveSubSectionId === id ) ) { return; } // Assign the active top and sub sections, apply classes activeTopSection = /** @type {HTMLElement|undefined} */ ( selectedTocSection.closest( `.${ TOP_SECTION_CLASS }` ) ); if ( activeTopSection ) { // T328089 Sometimes activeTopSection is null activeTopSection.classList.add( ACTIVE_TOP_SECTION_CLASS ); } activeSubSection = selectedTocSection; activeSubSection.classList.add( ACTIVE_SECTION_CLASS ); } /** * Removes the `ACTIVE_SECTION_CLASS` from all ToC sections. * */ function deactivateSections() { if ( activeSubSection ) { activeSubSection.classList.remove( ACTIVE_SECTION_CLASS ); activeSubSection = undefined; } if ( activeTopSection ) { activeTopSection.classList.remove( ACTIVE_TOP_SECTION_CLASS ); activeTopSection = undefined; } } /** * Scroll active section into view if necessary * * @param {string} id The id of the element to be scrolled to in the Table of Contents. */ function scrollToActiveSection( id ) { const section = document.getElementById( id ); if ( !section ) { return; } // Get currently visible active link let link = /** @type {HTMLElement|null} */( section.firstElementChild ); if ( link && !link.offsetParent ) { // If active link is a hidden subsection, use active parent link const { parent: activeTopId } = getActiveSectionIds(); const parentSection = document.getElementById( activeTopId || '' ); if ( parentSection ) { link = /** @type {HTMLElement|null} */( parentSection.firstElementChild ); } else { link = null; } } const isContainerScrollable = props.container.scrollHeight > props.container.clientHeight; if ( link && isContainerScrollable ) { const containerRect = props.container.getBoundingClientRect(); const linkRect = link.getBoundingClientRect(); // Pixels above or below the TOC where we start scrolling the active section into view const hiddenThreshold = 100; const midpoint = ( containerRect.bottom - containerRect.top ) / 2; const linkHiddenTopValue = containerRect.top - linkRect.top; // Because the bottom of the TOC can extend below the viewport, // min() is used to find the value where the active section first becomes hidden const linkHiddenBottomValue = linkRect.bottom - Math.min( containerRect.bottom, window.innerHeight ); // Respect 'prefers-reduced-motion' user preference const scrollBehavior = prefersReducedMotion() ? 'smooth' : undefined; // Manually increment and decrement TOC scroll rather than using scrollToView // in order to account for threshold if ( linkHiddenTopValue + hiddenThreshold > 0 ) { props.container.scrollTo( { top: props.container.scrollTop - linkHiddenTopValue - midpoint, behavior: scrollBehavior } ); } if ( linkHiddenBottomValue + hiddenThreshold > 0 ) { props.container.scrollTo( { top: props.container.scrollTop + linkHiddenBottomValue + midpoint, behavior: scrollBehavior } ); } } } /** * Adds the `EXPANDED_SECTION_CLASS` CSS class name * to a top level heading in the ToC. * * @param {string} id */ function expandSection( id ) { const tocSection = document.getElementById( id ); if ( !tocSection ) { return; } const topSection = /** @type {HTMLElement} */ ( tocSection.closest( `.${ TOP_SECTION_CLASS }` ) ); const toggle = topSection.querySelector( `.${ TOGGLE_CLASS }` ); if ( topSection && toggle && expandedSections.indexOf( topSection ) < 0 ) { toggle.setAttribute( 'aria-expanded', 'true' ); topSection.classList.add( EXPANDED_SECTION_CLASS ); expandedSections.push( topSection ); } } /** * Get the IDs of expanded sections. * * @return {Array<string>} */ function getExpandedSectionIds() { return expandedSections.map( ( s ) => s.id ); } /** * @param {string} id */ function changeActiveSection( id ) { const { parent: activeParentId, child: activeChildId } = getActiveSectionIds(); if ( id === activeParentId && id === activeChildId ) { return; } else { deactivateSections(); activateSection( id ); scrollToActiveSection( id ); } } /** * @param {string} id * @return {boolean} */ function isTopLevelSection( id ) { const section = document.getElementById( id ); return !!section && section.classList.contains( TOP_SECTION_CLASS ); } /** * Removes all `EXPANDED_SECTION_CLASS` CSS class names * from the top level sections in the ToC. * * @param {Array<string>} [selectedIds] */ function collapseSections( selectedIds ) { const sectionIdsToCollapse = selectedIds || getExpandedSectionIds(); expandedSections = expandedSections.filter( ( section ) => { const isSelected = sectionIdsToCollapse.indexOf( section.id ) > -1; const toggle = isSelected ? section.getElementsByClassName( TOGGLE_CLASS ) : undefined; if ( isSelected && toggle && toggle.length > 0 ) { toggle[ 0 ].setAttribute( 'aria-expanded', 'false' ); section.classList.remove( EXPANDED_SECTION_CLASS ); return false; } return true; } ); } /** * @param {string} id */ function toggleExpandSection( id ) { const expandedSectionIds = getExpandedSectionIds(); const indexOfExpandedSectionId = expandedSectionIds.indexOf( id ); if ( isTopLevelSection( id ) ) { if ( indexOfExpandedSectionId >= 0 ) { collapseSections( [ id ] ); } else { expandSection( id ); } } } /** * Set aria-expanded attribute for all toggle buttons. */ function initializeExpandedStatus() { const parentSections = props.container.querySelectorAll( `.${ TOP_SECTION_CLASS }` ); parentSections.forEach( ( section ) => { const expanded = section.classList.contains( EXPANDED_SECTION_CLASS ); const toggle = section.querySelector( `.${ TOGGLE_CLASS }` ); if ( toggle ) { toggle.setAttribute( 'aria-expanded', expanded.toString() ); } } ); } /** * Event handler for hash change event. */ function handleHashChange() { const hash = location.hash.slice( 1 ); const listItem = mw.util.getTargetFromFragment( `${ SECTION_ID_PREFIX }${ hash }` ); if ( !listItem ) { return; } expandSection( listItem.id ); changeActiveSection( listItem.id ); props.onHashChange( listItem.id ); } /** * Bind event listener for hash change events that match the hash of * LINK_CLASS. * * Note that if the user clicks a link inside the TOC, the onHeadingClick * callback will fire instead of the onHashChange callback, since it takes * precedence. */ function bindHashChangeListener() { window.addEventListener( 'hashchange', handleHashChange ); } /** * Unbinds event listener for hash change events. */ function unbindHashChangeListener() { window.removeEventListener( 'hashchange', handleHashChange ); } /** * Bind event listener for clicking on show/hide Table of Contents links. */ function bindPinnedToggleListeners() { const toggleButtons = document.querySelectorAll( '.vector-toc-pinnable-header button' ); toggleButtons.forEach( ( btn ) => { btn.addEventListener( 'click', () => { props.onTogglePinned(); } ); } ); } /** * Bind event listeners for clicking on section headings and toggle buttons. */ function bindSubsectionToggleListeners() { props.container.addEventListener( 'click', ( e ) => { if ( !( e.target instanceof HTMLElement ) ) { return; } const tocSection = /** @type {HTMLElement | null} */ ( e.target.closest( `.${ SECTION_CLASS }` ) ); if ( tocSection && tocSection.id ) { // In case section link contains HTML, // test if click occurs on any child elements. if ( e.target.closest( `.${ LINK_CLASS }` ) ) { // Temporarily unbind the hash change listener to avoid redundant // behavior caused by firing both the onHeadingClick callback and the // onHashChange callback. Instead, only fire the onHeadingClick // callback. unbindHashChangeListener(); expandSection( tocSection.id ); changeActiveSection( tocSection.id ); props.onHeadingClick( tocSection.id ); deferUntilFrame( () => { bindHashChangeListener(); }, 3 ); } // Toggle button does not contain child elements, // so classList check will suffice. if ( e.target.closest( `.${ TOGGLE_CLASS }` ) ) { toggleExpandSection( tocSection.id ); if ( props.onToggleClick ) { props.onToggleClick( tocSection.id ); } } } } ); } /** * Binds event listeners and sets the default state of the component. */ function initialize() { // Sync component state to the default rendered state of the table of contents. expandedSections = Array.from( props.container.querySelectorAll( `.${ EXPANDED_SECTION_CLASS }` ) ); // Initialize toggle buttons aria-expanded attribute. initializeExpandedStatus(); // Bind event listeners. bindSubsectionToggleListeners(); bindPinnedToggleListeners(); bindHashChangeListener(); } /** * Reexpands all sections that were expanded before the table of contents was reloaded. * Edited Sections are not reexpanded, as the ID of the edited section is changed after reload. */ function reExpandSections() { initializeExpandedStatus(); const expandedSectionIds = getExpandedSectionIds(); for ( const id of expandedSectionIds ) { expandSection( id ); } } /** * Updates button styling for the TOC toggle button when scrolled below the page title * * @param {boolean} scrollBelow */ function updateTocToggleStyles( scrollBelow ) { const TOC_TITLEBAR_TOGGLE_ID = 'vector-page-titlebar-toc-label'; const QUIET_BUTTON_CLASS = 'cdx-button--weight-quiet'; const tocToggle = document.getElementById( TOC_TITLEBAR_TOGGLE_ID ); if ( tocToggle ) { if ( scrollBelow ) { tocToggle.classList.remove( QUIET_BUTTON_CLASS ); } else { tocToggle.classList.add( QUIET_BUTTON_CLASS ); } } } /** * Reloads the table of contents from saved data * * @param {Section[]} sections * @return {Promise<any>} */ function reloadTableOfContents( sections ) { if ( sections.length < 1 ) { reloadPartialHTML( TOC_CONTENTS_ID, '' ); return Promise.resolve( [] ); } const load = () => mw.loader.using( 'mediawiki.template.mustache' ).then( () => { const { parent: activeParentId, child: activeChildId } = getActiveSectionIds(); reloadPartialHTML( TOC_CONTENTS_ID, getTableOfContentsHTML( sections ) ); // Reexpand sections that were expanded before the table of contents was reloaded. reExpandSections(); // reActivate the active sections deactivateSections(); if ( activeParentId ) { activateSection( activeParentId ); } if ( activeChildId ) { activateSection( activeChildId ); } } ); return new Promise( ( resolve ) => { load().then( () => { resolve( sections ); } ); } ); } /** * Replaces the contents of the given element with the given HTML * * @param {string} elementId * @param {string} html */ function reloadPartialHTML( elementId, html ) { const htmlElement = document.getElementById( elementId ); if ( htmlElement && html ) { htmlElement.innerHTML = html; } } /** * Generates the HTML for the table of contents. * * @param {Section[]} sections * @return {string} */ function getTableOfContentsHTML( sections ) { return getTableOfContentsListHtml( getTableOfContentsData( sections ) ); } /** * Generates the table of contents List HTML from the templates * * @param {Object} data * @return {string} */ function getTableOfContentsListHtml( data ) { const mustacheCompiler = mw.template.getCompiler( 'mustache' ); const compiledTemplateTocContents = mustacheCompiler.compile( templateTocContents ); // Identifier 'TableOfContents__line' is not in camel case // (template name is 'TableOfContents__line') const partials = { // eslint-disable-next-line camelcase TableOfContents__line: mustacheCompiler.compile( templateTocLine ) }; return compiledTemplateTocContents.render( data, partials ).html(); } /** * @param {Section[]} sections * @return {SectionsListData} */ function getTableOfContentsData( sections ) { const tableOfContentsLevel1Sections = getTableOfContentsSectionsData( sections, 1 ); return { 'msg-vector-toc-beginning': mw.message( 'vector-toc-beginning' ).text(), 'array-sections': tableOfContentsLevel1Sections, 'vector-is-collapse-sections-enabled': tableOfContentsLevel1Sections.length > 3 && sections.length >= tableOfContentsConfig.VectorTableOfContentsCollapseAtCount }; } /** * Prepares the data for rendering the table of contents, * nesting child sections within their parent sections. * This should yield the same result as the php function * VectorComponentTableOfContents::getTemplateData(), * please make sure to keep them in sync. * * @param {Section[]} sections * @param {number} toclevel * @return {Section[]} */ function getTableOfContentsSectionsData( sections, toclevel = 1 ) { const data = []; for ( let i = 0; i < sections.length; i++ ) { const section = sections[ i ]; if ( section.toclevel === toclevel ) { const childSections = getTableOfContentsSectionsData( sections.slice( i + 1 ), toclevel + 1 ); section[ 'array-sections' ] = childSections; section[ 'is-top-level-section' ] = toclevel === 1; section[ 'is-parent-section' ] = Object.keys( childSections ).length > 0; data.push( section ); } // Child section belongs to a higher parent. if ( section.toclevel < toclevel ) { return data; } } return data; } /** * Cleans up the hash change event listener to prevent memory leaks. This * should be called when the table of contents is permanently no longer * needed. * * @ignore */ function unmount() { unbindHashChangeListener(); } initialize(); /** * @typedef {Object} TableOfContents * @property {reloadTableOfContents} reloadTableOfContents * @property {changeActiveSection} changeActiveSection * @property {expandSection} expandSection * @property {toggleExpandSection} toggleExpandSection * @property {updateTocToggleStyles} updateTocToggleStyles * @property {unmount} unmount * @property {string} ACTIVE_SECTION_CLASS * @property {string} ACTIVE_TOP_SECTION_CLASS * @property {string} EXPANDED_SECTION_CLASS * @property {string} LINK_CLASS * @property {string} TOGGLE_CLASS */ return { reloadTableOfContents, expandSection, changeActiveSection, toggleExpandSection, updateTocToggleStyles, unmount, ACTIVE_SECTION_CLASS, ACTIVE_TOP_SECTION_CLASS, EXPANDED_SECTION_CLASS, LINK_CLASS, TOGGLE_CLASS }; }; PK ! d��G �G stickyHeader.jsnu �Iw�� /** * Functions and variables to implement sticky header. */ const initSearchToggle = require( './searchToggle.js' ), updateWatchIcon = require( './watchstar.js' ).updateWatchIcon, STICKY_HEADER_ID = 'vector-sticky-header', STICKY_HEADER_APPENDED_ID = '-sticky-header', STICKY_HEADER_APPENDED_PARAM = [ 'wvprov', 'sticky-header' ], STICKY_HEADER_VISIBLE_CLASS = 'vector-sticky-header-visible', STICKY_HEADER_USER_MENU_CONTAINER_SELECTOR = '.vector-sticky-header-icon-end .vector-user-links', FIRST_HEADING_ID = 'firstHeading', USER_LINKS_DROPDOWN_ID = 'vector-user-links-dropdown', ULS_STICKY_CLASS = 'uls-dialog-sticky', ULS_HIDE_CLASS = 'uls-dialog-sticky-hide', SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle', STICKY_HEADER_EXPERIMENT_NAME = 'vector.sticky_header'; /** * Copies attribute from an element to another. * * @param {Element} from * @param {Element} to * @param {string} attribute */ function copyAttribute( from, to, attribute ) { const fromAttr = from.getAttribute( attribute ); if ( fromAttr ) { to.setAttribute( attribute, fromAttr ); } } /** * Show the sticky header. */ function show() { document.body.classList.add( STICKY_HEADER_VISIBLE_CLASS ); document.body.classList.remove( ULS_HIDE_CLASS ); } /** * Hide the sticky header. */ function hide() { document.body.classList.remove( STICKY_HEADER_VISIBLE_CLASS ); document.body.classList.add( ULS_HIDE_CLASS ); // Dismiss dropdown menus and search if active const stickyHeader = /** @type {HTMLElement} */ ( document.getElementById( STICKY_HEADER_ID ) ); if ( stickyHeader && stickyHeader.contains( document.activeElement ) ) { document.body.click(); } } /** * Copies attribute from an element to another. * * @param {Element} from * @param {Element} to */ function copyButtonAttributes( from, to ) { copyAttribute( from, to, 'href' ); copyAttribute( from, to, 'title' ); // Copy button labels if ( to.lastElementChild && from.lastElementChild ) { to.lastElementChild.innerHTML = from.lastElementChild.textContent || ''; } } /** * Suffixes an attribute with a value that indicates it * relates to the sticky header to support click tracking instrumentation. * * @param {Element} node * @param {string} attribute */ function suffixStickyAttribute( node, attribute ) { const value = node.getAttribute( attribute ); if ( value ) { node.setAttribute( attribute, value + STICKY_HEADER_APPENDED_ID ); } } /** * Suffixes the href attribute of a node with a value that indicates it * relates to the sticky header to support tracking instrumentation. * * Distinct from suffixStickyAttribute as it's intended to support followed * links recording their origin. * * @param {HTMLAnchorElement} node */ function suffixStickyHref( node ) { const url = new URL( node.href ); if ( url && !url.searchParams.has( STICKY_HEADER_APPENDED_PARAM[ 0 ] ) ) { url.searchParams.append( STICKY_HEADER_APPENDED_PARAM[ 0 ], STICKY_HEADER_APPENDED_PARAM[ 1 ] ); node.href = url.toString(); } } /** * Undoes the effect of suffixStickyHref * * @param {HTMLAnchorElement} node */ function unsuffixStickyHref( node ) { const url = new URL( node.href ); url.searchParams.delete( STICKY_HEADER_APPENDED_PARAM[ 0 ] ); node.href = url.toString(); } /** * Makes a node trackable by our click tracking instrumentation. * * @param {Element} node */ function makeNodeTrackable( node ) { suffixStickyAttribute( node, 'id' ); suffixStickyAttribute( node, 'data-event-name' ); } /** * @param {Element} node */ function removeNode( node ) { if ( node.parentNode ) { node.parentNode.removeChild( node ); } } /** * Ensures a sticky header button has the correct attributes * * @param {Element} watchLink * @param {boolean} isWatched The page is watched */ function updateStickyWatchlink( watchLink, isWatched ) { watchLink.setAttribute( 'data-event-name', isWatched ? 'watch-sticky-header' : 'unwatch-sticky-header' ); } /** * @param {NodeList} nodes * @param {string} className */ function removeClassFromNodes( nodes, className ) { Array.prototype.forEach.call( nodes, ( node ) => { node.classList.remove( className ); } ); } /** * @param {NodeList} nodes */ function removeNodes( nodes ) { Array.prototype.forEach.call( nodes, ( node ) => { node.parentNode.removeChild( node ); } ); } /** * Callback for watchsar * * @param {JQuery} $link Watchstar link * @param {boolean} isWatched The page is watched */ function watchstarCallback( $link, isWatched ) { updateStickyWatchlink( /** @type {HTMLAnchorElement} */( $link[ 0 ] ), isWatched ); } /** * Makes sticky header icons functional for modern Vector. * * @param {Element} header * @param {Element|null} history * @param {Element|null} talk * @param {Element|null} subject * @param {Element|null} watch */ function prepareIcons( header, history, talk, subject, watch ) { const historySticky = header.querySelector( '#ca-history-sticky-header' ), talkSticky = header.querySelector( '#ca-talk-sticky-header' ), subjectSticky = header.querySelector( '#ca-subject-sticky-header' ), watchSticky = header.querySelector( '#ca-watchstar-sticky-header' ); if ( !historySticky || !talkSticky || !subjectSticky || !watchSticky ) { throw new Error( 'Sticky header has unexpected HTML' ); } if ( history ) { copyButtonAttributes( history, historySticky ); } else { removeNode( historySticky ); } if ( talk ) { copyButtonAttributes( talk, talkSticky ); } else { removeNode( talkSticky ); } if ( subject ) { copyButtonAttributes( subject, subjectSticky ); } else { removeNode( subjectSticky ); } if ( watch && watch.parentNode instanceof Element ) { const watchContainer = watch.parentNode; const isTemporaryWatch = watchContainer.classList.contains( 'mw-watchlink-temp' ); const isWatched = isTemporaryWatch || watchContainer.getAttribute( 'id' ) === 'ca-unwatch'; const watchIcon = /** @type {HTMLElement} */ ( watchSticky.querySelector( '.vector-icon' ) ); // Initialize sticky watchlink copyButtonAttributes( watch, watchSticky ); updateWatchIcon( watchIcon, isWatched, isTemporaryWatch ? '' : 'infinity' ); updateStickyWatchlink( watchSticky, isWatched ); const watchLib = require( /** @type {string} */( 'mediawiki.page.watch.ajax' ) ); // jQuery required as parameter for external API: // eslint-disable-next-line no-jquery/no-jquery-constructor watchLib.watchstar( $( watchSticky ), mw.config.get( 'wgRelevantPageName' ), watchstarCallback ); } else { removeNode( watchSticky ); } } /** * Render sticky header edit or protected page icons for modern Vector. * * @param {Element} header * @param {Element|null} primaryEdit * @param {boolean} isProtected * @param {Element|null} secondaryEdit * @param {Element|null} addSection * @param {Function} disableStickyHeader function to call to disable the sticky * header. */ function prepareEditIcons( header, primaryEdit, isProtected, secondaryEdit, addSection, disableStickyHeader ) { /** * @param {string} selector * @return {HTMLAnchorElement|null} */ const getAnchorElement = ( selector ) => header.querySelector( selector ); const primaryEditSticky = getAnchorElement( '#ca-ve-edit-sticky-header' ), protectedSticky = getAnchorElement( '#ca-viewsource-sticky-header' ), wikitextSticky = getAnchorElement( '#ca-edit-sticky-header' ), addSectionSticky = getAnchorElement( '#ca-addsection-sticky-header' ); if ( addSectionSticky ) { if ( addSection ) { copyButtonAttributes( addSection, addSectionSticky ); suffixStickyHref( addSectionSticky ); } else { removeNode( addSectionSticky ); } } // If no primary edit icon is present the feature is disabled. if ( !primaryEditSticky || !wikitextSticky || !protectedSticky ) { return; } if ( !primaryEdit ) { removeNode( protectedSticky ); removeNode( wikitextSticky ); removeNode( primaryEditSticky ); return; } else if ( isProtected ) { removeNode( wikitextSticky ); removeNode( primaryEditSticky ); copyButtonAttributes( primaryEdit, protectedSticky ); suffixStickyHref( protectedSticky ); } else { removeNode( protectedSticky ); copyButtonAttributes( primaryEdit, primaryEditSticky ); suffixStickyHref( primaryEditSticky ); primaryEditSticky.addEventListener( 'click', ( ev ) => { const target = ev.target; // T336639: // eslint-disable-next-line no-jquery/no-jquery-constructor const $ve = $( primaryEdit ); if ( target && $ve.length ) { const link = /** @type {HTMLAnchorElement} */( $ve[ 0 ] ); // eslint-disable-next-line no-jquery/no-other-utils const event = $.Event( 'click' ); suffixStickyHref( link ); // eslint-disable-next-line no-jquery/no-trigger $ve.trigger( event ); unsuffixStickyHref( link ); // The link has been progressively enhanced. if ( event.isDefaultPrevented() ) { disableStickyHeader(); ev.preventDefault(); } } } ); if ( secondaryEdit ) { copyButtonAttributes( secondaryEdit, wikitextSticky ); suffixStickyHref( wikitextSticky ); wikitextSticky.addEventListener( 'click', ( ev ) => { const target = ev.target; if ( target ) { // T336639: // eslint-disable-next-line no-jquery/no-jquery-constructor const $edit = $( secondaryEdit ); if ( $edit.length ) { const link = /** @type {HTMLAnchorElement} */( $edit[ 0 ] ); // eslint-disable-next-line no-jquery/no-other-utils const event = $.Event( 'click' ); suffixStickyHref( link ); // eslint-disable-next-line no-jquery/no-trigger $edit.trigger( event ); unsuffixStickyHref( link ); // The link has been progressively enhanced. if ( event.isDefaultPrevented() ) { disableStickyHeader(); ev.preventDefault(); } } } } ); } else { removeNode( wikitextSticky ); } } } /** * Check if element is in viewport. * * @param {Element} element * @return {boolean} */ function isInViewport( element ) { const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= ( window.innerHeight || document.documentElement.clientHeight ) && rect.right <= ( window.innerWidth || document.documentElement.clientWidth ) ); } /** * Add hooks for sticky header when Visual Editor is used. * * @param {Element} stickyIntersection intersection element * @param {IntersectionObserver} observer */ function addVisualEditorHooks( stickyIntersection, observer ) { // When Visual Editor is activated, hide the sticky header. mw.hook( 've.activationStart' ).add( () => { hide(); observer.unobserve( stickyIntersection ); } ); // 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( () => { // Wait for the next repaint or we might calculate that // sticky header should not be visible (T299114) requestAnimationFrame( () => { observer.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 ) ) { show(); observer.observe( stickyIntersection ); } } ); } /** * Clones the existing user menu (excluding items added by gadgets) and adds to the sticky header * ensuring it is not focusable and that elements are no longer collapsible (since the sticky header * itself collapses at low resolutions) and updates click tracking event names. Also wires up the * logout link so it works in a single click. * * @param {Element} userLinksDropdown * @return {Element} cloned userLinksDropdown */ function prepareUserLinksDropdown( userLinksDropdown ) { const // Type declaration needed because of https://github.com/Microsoft/TypeScript/issues/3734#issuecomment-118934518 userLinksDropdownClone = /** @type {Element} */( userLinksDropdown.cloneNode( true ) ), userLinksDropdownStickyElementsWithIds = userLinksDropdownClone.querySelectorAll( '[ id ], [ data-event-name ]' ); // Update all ids of the cloned user menu to make them unique. makeNodeTrackable( userLinksDropdownClone ); userLinksDropdownStickyElementsWithIds.forEach( makeNodeTrackable ); // Remove portlet links added by gadgets using mw.util.addPortletLink, T291426 removeNodes( userLinksDropdownClone.querySelectorAll( '.mw-list-item-js' ) ); removeClassFromNodes( userLinksDropdownClone.querySelectorAll( '.user-links-collapsible-item' ), 'user-links-collapsible-item' ); // Prevents user menu from being focusable, T290201 const userLinksDropdownCheckbox = userLinksDropdownClone.querySelector( 'input' ); if ( userLinksDropdownCheckbox ) { userLinksDropdownCheckbox.setAttribute( 'tabindex', '-1' ); } // Make the logout go through the API (T324638) const logoutLink = /** @type {HTMLAnchorElement} */( userLinksDropdownClone.querySelector( '#pt-logout-sticky-header a' ) ); if ( logoutLink ) { logoutLink.addEventListener( 'click', ( ev ) => { ev.preventDefault(); mw.hook( 'skin.logout' ).fire( logoutLink.href ); } ); } return userLinksDropdownClone; } /** * Makes sticky header functional for modern Vector. * * @param {Element} header * @param {Element} userLinksDropdown * @param {IntersectionObserver} stickyObserver * @param {Element} stickyIntersection */ function makeStickyHeaderFunctional( header, userLinksDropdown, stickyObserver, stickyIntersection ) { const userLinksDropdownStickyContainer = document.querySelector( STICKY_HEADER_USER_MENU_CONTAINER_SELECTOR ); // Clone the updated user menu to the sticky header. if ( userLinksDropdownStickyContainer ) { const clonedUserLinksDropdown = prepareUserLinksDropdown( userLinksDropdown ); userLinksDropdownStickyContainer.appendChild( clonedUserLinksDropdown ); } let namespaceName = mw.config.get( 'wgCanonicalNamespace' ); const namespaceNumber = mw.config.get( 'wgNamespaceNumber' ); if ( namespaceNumber >= 0 && namespaceNumber % 2 === 1 ) { // Remove '_talk' to get subject namespace namespaceName = namespaceName.slice( 0, -5 ); } // Title::getNamespaceKey() let namespaceKey = namespaceName.toLowerCase() || 'main'; if ( namespaceKey === 'file' ) { namespaceKey = 'image'; } const namespaceTabId = 'ca-nstab-' + namespaceKey; prepareIcons( header, document.querySelector( '#ca-history a' ), document.querySelector( '#ca-talk:not( .selected ) a' ), document.querySelector( '#' + namespaceTabId + ':not( .selected ) a' ), document.querySelector( '#ca-watch a, #ca-unwatch a' ) ); const veEdit = document.querySelector( '#ca-ve-edit a' ); const ceEdit = document.querySelector( '#ca-edit a' ); const protectedEdit = document.querySelector( '#ca-viewsource a' ); const isProtected = !!protectedEdit; // For sticky header edit A/B test, conditionally remove the edit icon by setting null. // Otherwise, use either protected, ve, or source edit (in that order). const primaryEdit = protectedEdit || veEdit || ceEdit; const secondaryEdit = veEdit ? ceEdit : null; const disableStickyHeader = () => { document.body.classList.remove( STICKY_HEADER_VISIBLE_CLASS ); stickyObserver.unobserve( stickyIntersection ); }; // When VectorPromoteAddTopic is set, #ca-addsection is the link itself const addSection = document.querySelector( '#ca-addsection a' ) || document.querySelector( 'a#ca-addsection' ); prepareEditIcons( header, primaryEdit, isProtected, secondaryEdit, addSection, disableStickyHeader ); stickyObserver.observe( stickyIntersection ); } /** * @param {Element} header */ function setupSearchIfNeeded( header ) { const searchToggle = header.querySelector( SEARCH_TOGGLE_SELECTOR ); if ( !document.body.classList.contains( 'skin-vector-search-vue' ) ) { return; } if ( searchToggle ) { initSearchToggle( searchToggle ); } } /** * Determines if sticky header should be visible for a given namespace. * * @param {number} namespaceNumber * @return {boolean} */ function isAllowedNamespace( namespaceNumber ) { // Corresponds to Main, User, Wikipedia, Template, Help, Category, Portal, Module. const allowedNamespaceNumbers = [ 0, 2, 4, 10, 12, 14, 100, 828 ]; // Also allow on all talk namespaces (compare NamespaceInfo::isTalk()). const isAllowedTalk = namespaceNumber > 0 && namespaceNumber % 2 !== 0; return isAllowedTalk || allowedNamespaceNumbers.indexOf( namespaceNumber ) > -1; } /** * Determines if sticky header should be visible for a given action. * * @param {string} action * @return {boolean} */ function isAllowedAction( action ) { const disallowedActions = [ 'history', 'edit' ], hasDiffId = mw.config.get( 'wgDiffOldId' ); return disallowedActions.indexOf( action ) < 0 && !hasDiffId; } /** * @typedef {Object} StickyHeaderProps * @property {Element} header * @property {Element} userLinksDropdown * @property {IntersectionObserver} observer * @property {Element} stickyIntersection */ /** * @param {StickyHeaderProps} props */ function initStickyHeader( props ) { makeStickyHeaderFunctional( props.header, props.userLinksDropdown, props.observer, props.stickyIntersection ); setupSearchIfNeeded( props.header ); addVisualEditorHooks( props.stickyIntersection, props.observer ); // Make sure ULS outside sticky header disables the sticky header behaviour. mw.hook( 'mw.uls.compact_language_links.open' ).add( ( $trigger ) => { const trigger = $trigger[ 0 ]; if ( trigger.id !== 'p-lang-btn-sticky-header' ) { const bodyClassList = document.body.classList; bodyClassList.remove( ULS_HIDE_CLASS ); bodyClassList.remove( ULS_STICKY_CLASS ); } } ); // Make sure ULS dialog is sticky. const langBtn = props.header.querySelector( '#p-lang-btn-sticky-header' ); if ( langBtn ) { langBtn.addEventListener( 'click', () => { const bodyClassList = document.body.classList; bodyClassList.remove( ULS_HIDE_CLASS ); bodyClassList.add( ULS_STICKY_CLASS ); } ); } } module.exports = { show, hide, prepareUserLinksDropdown, isAllowedNamespace, isAllowedAction, initStickyHeader, STICKY_HEADER_ID, FIRST_HEADING_ID, USER_LINKS_DROPDOWN_ID, STICKY_HEADER_EXPERIMENT_NAME }; PK ! �I0:� � popupNotification.lessnu �Iw�� .vector-popup-notification { font-size: @font-size-dropdown; p { margin: 0; &:last-child { padding-bottom: 0; } } } PK ! ]���� � scrollObserver.jsnu �Iw�� const SCROLL_TITLE_HOOK = 'vector.page_title_scroll', SCROLL_TITLE_CONTEXT_ABOVE = 'scrolled-above-page-title', SCROLL_TITLE_CONTEXT_BELOW = 'scrolled-below-page-title', SCROLL_TITLE_ACTION = 'scroll-to-top'; /** * Fire a hook to be captured by WikimediaEvents for scroll event logging. * * @param {string} direction the scroll direction */ function firePageTitleScrollHook( direction ) { /** * For use by WikimediaEvents only. * * @event vector.page_title_scroll * @internal * @property {string} context * @property {string} action */ if ( direction === 'down' ) { mw.hook( SCROLL_TITLE_HOOK ).fire( { context: SCROLL_TITLE_CONTEXT_BELOW } ); } else { mw.hook( SCROLL_TITLE_HOOK ).fire( { context: SCROLL_TITLE_CONTEXT_ABOVE, action: SCROLL_TITLE_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 ) { return new IntersectionObserver( ( 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, firePageTitleScrollHook }; PK ! �scW W languageButton.jsnu �Iw�� /** * Copies interwiki links to main menu * * Temporary solution to T287206, can be removed when the new ULS built in Vue.js * has been released and contains this */ function addInterwikiLinkToMainMenu() { const editLink = /** @type {HTMLElement|null} */ ( document.querySelector( '#p-lang-btn .wbc-editpage' ) ); if ( !editLink ) { return; } const title = editLink.getAttribute( 'title' ) || ''; const addInterlanguageLink = mw.util.addPortletLink( 'p-tb', editLink.getAttribute( 'href' ) || '#', // Original text is "Edit links". // Since its taken out of context the title is more descriptive. title, 'wbc-editpage', title ); if ( addInterlanguageLink ) { addInterlanguageLink.addEventListener( 'click', ( /** @type {Event} */ e ) => { e.preventDefault(); // redirect to the detached and original edit link editLink.click(); } ); } } /** * Checks if ULS is disabled, and makes sure the language dropdown continues * to work if it is. */ function checkIfULSDisabled() { const langModuleState = mw.loader.getState( 'ext.uls.interface' ); if ( langModuleState === null || langModuleState === 'registered' ) { document.documentElement.classList.add( 'vector-uls-disabled' ); } } /** * Initialize the language button. */ module.exports = function () { checkIfULSDisabled(); addInterwikiLinkToMainMenu(); }; PK ! \�ى� � clientPreferences.jsonnu �Iw�� { "vector-feature-custom-font-size": { "options": [ "0", "1", "2" ], "preferenceKey": "vector-font-size" }, "vector-feature-limited-width": { "options": [ "1", "0" ], "preferenceKey": "vector-limited-width" }, "skin-theme": { "options": [ "os", "day", "night" ], "preferenceKey": "vector-theme", "betaMessage": "vector-night-mode-beta-tag", "linkLabelMessage": "vector-night-mode-issue-reporting-link-label", "linkLabelUrl": "vector-night-mode-issue-reporting-notice-url" } } PK ! l"J J skin.jsnu �Iw�� const languageButton = require( './languageButton.js' ), pinnableElement = require( './pinnableElement.js' ), searchToggle = require( './searchToggle.js' ), echo = require( './echo.js' ), initExperiment = require( './AB.js' ), ABTestConfig = require( /** @type {string} */ ( './activeABTest.json' ) ), initSearchLoader = require( './searchLoader.js' ).initSearchLoader, portletsManager = require( './portlets.js' ), dropdownMenus = require( './dropdownMenus.js' ).dropdownMenus, tables = require( './tables.js' ).init, watchstar = require( './watchstar.js' ).init, setupIntersectionObservers = require( './setupIntersectionObservers.js' ), menuTabs = require( './menuTabs.js' ), userPreferences = require( './userPreferences.js' ), { isNightModeGadgetEnabled, disableNightModeForGadget, alterExclusionMessage, removeBetaNotice } = require( './disableNightModeIfGadget.js' ), teleportTarget = /** @type {HTMLElement} */require( /** @type {string} */ ( 'mediawiki.page.ready' ) ).teleportTarget; /** * Wait for first paint before calling this function. That's its whole purpose. * * Some CSS animations and transitions are "disabled" by default as a workaround to this old Chrome * bug, https://bugs.chromium.org/p/chromium/issues/detail?id=332189, which otherwise causes them to * render in their terminal state on page load. By adding the `vector-animations-ready` class to the * `html` root element **after** first paint, the animation selectors suddenly match causing the * animations to become "enabled" when they will work properly. A similar pattern is used in Minerva * (see T234570#5779890, T246419). * * Example usage in Less: * * ```less * .foo { * color: #f00; * transform: translateX( -100% ); * } * * // This transition will be disabled initially for JavaScript users. It will never be enabled for * // non-JavaScript users. * .vector-animations-ready .foo { * transition: transform 100ms ease-out; * } * ``` * * @param {Document} document * @return {void} */ function enableCssAnimations( document ) { document.documentElement.classList.add( 'vector-animations-ready' ); } /** * @param {Window} window * @return {void} */ function main( window ) { enableCssAnimations( window.document ); initSearchLoader( document ); languageButton(); echo(); portletsManager.main(); watchstar(); // Initialize the search toggle for the main header only. The sticky header // toggle is initialized after Codex search loads. const searchToggleElement = document.querySelector( '.mw-header .search-toggle' ); if ( searchToggleElement ) { searchToggle( searchToggleElement ); } pinnableElement.initPinnableElement(); // Initializes the TOC and sticky header, behaviour of which depend on scroll behaviour. setupIntersectionObservers.main(); // Apply body styles to teleported elements teleportTarget.classList.add( 'vector-body' ); // Load client preferences const appearanceMenuSelector = '#vector-appearance'; const appearanceMenuExists = document.querySelectorAll( appearanceMenuSelector ).length > 0; if ( appearanceMenuExists ) { mw.loader.using( [ 'skins.vector.clientPreferences', 'skins.vector.search.codex.styles', 'skins.vector.search.codex.scripts' ] ).then( () => { const clientPreferences = require( /** @type {string} */ ( 'skins.vector.clientPreferences' ) ); const clientPreferenceConfig = ( require( './clientPreferences.json' ) ); // Can be removed once wgVectorNightMode is removed. if ( document.documentElement.classList.contains( 'vector-feature-night-mode-disabled' ) ) { // @ts-ignore issues relating to delete operator are not relevant here. delete clientPreferenceConfig[ 'skin-theme' ]; } // while we're in beta, temporarily check if the night mode gadget is installed and // disable our night mode if so if ( isNightModeGadgetEnabled() ) { disableNightModeForGadget(); clientPreferences.render( appearanceMenuSelector, clientPreferenceConfig, userPreferences ); alterExclusionMessage(); removeBetaNotice(); } else { clientPreferences.render( appearanceMenuSelector, clientPreferenceConfig, userPreferences ); } } ); } dropdownMenus(); // menuTabs should follow `dropdownMenus` as that can move menu items from a // tab menu to a dropdown. menuTabs(); tables(); } /** * @param {Window} window * @return {void} */ function init( window ) { const now = mw.now(); // This is the earliest time we can run JS for users (and bucket anonymous // users for A/B tests). // Where the browser supports it, for a 10% sample of users // we record a value to give us a sense of the expected delay in running A/B tests or // disabling JS features. This will inform us on various things including what to expect // with regards to delay while running A/B tests to anonymous users. // When EventLogging is not available this will reject. // This code can be removed by the end of the Desktop improvements project. // https://www.mediawiki.org/wiki/Desktop_improvements mw.loader.using( 'ext.eventLogging' ).then( () => { if ( mw.eventLog && mw.eventLog.eventInSample( 100 /* 1 in 100 */ ) && window.performance && window.performance.timing && window.performance.timing.navigationStart ) { mw.track( 'timing.Vector.ready', now - window.performance.timing.navigationStart ); // milliseconds } } ); } init( window ); if ( ABTestConfig.enabled && !mw.user.isAnon() ) { initExperiment( ABTestConfig, String( mw.user.getId() ) ); } if ( document.readyState === 'interactive' || document.readyState === 'complete' ) { main( window ); } else { // This is needed when document.readyState === 'loading'. document.addEventListener( 'DOMContentLoaded', () => { main( window ); } ); } // Provider of skins.vector.js module: /** * skins.vector.js * * @stable for use inside WikimediaEvents ONLY. */ module.exports = { pinnableElement }; PK ! �it t disableNightModeIfGadget.jsnu �Iw�� const userPreferences = require( './userPreferences.js' ); /** * T365083 - Disable night mode if night mode gadget is enabled * * While our implementation of night mode is still in beta, we want to respect the existing gadget * and disable our version to avoid a double invert - that said, we will still provide a prompt for * the user to disable the gadget so they can try our night mode */ /** * Are any of the gadgets associated with the broader night mode gadget enabled? * Note: This is localized to the names of the gadget in our particular language * * @return {boolean} */ function isNightModeGadgetEnabled() { return mw.msg( 'vector-night-mode-gadget-names' ).split( '|' ).some( ( gadget ) => { const state = mw.loader.getState( `ext.gadget.${ gadget }` ); // the state is null if it's not installed or we're on the preference page, otherwise it's // registered if the user doesn't have it turned on - all other states we consider enabled return state !== null && state !== 'registered'; } ); } /** * Manually mark the page we're on as excluded */ function disableNightModeForGadget() { document.documentElement.classList.remove( 'skin-theme-clientpref-night', 'skin-theme-clientpref-os' ); document.documentElement.classList.add( 'skin-theme-clientpref--excluded' ); } /** * Modify the link to disable the gadget so that, when clicked, it will disable the night mode * gadget rather than simply take you to the page * Note: The gadget names are similarly localized to the current language * * @param {Element} container an html element containing a link */ function alterDisableLink( container ) { const gadgetNames = mw.msg( 'vector-night-mode-gadget-names' ); const link = container.querySelector( 'a' ); // if we can't disable the gadget, keep the link functional // (if we can't find a link, not much we can do) if ( !gadgetNames || !link ) { return; } link.removeAttribute( 'title' ); link.removeAttribute( 'href' ); link.style.display = 'inline'; link.addEventListener( 'click', () => { /** @type {Object.<string, number>} */ const disableOptions = {}; gadgetNames.split( '|' ).forEach( ( gadgetName ) => { disableOptions[ `gadget-${ gadgetName }` ] = 0; } ); userPreferences.saveOptions( disableOptions ).then( () => { window.location.reload(); } ); } ); } /** * Removes the link that solicits user feedback for dark mode. */ function removeBetaNotice() { const feedbackNoticeContainer = document.getElementById( 'skin-theme-beta-notice' ); if ( feedbackNoticeContainer ) { feedbackNoticeContainer.remove(); } } /** * Modify the default exclusion message to indicate that we've disabled night mode on the page due * to a conflicting gadget, providing a link to disable the gadget in favor of our night mode */ function alterExclusionMessage() { const noticeContainer = document.querySelector( '#skin-client-prefs-skin-theme .exclusion-notice' ); // if there's no exclusion notice, nothing we can do if ( !noticeContainer ) { return; } mw.loader.using( 'mediawiki.jqueryMsg' ).then( () => { // remove existing message noticeContainer.textContent = ''; mw.message( 'vector-night-mode-gadget-warning' ).parseDom().appendTo( noticeContainer ); alterDisableLink( noticeContainer ); } ); } module.exports = { isNightModeGadgetEnabled, disableNightModeForGadget, alterDisableLink, alterExclusionMessage, removeBetaNotice }; PK ! @� � searchToggle.jsnu �Iw�� const HEADER_CONTAINER_CLASS = 'vector-header-container', SEARCH_BOX_SELECTOR = '.vector-search-box', SEARCH_VISIBLE_CLASS = 'vector-header-search-toggled'; /** * Binds event handlers necessary for the searchBox to disappear when the user * clicks outside the searchBox. * * @param {HTMLElement} searchBox * @param {HTMLElement} header */ function bindSearchBoxHandler( searchBox, header ) { /** * @param {Event} ev * @ignore */ const clickHandler = ( ev ) => { if ( ev.target instanceof HTMLElement && // Check if the click target was a suggestion link. Codex clears the // suggestion elements from the DOM when a suggestion is clicked so we // can't test if the suggestion is a child of the searchBox. // // Note: The .closest API is feature detected in `initSearchToggle`. !ev.target.closest( '.cdx-typeahead-search .cdx-menu-item__content' ) && !searchBox.contains( ev.target ) ) { header.classList.remove( SEARCH_VISIBLE_CLASS ); document.removeEventListener( 'click', clickHandler ); } }; document.addEventListener( 'click', clickHandler ); } /** * Binds event handlers necessary for the searchBox to show when the toggle is * clicked. * * @param {HTMLElement} searchBox * @param {HTMLElement} header * @param {Element} searchToggle */ function bindToggleClickHandler( searchBox, header, searchToggle ) { /** * @param {Event} ev * @ignore */ const handler = ( ev ) => { // The toggle is an anchor element. Prevent the browser from navigating away // from the page when clicked. ev.preventDefault(); header.classList.add( SEARCH_VISIBLE_CLASS ); // Defer binding the search box handler until after the event bubbles to the // top of the document so that the handler isn't called when the user clicks // the search toggle. Event bubbled callbacks execute within the same task // in the event loop. // // Also, defer focusing the input to another task in the event loop. At the time // of this writing, Safari 14.0.3 has trouble changing the visibility of the // element and focusing the input within the same task. setTimeout( () => { bindSearchBoxHandler( searchBox, header ); const searchInput = /** @type {HTMLInputElement|null} */ ( searchBox.querySelector( 'input[type="search"]' ) ); if ( searchInput ) { const beforeScrollX = window.scrollX; const beforeScrollY = window.scrollY; searchInput.focus(); // For some reason, Safari 14,15 tends to undesirably change the scroll // position of `input` elements inside fixed position elements. // While an Internet search suggests similar problems with mobile Safari // it didn't yield any results for desktop Safari. // This line resets any unexpected scrolling that occurred while the // input received focus. // If you are in the future with a modern version of Safari, where 14 and 15 // receive a low amount of page views, please reference T297636 and test // to see whether this line of code can be removed. // Additionally, these lines might become unnecessary when/if Safari // supports the `preventScroll` focus option [1] in the future: // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#parameters if ( beforeScrollX !== undefined && beforeScrollY !== undefined ) { window.scroll( beforeScrollX, beforeScrollY ); } } } ); }; searchToggle.addEventListener( 'click', handler ); } /** * Enables search toggling behavior in a header given a toggle element (e.g. * search icon). When the toggle element is clicked, a class, * `SEARCH_VISIBLE_CLASS`, will be applied to a header matching the selector * `HEADER_SELECTOR` and the input inside the element, SEARCH_BOX_SELECTOR, will * be focused. This class can be used in CSS to show/hide the necessary * elements. When the user clicks outside of SEARCH_BOX_SELECTOR, the class will * be removed. * * @param {HTMLElement|Element} searchToggle */ module.exports = function initSearchToggle( searchToggle ) { const headerContainer = /** @type {HTMLElement|null} */ ( searchToggle.closest( `.${ HEADER_CONTAINER_CLASS }` ) ); const header = /** @type {HTMLElement|null} */ ( headerContainer && headerContainer.firstElementChild ); if ( !header ) { return; } const searchBox = /** @type {HTMLElement|null} */ ( header.querySelector( SEARCH_BOX_SELECTOR ) ); if ( !searchBox ) { return; } bindToggleClickHandler( searchBox, header, searchToggle ); }; PK ! ?J popupNotification.jsnu �Iw�� /** @module PopupNotification */ // Store active notifications to only show one at a time, for use inside clearHints and showHint const /** @type {Record<string,OoUiPopupWidget>} */ activeNotification = {}; /** * Adds and show a popup to the user to point them to the new location of the element * * @param {HTMLElement} container * @param {string} message * @param {string} id * @param {string[]} [classes] * @param {number|false} [timeout] * @param {Function} [onDismiss] * @return {JQuery.Promise<OoUiPopupWidget|undefined>} */ function add( container, message, id, classes = [], timeout = 4000, onDismiss = () => {} ) { /** * @type {OoUiPopupWidget} */ let popupWidget; // load oojs-ui if it's not already loaded // FIXME: This should be replaced with Codex. return mw.loader.using( 'oojs-ui-core' ).then( () => { // use existing hint. if ( id && activeNotification[ id ] ) { return activeNotification[ id ]; } const content = document.createElement( 'p' ); content.textContent = message; popupWidget = new OO.ui.PopupWidget( { // eslint-disable-next-line no-jquery/no-jquery-constructor $content: $( content ), padded: true, autoClose: timeout !== false, head: timeout === false, anchor: true, align: 'center', position: 'below', classes: [ 'vector-popup-notification' ].concat( classes ), container } ); // eslint-disable-next-line no-jquery/no-other-methods popupWidget.$element.appendTo( container ); popupWidget.on( 'closing', () => { onDismiss(); } ); if ( popupWidget && id ) { activeNotification[ id ] = popupWidget; } return popupWidget; } ); } /** * Hides the popup widget * * @param {OoUiPopupWidget} popupWidget popupWidget from oojs-ui * cannot use type because it's not loaded yet */ function hide( popupWidget ) { popupWidget.toggle( false ); } /** * Shows the popup widget * * @param {OoUiPopupWidget} popupWidget popupWidget from oojs-ui * cannot use type because it's not loaded yet * @param {number|false} [timeout] use false if user must dismiss it themselves. */ function show( popupWidget, timeout = 4000 ) { popupWidget.toggle( true ); popupWidget.toggleClipping( true ); // hide the popup after timeout ms if ( timeout === false ) { return; } setTimeout( () => { hide( popupWidget ); }, timeout ); } /** * Hides all popups * */ function hideAll() { for ( const key in activeNotification ) { const popupWidget = activeNotification[ key ]; hide( popupWidget ); } } module.exports = { add, hide, hideAll, show }; PK ! 2��ٽ � echo.jsnu �Iw�� /** * Upgrades Echo for icon consistency. * Undos work inside Echo to replace our button. */ function init() { if ( document.querySelectorAll( '#pt-notifications-alert a, #pt-notifications-notice a' ).length !== 2 ) { return; } mw.hook( 'ext.echo.NotificationBadgeWidget.onInitialize' ).add( ( badge ) => { const element = badge.$element[ 0 ]; element.classList.add( 'mw-list-item' ); const anchor = /** @type {HTMLElement} */ ( element.querySelector( 'a' ) ); anchor.classList.add( 'cdx-button', 'cdx-button--weight-quiet', 'cdx-button--icon-only', 'cdx-button--fake-button', 'cdx-button--fake-button--enabled' ); // Icon classes shouldn't go on the same element as button classes // However this cant be avoided due to Echo button's implementation // which directly sets the contents of the anchor element every update // which would clear out any icon children that we define if ( element.id === 'pt-notifications-alert' ) { anchor.classList.add( 'vector-icon' ); } if ( element.id === 'pt-notifications-notice' ) { anchor.classList.add( 'vector-icon' ); } // Workaround T343838 anchor.classList.add( 'skin-invert' ); } ); } module.exports = init; PK ! stickyHeaderAB.jsnu �Iw�� PK ! �~ŋ � tableOfContentsConfig.jsonnu �Iw�� { "@doc": "This is a virtual JSON generated by ResourceLoader. This file is used by Jest.", "VectorTableOfContentsCollapseAtCount": 20 } PK ! �z�5M M dropdownMenus.jsnu �Iw�� /** @interface CheckboxHack */ const checkboxHack = /** @type {CheckboxHack} */ require( /** @type {string} */( 'mediawiki.page.ready' ) ).checkboxHack, CHECKBOX_HACK_CONTAINER_SELECTOR = '.vector-dropdown', CHECKBOX_HACK_CHECKBOX_SELECTOR = '.vector-dropdown-checkbox', CHECKBOX_HACK_BUTTON_SELECTOR = '.vector-dropdown-label', CHECKBOX_HACK_TARGET_SELECTOR = '.vector-dropdown-content'; /** * Enhance dropdownMenu functionality and accessibility using core's checkboxHack. * * @param {HTMLElement[]|NodeList} [containers] */ function dropdownMenus( containers ) { // Search for all dropdown containers using the CHECKBOX_HACK_CONTAINER_SELECTOR. containers = containers || document.querySelectorAll( CHECKBOX_HACK_CONTAINER_SELECTOR ); Array.prototype.forEach.call( containers, ( container ) => { const checkbox = container.querySelector( CHECKBOX_HACK_CHECKBOX_SELECTOR ), button = container.querySelector( CHECKBOX_HACK_BUTTON_SELECTOR ), target = container.querySelector( CHECKBOX_HACK_TARGET_SELECTOR ); if ( !( checkbox && button && target ) ) { return; } checkboxHack.bind( window, checkbox, button, target ); } ); } /** * Create an icon element to be appended inside the anchor tag. * * @param {HTMLElement|null} menuElement * @param {HTMLElement|null} parentElement * @param {string|null} id * * @return {HTMLElement|undefined} */ function createIconElement( menuElement, parentElement, id ) { // Only the p-personal menu in the user links dropdown supports icons const isIconCapable = menuElement && [ 'p-personal', 'p-personal-sticky-header' ].indexOf( menuElement.getAttribute( 'id' ) || 'p-unknown' ) > -1; if ( !isIconCapable || !parentElement ) { return; } const iconElement = document.createElement( 'span' ); iconElement.classList.add( 'vector-icon' ); if ( id ) { // The following class allows gadgets developers to style or hide an icon. // * mw-ui-icon-vector-gadget-<id> // The class is considered stable and should not be removed without // a #user-notice. iconElement.classList.add( 'mw-ui-icon-vector-gadget-' + id ); } return iconElement; } /** * Calculate the available width for adding links in the veiws menu, * i.e. the remaining space in the toolbar between the right-navigation * and left-navigation elements. * * @return {number} remaining available pixels in page toolbar or Zero * if remaining space is negative. */ function getAvailableViewMenuWidth() { const // Vector toolbar containing namespace, views, more menu etc. toolbar = document.querySelector( '.vector-page-toolbar-container' ), // Assumes all left-side menus are wrapped in a single nav element. // Need to get child width since this node is flex-grow: 1; leftToolbarItems = document.querySelector( '#left-navigation > nav' ), // Right side elements are flex-grow:0 so top-level width is sufficient. rightToolbarItems = document.getElementById( 'right-navigation' ); // Views menu collapses into "more" menu at this resolution. // Move the link from views to actions menu in this situation. if ( window.innerWidth < 720 ) { return 0; } // If any of our assumption about the DOM are wrong, return 0 // in order to place the link in a known menu instead. if ( !( toolbar && leftToolbarItems && rightToolbarItems ) ) { return 0; } // returning zero instead of negative number makes boolean conversion easier. return Math.max( 0, toolbar.clientWidth - leftToolbarItems.clientWidth - rightToolbarItems.clientWidth ); } const /** @type {Array<HTMLElement>} */handledLinks = []; /** * Adds icon placeholder for gadgets to use. * * @typedef {Object} PortletLinkData * @property {string|null} id */ /** * @param {HTMLElement} item * @param {PortletLinkData} data */ function addPortletLinkHandler( item, data ) { const linkIsHandled = handledLinks.indexOf( item ); let iconElement; if ( linkIsHandled >= 0 ) { return; } else { handledLinks.push( item ); } // assign variables after early return. const link = item.querySelector( 'a' ); const menuElement = /** @type {HTMLElement} */( item.closest( '.vector-menu' ) ); if ( !menuElement ) { return; } if ( data.id ) { iconElement = createIconElement( menuElement, link, data.id ); } // The views menu has limited space so we need to decide whether there is space // to accommodate the new item and if not to redirect to the more dropdown. if ( menuElement.id === 'p-views' ) { const availableWidth = getAvailableViewMenuWidth(); const moreDropdown = document.querySelector( '#p-cactions ul' ); if ( moreDropdown && !availableWidth ) { moreDropdown.appendChild( item ); // reveal if hidden mw.util.showPortlet( 'p-cactions' ); } } if ( link && iconElement ) { link.prepend( iconElement ); } } // Enhance previously added items. Array.prototype.forEach.call( document.querySelectorAll( '.mw-list-item-js' ), ( item ) => { addPortletLinkHandler( item, { id: item.getAttribute( 'id' ) } ); } ); mw.hook( 'util.addPortletLink' ).add( addPortletLinkHandler ); module.exports = { dropdownMenus, addPortletLinkHandler: addPortletLinkHandler }; PK ! Ds��� � stickyHeader.lessnu �Iw�� @spacing-search-title-divider: 30px; @font-size-sticky-header-links: unit( 14 / @font-size-browser, em ); // Equals `0.875em`. @import '../skins.vector.styles/variables.less'; .vector-sticky-header-container { position: fixed; top: 0; left: 0; right: 0; z-index: @z-index-header; // Hide sticky header until visible class is applied to the body transition: @transition-sticky-header; display: none; transform: translateY( -100% ); opacity: 0; } .vector-sticky-header { display: flex; align-items: center; justify-content: space-between; border-bottom: @border-width-base @border-style-base @border-color-content-box; // // Layout // &-start, &-end, &-icons, &-buttons, &-context-bar { display: flex; align-items: center; } &-start { flex-grow: 1; min-width: 0; } // Apply nowrap to title and buttons // Must apply to &-ccontext-bar-primary rather than &-context-bar or &-start // to avoid applying nowrap to the collapsed TOC menu &-context-bar-primary, &-end { white-space: nowrap; } // // Components // &-context-bar { border-left: @border-width-base @border-style-base #c8c8c8; padding-left: @spacing-search-title-divider; min-width: 0; margin-left: @spacing-search-title-divider; } &-context-bar-primary { overflow: hidden; font-family: @font-family-serif; // T296320 closest standardized option to 22px (24px) font-size: @font-size-heading-2; text-overflow: ellipsis; // T300134 Prevent text from wrapping in the sticky header. wbr { display: none; } } &-buttons { // Reduce language and add topic button font size font-size: @font-size-sticky-header-links; } // Spacing between buttons &-icons, &-buttons { column-gap: 8px; } .vector-search-box { // Hide the search box until the user toggles it. display: none; } &.vector-header-search-toggled { // .vector-sticky-header-search-toggle left border (1px) + left padding (12px) // - .cdx-text-input__start-icon left offset (9px [1]) = 4px // [1] see https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/text-input/TextInput.vue#257 @margin-start-search-box: 4px; // .vector-sticky-header-search-toggle left border (1px) + left padding (12px) // - .cdx-text-input__start-icon left offset (22px [2]) = -9px // [2] see https://gerrit.wikimedia.org/r/plugins/gitiles/design/codex/+/refs/tags/v0.1.0-alpha.8/packages/codex/src/components/typeahead-search/TypeaheadSearch.vue#708 @margin-start-search-box-with-thumbnail: -9px; .vector-sticky-header-search-toggle, .vector-sticky-header-context-bar { display: none; } .vector-search-box { display: block; margin-left: @margin-start-search-box; } // T296318 Decrease the start margin of the search box to account for the // icon's increased start position when the search component has thumbnails. .vector-search-box-show-thumbnail { margin-left: @margin-start-search-box-with-thumbnail; .cdx-text-input__start-icon { color: @color-base; } } } } // T298836 Sticky header is only shown at higher resolutions @media ( min-width: @min-width-breakpoint-desktop ) { .client-js.vector-sticky-header-enabled { // T290518: When the sticky header is enabled (feature flag is on, js is // enabled, and viewport is at higher resolutions), add scroll padding to the // root element. This is needed so that the sticky header does not overlap the // top of an element when the URI has a hash fragment (e.g. when the user clicks // a jump link) and when the user tabs through elements in reverse order. // // Please note that this class must be independent of the // .vector-sticky-header-visible class to correctly handle situations where the // sticky header isn't visible yet but we still need scroll padding applied // (e.g. when the user navigates to a page with a hash fragment in the URI). scroll-padding-top: ~'calc( @{height-header} + @{scroll-padding-top} )'; .vector-sticky-header-container { // Sticky header is only enabled for js users and when feature flag is enabled display: flex; } .vector-sticky-header-visible .vector-sticky-header-container { // Show sticky header with transition when visible class is applied to the body opacity: @opacity-base; transform: translateY( 0 ); } .vector-sticky-pinned-container { top: ~'calc( @{height-header} + @{grid-row-gap} )'; max-height: ~'calc( 100vh - @{height-header} - (@{grid-row-gap} * 2) )'; } // - T289817 `.mw-sticky-header-element` provides an API for template developers // to make their templates compatible with the Vector 2022 sticky header. // @stable See the Integration notes for developers section at // https://www.mediawiki.org/wiki/Reading/Web/Desktop_Improvements/Features/Sticky_Header // - `.charts-stickyhead th` makes chart and table headers appear below the sticky header. // - Unlike the rule above targeting toc & page tools, this does not need extra padding // for the context box, since these elements are inside the content container. .mw-sticky-header-element, .charts-stickyhead th { /* stylelint-disable-next-line declaration-no-important */ top: @height-header !important; } } } PK ! ���u sectionObserver.jsnu �Iw�� /** @module SectionObserver */ /** * @callback OnIntersection * @param {HTMLElement} element The section that triggered the new intersection change. */ /** * @typedef {Object} SectionObserverProps * @property {NodeList} elements A list of HTML elements to observe for * intersection changes. This list can be updated through the `elements` setter. * @property {OnIntersection} onIntersection Called when a new intersection is observed. * @property {number} [topMargin] The number of pixels to shrink the top of * the viewport's bounding box before calculating intersections. This is useful * for sticky elements (e.g. sticky headers). Defaults to 0 pixels. * @property {number} [throttleMs] The number of milliseconds that the scroll * handler should be throttled. */ /** * @callback initSectionObserver * @param {SectionObserverProps} props * @return {SectionObserver} */ /** * Observe intersection changes with the viewport for one or more elements. This * is intended to be used with the headings in the content so that the * corresponding section(s) in the table of contents can be "activated" (e.g. * bolded). * * When sectionObserver notices a new intersection change, the * `props.onIntersection` callback will be fired with the corresponding section * as a param. * * Because sectionObserver uses a scroll event listener (in combination with * IntersectionObserver), the changes are throttled to a default maximum rate of * 200ms so that the main thread is not excessively blocked. * IntersectionObserver is used to asynchronously calculate the positions of the * observed tags off the main thread and in a manner that does not cause * expensive forced synchronous layouts. * * @param {SectionObserverProps} props * @return {SectionObserver} */ module.exports = function sectionObserver( props ) { props = Object.assign( { topMargin: 0, throttleMs: 200, onIntersection: () => {} }, props ); let /** @type {number | undefined} */ timeoutId; let /** @type {HTMLElement | undefined} */ current; const observer = new IntersectionObserver( ( entries ) => { let /** @type {IntersectionObserverEntry | undefined} */ closestNegativeEntry; let /** @type {IntersectionObserverEntry | undefined} */ closestPositiveEntry; const topMargin = /** @type {number} */ ( props.topMargin ); entries.forEach( ( entry ) => { const top = entry.boundingClientRect.top - topMargin; if ( top > 0 && ( closestPositiveEntry === undefined || top < closestPositiveEntry.boundingClientRect.top - topMargin ) ) { closestPositiveEntry = entry; } if ( top <= 0 && ( closestNegativeEntry === undefined || top > closestNegativeEntry.boundingClientRect.top - topMargin ) ) { closestNegativeEntry = entry; } } ); const closestTag = /** @type {HTMLElement} */ ( closestNegativeEntry ? closestNegativeEntry.target : /** @type {IntersectionObserverEntry} */ ( closestPositiveEntry ).target ); // If the intersection is new, fire the `onIntersection` callback. if ( current !== closestTag ) { props.onIntersection( closestTag ); } current = closestTag; // When finished finding the intersecting element, stop observing all // observed elements. The scroll event handler will be responsible for // throttling and reobserving the elements again. Because we don't have a // wrapper element around our content headings and their children, we can't // rely on IntersectionObserver (which is optimized to detect intersecting // elements *within* the viewport) to reliably fire this callback without // this manual step. Instead, we offload the work of calculating the // position of each element in an efficient manner to IntersectionObserver, // but do not use it to detect when a new element has entered the viewport. observer.disconnect(); } ); /** * Calculate the intersection of each observed element. */ function calcIntersection() { // IntersectionObserver will asynchronously calculate the boundingClientRect // of each observed element off the main thread after `observe` is called. props.elements.forEach( ( element ) => { if ( !element.parentNode ) { mw.log.warn( 'Element being observed is not in DOM', element ); return; } observer.observe( /** @type {HTMLElement} */ ( element ) ); } ); } function handleScroll() { // Throttle the scroll event handler to fire at a rate limited by `props.throttleMs`. if ( !timeoutId ) { timeoutId = window.setTimeout( () => { calcIntersection(); timeoutId = undefined; }, props.throttleMs ); } } function bindScrollListener() { window.addEventListener( 'scroll', handleScroll ); } function unbindScrollListener() { window.removeEventListener( 'scroll', handleScroll ); } /** * Pauses intersection observation until `resume` is called. */ function pause() { unbindScrollListener(); clearTimeout( timeoutId ); timeoutId = undefined; // Assume current is no longer valid while paused. current = undefined; } /** * Resumes intersection observation. */ function resume() { bindScrollListener(); } /** * Cleans up event listeners and intersection observer. Should be called when * the observer is permanently no longer needed. */ function unmount() { unbindScrollListener(); observer.disconnect(); } /** * Set a list of HTML elements to observe for intersection changes. * * @param {NodeList} list */ function setElements( list ) { props.elements = list; } bindScrollListener(); /** * @typedef {Object} SectionObserver * @property {calcIntersection} calcIntersection * @property {pause} pause * @property {resume} resume * @property {unmount} unmount * @property {setElements} setElements */ return { calcIntersection, pause, resume, unmount, setElements }; }; PK ! �CQ�8 8 deferUntilFrame.jsnu �Iw�� /** * Helper method that calls a specified callback before the browser has * performed a specified number of repaints. * * Uses `requestAnimationFrame` under the hood to determine the next repaint. * * @param {Function} callback * @param {number} frameCount The number of frames to wait before calling the * specified callback. */ function deferUntilFrame( callback, frameCount ) { if ( frameCount === 0 ) { callback(); return; } requestAnimationFrame( () => { deferUntilFrame( callback, frameCount - 1 ); } ); } module.exports = deferUntilFrame; PK ! ��P'-( -( pinnableElement.jsnu �Iw�� const features = require( './features.js' ); const PINNED_HEADER_CLASS = 'vector-pinnable-header-pinned'; const UNPINNED_HEADER_CLASS = 'vector-pinnable-header-unpinned'; const popupNotification = require( './popupNotification.js' ); /** * Callback for matchMedia listener that overrides the pinnable header's stored state * at a certain breakpoint and forces it to unpin. * Usage of 'e.matches' assumes a `max-width` not `min-width` media query. * * @param {HTMLElement} header * @param {MediaQueryList|MediaQueryListEvent} e */ function disablePinningAtBreakpoint( header, e ) { const { pinnableElementId, pinnedContainerId, unpinnedContainerId, featureName } = header.dataset; const savedPinnedState = JSON.parse( header.dataset.savedPinnedState || 'false' ); // (typescript null check) if ( !( pinnableElementId && unpinnedContainerId && pinnedContainerId && featureName ) ) { return; } if ( e.matches && savedPinnedState === true ) { features.toggleDocClasses( featureName, false ); movePinnableElement( pinnableElementId, unpinnedContainerId ); } if ( !e.matches && savedPinnedState === true ) { features.toggleDocClasses( featureName, true ); movePinnableElement( pinnableElementId, pinnedContainerId ); } } /** * Saves the persistent pinnable state in the element's dataset * so that it can be overridden at lower resolutions and the * reverted to at wider resolutions. * * This is not necessarily the elements current state, but it * seeks to represent the state of the saved user preference. * * @param {HTMLElement} header */ function setSavedPinnableState( header ) { header.dataset.savedPinnedState = String( isPinned( header ) ); } /** * Toggle classes on the body and pinnable element * * @param {HTMLElement} header pinnable element */ function togglePinnableClasses( header ) { const featureName = /** @type {string} */ ( header.dataset.featureName ); // Leverage features.js to toggle the body classes and persist the state // for logged-in users. features.toggle( featureName ); // Toggle pinned class header.classList.toggle( PINNED_HEADER_CLASS ); header.classList.toggle( UNPINNED_HEADER_CLASS ); } /** * Create the indicators for the pinnable element * * @param {string} pinnableElementId */ function addPinnableElementIndicator( pinnableElementId ) { const dropdownSelector = document.querySelector( `#${ pinnableElementId }-dropdown` ); const container = dropdownSelector && dropdownSelector.parentElement; if ( container ) { // Possible messages include: // * vector-page-tools-unpinned-popup // * vector-main-menu-unpinned-popup const message = mw.msg( `${ pinnableElementId }-unpinned-popup` ); popupNotification.add( container, message, pinnableElementId ) .then( ( popupWidget ) => { if ( popupWidget ) { popupNotification.show( popupWidget ); } } ); } } /** * Event handler that toggles the pinnable elements pinned state. * Also moves the pinned element when those params are provided * (via data attributes). * * @param {HTMLElement} header PinnableHeader element. */ function pinnableElementClickHandler( header ) { const { pinnableElementId, pinnedContainerId, unpinnedContainerId } = header.dataset; togglePinnableClasses( header ); const isPinnedElement = isPinned( header ); // Optional functionality of moving the pinnable element in the DOM // to different containers based on it's pinned status if ( pinnableElementId && pinnedContainerId && unpinnedContainerId ) { setSavedPinnableState( header ); const newContainerId = isPinnedElement ? pinnedContainerId : unpinnedContainerId; movePinnableElement( pinnableElementId, newContainerId ); window.dispatchEvent( new Event( 'resize' ) ); setFocusAfterToggle( pinnableElementId ); if ( !isPinnedElement ) { addPinnableElementIndicator( pinnableElementId ); } } } /** * Sets focus on the correct toggle button depending on the pinned state. * Also opens the dropdown containing the unpinned element. * * @param {string} pinnableElementId */ function setFocusAfterToggle( pinnableElementId ) { let focusElement; const pinnableElement = document.getElementById( pinnableElementId ); const header = /** @type {HTMLElement|null} */ ( pinnableElement && pinnableElement.querySelector( '.vector-pinnable-header' ) ); if ( !pinnableElement || !header ) { return; } if ( isPinned( header ) ) { focusElement = /** @type {HTMLElement|null} */ ( pinnableElement.querySelector( '.vector-pinnable-header-unpin-button' ) ); } else { const dropdown = pinnableElement.closest( '.vector-dropdown' ); focusElement = /** @type {HTMLInputElement|null} */ ( dropdown && dropdown.querySelector( '.vector-menu-checkbox' ) ); } if ( focusElement ) { focusElement.focus(); } } /** * Binds all the toggle buttons in a pinnableElement * to the click handler that enables pinnability. * * @param {HTMLElement} header */ function bindPinnableToggleButtons( header ) { const toggleButtons = header.querySelectorAll( '.vector-pinnable-header-toggle-button' ); toggleButtons.forEach( ( button ) => { button.addEventListener( 'click', pinnableElementClickHandler.bind( null, header ) ); } ); } /** * Binds pinnable breakpoint to allow automatic unpinning * of pinnable elements with pinnedContainerId and unpinnedContainerId defined * * @param {HTMLElement} header */ function bindPinnableBreakpoint( header ) { const { pinnedContainerId, unpinnedContainerId } = header.dataset; if ( !unpinnedContainerId || !pinnedContainerId ) { return; } const pinnableBreakpoint = window.matchMedia( '(max-width: 1119px)' ); // Set saved pinned state for narrow breakpoint behaviour. setSavedPinnableState( header ); // Check the breakpoint in case an override is needed on pageload. disablePinningAtBreakpoint( header, pinnableBreakpoint ); // Add match media handler. if ( pinnableBreakpoint.addEventListener ) { pinnableBreakpoint.addEventListener( 'change', disablePinningAtBreakpoint.bind( null, header ) ); } else { // Before Safari 14, MediaQueryList is based on EventTarget, // so you must use addListener() and removeListener() to observe media query lists. pinnableBreakpoint.addListener( disablePinningAtBreakpoint.bind( null, header ) ); } } /** * @param {HTMLElement} header * @return {boolean} Returns true if the element is pinned and false otherwise. */ function isPinned( header ) { const featureName = /** @type {string} */ ( header.dataset.featureName ); return features.isEnabled( featureName ); } /** * Ensures the header classes are in sync with the pinnable headers state * in the case that it's moved via movePinnableElement(). * * @param {HTMLElement} pinnableElement */ function updatePinnableHeaderClass( pinnableElement ) { const header = pinnableElement.querySelector( '.vector-pinnable-header' ); // Because Typescript if ( !header || !( header instanceof HTMLElement ) ) { return; } // Toggle header classes if ( isPinned( header ) ) { header.classList.add( PINNED_HEADER_CLASS ); header.classList.remove( UNPINNED_HEADER_CLASS ); } else { header.classList.remove( PINNED_HEADER_CLASS ); header.classList.add( UNPINNED_HEADER_CLASS ); } } /** * @param {string} pinnableElementId * @param {string} newContainerId */ function movePinnableElement( pinnableElementId, newContainerId ) { const pinnableElem = document.getElementById( pinnableElementId ); const newContainer = document.getElementById( newContainerId ); const currContainer = /** @type {HTMLElement} */ ( pinnableElem && pinnableElem.parentElement ); if ( !pinnableElem || !newContainer || !currContainer ) { return; } // Avoid moving element if unnecessary if ( currContainer.id !== newContainerId ) { newContainer.insertAdjacentElement( 'beforeend', pinnableElem ); updatePinnableHeaderClass( pinnableElem ); } popupNotification.hideAll(); } /** * Update the pinnable element location in the DOM based off of whether its pinned or not. * This is only necessary with pinnable elements that use client preferences (i.e. appearance menu) * as all other pinnable elements should be serverside rendered in the correct location * * @param {HTMLElement} header */ function updatePinnableElementLocation( header ) { const newContainerId = isPinned( header ) ? header.dataset.pinnedContainerId : header.dataset.unpinnedContainerId; if ( header.dataset.pinnableElementId && newContainerId ) { movePinnableElement( header.dataset.pinnableElementId, newContainerId ); } } function initPinnableElement() { const pinnableHeader = /** @type {NodeListOf<HTMLElement>} */ ( document.querySelectorAll( '.vector-pinnable-header' ) ); pinnableHeader.forEach( ( header ) => { if ( header.dataset.featureName && header.dataset.pinnableElementId ) { bindPinnableToggleButtons( header ); bindPinnableBreakpoint( header ); updatePinnableElementLocation( header ); } } ); } // T349924: Remove hasPinnedElements after one cycle of analyticsPinnedState() merge. /** * Checks if at least one of the elements in the HTML document is pinned based on CSS class names. * * @method * @return {boolean} True if at least one pinned element is found, otherwise false. */ function hasPinnedElements() { const suffixesToCheck = [ 'pinned-clientpref-1', 'pinned-enabled' ]; const htmlElement = document.documentElement; return Array.from( htmlElement.classList ).some( ( className ) => suffixesToCheck.some( ( suffix ) => className.endsWith( suffix ) ) ); } /** * @stable for use in WikimediaEvents only. * Checks if at least one of the elements in the HTML document is pinned based on CSS class names. * * @method * @return {boolean} True if at least one pinned element is found, otherwise false. */ function analyticsPinnedState() { const htmlElement = document.documentElement; return htmlElement.classList.contains( 'vector-feature-main-menu-pinned-enabled' ) || htmlElement.classList.contains( 'vector-feature-page-tools-pinned-enabled' ); } module.exports = { // T349924: Remove hasPinnedElements. hasPinnedElements, analyticsPinnedState, initPinnableElement, movePinnableElement, setFocusAfterToggle, isPinned, PINNED_HEADER_CLASS, UNPINNED_HEADER_CLASS }; PK ! 3kf�� � portlets.jsnu �Iw�� const dropdownMenus = require( './dropdownMenus.js' ); /** * An object containing the data to help create a portlet. * * @typedef {Object} Hint * @property {string} type */ /** * Creates default portlet. * * @param {Element} portlet * @param {boolean} isDropdown * @return {Element} */ function addDefaultPortlet( portlet, isDropdown ) { const ul = portlet.querySelector( 'ul' ); if ( !ul ) { return portlet; } ul.classList.add( 'vector-menu-content-list' ); const label = portlet.querySelector( 'label' ); if ( label ) { const labelDiv = document.createElement( 'div' ); labelDiv.classList.add( 'vector-menu-heading' ); if ( !isDropdown ) { labelDiv.innerHTML = label.textContent || ''; portlet.insertBefore( labelDiv, label ); label.remove(); } } let wrapper = portlet.querySelector( 'div:last-child' ); if ( wrapper ) { ul.remove(); wrapper.appendChild( ul ); wrapper.classList.add( 'vector-menu-content' ); } else { wrapper = document.createElement( 'div' ); wrapper.classList.add( 'vector-menu-content' ); ul.remove(); wrapper.appendChild( ul ); portlet.appendChild( wrapper ); } portlet.classList.add( 'vector-menu' ); return portlet; } /** * A hook handler for util.addPortlet hook. * It creates a portlet based on the hint, and adabt it to vector skin. * * @param {Element} content * @return {Element} */ function makeDropdown( content ) { const id = content.id; const label = content.querySelector( 'label' ); if ( !content.parentNode || !label ) { return content; } label.id = `${ id }-dropdown-label`; label.setAttribute( 'for', `${ id }-dropdown-checkbox` ); label.classList.add( 'vector-dropdown-label' ); label.setAttribute( 'aria-hidden', 'true' ); const labelSpan = document.createElement( 'span' ); labelSpan.textContent = label.textContent; label.textContent = ''; labelSpan.classList.add( 'vector-dropdown-label-text' ); label.appendChild( labelSpan ); const dropdown = document.createElement( 'div' ); const checkbox = document.createElement( 'input' ); const dropdownContent = document.createElement( 'div' ); dropdownContent.classList.add( 'vector-dropdown-content' ); checkbox.type = 'checkbox'; checkbox.id = `${ id }-dropdown-checkbox`; checkbox.setAttribute( 'role', 'button' ); checkbox.setAttribute( 'aria-haspopup', 'true' ); checkbox.setAttribute( 'data-event-name', `ui.dropdown-${ id }-dropdown` ); checkbox.classList.add( 'vector-dropdown-checkbox' ); checkbox.setAttribute( 'aria-label', label.textContent || '' ); dropdown.id = `${ id }-dropdown`; dropdown.classList.add( 'vector-dropdown', `${ id }-dropdown` ); dropdown.appendChild( checkbox ); dropdown.appendChild( label ); dropdown.appendChild( dropdownContent ); content.parentNode.insertBefore( dropdown, content ); dropdownContent.appendChild( content ); dropdownMenus.dropdownMenus( [ dropdown ] ); return dropdown; } /** * A hook handler for util.addPortlet hook. * It creates a portlet based on the hint, and adapt it to vector skin. * If #p-cactions is used, the new portlet will be converted into a dropdown. * * @param {Element} portlet * @param {string|null} before * @return {Element} */ function addPortletHandler( portlet, before ) { const isDropdown = !!( before && before === '#p-cactions' ); portlet.classList.remove( 'mw-portlet-js' ); const transformedPortlet = addDefaultPortlet( portlet, isDropdown ); if ( isDropdown ) { const pageToolsDropdown = document.querySelector( '#vector-page-tools-dropdown' ); const pageToolsMarker = pageToolsDropdown ? pageToolsDropdown.parentNode : null; // Guard against unexpected changes to HTML. if ( pageToolsMarker === null || !pageToolsMarker.parentNode ) { throw new Error( 'Vector 2022 addPortletLink: No #vector-page-tools-dropdown element in the DOM.' ); } const dropdown = makeDropdown( transformedPortlet ); pageToolsMarker.parentNode.insertBefore( dropdown, pageToolsMarker ); return transformedPortlet; } return transformedPortlet; } /** * @return {{addPortletHandler: (function(Element, string): Element)}} */ function main() { mw.hook( 'util.addPortlet' ).add( addPortletHandler ); // Update any portlets that were created prior to the hook being registered. document.querySelectorAll( '.mw-portlet-js' ).forEach( ( node ) => { const nextID = node && node.nextElementSibling && node.nextElementSibling.id; addPortletHandler( node, nextID ? `#${ nextID }` : null ); } ); return { addPortletHandler }; } module.exports = { main, addPortletHandler }; PK ! ����� � tables.jsnu �Iw�� const config = require( './config.json' ); const init = () => { if ( !config.VectorWrapTablesTemporary ) { return; } const tables = document.querySelectorAll( '.mw-parser-output > table.wikitable' ); let numberBigTables = 0; Array.from( tables ).forEach( ( table ) => { const styles = window.getComputedStyle( table ); const isFloat = styles.getPropertyValue( 'float' ) === 'right' || styles.getPropertyValue( 'float' ) === 'left'; // Don't wrap tables within tables const parent = table.parentElement; if ( parent && !parent.matches( '.noresize' ) && !parent.closest( 'table' ) && !isFloat ) { const tableRect = table.getBoundingClientRect(); const tableWidth = tableRect && tableRect.width; const wrapper = document.createElement( 'div' ); wrapper.classList.add( 'noresize' ); parent.insertBefore( wrapper, table ); wrapper.appendChild( table ); if ( tableWidth > 948 ) { numberBigTables++; } } } ); if ( numberBigTables > 0 ) { // @ts-ignore mw.errorLogger.logError( new Error( `T374493: ${ numberBigTables } tables wrapped` ), 'error.web-team' ); } }; module.exports = { init }; PK ! -+��J J menuTabs.jsnu �Iw�� const TABS_SELECTOR = '.vector-menu-tabs', LIST_ITEM_JS_SELECTOR = '.mw-list-item-js', NO_ICON_CLASS = 'vector-tab-noicon'; /** * T320691: Add a `.vector-tab-noicon` class to any tabbed menu item that is * added by a gadget so that the menu item has the correct padding and margin. * * @param {HTMLElement} item */ function addNoIconClass( item ) { item.classList.add( NO_ICON_CLASS ); } function init() { // Enhance previously added items. Array.prototype.forEach.call( document.querySelectorAll( TABS_SELECTOR + ' ' + LIST_ITEM_JS_SELECTOR ), addNoIconClass ); mw.hook( 'util.addPortletLink' ).add( /** * @param {HTMLElement} item */ ( item ) => { // Check if this menu item belongs to a tabs menu. if ( item.closest( TABS_SELECTOR ) ) { addNoIconClass( item ); } } ); } module.exports = init; PK ! jǨ�f f tableOfContents.lessnu �Iw�� @import 'mediawiki.skin.variables.less'; .mw-ui-icon-wikimedia-expand { .cdx-mixin-css-icon( @cdx-icon-expand ); // Temporary generic small icon class // FIXME: Replace with proper Codex CSS icon usage in T344183 .cdx-mixin-css-icon-background( @size-icon-x-small ); .cdx-mixin-css-icon-size( @size-icon-x-small ); .cdx-mixin-css-icon-alignment( ); } PK ! %�e, , userPreferences.jsnu �Iw�� let /** @type {MwApi} */ api; /** * @param {Object<string,string|number>} options * @return {JQuery.Promise<Object>} */ function saveOptions( options ) { api = api || new mw.Api(); // @ts-ignore return api.saveOptions( options, { global: 'update' } ); } module.exports = { saveOptions }; PK ! <>z z index.lessnu �Iw�� @import 'tableOfContents.less'; @import 'popupNotification.less'; @import 'stickyHeader.less'; @import 'bottomDock.less'; PK ! t<è � config.jsonnu �Iw�� { "@doc": "This file describes the shape of the AB config. It exists to support jest and TypeScript.", "VectorSearchApiUrl": "", "VectorWrapTablesTemporary": true } PK ! Z��� � searchLoader.jsnu �Iw�� /** * Disabling this rule as it's only necessary for * combining multiple class names and documenting the output. * That doesn't happen in this file but the linter still throws an error. * https://github.com/wikimedia/eslint-plugin-mediawiki/blob/master/docs/rules/class-doc.md */ /** @interface VectorResourceLoaderVirtualConfig */ /** @interface MediaWikiPageReadyModule */ const /** @type {VectorResourceLoaderVirtualConfig} */ config = require( /** @type {string} */ ( './config.json' ) ), // T251544: Collect search performance metrics to compare Vue search with // mediawiki.searchSuggest performance. CAN_TEST_SEARCH = !!( window.performance && !!performance.mark && !!performance.measure && performance.getEntriesByName ), LOAD_START_MARK = 'mwVectorVueSearchLoadStart', LOAD_END_MARK = 'mwVectorVueSearchLoadEnd', LOAD_MEASURE = 'mwVectorVueSearchLoadStartToLoadEnd'; /** * Loads the search module via `mw.loader.using` on the element's * focus event. Or, if the element is already focused, loads the * search module immediately. * After the search module is loaded, executes a function to remove * the loading indicator. * * @param {Element} element search input. * @param {string} moduleName resourceLoader module to load. * @param {string|null} startMarker * @param {null|function(): void} afterLoadFn function to execute after search module loads. */ function loadSearchModule( element, moduleName, startMarker, afterLoadFn ) { const SHOULD_TEST_SEARCH = CAN_TEST_SEARCH && moduleName === 'skins.vector.search'; function requestSearchModule() { if ( SHOULD_TEST_SEARCH && startMarker !== null && afterLoadFn !== null ) { performance.mark( startMarker ); mw.loader.using( moduleName, afterLoadFn ); } else { mw.loader.load( moduleName ); } element.removeEventListener( 'focus', requestSearchModule ); } if ( document.activeElement === element ) { requestSearchModule(); } else { element.addEventListener( 'focus', requestSearchModule ); } } /** * Marks when the lazy load has completed. * * @param {string} startMarker * @param {string} endMarker * @param {string} measureMarker */ function markLoadEnd( startMarker, endMarker, measureMarker ) { if ( performance.getEntriesByName( startMarker ).length ) { performance.mark( endMarker ); performance.measure( measureMarker, startMarker, endMarker ); } } /** * Initialize the loading of the search module as well as the loading indicator. * Only initialize the loading indicator when not using the core search module. * * @param {Document} document */ function initSearchLoader( document ) { const searchBoxes = document.querySelectorAll( '.vector-search-box' ); // Allow developers to defined $wgVectorSearchApiUrl in LocalSettings to target different APIs if ( config.VectorSearchApiUrl ) { mw.config.set( 'wgVectorSearchApiUrl', config.VectorSearchApiUrl ); } if ( !searchBoxes.length ) { return; } /** * If we are in a browser that doesn't support ES6 fall back to non-JS version. */ if ( mw.loader.getState( 'skins.vector.search' ) === null ) { document.body.classList.remove( 'skin-vector-search-vue' ); return; } Array.prototype.forEach.call( searchBoxes, ( searchBox ) => { const searchInner = searchBox.querySelector( 'form > div' ), searchInput = searchBox.querySelector( 'input[name="search"]' ), isPrimarySearch = searchInput && searchInput.getAttribute( 'id' ) === 'searchInput'; if ( !searchInput || !searchInner ) { return; } // Remove tooltips while Vue search is still loading searchInput.setAttribute( 'autocomplete', 'off' ); loadSearchModule( searchInput, 'skins.vector.search', isPrimarySearch ? LOAD_START_MARK : null, // Note, loading Vue.js will remove the element from the DOM. () => { if ( isPrimarySearch ) { markLoadEnd( LOAD_START_MARK, LOAD_END_MARK, LOAD_MEASURE ); } } ); } ); } module.exports = { initSearchLoader: initSearchLoader }; PK ! �H"��4 �4 setupIntersectionObservers.jsnu �Iw�� // Enable Vector features limited to ES6 browse const stickyHeader = require( './stickyHeader.js' ), scrollObserver = require( './scrollObserver.js' ), initExperiment = require( './AB.js' ), initSectionObserver = require( './sectionObserver.js' ), initTableOfContents = require( './tableOfContents.js' ), pinnableElement = require( './pinnableElement.js' ), popupNotification = require( './popupNotification.js' ), features = require( './features.js' ), deferUntilFrame = require( './deferUntilFrame.js' ), ABTestConfig = require( /** @type {string} */ ( './activeABTest.json' ) ), STICKY_HEADER_VISIBLE_CLASS = 'vector-sticky-header-visible', TOC_ID = 'vector-toc', BODY_CONTENT_ID = 'bodyContent', // Support two variants of heading markup: (see T13555, T358452) // (old) <h2> <span class="mw-headline" id="...">...</span> ... </h2> // (new) <div class="mw-heading"> <h2 id="...">...</h2> ... </div> // [more information: https://www.mediawiki.org/wiki/Heading_HTML_changes] HEADING_TAGS = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], HEADING_SELECTOR = [ '.mw-heading', ...HEADING_TAGS.map( ( tag ) => `${ tag }:not([id])` ) ] .map( ( sel ) => `.mw-parser-output ${ sel }` ).join( ', ' ), HEADLINE_SELECTOR = [ '.mw-headline', ...HEADING_TAGS.map( ( tag ) => `${ tag }[id]` ) ] .map( ( sel ) => `.mw-parser-output ${ sel }` ).join( ', ' ), TOC_SECTION_ID_PREFIX = 'toc-', PAGE_TITLE_INTERSECTION_CLASS = 'vector-below-page-title'; const belowDesktopMedia = window.matchMedia( '(max-width: 1119px)' ); /** * @callback OnIntersection * @param {HTMLElement} element The section that triggered the new intersection change. */ /** * @ignore * @param {Function} changeActiveSection * @return {OnIntersection} */ const getHeadingIntersectionHandler = ( changeActiveSection ) => /** * @param {HTMLElement} section */ // eslint-disable-next-line implicit-arrow-linebreak ( section ) => { const headline = section.classList.contains( 'mw-body-content' ) ? section : section.querySelector( HEADLINE_SELECTOR ); if ( headline ) { changeActiveSection( `${ TOC_SECTION_ID_PREFIX }${ headline.id }` ); } }; /** * Initialize sticky header AB tests and determine whether to show the sticky header * based on which buckets the user is in. * * @typedef {Object} InitStickyHeaderABTests * @property {boolean} showStickyHeader - Should the sticky header be shown * @param {ABTestConfig} abConfig * @param {boolean} isStickyHeaderFeatureAllowed and the user is logged in * @param {function(ABTestConfig): initExperiment.WebABTest} getEnabledExperiment * @return {InitStickyHeaderABTests} */ function initStickyHeaderABTests( abConfig, isStickyHeaderFeatureAllowed, getEnabledExperiment ) { let showStickyHeader = isStickyHeaderFeatureAllowed, stickyHeaderExperiment; // One of the sticky header AB tests is specified in the config const abTestName = abConfig.name, isStickyHeaderExperiment = abTestName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME; // Determine if user is eligible for sticky header AB test if ( isStickyHeaderFeatureAllowed && // The sticky header can be shown on the page abConfig.enabled && // An AB test config is enabled isStickyHeaderExperiment // The AB test is one of the sticky header experiments ) { // If eligible, initialize the AB test stickyHeaderExperiment = getEnabledExperiment( abConfig ); // If running initial or edit AB test, show sticky header to treatment groups // only. Unsampled and control buckets do not see sticky header. if ( abTestName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ) { showStickyHeader = stickyHeaderExperiment.isInTreatmentBucket(); } } return { showStickyHeader }; } /* * Updates TOC's location in the DOM (in sidebar or sticky header) * depending on if the TOC is collapsed and if the sticky header is visible * * @return {void} */ const updateTocLocation = () => { const isPinned = features.isEnabled( 'toc-pinned' ); const isStickyHeaderVisible = document.body.classList.contains( STICKY_HEADER_VISIBLE_CLASS ); const isBelowDesktop = belowDesktopMedia.matches; const pinnedContainerId = 'vector-toc-pinned-container'; const stickyHeaderUnpinnedContainerId = 'vector-sticky-header-toc-unpinned-container'; const pageTitlebarUnpinnedContainerId = 'vector-page-titlebar-toc-unpinned-container'; let newContainerId = ''; if ( isPinned ) { if ( isBelowDesktop ) { // Automatically move the ToC into the page titlebar when pinned on smaller resolutions newContainerId = pageTitlebarUnpinnedContainerId; } else { newContainerId = pinnedContainerId; } } else { if ( isStickyHeaderVisible && !isBelowDesktop ) { newContainerId = stickyHeaderUnpinnedContainerId; } else { newContainerId = pageTitlebarUnpinnedContainerId; } } pinnableElement.movePinnableElement( TOC_ID, newContainerId ); }; /** * Return the computed value of the `scroll-margin-top` CSS property of the document element * which is also used for the scroll intersection threshold (T317661). * * @return {number} Value of scroll-margin-top OR 75 if falsy. * 75 derived from @scroll-padding-top LESS variable * https://gerrit.wikimedia.org/r/c/mediawiki/skins/Vector/+/894696/3/resources/common/variables.less ? */ function getDocumentScrollPaddingTop() { const defaultScrollPaddingTop = 75; const documentStyles = getComputedStyle( document.documentElement ); const scrollPaddingTopString = documentStyles.getPropertyValue( 'scroll-padding-top' ); return ( parseInt( scrollPaddingTopString, 10 ) || defaultScrollPaddingTop ); } /** * @param {HTMLElement|null} tocElement * @param {HTMLElement|null} bodyContent * @param {initSectionObserver} initSectionObserverFn * @return {tableOfContents|null} */ const setupTableOfContents = ( tocElement, bodyContent, initSectionObserverFn ) => { if ( !( tocElement && bodyContent ) ) { return null; } const handleTocSectionChange = () => { // eslint-disable-next-line no-use-before-define sectionObserver.pause(); // T297614: We want the link that the user has clicked inside the TOC or the // section that corresponds to the hashchange event to be "active" (e.g. // bolded) regardless of whether the browser's scroll position corresponds // to that section. Therefore, we need to temporarily ignore section // observer until the browser has finished scrolling to the section (if // needed). // // However, because the scroll event happens asynchronously after the user // clicks on a link and may not even happen at all (e.g. the user has // scrolled all the way to the bottom and clicks a section that is already // in the viewport), determining when we should resume section observer is a // bit tricky. // // Because a scroll event may not even be triggered after clicking the link, // we instead allow the browser to perform a maximum number of repaints // before resuming sectionObserver. Per T297614#7687656, Firefox 97.0 wasn't // consistently activating the table of contents section that the user // clicked even after waiting 2 frames. After further investigation, it // sometimes waits up to 3 frames before painting the new scroll position so // we have that as the limit. deferUntilFrame( () => { // eslint-disable-next-line no-use-before-define sectionObserver.resume(); }, 3 ); }; const tableOfContents = initTableOfContents( { container: tocElement, onHeadingClick: handleTocSectionChange, onHashChange: handleTocSectionChange, onTogglePinned: () => { updateTocLocation(); pinnableElement.setFocusAfterToggle( TOC_ID ); if ( !features.isEnabled( 'toc-pinned' ) ) { const isStickyHeaderVisible = document.body.classList .contains( STICKY_HEADER_VISIBLE_CLASS ); const containerSelector = !isStickyHeaderVisible ? '.vector-page-titlebar .vector-toc-landmark' : '#vector-sticky-header .vector-toc-landmark'; const container = /** @type {HTMLElement} */( document.querySelector( containerSelector ) ); if ( container ) { const containerId = !isStickyHeaderVisible ? 'toc-page-titlebar' : 'toc-sticky-header'; popupNotification.add( container, mw.message( 'vector-toc-unpinned-popup' ).text(), containerId ) .then( ( popupWidget ) => { if ( popupWidget ) { popupNotification.show( popupWidget ); } } ); } } } } ); const elements = () => bodyContent.querySelectorAll( `${ HEADING_SELECTOR }, .mw-body-content` ); const sectionObserver = initSectionObserverFn( { elements: elements(), topMargin: getDocumentScrollPaddingTop(), onIntersection: getHeadingIntersectionHandler( tableOfContents.changeActiveSection ) } ); const updateElements = () => { sectionObserver.resume(); sectionObserver.setElements( elements() ); }; mw.hook( 've.activationStart' ).add( () => { sectionObserver.pause(); } ); mw.hook( 'wikipage.tableOfContents' ).add( ( sections ) => { tableOfContents.reloadTableOfContents( sections ).then( () => { /** * @stable for use in gadgets and extensions * @since 1.40 */ mw.hook( 'wikipage.tableOfContents.vector' ).fire( sections ); updateElements(); } ); } ); mw.hook( 've.deactivationComplete' ).add( updateElements ); const setInitialActiveSection = () => { const hash = location.hash.slice( 1 ); // If hash fragment is blank, determine the active section with section // observer. if ( hash === '' ) { sectionObserver.calcIntersection(); return; } // T325086: If hash fragment is present and corresponds to a toc section, // expand the section. const hashSection = /** @type {HTMLElement|null} */ ( mw.util.getTargetFromFragment( `${ TOC_SECTION_ID_PREFIX }${ hash }` ) ); if ( hashSection ) { tableOfContents.expandSection( hashSection.id ); } // T325086: If hash fragment corresponds to a section AND the user is at // bottom of page, activate the section. Otherwise, use section observer to // calculate the active section. // // Note that even if a hash fragment is present, it's possible for the // browser to scroll to a position that is different from the position of // the section that corresponds to the hash fragment. This can happen when // the browser remembers a prior scroll position after refreshing the page, // for example. if ( hashSection && Math.round( window.innerHeight + window.scrollY ) >= document.body.scrollHeight ) { tableOfContents.changeActiveSection( hashSection.id ); } else { // Fallback to section observer's calculation for the active section. sectionObserver.calcIntersection(); } }; setInitialActiveSection(); return tableOfContents; }; /** * @return {void} */ const main = () => { const isIntersectionObserverSupported = 'IntersectionObserver' in window; // // Table of contents // const tocElement = document.getElementById( TOC_ID ); const bodyContent = document.getElementById( BODY_CONTENT_ID ); const isToCUpdatingAllowed = isIntersectionObserverSupported && window.requestAnimationFrame; const tableOfContents = isToCUpdatingAllowed ? setupTableOfContents( tocElement, bodyContent, initSectionObserver ) : null; // // Sticky header // const stickyHeaderElement = document.getElementById( stickyHeader.STICKY_HEADER_ID ), stickyIntersection = document.getElementById( stickyHeader.FIRST_HEADING_ID ), userLinksDropdown = document.getElementById( stickyHeader.USER_LINKS_DROPDOWN_ID ), allowedNamespace = stickyHeader.isAllowedNamespace( mw.config.get( 'wgNamespaceNumber' ) ), allowedAction = stickyHeader.isAllowedAction( mw.config.get( 'wgAction' ) ); const isStickyHeaderAllowed = !!stickyHeaderElement && !!stickyIntersection && !!userLinksDropdown && allowedNamespace && allowedAction && isIntersectionObserverSupported; const { showStickyHeader } = initStickyHeaderABTests( ABTestConfig, isStickyHeaderAllowed && !mw.user.isAnon(), ( config ) => initExperiment( config, String( mw.user.getId() ) ) ); // Set up intersection observer for page title // Used to show/hide sticky header and add class used by collapsible TOC (T307900) const observer = scrollObserver.initScrollObserver( () => { if ( isStickyHeaderAllowed && showStickyHeader ) { stickyHeader.show(); updateTocLocation(); } document.body.classList.add( PAGE_TITLE_INTERSECTION_CLASS ); if ( tableOfContents ) { tableOfContents.updateTocToggleStyles( true ); } scrollObserver.firePageTitleScrollHook( 'down' ); }, () => { if ( isStickyHeaderAllowed && showStickyHeader ) { stickyHeader.hide(); updateTocLocation(); } document.body.classList.remove( PAGE_TITLE_INTERSECTION_CLASS ); if ( tableOfContents ) { tableOfContents.updateTocToggleStyles( false ); } scrollObserver.firePageTitleScrollHook( 'up' ); } ); // Handle toc location when sticky header is hidden on lower viewports belowDesktopMedia.onchange = () => { updateTocLocation(); }; updateTocLocation(); if ( !showStickyHeader ) { stickyHeader.hide(); } if ( isStickyHeaderAllowed && showStickyHeader ) { stickyHeader.initStickyHeader( { header: stickyHeaderElement, userLinksDropdown, observer, stickyIntersection } ); } else if ( stickyIntersection ) { observer.observe( stickyIntersection ); } }; module.exports = { main, test: { setupTableOfContents, initStickyHeaderABTests, getHeadingIntersectionHandler } }; PK ! �ye bottomDock.lessnu �Iw�� .client-js .vector-settings { display: block; position: fixed; bottom: 8px; right: 8px; z-index: @z-index-stacking-1; ul { padding: 0; list-style: none; display: flex; flex-direction: column-reverse; align-items: center; gap: 8px 8px; } } PK ! ��:� � features.jsnu �Iw�� /** @interface MwApi */ const debounce = require( /** @type {string} */ ( 'mediawiki.util' ) ).debounce; const userPreferences = require( './userPreferences.js' ); /** * Saves preference to user preferences and/or cookies. * * @param {string} feature * @param {boolean} enabled */ function save( feature, enabled ) { if ( !mw.user.isNamed() ) { switch ( feature ) { case 'toc-pinned': case 'limited-width': case 'appearance-pinned': // Save the setting under the new system mw.user.clientPrefs.set( `vector-feature-${ feature }`, enabled ? '1' : '0' ); break; default: // not a supported anonymous preference break; } } else { debounce( () => { userPreferences.saveOptions( { [ `vector-${ feature }` ]: enabled ? 1 : 0 } ); }, 500 )(); } } /** * @param {string} name feature name * @param {boolean} [override] option to force enabled or disabled state. * @param {boolean} [isNotClientPreference] the feature is not a client preference, * so does not persist for logged out users. * @return {boolean} The new feature state (false=disabled, true=enabled). * @throws {Error} if unknown feature toggled. */ function toggleDocClasses( name, override, isNotClientPreference ) { const suffixEnabled = isNotClientPreference ? 'enabled' : 'clientpref-1'; const suffixDisabled = isNotClientPreference ? 'disabled' : 'clientpref-0'; const featureClassEnabled = `vector-feature-${ name }-${ suffixEnabled }`, classList = document.documentElement.classList, featureClassDisabled = `vector-feature-${ name }-${ suffixDisabled }`, // If neither of the classes can be found it is a legacy feature isLoggedInOnlyFeature = !classList.contains( featureClassDisabled ) && !classList.contains( featureClassEnabled ); // Check in legacy mode. if ( isLoggedInOnlyFeature && !isNotClientPreference ) { // try again using the legacy classes return toggleDocClasses( name, override, true ); } else if ( classList.contains( featureClassDisabled ) || override === true ) { classList.remove( featureClassDisabled ); classList.add( featureClassEnabled ); return true; } else if ( classList.contains( featureClassEnabled ) || override === false ) { classList.add( featureClassDisabled ); classList.remove( featureClassEnabled ); return false; } else { throw new Error( `Attempt to toggle unknown feature: ${ name }` ); } } /** * @param {string} name * @throws {Error} if unknown feature toggled. */ function toggle( name ) { const featureState = toggleDocClasses( name ); save( name, featureState ); } /** * Checks if the feature is enabled. * * @param {string} name * @return {boolean} */ function isEnabled( name ) { return document.documentElement.classList.contains( getClass( name, true ) ) || document.documentElement.classList.contains( getClass( name, true, true ) ); } /** * Get name of feature class. * * @param {string} name * @param {boolean} featureEnabled * @param {boolean} [isClientPreference] whether the feature is also a client preference * @return {string} */ function getClass( name, featureEnabled, isClientPreference ) { if ( featureEnabled ) { const suffix = isClientPreference ? 'clientpref-1' : 'enabled'; return `vector-feature-${ name }-${ suffix }`; } else { const suffix = isClientPreference ? 'clientpref-0' : 'disabled'; return `vector-feature-${ name }-${ suffix }`; } } module.exports = { getClass, isEnabled, toggle, toggleDocClasses }; PK ! ru��Y Y activeABTest.jsonnu �Iw�� { "@doc": "This file describes the shape of the AB config. It exists to support jest" } PK ! !�1� � AB.jsnu �Iw�� /** @module WebABTest */ const EXCLUDED_BUCKET = 'unsampled'; const TREATMENT_BUCKET_SUBSTRING = 'treatment'; const WEB_AB_TEST_ENROLLMENT_HOOK = 'mediawiki.web_AB_test_enrollment'; let initialized = false; /** * @typedef {Function} TreatmentBucketFunction * @param {string} [a] * @return {boolean} */ /** * @typedef {Object} WebABTest * @property {string} name * @property {function(): string} getBucket * @property {function(string): boolean} isInBucket * @property {function(): boolean} isInSample * @property {TreatmentBucketFunction} isInTreatmentBucket */ /** * @typedef {Object} SamplingRate * @property {number} samplingRate The desired sampling rate for the group in the range [0, 1]. */ /** * @typedef {Object} WebABTestProps * @property {string} name The name of the experiment. * @property {Record<string, SamplingRate>} buckets Dict with bucket name as key and SamplingRate * object as value. There must be an `unsampled` bucket that represents a * population excluded from the experiment. Additionally, the treatment * bucket(s) must include a case-insensitive `treatment` substring in their name * (e.g. `treatment`, `stickyHeaderTreatment`, `sticky-header-treatment`). * @property {string} [token] Token that uniquely identifies the subject for the * duration of the experiment. If bucketing/server occurs on the server and the * bucket is a class on the body tag, this can be ignored. Otherwise, it is * required. */ /** * Initializes an AB test experiment. * * Example props: * * webABTest({ * name: 'nameOfExperiment', * buckets: { * unsampled: { * samplingRate: 0.25 * }, * control: { * samplingRate: 0.25 * }, * treatment1: { * samplingRate: 0.25 * }, * treatment2: { * samplingRate: 0.25 * } * }, * token: 'token' * }); * * @param {WebABTestProps} props * @param {string} token * @param {boolean} [forceInit] forces initialization of init event. * Bypasses caching. Do not use outside tests. * @return {WebABTest} */ module.exports = function webABTest( props, token, forceInit ) { let /** @type {string} */ cachedBucket; /** * Get the names of all the buckets from props.buckets. * * @return {string[]} */ function getBucketNames() { return Object.keys( props.buckets ); } /** * Get the name of the bucket from the class added to the body tag. * * @return {?string} */ function getBucketFromHTML() { for ( const bucketName of getBucketNames() ) { if ( document.body.classList.contains( `${ props.name }-${ bucketName }` ) ) { return bucketName; } } return null; } /** * Get the name of the bucket the subject is assigned to for A/B testing. * * @throws {Error} Will throw an error if token is undefined and body tag has * not been assigned a bucket. * @return {string} the name of the bucket the subject is assigned. */ function getBucket() { if ( cachedBucket ) { // If we've already cached the bucket, return early. return cachedBucket; } const bucketFromHTML = getBucketFromHTML(); // If bucketing/sampling already occurred on the server, return that bucket // instead of trying to do it on the client. if ( bucketFromHTML ) { cachedBucket = bucketFromHTML; return bucketFromHTML; } if ( token === undefined ) { throw new Error( 'Tried to call `getBucket` with an undefined token' ); } cachedBucket = mw.experiments.getBucket( { name: props.name, enabled: true, buckets: getBucketNames().reduce( ( /** @type {Record<string, number>} */ buckets, key ) => { buckets[ key ] = props.buckets[ key ].samplingRate; return buckets; }, {} ) }, token ); return cachedBucket; } /** * Whether or not the subject is included in the experiment. * * @return {boolean} */ function isInSample() { return getBucket() !== EXCLUDED_BUCKET; } /** * Determine if subject is in a particular bucket. * * @param {string} targetBucket The target test bucket. * @return {boolean} Whether the subject is in the test bucket. */ function isInBucket( targetBucket ) { return getBucket() === targetBucket; } /** * Whether or not the subject has been bucketed in a treatment bucket as * defined by the bucket name containing the case-insensitive 'treatment', * 'treatment1', or 'treatment2' substring (e.g. 'treatment', 'treatment1', * 'sticky-header-treatment1' and 'stickyHeaderTreatment2' are all assumed * to be treatment buckets). * * @param {string|null} [treatmentBucketName] lowercase name of bucket. * @return {boolean} */ function isInTreatmentBucket( treatmentBucketName = '' ) { const bucketLowerCase = getBucket().toLowerCase(); // Array.prototype.includes` is ES7 return bucketLowerCase.indexOf( `${ TREATMENT_BUCKET_SUBSTRING }${ treatmentBucketName }` ) > -1; } /** * Initialize and fire hook to register A/B test enrollment if necessary. */ function init() { // Send data to WikimediaEvents to log A/B test initialization if the subject // has been sampled into the experiment. if ( isInSample() ) { /** * For use by WikimediaEvents only * * @event mediawiki.web_AB_test_enrollment * @internal * @property {string} context * @property {string} action */ mw.hook( WEB_AB_TEST_ENROLLMENT_HOOK ).fire( { group: getBucket(), experimentName: props.name } ); // don't run this again. initialized = true; } } if ( !initialized || forceInit ) { init(); } /** * @typedef {Object} WebABTest * @property {string} name * @property {getBucket} getBucket * @property {isInBucket} isInBucket * @property {isInSample} isInSample * @property {isInTreatmentBucket} isInTreatmentBucket */ return { name: props.name, getBucket, isInBucket, isInSample, isInTreatmentBucket }; }; PK ! I��X� � watchstar.jsnu �Iw�� PK ! V�?5L 5L � tableOfContents.jsnu �Iw�� PK ! d��G �G XQ stickyHeader.jsnu �Iw�� PK ! �I0:� � U� popupNotification.lessnu �Iw�� PK ! ]���� � � scrollObserver.jsnu �Iw�� PK ! �scW W 4� languageButton.jsnu �Iw�� PK ! \�ى� � ̥ clientPreferences.jsonnu �Iw�� PK ! l"J J � skin.jsnu �Iw�� PK ! �it t �� disableNightModeIfGadget.jsnu �Iw�� PK ! @� � F� searchToggle.jsnu �Iw�� PK ! ?J � popupNotification.jsnu �Iw�� PK ! 2��ٽ � b� echo.jsnu �Iw�� PK ! V� stickyHeaderAB.jsnu �Iw�� PK ! �~ŋ � �� tableOfContentsConfig.jsonnu �Iw�� PK ! �z�5M M l� dropdownMenus.jsnu �Iw�� PK ! Ds��� � � stickyHeader.lessnu �Iw�� PK ! ���u � sectionObserver.jsnu �Iw�� PK ! �CQ�8 8 .0 deferUntilFrame.jsnu �Iw�� PK ! ��P'-( -( �2 pinnableElement.jsnu �Iw�� PK ! 3kf�� � [ portlets.jsnu �Iw�� PK ! ����� � $m tables.jsnu �Iw�� PK ! -+��J J �q menuTabs.jsnu �Iw�� PK ! jǨ�f f mu tableOfContents.lessnu �Iw�� PK ! %�e, , w userPreferences.jsnu �Iw�� PK ! <>z z �x index.lessnu �Iw�� PK ! t<è � 9y config.jsonnu �Iw�� PK ! Z��� � z searchLoader.jsnu �Iw�� PK ! �H"��4 �4 �� setupIntersectionObservers.jsnu �Iw�� PK ! �ye � bottomDock.lessnu �Iw�� PK ! ��:� � Z� features.jsnu �Iw�� PK ! ru��Y Y 1� activeABTest.jsonnu �Iw�� PK ! !�1� � �� AB.jsnu �Iw�� PK � ��
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0.01 |
proxy
|
phpinfo
|
ÐаÑтройка