diff --git a/bundlesize.config.json b/bundlesize.config.json index edac214d..b4b5b6a3 100644 --- a/bundlesize.config.json +++ b/bundlesize.config.json @@ -1,7 +1,7 @@ [ { "resourceModule": "skins.vector.styles.legacy", - "maxSize": "7.8 kB" + "maxSize": "7.9 kB" }, { "resourceModule": "skins.vector.styles", diff --git a/includes/SkinVector.php b/includes/SkinVector.php index a9ddb613..02710893 100644 --- a/includes/SkinVector.php +++ b/includes/SkinVector.php @@ -68,6 +68,30 @@ class SkinVector extends SkinMustache { 'tabindex' => '-1', 'class' => 'sticky-header-icon' ]; + private const EDIT_VE_ICON = [ + 'href' => '#', + 'id' => 'ca-ve-edit-sticky-header', + 'event' => 've-edit-sticky-header', + 'icon' => 'wikimedia-edit', + 'is-quiet' => true, + 'class' => 'sticky-header-icon' + ]; + private const EDIT_WIKITEXT_ICON = [ + 'href' => '#', + 'id' => 'ca-edit-sticky-header', + 'event' => 'wikitext-edit-sticky-header', + 'icon' => 'wikimedia-wikiText', + 'is-quiet' => true, + 'class' => 'sticky-header-icon' + ]; + private const EDIT_PROTECTED_ICON = [ + 'href' => '#', + 'id' => 'ca-viewsource-sticky-header', + 'event' => 've-edit-protected-sticky-header', + 'icon' => 'wikimedia-editLock', + 'is-quiet' => true, + 'class' => 'sticky-header-icon' + ]; private const SEARCH_EXPANDING_CLASS = 'vector-search-box-show-thumbnail'; private const STICKY_HEADER_ENABLED_CLASS = 'vector-sticky-header-enabled'; @@ -353,7 +377,7 @@ class SkinVector extends SkinMustache { * @param array $searchBoxData * @return array */ - private function getStickyHeaderData( $searchBoxData ) { + private function getStickyHeaderData( $searchBoxData ): array { return [ 'data-primary-action' => !$this->shouldHideLanguages() ? $this->getULSButtonData() : null, 'data-button-start' => [ @@ -365,7 +389,11 @@ class SkinVector extends SkinMustache { ], 'data-search' => $searchBoxData, 'data-buttons' => [ - self::TALK_ICON, self::HISTORY_ICON, self::NO_ICON, self::NO_ICON + self::TALK_ICON, + self::HISTORY_ICON, + self::EDIT_VE_ICON, + self::EDIT_PROTECTED_ICON, + self::EDIT_WIKITEXT_ICON ] ]; } diff --git a/jsdoc.json b/jsdoc.json index 9c3e5bfa..ae9061dc 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -22,6 +22,7 @@ "Element": "https://developer.mozilla.org/docs/Web/API/Element", "Event": "https://developer.mozilla.org/docs/Web/API/Event", "HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement", + "Node": "https://developer.mozilla.org/docs/Web/API/Node", "NodeList": "https://developer.mozilla.org/docs/Web/API/NodeList", "HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement", "\"removeEventListener\"": "https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener", diff --git a/resources/skins.vector.js/stickyHeader.js b/resources/skins.vector.js/stickyHeader.js index c179e21a..f3656963 100644 --- a/resources/skins.vector.js/stickyHeader.js +++ b/resources/skins.vector.js/stickyHeader.js @@ -7,8 +7,7 @@ var FIRST_HEADING_ID = 'firstHeading', USER_MENU_ID = 'p-personal', VECTOR_USER_LINKS_SELECTOR = '.vector-user-links', - SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle', - OTHER_STICKY_ELEMENT_SELECTORS = '.charts-stickyhead th'; + SEARCH_TOGGLE_SELECTOR = '.vector-sticky-header-search-toggle'; /** * Copies attribute from an element to another. @@ -48,6 +47,23 @@ function makeNodeTrackable( node ) { suffixStickyAttribute( node, 'data-event-name' ); } +/** + * + * @param {null|HTMLElement|Node} node + * @return {HTMLElement} + */ +function toHTMLElement( node ) { + // @ts-ignore + return node; +} + +/** + * @param {HTMLElement} node + */ +function removeNode( node ) { + toHTMLElement( node.parentNode ).removeChild( node ); +} + /** * Makes sticky header icons functional for modern Vector. * @@ -77,6 +93,76 @@ function prepareIcons( header, history, talk ) { } } +/** + * Render sticky header edit or protected page icons for modern Vector. + * + * @param {HTMLElement} header + * @param {HTMLElement|null} primaryEdit + * @param {boolean} isProtected + * @param {HTMLElement|null} secondaryEdit + */ +function prepareEditIcons( + header, + primaryEdit, + isProtected, + secondaryEdit +) { + var + primaryEditSticky = toHTMLElement( + header.querySelector( + '#ca-ve-edit-sticky-header' + ) + ), + protectedSticky = toHTMLElement( + header.querySelector( + '#ca-viewsource-sticky-header' + ) + ), + wikitextSticky = toHTMLElement( + header.querySelector( + '#ca-edit-sticky-header' + ) + ); + + if ( !primaryEdit ) { + removeNode( protectedSticky ); + removeNode( wikitextSticky ); + removeNode( primaryEditSticky ); + return; + } else if ( isProtected ) { + removeNode( wikitextSticky ); + removeNode( primaryEditSticky ); + copyAttribute( primaryEdit, protectedSticky, 'href' ); + copyAttribute( primaryEdit, protectedSticky, 'title' ); + } else { + removeNode( protectedSticky ); + copyAttribute( primaryEdit, primaryEditSticky, 'href' ); + copyAttribute( primaryEdit, primaryEditSticky, 'title' ); + if ( secondaryEdit ) { + copyAttribute( secondaryEdit, wikitextSticky, 'href' ); + copyAttribute( secondaryEdit, wikitextSticky, 'title' ); + } else { + removeNode( wikitextSticky ); + } + } +} + +/** + * Check if element is in viewport. + * + * @param {HTMLElement} element + * @return {boolean} + */ +function isInViewport( element ) { + var rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= ( window.innerHeight || document.documentElement.clientHeight ) && + rect.right <= ( window.innerWidth || document.documentElement.clientWidth ) + ); +} + /** * Makes sticky header functional for modern Vector. * @@ -136,13 +222,42 @@ function makeStickyHeaderFunctional( document.querySelector( '#ca-talk a' ) ); - // Apply offset for other sticky elements on page if applicable. - var otherStickyElements = document.querySelectorAll( OTHER_STICKY_ELEMENT_SELECTORS ); - Array.prototype.forEach.call( otherStickyElements, function ( el ) { - el.classList.add( 'mw-sticky-header-element' ); - } ); + var veEdit = document.querySelector( '#ca-ve-edit a' ); + var ceEdit = document.querySelector( '#ca-edit a' ); + var protectedEdit = document.querySelector( '#ca-viewsource a' ); + var isProtected = !!protectedEdit; + var primaryEdit = protectedEdit || ( veEdit || ceEdit ); + var secondaryEdit = veEdit ? ceEdit : null; + + prepareEditIcons( + header, + toHTMLElement( primaryEdit ), + isProtected, + toHTMLElement( secondaryEdit ) + ); stickyObserver.observe( stickyIntersection ); + + // When Visual Editor is activated, hide sticky header. + mw.hook( 've.activationComplete' ).add( function () { + // eslint-disable-next-line mediawiki/class-doc + header.classList.remove( STICKY_HEADER_VISIBLE_CLASS ); + stickyObserver.unobserve( stickyIntersection ); + } ); + + // When Visual Editor is deactivated, by cliking "read" tab at top of page, show sticky header. + mw.hook( 've.deactivationComplete' ).add( function () { + stickyObserver.observe( stickyIntersection ); + } ); + + // After saving edits, re-apply the sticky header if the target is not in the viewport. + mw.hook( 'postEdit.afterRemoval' ).add( function () { + if ( !isInViewport( stickyIntersection ) ) { + // eslint-disable-next-line mediawiki/class-doc + header.classList.add( STICKY_HEADER_VISIBLE_CLASS ); + stickyObserver.observe( stickyIntersection ); + } + } ); } /** diff --git a/resources/skins.vector.styles/components/StickyHeader.less b/resources/skins.vector.styles/components/StickyHeader.less index 61f4c761..f2daefa8 100644 --- a/resources/skins.vector.styles/components/StickyHeader.less +++ b/resources/skins.vector.styles/components/StickyHeader.less @@ -56,31 +56,26 @@ // // Layout // - &-start { + &-start, + &-end, + &-icons, + &-context-bar { display: flex; align-items: center; - flex-grow: 1; } - &-end { - display: flex; - align-items: center; + &-start { + flex-grow: 1; } // // Components // - &-icons, - &-context-bar { - display: flex; - align-items: center; - white-space: nowrap; - margin: 0 15px; - padding-left: 30px; - } - &-context-bar { border-left: 1px solid #c8c8c8; + margin: 0 15px; + padding-left: 30px; + white-space: nowrap; } &-context-bar-primary { @@ -147,7 +142,8 @@ // T289817 Override other sticky element offsets to ensure that other // sticky elements (i.e. table headers) appear below the sticky header. - .mw-sticky-header-element { + .mw-sticky-header-element, + .charts-stickyhead th { /* stylelint-disable-next-line declaration-no-important */ top: @height-sticky-header !important; } diff --git a/skin.json b/skin.json index 32c676d3..c74a4031 100644 --- a/skin.json +++ b/skin.json @@ -176,7 +176,10 @@ "variants": [], "icons": [ "history", - "speechBubbles" + "speechBubbles", + "edit", + "editLock", + "wikiText" ] }, "skins.vector.icons": {