Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5956182
pinnableElement.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
10 KB
Referenced Files
None
Subscribers
None
pinnableElement.js
View Options
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
};
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 3, 04:15 (3 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
aa/2b/048759475606357f90c659b70df8
Default Alt Text
pinnableElement.js (10 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment