Файловый менеджер - Редактировать - /var/www/html/skins.minerva.scripts.zip
Ðазад
PK ! 4��2� � watchstar.jsnu �Iw�� const watchstar = require( 'mediawiki.page.watch.ajax' ).watchstar; const WATCHED_ICON_CLASS = 'minerva-icon--unStar'; const TEMP_WATCHED_ICON_CLASS = 'minerva-icon--halfStar'; const UNWATCHED_ICON_CLASS = 'minerva-icon--star'; /** * Tweaks the global watchstar handler in core to use the correct classes for Minerva. * * @param {jQuery} $icon * @ignore */ function init( $icon ) { const $watchlink = $icon.find( 'a' ); watchstar( $watchlink, mw.config.get( 'wgRelevantPageName' ), toggleClasses ); } /** * @param {jQuery} $link * @param {boolean} isWatched * @param {string} expiry * @private */ function toggleClasses( $link, isWatched, expiry ) { const $icon = $link.find( '.minerva-icon' ); $icon.removeClass( [ WATCHED_ICON_CLASS, UNWATCHED_ICON_CLASS, TEMP_WATCHED_ICON_CLASS ] ) .addClass( () => { let classes = UNWATCHED_ICON_CLASS; if ( isWatched ) { if ( expiry !== null && expiry !== undefined && expiry !== 'infinity' ) { classes = TEMP_WATCHED_ICON_CLASS; } else { classes = WATCHED_ICON_CLASS; } } return classes; } ); } module.exports = { init: init, test: { toggleClasses, TEMP_WATCHED_ICON_CLASS, WATCHED_ICON_CLASS, UNWATCHED_ICON_CLASS } }; PK ! 4�,�b b TitleUtil.jsnu �Iw�� // Someone has to maintain this wherever it lives. If it live in Core, it becomes a public API. // If it lives in some client-side target of mediawiki-title that accepts a MediaWiki config instead // of a SiteInfo, it still becomes a public API. If it lives where used, it becomes a copy and paste // implementation where each copy can deviate but deletion is easy. See additional discussion in // T218358 and I95b08e77eece5cd4dae62f6f237d492d6b0fe42b. const UriUtil = require( './UriUtil.js' ); /** * Returns the decoded wiki page title referenced by the passed link as a string when parsable. * The title query parameter is returned, if present. Otherwise, a heuristic is used to attempt * to extract the title from the path. * * The API is the source of truth for page titles. This function should only be used in * circumstances where the API cannot be consulted. * * Assuming the current page is on metawiki, consider the following example links and * `newFromUri()` outputs: * * https://meta.wikimedia.org/wiki/Foo → Foo (path title) * http://meta.wikimedia.org/wiki/Foo → Foo (mismatching protocol) * /wiki/Foo → Foo (relative URI) * /w/index.php?title=Foo → Foo (title query parameter) * /wiki/Talk:Foo → Talk:Foo (non-main namespace URI) * /wiki/Foo bar → Foo_bar (name with spaces) * /wiki/Foo%20bar → Foo_bar (name with percent encoded spaces) * /wiki/Foo+bar → Foo+bar (name with +) * /w/index.php?title=Foo%2bbar → Foo+bar (query parameter with +) * / → null (mismatching article path) * /wiki/index.php?title=Foo → null (mismatching script path) * https://archive.org/ → null (mismatching host) * https://foo.wikimedia.org/ → null (mismatching host) * https://en.wikipedia.org/wiki/Bar → null (mismatching host) * * This function invokes `Uri.isInternal()` to validate that this link is assuredly a local * wiki link and that the internal usage of both the title query parameter and value of * wgArticlePath are relevant. * * This function doesn't throw. `null` is returned for any unparseable input. * * @ignore * @param {mw.Uri|Object|string} [uri] Passed to Uri. * @param {Object|boolean} [options] Passed to Uri. * @param {Object|boolean} [options.validateReadOnlyLink] If true, only links that would show a * page for reading are considered. E.g., `/wiki/Foo` and `/w/index.php?title=Foo` would * validate but `/w/index.php?title=Foo&action=bar` would not. * @return {mw.Title|null} A Title or `null`. */ function newFromUri( uri, options ) { let mwUri; let title; try { // uri may or may not be a Uri but the Uri constructor accepts a Uri parameter. mwUri = new mw.Uri( uri, options ); } catch ( e ) { return null; } if ( !UriUtil.isInternal( mwUri ) ) { return null; } if ( ( options || {} ).validateReadOnlyLink && !isReadOnlyUri( mwUri ) ) { // An unknown query parameter is used. This may not be a read-only link. return null; } if ( mwUri.query.title ) { // True if input starts with wgScriptPath. const regExp = new RegExp( '^' + mw.util.escapeRegExp( mw.config.get( 'wgScriptPath' ) ) + '/' ); // URL has a nonempty `title` query parameter like `/w/index.php?title=Foo`. The script // path should match. const matches = regExp.test( mwUri.path ); if ( !matches ) { return null; } // The parameter was already decoded at Uri construction. title = mwUri.query.title; } else { // True if input starts with wgArticlePath and ends with a nonempty page title. The // first matching group (index 1) is the page title. const regExp = new RegExp( '^' + mw.util.escapeRegExp( mw.config.get( 'wgArticlePath' ) ).replace( '\\$1', '(.+)' ) ); // No title query parameter is present so the URL may be "pretty" like `/wiki/Foo`. // `Uri.path` should not contain query parameters or a fragment, as is assumed in // `Uri.getRelativePath()`. Try to isolate the title. const matches = regExp.exec( mwUri.path ); if ( !matches || !matches[ 1 ] ) { return null; } try { // `Uri.path` was not previously decoded, as is assumed in `Uri.getRelativePath()`, // and decoding may now fail. Do not use `Uri.decode()` which is designed to be // paired with `Uri.encode()` and replaces `+` characters with spaces. title = decodeURIComponent( matches[ 1 ] ); } catch ( e ) { return null; } } // Append the fragment, if present. title += mwUri.fragment ? '#' + mwUri.fragment : ''; return mw.Title.newFromText( title ); } /** * Validates that the passed link is for reading. * * The following links return true: * /wiki/Foo * /w/index.php?title=Foo * /w/index.php?oldid=123 * * The following links return false: * /w/index.php?title=Foo&action=bar * * @private * @static * @method isReadOnlyUri * @param {mw.Uri} uri A Uri to an internal wiki page. * @return {boolean} True if uri has no query parameters or only known parameters for reading. */ function isReadOnlyUri( uri ) { const length = Object.keys( uri.query ).length; return length === ( ( 'oldid' in uri.query ? 1 : 0 ) + ( 'title' in uri.query ? 1 : 0 ) ); } module.exports = { newFromUri }; PK ! �" " TabScroll.jsnu �Iw�� let scrollLeftStyle = null; function testScrollLeftStyle() { if ( scrollLeftStyle !== null ) { return scrollLeftStyle; } // Detect which scrollLeft style the browser uses // Adapted from <https://github.com/othree/jquery.rtl-scroll-type>. // Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License. // Adaptation copied from OO.ui.Element.static.getScrollLeft const $definer = $( '<div>' ).attr( { dir: 'rtl', style: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;' } ).text( 'ABCD' ); $definer.appendTo( document.body ); const definer = $definer[ 0 ]; if ( definer.scrollLeft > 0 ) { // Safari, Chrome scrollLeftStyle = 'default'; } else { definer.scrollLeft = 1; if ( definer.scrollLeft === 0 ) { // Firefox, old Opera scrollLeftStyle = 'negative'; } else { // Internet Explorer, Edge scrollLeftStyle = 'reverse'; } } $definer.remove(); return scrollLeftStyle; } /** * When tabs are present and one is selected, scroll the selected tab into view. * * @ignore */ function initTabsScrollPosition() { // eslint-disable-next-line no-jquery/no-global-selector const $selectedTab = $( '.minerva__tab.selected' ); if ( $selectedTab.length !== 1 ) { return; } const selectedTab = $selectedTab.get( 0 ); const $tabContainer = $selectedTab.closest( '.minerva__tab-container' ); const tabContainer = $tabContainer.get( 0 ); const maxScrollLeft = tabContainer.scrollWidth - tabContainer.clientWidth; const dir = $tabContainer.css( 'direction' ) || 'ltr'; /** * Set tabContainer.scrollLeft, with adjustments for browser inconsistencies in RTL * * @param {number} sl New .scrollLeft value, in 'default' (WebKit) style */ function setScrollLeft( sl ) { if ( dir === 'ltr' ) { tabContainer.scrollLeft = sl; return; } if ( testScrollLeftStyle() === 'reverse' ) { sl = maxScrollLeft - sl; } else if ( testScrollLeftStyle() === 'negative' ) { sl = -( maxScrollLeft - sl ); } tabContainer.scrollLeft = sl; } const leftMostChild = dir === 'ltr' ? tabContainer.firstElementChild : tabContainer.lastElementChild; const rightMostChild = dir === 'ltr' ? tabContainer.lastElementChild : tabContainer.firstElementChild; // If the tab is wider than the container (doesn't fit), this value will be negative const widthDiff = tabContainer.clientWidth - selectedTab.clientWidth; if ( selectedTab === leftMostChild ) { // The left-most tab is selected. If the tab fits, scroll all the way to the left. // If the tab doesn't fit, align its start edge with the container's start edge. if ( dir === 'ltr' || widthDiff >= 0 ) { setScrollLeft( 0 ); } else { setScrollLeft( -widthDiff ); } } else if ( selectedTab === rightMostChild ) { // The right-most tab is selected. If the tab fits, scroll all the way to the right. // If the tab doesn't fit, align its start edge with the container's start edge. if ( dir === 'rtl' || widthDiff >= 0 ) { setScrollLeft( maxScrollLeft ); } else { setScrollLeft( maxScrollLeft + widthDiff ); } } else { // The selected tab is not the left-most or right-most, it's somewhere in the middle const tabPosition = $selectedTab.position(); const containerPosition = $tabContainer.position(); // Position of the left edge of $selectedTab relative to the left edge of $tabContainer const left = tabPosition.left - containerPosition.left; // Because the calculations above use the existing .scrollLeft from the browser, // we should not use setScrollLeft() here. Instead, we rely on the fact that scrollLeft // increases to the left in the 'default' and 'negative' modes, and to the right in // the 'reverse' mode, so we can add/subtract a delta to/from scrollLeft accordingly. let increaseScrollLeft; if ( widthDiff >= 0 ) { // The tab fits, center it increaseScrollLeft = left - widthDiff / 2; } else if ( dir === 'ltr' ) { // The tab doesn't fit (LTR), align its left edge with the container's left edge increaseScrollLeft = left; } else { // The tab doesn't fit (RTL), align its right edge with the container's right edge increaseScrollLeft = left - widthDiff; } tabContainer.scrollLeft += increaseScrollLeft * ( testScrollLeftStyle() === 'reverse' ? -1 : 1 ); } } module.exports = { initTabsScrollPosition: initTabsScrollPosition }; PK ! ����� � mobileRedirect.jsnu �Iw�� const drawers = require( './drawers.js' ); /* * Warn people if they're trying to switch to desktop but have cookies disabled. */ module.exports = function ( amcOutreach, currentPage ) { /** * Checks whether cookies are enabled * * @method * @ignore * @return {boolean} Whether cookies are enabled */ function cookiesEnabled() { // If session cookie already set, return true if ( mw.cookie.get( 'mf_testcookie' ) === 'test_value' ) { return true; // Otherwise try to set mf_testcookie and return true if it was set } else { mw.cookie.set( 'mf_testcookie', 'test_value', { path: '/' } ); return mw.cookie.get( 'mf_testcookie' ) === 'test_value'; } } /** * An event handler for the toggle to desktop link. * If cookies are enabled it will redirect you to desktop site as described in * the link href associated with the handler. * If cookies are not enabled, show a toast and die. * * @method * @ignore * @return {boolean|undefined} */ function desktopViewClick() { if ( !cookiesEnabled() ) { mw.notify( mw.msg( 'mobile-frontend-cookies-required' ), { type: 'error' } ); // Prevent default action return false; } } /** * @method * @ignore * @param {jQuery.Event} ev * @return {boolean|undefined} */ function amcDesktopClickHandler( ev ) { const self = this; const executeWrappedEvent = function () { if ( desktopViewClick() === false ) { return false; } window.location = self.href; }; const amcCampaign = amcOutreach.loadCampaign(); const onDismiss = function () { executeWrappedEvent(); }; const drawer = amcCampaign.showIfEligible( amcOutreach.ACTIONS.onDesktopLink, onDismiss, currentPage.title ); if ( drawer ) { ev.preventDefault(); // stopPropagation is needed to prevent drawer from immediately closing // when shown (drawers.js adds a click event to window when drawer is // shown ev.stopPropagation(); drawers.displayDrawer( drawer, {} ); drawers.lockScroll(); return; } return executeWrappedEvent(); } // eslint-disable-next-line no-jquery/no-global-selector $( '#mw-mf-display-toggle' ).on( 'click', amcDesktopClickHandler ); }; PK ! ( �-� � setup.jsnu �Iw�� /** * This setups the Minerva skin. * It should run without errors even if MobileFrontend is not installed. * * @ignore */ const ms = require( 'mobile.startup' ); const reportIfNightModeWasDisabledOnPage = require( './reportIfNightModeWasDisabledOnPage.js' ); const addPortletLink = require( './addPortletLink.js' ); const teleportTarget = require( 'mediawiki.page.ready' ).teleportTarget; function init() { const permissions = mw.config.get( 'wgMinervaPermissions' ) || {}; // eslint-disable-next-line no-jquery/no-global-selector const $watch = $( '#page-actions-watch' ); if ( permissions.watch ) { require( './watchstar.js' ).init( $watch ); } addPortletLink.init(); mw.hook( 'util.addPortletLink' ).add( addPortletLink.hookHandler ); // Setup Minerva with MobileFrontend if ( ms && !ms.stub ) { require( './initMobile.js' )(); } else { // MOBILEFRONTEND IS NOT INSTALLED. // setup search for desktop Minerva at mobile resolution without MobileFrontend. require( './searchSuggestReveal.js' )(); } // This hot fix should be reviewed and possibly removed circa January 2021. // It's assumed that Apple will prioritize fixing this bug in one of its next releases. // See T264376. if ( navigator.userAgent.match( /OS 14_[0-9]/ ) ) { document.body.classList.add( 'hotfix-T264376' ); } // Apply content styles to teleported elements teleportTarget.classList.add( 'content' ); reportIfNightModeWasDisabledOnPage( document.documentElement, mw.user.options, mw.user.isNamed() ); } if ( !window.QUnit ) { init(); } module.exports = { // Version number allows breaking changes to be detected by other extensions VERSION: 1 }; PK ! �� � references.jsnu �Iw�� const drawers = require( './drawers.js' ); module.exports = function () { const mobile = require( 'mobile.startup' ); const references = mobile.references; const currentPage = mobile.currentPage(); const currentPageHTMLParser = mobile.currentPageHTMLParser(); const ReferencesHtmlScraperGateway = mobile.references.ReferencesHtmlScraperGateway; const gateway = new ReferencesHtmlScraperGateway( new mw.Api() ); /** * Event handler to show reference when a reference link is clicked * * @ignore * @param {jQuery.Event} ev Click event of the reference element */ function showReference( ev ) { const $dest = $( ev.currentTarget ); let href = $dest.attr( 'href' ); ev.preventDefault(); // If necessary strip the URL portion of the href so we are left with the // fragment const i = href.indexOf( '#' ); if ( i > 0 ) { href = href.slice( i ); } references.showReference( href, currentPage, $dest.text(), currentPageHTMLParser, gateway, { onShow: function () { drawers.lockScroll(); }, onShowNestedReference: true, onBeforeHide: drawers.discardDrawer }, ( oldDrawer, newDrawer ) => { oldDrawer.hide(); drawers.displayDrawer( newDrawer, {} ); } ).then( ( drawer ) => { drawers.displayDrawer( drawer, {} ); } ); } /** * Event handler to show reference when a reference link is clicked. * Delegates to `showReference` once the references drawer is ready. * * @ignore * @param {jQuery.Event} ev Click event of the reference element */ function onClickReference( ev ) { showReference( ev ); } function init() { // Make references clickable and show a drawer when clicked on. $( document ).on( 'click', 'sup.reference a', onClickReference ); } init(); }; PK ! ��2�y y drawers.jsnu �Iw�� const $drawerContainer = $( document.body ); const BODY_CLASS_SCROLL_LOCKED = 'has-drawer--with-scroll-locked'; /** * Discard a drawer from display on the page. * * @private * @param {Drawer} drawer */ function discardDrawer( drawer ) { // remove the class $drawerContainer.removeClass( BODY_CLASS_SCROLL_LOCKED ); // FIXME: queue removal from DOM (using setTimeout so that any animations have time to run) // This works around an issue in MobileFrontend that the Drawer onBeforeHide method is // called /before/ the animation for closing has completed. This needs to be accounted // for in Drawer so this function can be synchronous. setTimeout( () => { // detach the node from the DOM. Use detach rather than remove to allow reuse without // losing any existing events. drawer.$el.detach(); }, 100 ); } /** * Lock scroll of viewport. * * @ignore */ function lockScroll() { $drawerContainer.addClass( BODY_CLASS_SCROLL_LOCKED ); } /** * @param {Drawer} drawer to display * @param {Object} options for display * @param {boolean} options.hideOnScroll whether a scroll closes the drawer * @ignore */ function displayDrawer( drawer, options ) { $drawerContainer.append( drawer.$el ); drawer.show(); if ( options.hideOnScroll ) { $( window ).one( 'scroll.drawer', () => { drawer.hide(); } ); } } module.exports = { displayDrawer, lockScroll, discardDrawer }; PK ! �d5K menu.jsnu �Iw�� const BODY_NOTIFICATIONS_REVEAL_CLASS = 'navigation-enabled secondary-navigation-enabled'; /** * Wire up the main menu * * @ignore */ function init() { // See I09c27a084100b223662f84de6cbe01bebe1fe774 // will trigger every time the Echo notification is opened or closed. // This controls the drawer like behaviour of notifications // on tablet in mobile mode. mw.hook( 'echo.mobile' ).add( ( isOpen ) => { $( document.body ).toggleClass( BODY_NOTIFICATIONS_REVEAL_CLASS, isOpen ); } ); } module.exports = { init }; PK ! �V6N% % preInit.jsnu �Iw�� module.exports = function () { const menus = require( './menu.js' ); // setup main menu menus.init(); ( function ( wgRedirectedFrom ) { // If the user has been redirected, then show them a toast message (see // https://phabricator.wikimedia.org/T146596). if ( wgRedirectedFrom === null ) { return; } const redirectedFrom = mw.Title.newFromText( wgRedirectedFrom ); if ( redirectedFrom ) { // mw.Title.getPrefixedText includes the human-readable namespace prefix. const title = redirectedFrom.getPrefixedText(); const $msg = $( '<div>' ).html( mw.message( 'mobile-frontend-redirected-from', title ).parse() ); $msg.find( 'a' ).attr( 'href', mw.util.getUrl( title, { redirect: 'no' } ) ); mw.notify( $msg ); } }( mw.config.get( 'wgRedirectedFrom' ) ) ); }; PK ! ��m� � Toolbar.lessnu �Iw�� @import 'mediawiki.skin.variables'; .minerva-read-only { // stylelint-disable-next-line selector-max-id #page-actions-edit a .minerva-icon { .cdx-mixin-css-icon( @cdx-icon-edit-lock, @color-subtle); } } PK ! �ijf toc.lessnu �Iw�� @import 'mediawiki.skin.variables.less'; .toc-title-icon { position: absolute; left: @spacing-horizontal-button; .cdx-mixin-css-icon( @cdx-icon-list-bullet, @param-is-button-icon: false ); } .toc-title-state-icon { position: absolute; right: @spacing-horizontal-button; .cdx-mixin-css-icon( @cdx-icon-expand, @param-is-button-icon: false, @param-size-icon: @size-icon-small); } // Flip the arrow in table of contents when toggled .toctogglecheckbox:checked ~ .toctitle .toc-title-state-icon { transform: rotate( -180deg ); } PK ! }�q�� � % reportIfNightModeWasDisabledOnPage.jsnu �Iw�� /** * @private * @return {boolean} */ function reportDisabled() { mw.notify( mw.msg( 'skin-minerva-night-mode-unavailable' ) ); return true; } /** * @ignore * @param {Document} doc * @return {boolean} whether it was reported as disabled. */ function reportIfNightModeWasDisabledOnPage( doc ) { if ( !doc.classList.contains( 'skin-night-mode-page-disabled' ) ) { return false; } // Cast to string. let userExpectedNightMode = `${ mw.user.options.get( 'minerva-theme' ) }`; if ( !mw.user.isNamed() ) { // bit more convoulated here and will break with upstream changes... // this is protected by an integration test in integration.test.js const cookieValue = mw.cookie.get( 'mwclientpreferences' ) || ''; const match = cookieValue.match( /skin-theme-clientpref-(\S+)/ ); if ( match ) { // we found something in the cookie. userExpectedNightMode = match[ 1 ]; } } if ( userExpectedNightMode === 'night' ) { return reportDisabled(); } else if ( userExpectedNightMode === 'os' && matchMedia( '( prefers-color-scheme: dark )' ).matches ) { return reportDisabled(); } else { return false; } } module.exports = reportIfNightModeWasDisabledOnPage; PK ! w=� > > search.jsnu �Iw�� module.exports = function () { const mobile = require( 'mobile.startup' ); const SearchOverlay = mobile.search.SearchOverlay; const SearchGateway = mobile.search.SearchGateway; const overlayManager = mobile.getOverlayManager(); // eslint-disable-next-line no-jquery/no-global-selector const $searchInput = $( '#searchInput' ); const placeholder = $searchInput.attr( 'placeholder' ); const defaultSearchPage = $searchInput.siblings( 'input[name=title]' ).val(); // eslint-disable-next-line no-jquery/no-global-selector const $searchBar = $( '#searchInput, #searchIcon, .skin-minerva-search-trigger' ); const searchRoute = new RegExp( /\/search/ ); let searchOverlayInstance; // Only continue on mobile devices as it breaks desktop search // See https://phabricator.wikimedia.org/T108432 if ( mw.config.get( 'skin' ) !== 'minerva' ) { return; } /** * Hide the search overlay on pageload before the search route * is registered with the overlayManager. * Allows the usage of history.back() to close searchOverlay by * preventing the situation described in https://phabricator.wikimedia.org/T102946 */ function removeSearchOnPageLoad() { if ( searchRoute.test( overlayManager.router.getPath() ) ) { // TODO: replace when router supports replaceState https://phabricator.wikimedia.org/T189173 history.replaceState( '', document.title, window.location.pathname ); } } function getSearchOverlay() { if ( !searchOverlayInstance ) { searchOverlayInstance = new SearchOverlay( { router: overlayManager.router, gatewayClass: SearchGateway, api: new mw.Api(), autocapitalize: $searchInput.attr( 'autocapitalize' ), searchTerm: $searchInput.val(), placeholderMsg: placeholder, defaultSearchPage: defaultSearchPage } ); } return searchOverlayInstance; } removeSearchOnPageLoad(); overlayManager.add( searchRoute, getSearchOverlay ); // Apparently needed for main menu to work correctly. $searchBar.prop( 'readonly', true ); /** * Trigger overlay on touchstart so that the on-screen keyboard on iOS * can be triggered immidiately after on touchend. The keyboard can't be * triggered unless the element is already visible. * Touchstart makes the overlay visible, touchend brings up the keyboard afterwards. */ $searchBar.on( 'touchstart click', ( ev ) => { ev.preventDefault(); overlayManager.router.navigate( '/search' ); } ); $searchBar.on( 'touchend', ( ev ) => { ev.preventDefault(); /** * Manually triggering focus event because on-screen keyboard only * opens when `focus()` is called from a "user context event", * Calling it from the route callback above (which calls SearchOverlay#show) * doesn't work. * http://stackoverflow.com/questions/6837543/show-virtual-keyboard-on-mobile-phones-in-javascript */ getSearchOverlay().showKeyboard(); } ); }; PK ! P��� � styles.lessnu �Iw�� @import '../../minerva.less/minerva.variables.less'; @import '../../minerva.less/minerva.mixins.less'; @import 'mediawiki.mixins.less'; @import 'toc.less'; @import 'page-issues/styles.less'; @import 'Toolbar.less'; .minerva-icon--download { .cdx-mixin-css-icon( @cdx-icon-download, @color-subtle ); } .minerva-icon--listBullet { .cdx-mixin-css-icon( @cdx-icon-list-bullet, @color-subtle ); } .mw-mf-page-center__mask { position: absolute; top: 0; left: 0; right: 0; opacity: 0; bottom: 0; background: @opaque; z-index: @z-index-above-content; // don't use display: none because it's not animatable visibility: hidden; transition: opacity @transition-duration-base ease-in-out; } .navigation-enabled { .mw-mf-page-center__mask { visibility: visible; opacity: 0.5; } } // Last modified bar styles a.last-modified-bar.active { background-color: @background-color-progressive; color: @color-inverted-fixed !important; .last-modified-text-accent, a { color: @color-inverted-fixed; } .minerva-icon { background-color: @color-inverted-fixed; } } // Used by last modified bar and in MobileFrontend (i.e. MobileSpecialPageFeed.php, ImageCarousel.js) .truncated-text { .text-overflow( @visible: false ); } @keyframes fadeInImage { from { opacity: 0; } to { opacity: 1; } } // When the image has loaded transition background color and image opacity // for a fade-in effect img.image-lazy-loaded { animation: fadeInImage 300ms ease-in; } PK ! =2Wn8 n8 initMobile.jsnu �Iw�� /** * Initialise code that requires MobileFrontend. */ module.exports = function () { const ms = require( 'mobile.startup' ), PageHTMLParser = ms.PageHTMLParser, permissions = mw.config.get( 'wgMinervaPermissions' ) || {}, notifyOnPageReload = ms.notifyOnPageReload, time = ms.time, preInit = require( './preInit.js' ), mobileRedirect = require( './mobileRedirect.js' ), search = require( './search.js' ), references = require( './references.js' ), TitleUtil = require( './TitleUtil.js' ), issues = require( './page-issues/index.js' ), Toolbar = require( './Toolbar.js' ), ToggleList = require( '../../includes/Skins/ToggleList/ToggleList.js' ), TabScroll = require( './TabScroll.js' ), router = require( 'mediawiki.router' ), ctaDrawers = require( './ctaDrawers.js' ), drawers = require( './drawers.js' ), desktopMMV = mw.loader.getState( 'mmv.bootstrap' ), overlayManager = ms.getOverlayManager(), currentPage = ms.currentPage(), currentPageHTMLParser = ms.currentPageHTMLParser(), api = new mw.Api(), namespaceIDs = mw.config.get( 'wgNamespaceIds' ); /** * Event handler for clicking on an image thumbnail * * @param {MouseEvent} ev * @ignore */ function onClickImage( ev ) { // Do not interfere when a modifier key is pressed. if ( ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey ) { return; } const el = ev.target.closest( PageHTMLParser.THUMB_SELECTOR ); if ( !el ) { return; } const thumb = currentPageHTMLParser.getThumbnail( $( el ) ); if ( !thumb ) { return; } ev.preventDefault(); routeThumbnail( thumb ); } /** * @param {jQuery.Element} thumbnail * @ignore */ function routeThumbnail( thumbnail ) { router.navigate( '#/media/' + encodeURIComponent( thumbnail.getFileName() ) ); } /** * Add routes to images and handle clicks * * @method * @ignore * @param {HTMLElement} container Container to search within */ function initMediaViewer( container ) { // T360781 Ensure correct type before using `addEventListener`. if ( container instanceof HTMLElement ) { container.addEventListener( 'click', onClickImage ); } } /** * Hijack the Special:Languages link and replace it with a trigger to a languageOverlay * that displays the same data * * @ignore */ function initButton() { // This catches language selectors in page actions and in secondary actions (e.g. Main Page) // eslint-disable-next-line no-jquery/no-global-selector const $primaryBtn = $( '.language-selector' ); if ( $primaryBtn.length ) { // We only bind the click event to the first language switcher in page $primaryBtn.on( 'click', ( ev ) => { ev.preventDefault(); if ( $primaryBtn.attr( 'href' ) || $primaryBtn.find( 'a' ).length ) { router.navigate( '/languages' ); } else { mw.notify( mw.msg( 'mobile-frontend-languages-not-available' ), { tag: 'languages-not-available' } ); } } ); } } /** * Returns a rejected promise if MultimediaViewer is available. Otherwise * returns the mediaViewerOverlay * * @method * @ignore * @param {string} title the title of the image * @return {void|Overlay} note must return void if the overlay should not show (see T262703) * otherwise an Overlay is expected and this can lead to e.on/off is not a function */ function makeMediaViewerOverlayIfNeeded( title ) { if ( mw.loader.getState( 'mmv.bootstrap' ) === 'ready' ) { // This means MultimediaViewer has been installed and is loaded. // Avoid loading it (T169622) return; } try { title = decodeURIComponent( title ); } catch ( e ) { // e.g. https://ro.m.wikipedia.org/wiki/Elisabeta_I_a_Angliei#/media/Fi%C8%18ier:Elizabeth_I_Rainbow_Portrait.jpg return; } return ms.mediaViewer.overlay( { api, thumbnails: currentPageHTMLParser.getThumbnails(), title } ); } // Routes overlayManager.add( /^\/media\/(.+)$/, makeMediaViewerOverlayIfNeeded ); overlayManager.add( /^\/languages$/, () => ms.languages.languageOverlay() ); // Register a LanguageInfo overlay which has no built-in functionality; // a hook is fired when a language is selected, and extensions can respond // to that hook. See GrowthExperiments WelcomeSurvey feature (in gerrit // Ib558dc7c46cc56ff667957f9126bbe0471d25b8e for example usage). overlayManager.add( /^\/languages\/all$/, () => ms.languages.languageInfoOverlay( api, true ) ); overlayManager.add( /^\/languages\/all\/no-suggestions$/, () => ms.languages.languageInfoOverlay( api, false ) ); // Setup $( () => { initButton(); } ); /** * Initialisation function for last modified module. * * Enhances an element representing a time * to show a human friendly date in seconds, minutes, hours, days * months or years * * @ignore * @param {jQuery} $lastModifiedLink */ function initHistoryLink( $lastModifiedLink ) { const ts = $lastModifiedLink.data( 'timestamp' ); if ( ts ) { const username = $lastModifiedLink.data( 'user-name' ) || false; const gender = $lastModifiedLink.data( 'user-gender' ); const delta = time.getTimeAgoDelta( parseInt( ts, 10 ) ); if ( time.isRecent( delta ) ) { const $bar = $lastModifiedLink.closest( '.last-modified-bar' ); $bar.addClass( 'active' ); } const $msg = $( '<span>' ) // The new element should maintain the non-js element's CSS classes. .attr( 'class', $lastModifiedLink.attr( 'class' ) ) .html( time.getLastModifiedMessage( ts, username, gender, // For cached HTML $lastModifiedLink.attr( 'href' ) ) ); $lastModifiedLink.replaceWith( $msg ); } } /** * @method * @param {jQuery.Event} ev */ function amcHistoryClickHandler( ev ) { const self = this; const amcOutreach = ms.amcOutreach; const amcCampaign = amcOutreach.loadCampaign(); const onDismiss = function () { notifyOnPageReload( mw.msg( 'mobile-frontend-amc-outreach-dismissed-message' ) ); window.location = self.href; }; const drawer = amcCampaign.showIfEligible( amcOutreach.ACTIONS.onHistoryLink, onDismiss, currentPage.title, 'action=history' ); if ( drawer ) { ev.preventDefault(); // stopPropagation is needed to prevent drawer from immediately closing // when shown (drawers.js adds a click event to window when drawer is // shown ev.stopPropagation(); drawers.displayDrawer( drawer, {} ); drawers.lockScroll(); } } /** * @method * @param {jQuery} $lastModifiedLink * @ignore */ function initAmcHistoryLink( $lastModifiedLink ) { $lastModifiedLink.one( 'click', amcHistoryClickHandler ); } /** * Initialisation function for last modified times * * Enhances .modified-enhancement element * to show a human friendly date in seconds, minutes, hours, days * months or years * * @ignore */ function initModifiedInfo() { // eslint-disable-next-line no-jquery/no-global-selector $( '.modified-enhancement' ).each( ( _i, el ) => { initHistoryLink( $( el ) ); } ); Array.prototype.forEach.call( document.querySelectorAll( '.mw-diff-timestamp' ), ( tsNode ) => { const ts = tsNode.dataset.timestamp; if ( ts ) { const ago = time.getTimeAgoDelta( parseInt( ( new Date( ts ) ).getTime() / 1000, 10 ) ); // Supported messages: // * skin-minerva-time-ago-seconds // * skin-minerva-time-ago-minutes // * skin-minerva-time-ago-hours // * skin-minerva-time-ago-days // * skin-minerva-time-ago-months // * skin-minerva-time-ago-years tsNode.textContent = mw.msg( `skin-minerva-time-ago-${ ago.unit }`, mw.language.convertNumber( ago.value ) ); } } ); } /** * Initialisation function for user creation module. * * Enhances an element representing a time * to show a human friendly date in seconds, minutes, hours, days * months or years * * @ignore * @param {jQuery} [$tagline] */ function initRegistrationDate( $tagline ) { const ts = $tagline.data( 'userpage-registration-date' ); if ( ts ) { const msg = time.getRegistrationMessage( ts, $tagline.data( 'userpage-gender' ) ); $tagline.text( msg ); } } /** * Initialisation function for registration date on user page * * Enhances .tagline-userpage element * to show human friendly date in seconds, minutes, hours, days * months or years * * @ignore */ function initRegistrationInfo() { // eslint-disable-next-line no-jquery/no-global-selector $( '#tagline-userpage' ).each( ( _i, el ) => { initRegistrationDate( $( el ) ); } ); } /** * Tests a URL to determine if it links to a local User namespace page or not. * * Assuming the current page visited is hosted on metawiki, the following examples would return * true: * * https://meta.wikimedia.org/wiki/User:Foo * /wiki/User:Foo * /wiki/User:Nonexistent_user_page * * The following examples return false: * * https://en.wikipedia.org/wiki/User:Foo * /wiki/Foo * /wiki/User_talk:Foo * * @param {string} url * @return {boolean} */ function isUserUri( url ) { const title = TitleUtil.newFromUri( url ); const namespace = title ? title.getNamespaceId() : undefined; return namespace === namespaceIDs.user; } /** * Strip the edit action from red links to nonexistent User namespace pages. * * @param {jQuery} $redLinks */ function initUserRedLinks( $redLinks ) { $redLinks.filter( // Filter out non-User namespace pages. ( _, element ) => isUserUri( element.href ) ).each( ( _, element ) => { const uri = new mw.Uri( element.href ); if ( uri.query.action !== 'edit' ) { // Nothing to strip. return; } // Strip the action. delete uri.query.action; // Update the element with the new link. element.href = uri.toString(); } ); } /** * Wires up the notification badge to Echo extension */ function setupEcho() { const echoBtn = document.querySelector( '.minerva-notifications .mw-echo-notification-badge-nojs' ); if ( echoBtn ) { echoBtn.addEventListener( 'click', ( ev ) => { router.navigate( '#/notifications' ); // prevent navigation to original Special:Notifications URL // DO NOT USE stopPropagation or you'll break click tracking in WikimediaEvents ev.preventDefault(); // Mark as read. echoBtn.dataset.counterNum = 0; echoBtn.dataset.counterText = mw.msg( 'echo-badge-count', mw.language.convertNumber( 0 ) ); } ); } } $( () => { // eslint-disable-next-line no-jquery/no-global-selector const $watch = $( '#page-actions-watch' ); const toolbarElement = document.querySelector( Toolbar.selector ); const userMenu = document.querySelector( '.minerva-user-menu' ); // See UserMenuDirector. const navigationDrawer = document.querySelector( '.navigation-drawer' ); // The `minerva-animations-ready` class can be used by clients to prevent unwanted // CSS transitions from firing on page load in some browsers (see // https://bugs.chromium.org/p/chromium/issues/detail?id=332189 as well as // https://phabricator.wikimedia.org/T234570#5779890). Since JS adds this // class after the CSS transitions loads, this issue is circumvented. See // MainMenu.less for an example of how this is used. $( document.body ).addClass( 'minerva-animations-ready' ); // eslint-disable-next-line no-jquery/no-global-selector $( '.mw-mf-page-center__mask' ).on( 'click', ( ev ) => { const path = router.getPath(); // avoid jumping to the top of the page and polluting history by avoiding the // resetting of the hash unless the hash is being utilised (T237015). if ( !path ) { ev.preventDefault(); } } ); // Init: // - main menu closes when you click outside of it // - redirects show a toast. preInit(); // - references references(); // - search search(); // - mobile redirect mobileRedirect( ms.amcOutreach, currentPage ); // Enhance timestamps on last-modified bar and watchlist // to show relative time. initModifiedInfo(); initRegistrationInfo(); // eslint-disable-next-line no-jquery/no-global-selector initAmcHistoryLink( $( '.last-modified-bar__text a' ) ); if ( toolbarElement ) { Toolbar.bind( window, toolbarElement ); // Update the edit icon and add a download icon. Toolbar.render( window, toolbarElement ); } if ( userMenu ) { ToggleList.bind( window, userMenu ); } if ( navigationDrawer ) { ToggleList.bind( window, navigationDrawer ); const navigationDrawerMask = navigationDrawer.querySelector( '.main-menu-mask' ); // The 'for' attribute is used to close the drawer when the mask is clicked without JS // Since we are using JS to enhance the drawer behavior, we need to // remove the attribute to prevent the drawer from being toggled twice navigationDrawerMask.removeAttribute( 'for' ); } TabScroll.initTabsScrollPosition(); // Setup the issues banner on the page // Pages which dont exist (id 0) cannot have issues if ( !currentPage.isMissing && !currentPage.titleObj.isTalkPage() ) { issues.init( overlayManager, currentPageHTMLParser ); } // If MobileFrontend installed we add a table of contents icon to the table of contents. // This should probably be done in the parser. // setup toc icons mw.hook( 'wikipage.content' ).add( ( $container ) => { // If the MMV module is missing or disabled from the page, initialise our version if ( desktopMMV === null || desktopMMV === 'registered' ) { initMediaViewer( $container[ 0 ] ); } // Mutate TOC. const $toctitle = $container.find( '.toctitle' ); $( '<span>' ).addClass( 'toc-title-icon' ).prependTo( $toctitle ); $( '<span>' ).addClass( 'toc-title-state-icon' ).appendTo( $toctitle ); // Init red links. const $redLinks = currentPageHTMLParser.getRedLinks(); ctaDrawers.initRedlinksCta( $redLinks.filter( // Filter out local User namespace pages. ( _, element ) => !isUserUri( element.href ) ) ); initUserRedLinks( $redLinks ); } ); // wire up watch icon if necessary if ( permissions.watchable && !permissions.watch ) { ctaDrawers.initWatchstarCta( $watch ); } // If Echo is installed, wire it up. const echoState = mw.loader.getState( 'ext.echo.mobile' ); // If Echo is installed, set it up. if ( echoState !== null && echoState !== 'registered' ) { setupEcho(); } } ); }; PK ! j�t page-issues/index.jsnu �Iw�� /** * @typedef {Object.<string, IssueSummary[]>} IssueSummaryMap * @ignore */ const PageHTMLParser = require( 'mobile.startup' ).PageHTMLParser; const KEYWORD_ALL_SECTIONS = 'all'; const namespaceIds = mw.config.get( 'wgNamespaceIds' ); const NS_MAIN = namespaceIds[ '' ]; const NS_CATEGORY = namespaceIds.category; const CURRENT_NS = mw.config.get( 'wgNamespaceNumber' ); const features = mw.config.get( 'wgMinervaFeatures', {} ); const pageIssuesParser = require( './parser.js' ); const pageIssuesOverlay = require( './overlay/pageIssuesOverlay.js' ); const pageIssueFormatter = require( './page/pageIssueFormatter.js' ); // When the query string flag is set force on new treatment. // When wgMinervaPageIssuesNewTreatment is the default this line can be removed. const QUERY_STRING_FLAG = mw.util.getParamValue( 'minerva-issues' ); const newTreatmentEnabled = features.pageIssues || QUERY_STRING_FLAG; /** * Render a banner in a containing element. * if in group B, a learn more link will be append to any amboxes inside $container * if in group A or control, any amboxes in container will be removed and a link "page issues" * will be rendered above the heading. * This function comes with side effects. It will populate a global "allIssues" object which * will link section numbers to issues. * * @param {PageHTMLParser} pageHTMLParser parser to search for page issues * @param {string} labelText what the label of the page issues banner should say * @param {string} section that the banner and its issues belong to. * If string KEYWORD_ALL_SECTIONS banner should apply to entire page. * @param {boolean} inline - if true the first ambox in the section will become the entry point * for the issues overlay * and if false, a link will be rendered under the heading. * @param {OverlayManager} overlayManager * @ignore * * @return {{ambox: jQuery, issueSummaries: IssueSummary[]}} */ function insertBannersOrNotice( pageHTMLParser, labelText, section, inline, overlayManager ) { const issueUrl = section === KEYWORD_ALL_SECTIONS ? '#/issues/' + KEYWORD_ALL_SECTIONS : '#/issues/' + section; const selector = [ '.ambox', '.tmbox', '.cmbox', '.fmbox' ].join( ',' ); const issueSummaries = []; const $metadata = section === KEYWORD_ALL_SECTIONS ? pageHTMLParser.$el.find( selector ) : // find heading associated with the section pageHTMLParser.findChildInSectionLead( parseInt( section, 10 ), selector ); // clean it up a little $metadata.find( '.NavFrame' ).remove(); $metadata.each( ( _i, el ) => { const $el = $( el ); if ( $el.find( selector ).length === 0 ) { const issueSummary = pageIssuesParser.extract( $el ); // Some issues after "extract" has been run will have no text. // For example in Template:Talk header the table will be removed and no issue found. // These should not be rendered. if ( issueSummary.text ) { issueSummaries.push( issueSummary ); } } } ); if ( inline ) { issueSummaries.forEach( ( issueSummary, i ) => { const isGrouped = issueSummary.issue.grouped; const lastIssueIsGrouped = issueSummaries[ i - 1 ] && issueSummaries[ i - 1 ].issue.grouped; const multiple = isGrouped && !lastIssueIsGrouped; // only render the first grouped issue of each group pageIssueFormatter.insertPageIssueBanner( issueSummary, mw.msg( 'skin-minerva-issue-learn-more' ), issueUrl, overlayManager, multiple ); } ); } else if ( issueSummaries.length ) { pageIssueFormatter.insertPageIssueNotice( labelText, section ); } return { ambox: $metadata, issueSummaries: issueSummaries }; } /** * Obtains the list of issues for the current page and provided section * * @ignore * @param {IssueSummaryMap} allIssues Mapping section {number} * to {IssueSummary} * @param {number|string} section either KEYWORD_ALL_SECTIONS or a number relating to the * section the issues belong to * @return {jQuery[]} array of all issues. */ function getIssues( allIssues, section ) { if ( section !== KEYWORD_ALL_SECTIONS ) { return allIssues[ section ] || []; } // Note section.all may not exist, depending on the structure of the HTML page. // It will only exist when Minerva has been run in desktop mode. // If it's absent, we'll reduce all the other lists into one. return allIssues[ KEYWORD_ALL_SECTIONS ] || Object.keys( allIssues ).reduce( ( all, key ) => all.concat( allIssues[ key ] ), [] ); } /** * Scan an element for any known cleanup templates and replace them with a button * that opens them in a mobile friendly overlay. * * @ignore * @param {OverlayManager} overlayManager * @param {PageHTMLParser} pageHTMLParser */ function initPageIssues( overlayManager, pageHTMLParser ) { let section; let issueSummaries = []; const allIssues = {}; const $lead = pageHTMLParser.getLeadSectionElement(); const issueOverlayShowAll = CURRENT_NS === NS_CATEGORY || !$lead; const inline = newTreatmentEnabled && CURRENT_NS === NS_MAIN; // set A-B test class. // When wgMinervaPageIssuesNewTreatment is the default this can be removed. if ( newTreatmentEnabled ) { $( document.documentElement ).addClass( 'issues-group-B' ); } if ( CURRENT_NS === NS_CATEGORY ) { section = KEYWORD_ALL_SECTIONS; // e.g. Template:English variant category; Template:WikiProject issueSummaries = insertBannersOrNotice( pageHTMLParser, mw.msg( 'mobile-frontend-meta-data-issues-header' ), section, inline, overlayManager ).issueSummaries; allIssues[ section ] = issueSummaries; } else if ( CURRENT_NS === NS_MAIN ) { const label = mw.msg( 'mobile-frontend-meta-data-issues-header' ); if ( issueOverlayShowAll ) { section = KEYWORD_ALL_SECTIONS; issueSummaries = insertBannersOrNotice( pageHTMLParser, label, section, inline, overlayManager ).issueSummaries; allIssues[ section ] = issueSummaries; } else { // parse lead section = '0'; issueSummaries = insertBannersOrNotice( pageHTMLParser, label, section, inline, overlayManager ).issueSummaries; allIssues[ section ] = issueSummaries; if ( newTreatmentEnabled ) { // parse other sections but only in group B. In treatment A no issues are shown // for sections. pageHTMLParser.$el.find( PageHTMLParser.HEADING_SELECTOR ).each( ( i, headingEl ) => { const $headingEl = $( headingEl ); // section number is absent on protected pages, when this is the case // use i, otherwise icon will not show (T340910) const sectionNum = $headingEl.find( '.edit-page' ).data( 'section' ) || i; // Note certain headings matched using // PageHTMLParser.HEADING_SELECTOR may not be headings and will // not have a edit link. E.g. table of contents. if ( sectionNum ) { // Render banner for sectionNum associated with headingEl inside // Page section = sectionNum.toString(); issueSummaries = insertBannersOrNotice( pageHTMLParser, label, section, inline, overlayManager ).issueSummaries; allIssues[ section ] = issueSummaries; } } ); } } } // Setup the overlay route. overlayManager.add( new RegExp( '^/issues/(\\d+|' + KEYWORD_ALL_SECTIONS + ')$' ), ( s ) => pageIssuesOverlay( getIssues( allIssues, s ), s, CURRENT_NS ) ); } module.exports = { init: initPageIssues, test: { insertBannersOrNotice: insertBannersOrNotice } }; PK ! � #� � ( page-issues/overlay/IssueNotice.mustachenu �Iw�� <div class="issue-notice" data-severity="{{#issue}}{{severity}}{{/issue}}"> <div class="issue-details">{{{text}}}</div> </div> PK ! c g g "