mirror of
https://github.com/StarCitizenTools/mediawiki-skins-Citizen.git
synced 2024-11-24 06:24:22 +00:00
feat(overflow): ✨ add overflow scroll button when using a pointer device
This commit is contained in:
parent
abc8176638
commit
55d413eeda
|
@ -6,11 +6,12 @@ const config = require( './config.json' );
|
|||
* @class
|
||||
*/
|
||||
class OverflowElement {
|
||||
constructor( element ) {
|
||||
constructor( element, isPointerDevice ) {
|
||||
this.element = element;
|
||||
this.isPointerDevice = isPointerDevice;
|
||||
this.elementWidth = 0;
|
||||
this.wrapperScrollLeft = 0;
|
||||
this.wrapperWidth = 0;
|
||||
this.contentScrollLeft = 0;
|
||||
this.contentWidth = 0;
|
||||
this.onScroll = mw.util.throttle( this.onScroll.bind( this ), 250 );
|
||||
this.updateState = this.updateState.bind( this );
|
||||
}
|
||||
|
@ -36,48 +37,48 @@ class OverflowElement {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if the state of the overflow element has changed by comparing the current element width, wrapper scroll left,
|
||||
* and wrapper width with the cached values. Returns true if any of the values have changed, otherwise returns false.
|
||||
* Checks if the state of the overflow element has changed by comparing the current element width, content scroll left,
|
||||
* and content width with the cached values. Returns true if any of the values have changed, otherwise returns false.
|
||||
*
|
||||
* @return {boolean} - True if the state has changed, false otherwise.
|
||||
*/
|
||||
hasStateChanged() {
|
||||
return (
|
||||
this.element.scrollWidth !== this.elementWidth ||
|
||||
Math.round( this.wrapper.scrollLeft ) !== this.wrapperScrollLeft ||
|
||||
this.wrapper.offsetWidth !== this.wrapperWidth
|
||||
Math.round( this.content.scrollLeft ) !== this.contentScrollLeft ||
|
||||
this.content.offsetWidth !== this.contentWidth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the element has overflowed horizontally by comparing the element width with the wrapper width.
|
||||
* Checks if the element has overflowed horizontally by comparing the element width with the content width.
|
||||
*
|
||||
* @return {boolean} - True if the element has overflowed, false otherwise.
|
||||
*/
|
||||
hasOverflowed() {
|
||||
return this.elementWidth > this.wrapperWidth;
|
||||
return this.elementWidth > this.contentWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of the overflow element by calculating the element width, wrapper scroll left, and wrapper width.
|
||||
* Updates the state of the overflow element by calculating the element width, content scroll left, and content width.
|
||||
* If the width values are invalid, logs an error and returns.
|
||||
* Compares the current state with the previous state and updates the cache if there is a change.
|
||||
* Toggles classes on the wrapper element based on the overflow state (left or right).
|
||||
* Toggles classes on the content element based on the overflow state (left or right).
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
updateState() {
|
||||
const elementWidth = this.element.scrollWidth;
|
||||
const wrapperScrollLeft = Math.round( this.wrapper.scrollLeft );
|
||||
const wrapperWidth = this.wrapper.offsetWidth;
|
||||
const contentScrollLeft = Math.round( this.content.scrollLeft );
|
||||
const contentWidth = this.content.offsetWidth;
|
||||
|
||||
if ( !this.hasStateChanged() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elementWidth = elementWidth;
|
||||
this.wrapperScrollLeft = wrapperScrollLeft;
|
||||
this.wrapperWidth = wrapperWidth;
|
||||
this.contentScrollLeft = contentScrollLeft;
|
||||
this.contentWidth = contentWidth;
|
||||
|
||||
let isLeftOverflowing, isRightOverflowing;
|
||||
|
||||
|
@ -85,8 +86,9 @@ class OverflowElement {
|
|||
isLeftOverflowing = false;
|
||||
isRightOverflowing = false;
|
||||
} else {
|
||||
isLeftOverflowing = this.wrapperScrollLeft > 0;
|
||||
isRightOverflowing = this.wrapperScrollLeft + this.wrapperWidth < this.elementWidth;
|
||||
isLeftOverflowing = this.contentScrollLeft > 0;
|
||||
isRightOverflowing =
|
||||
this.contentScrollLeft + this.contentWidth < this.elementWidth;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame( () => {
|
||||
|
@ -105,7 +107,8 @@ class OverflowElement {
|
|||
*/
|
||||
handleInheritedClasses() {
|
||||
const inheritedClasses = config.wgCitizenOverflowInheritedClasses;
|
||||
const filteredClasses = inheritedClasses.filter( ( cls ) => this.element.classList.contains( cls ) );
|
||||
const filteredClasses = inheritedClasses.filter( ( cls ) => this.element.classList.contains( cls )
|
||||
);
|
||||
|
||||
filteredClasses.forEach( ( cls ) => {
|
||||
if ( !this.wrapper.classList.contains( cls ) ) {
|
||||
|
@ -130,21 +133,88 @@ class OverflowElement {
|
|||
*/
|
||||
wrap() {
|
||||
if ( !this.element || !this.element.parentNode ) {
|
||||
mw.log.error( '[Citizen] Element or element.parentNode is null or undefined. Please check if the element or element.parentNode is null or undefined.' );
|
||||
mw.log.error(
|
||||
'[Citizen] Element or element.parentNode is null or undefined. Please check if the element or element.parentNode is null or undefined.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parentNode = this.element.parentNode;
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const wrapper = document.createElement( 'div' );
|
||||
wrapper.className = 'citizen-overflow-wrapper';
|
||||
|
||||
this.handleInheritedClasses();
|
||||
fragment.appendChild( wrapper );
|
||||
|
||||
const content = document.createElement( 'div' );
|
||||
content.className = 'citizen-overflow-content';
|
||||
wrapper.appendChild( content );
|
||||
|
||||
const parentNode = this.element.parentNode;
|
||||
parentNode.insertBefore( fragment, this.element );
|
||||
content.appendChild( this.element );
|
||||
|
||||
parentNode.insertBefore( wrapper, this.element );
|
||||
wrapper.appendChild( this.element );
|
||||
this.wrapper = wrapper;
|
||||
this.content = content;
|
||||
|
||||
if ( this.isPointerDevice ) {
|
||||
const nav = document.createElement( 'div' );
|
||||
nav.className = 'citizen-overflow-nav';
|
||||
|
||||
const leftButton = document.createElement( 'button' );
|
||||
leftButton.className =
|
||||
'citizen-overflow-navButton citizen-overflow-navButton-left citizen-ui-icon mw-ui-icon-wikimedia-collapse';
|
||||
nav.appendChild( leftButton );
|
||||
|
||||
const rightButton = document.createElement( 'button' );
|
||||
rightButton.className =
|
||||
'citizen-overflow-navButton citizen-overflow-navButton-right citizen-ui-icon mw-ui-icon-wikimedia-collapse';
|
||||
nav.appendChild( rightButton );
|
||||
|
||||
wrapper.appendChild( nav );
|
||||
this.nav = nav;
|
||||
}
|
||||
} catch ( error ) {
|
||||
mw.log.error( `[Citizen] Error occurred while wrapping element: ${ error.message }` );
|
||||
mw.log.error(
|
||||
`[Citizen] Error occurred while wrapping element: ${ error.message }`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the content element by the specified offset.
|
||||
*
|
||||
* @param {number} offset - The amount by which to scroll the content element.
|
||||
* @return {void}
|
||||
*/
|
||||
scrollContent( offset ) {
|
||||
const delta = this.content.scrollWidth - this.content.offsetWidth;
|
||||
const scrollLeft = Math.ceil( this.content.scrollLeft ) + offset;
|
||||
|
||||
window.requestAnimationFrame( () => {
|
||||
this.content.scrollLeft = Math.min( Math.max( scrollLeft, 0 ), delta );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on the navigation buttons.
|
||||
* Scrolls the content element left or right based on the button clicked.
|
||||
*
|
||||
* @param {Event} event - The click event object.
|
||||
* @return {void}
|
||||
*/
|
||||
onClick( event ) {
|
||||
const target = event.target;
|
||||
if ( !target.classList.contains( 'citizen-overflow-navButton' ) ) {
|
||||
return;
|
||||
}
|
||||
const offset = this.wrapper.offsetWidth / 2;
|
||||
if ( target.classList.contains( 'citizen-overflow-navButton-left' ) ) {
|
||||
this.scrollContent( -offset );
|
||||
} else if (
|
||||
target.classList.contains( 'citizen-overflow-navButton-right' )
|
||||
) {
|
||||
this.scrollContent( offset );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,27 +230,33 @@ class OverflowElement {
|
|||
/**
|
||||
* Resumes the functionality of the overflow element by updating its state, adding a scroll event listener, and observing element resize.
|
||||
* Calls the 'updateState' method to update the state of the overflow element.
|
||||
* Adds a scroll event listener to the wrapper element to handle scroll events by calling the 'onScroll' method.
|
||||
* Adds a scroll event listener to the content element to handle scroll events by calling the 'onScroll' method.
|
||||
* Observes the element for resize changes using the 'resizeObserver'.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
resume() {
|
||||
this.updateState();
|
||||
this.wrapper.addEventListener( 'scroll', this.onScroll );
|
||||
this.content.addEventListener( 'scroll', this.onScroll );
|
||||
this.resizeObserver.observe( this.element );
|
||||
if ( this.isPointerDevice ) {
|
||||
this.nav.addEventListener( 'click', this.onClick.bind( this ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the functionality of the overflow element by removing the scroll event listener and stopping observation of element resize.
|
||||
* Removes the scroll event listener from the wrapper element that triggers the 'onScroll' method.
|
||||
* Removes the scroll event listener from the content element that triggers the 'onScroll' method.
|
||||
* Stops observing resize changes of the element using the 'resizeObserver'.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
pause() {
|
||||
this.wrapper.removeEventListener( 'scroll', this.onScroll );
|
||||
this.content.removeEventListener( 'scroll', this.onScroll );
|
||||
this.resizeObserver.unobserve( this.element );
|
||||
if ( this.isPointerDevice ) {
|
||||
this.nav.removeEventListener( 'click', this.onClick );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -238,21 +314,27 @@ class OverflowElement {
|
|||
function init( bodyContent ) {
|
||||
const nowrapClasses = config.wgCitizenOverflowNowrapClasses;
|
||||
if ( !nowrapClasses || !Array.isArray( nowrapClasses ) ) {
|
||||
mw.log.error( '[Citizen] Invalid or missing $wgCitizenOverflowNowrapClasses. Cannot proceed with wrapping element.' );
|
||||
mw.log.error(
|
||||
'[Citizen] Invalid or missing $wgCitizenOverflowNowrapClasses. Cannot proceed with wrapping element.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const overflowElements = bodyContent.querySelectorAll( '.citizen-overflow, .wikitable:not( .wikitable .wikitable )' );
|
||||
const overflowElements = bodyContent.querySelectorAll(
|
||||
'.citizen-overflow, .wikitable:not( .wikitable .wikitable )'
|
||||
);
|
||||
if ( !overflowElements.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPointerDevice = window.matchMedia( '(hover: hover) and (pointer: fine)' ).matches;
|
||||
|
||||
overflowElements.forEach( ( el ) => {
|
||||
if ( nowrapClasses.some( ( cls ) => el.classList.contains( cls ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overflowElement = new OverflowElement( el );
|
||||
const overflowElement = new OverflowElement( el, isPointerDevice );
|
||||
overflowElement.init();
|
||||
} );
|
||||
}
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
@overflow-affordnance-size: 2rem;
|
||||
|
||||
.mask-gradient(@direction, @color1, @color2, @color3: null , @color4: null) {
|
||||
.mask-gradient(@direction, @color1, @color2, @color3: null, @color4: null) {
|
||||
-webkit-mask-image: linear-gradient( @direction, @color1, @color2 );
|
||||
mask-image: linear-gradient( @direction, @color1, @color2 );
|
||||
|
||||
& when not(@color3 = null), not(@color4 = null) {
|
||||
& when not(@color3 =null),
|
||||
not(@color4 =null) {
|
||||
-webkit-mask-image: linear-gradient( @direction, @color1, @color2, @color3, @color4 );
|
||||
mask-image: linear-gradient( @direction, @color1, @color2, @color3, @color4 );
|
||||
}
|
||||
}
|
||||
|
||||
.hideOverflowButton() {
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.showOverflowButton() {
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
// Elements enhanced by overflowElements.js
|
||||
.citizen-overflow {
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-content {
|
||||
overflow-x: auto;
|
||||
|
||||
.wikitable {
|
||||
|
@ -20,17 +36,69 @@
|
|||
max-width: none;
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
.citizen-overflow--left > & {
|
||||
.mask-gradient(90deg, transparent, #000 @overflow-affordnance-size);
|
||||
}
|
||||
|
||||
.citizen-overflow--right > & {
|
||||
.mask-gradient(270deg, transparent, #000 @overflow-affordnance-size);
|
||||
}
|
||||
|
||||
.citizen-overflow--left.citizen-overflow--right > & {
|
||||
.mask-gradient(90deg, transparent, #000 @overflow-affordnance-size, #000 ~'calc( 100% - @{overflow-affordnance-size} )', transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&--left {
|
||||
.mask-gradient(90deg, transparent, #000 @overflow-affordnance-size);
|
||||
&-nav {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var( --space-md );
|
||||
margin-bottom: var( --space-md );
|
||||
}
|
||||
|
||||
&--right {
|
||||
.mask-gradient(270deg, transparent, #000 @overflow-affordnance-size);
|
||||
}
|
||||
&-navButton {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var( --border-radius--small );
|
||||
.hideOverflowButton();
|
||||
|
||||
&--left&--right {
|
||||
.mask-gradient(90deg, transparent, #000 @overflow-affordnance-size, #000 ~'calc( 100% - @{overflow-affordnance-size} )', transparent);
|
||||
&-left {
|
||||
.citizen-overflow--left & {
|
||||
.showOverflowButton();
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: rotate( -90deg );
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
.citizen-overflow--right & {
|
||||
.showOverflowButton();
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: rotate( 90deg );
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var( --background-color-quiet--hover );
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var( --background-color-quiet--active );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.citizen-animations-ready .citizen-overflow-content {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue