docbrown/app/javascript/topNavigation/utilities.js
Joshua Wehner 30c0485507
Frontend for multiple reactions (v1) (#18808)
* Remove extraneous comment (see 9361d2 and 5c18f8)

* Flexible, multiple reaction types

* Fix reaction counts

* Re-use svg for active state for now

* Update yml, update spec

* Possible temp fix for failing tests

* Colorize reaction icon svgs

* Reaction engagement above post title

* Index reactions engagements (for logged-out)

* Maybe readinglist is special

* Try using crayons' dropdown as a drawer?

* readinglist isn't really public now

* feat: update the styles for the reaction drawer

* Grey background highlight, turn off border/shadow

* Read our feature flag docs, saw this was recommended

* Missed fifth emoji: party/tada

* Fix JS test errors

* Update test with tada

* Suppress flashing engagements when no public reactions

* Liberate jump-to-comments from unicorn replacement

* 'Add reaction' on tooltip

* Don't show reaction emoji on index unless it's been used

* rubocop

* Update heart+ total count when toggling

* Do not include 'readinglist' in drawer/public counts

* Fix semi-public readinglist so that icon is badged for current user

* Tweak heart-plus svg

* Style tweak: border on active reaction

* Show reacted icon on drawer trigger for 1.5 sec

* Tweak styles for engagements bar

* Style tweaks for multiple engagements (#index)

* Trying to get size working through crayons/inline_svg

* Sparkle hearts

* Restore unicorn

* Make heart-plus-active work when user has an active reaction

* Try 'hoverdown' a dropdown that activates with hover

* Long touch?

* Tap *outside* the drawer to close

* Mobile reaction drawer is also supposed to be columns

* More reaction count cleanup

* Final emoticons maybe?

* Fix reaction bug when feature disabled

* Remove readinglist from public reaction counts

* Update specs for new reaction categories

* Shuffle makes specs flaky

* Order does not matter

* rubocop

* Update to preserve readinglist analytics

* Shuffle makes specs flaky

* Fix flickering images, remove icon highlight for now

* Don't update total for readinglist

* reactions_by_user_id

* Try renaming this observer function

* Try unid ids for SVGs

* Remove local test file

* Simplistic test for the unique svg transform

* Fix javascript for current SVG

* Signifcant string literals in this case, rubocop

* Use the right expected output

Co-authored-by: Ridhwana <ridhwana.khan16@gmail.com>
2023-01-23 16:00:50 +01:00

172 lines
5.5 KiB
JavaScript

/* global isTouchDevice */
function closeHeaderMenu(memberMenu, menuNavButton) {
menuNavButton.setAttribute('aria-expanded', 'false');
memberMenu.classList.remove('desktop', 'showing');
delete memberMenu.dataset.clicked;
}
const firstItem = document.getElementById('first-nav-link');
function openHeaderMenu(memberMenu, menuNavButton) {
menuNavButton.setAttribute('aria-expanded', 'true');
memberMenu.classList.add('showing');
if (!firstItem) {
return;
}
// Focus on the first item in the menu
(function focusFirstItem() {
if (document.activeElement === firstItem) {
// The first element has focus
return;
}
firstItem.focus();
// requestAnimationFrame is faster and more reliable than setTimeout
// https://swizec.com/blog/how-to-wait-for-dom-elements-to-show-up-in-modern-browsers
window.requestAnimationFrame(focusFirstItem);
})();
}
/**
* Initializes the member navigation menu events.
*
* @param {HTMLElement} memberTopMenu The member menu in the top right navigation.
* @param {HTMLElement} menuNavButton The button to activate the member navigation menu.
*/
export function initializeMemberMenu(memberTopMenu, menuNavButton) {
// Typically using CSS for hovering for the menu is the way to go. But... since we use InstantClick for
// loading pages, the top header navigation never changes in terms of the DOM references.
// Because of this, we're using mouse events to mouseover/mouseout on the member's avatar
// to attach styles to get it to show the menu so that this works on desktop and mobile.
if (navigator.userAgent === 'DEV-Native-ios') {
document.body.classList.add('dev-ios-native-body');
}
const { classList } = memberTopMenu;
menuNavButton.addEventListener('click', (_event) => {
if (classList.contains('showing') && memberTopMenu.dataset.clicked) {
closeHeaderMenu(memberTopMenu, menuNavButton);
menuNavButton.focus();
} else {
openHeaderMenu(memberTopMenu, menuNavButton);
memberTopMenu.dataset.clicked = 'clicked';
}
});
if (isTouchDevice()) {
memberTopMenu.addEventListener('focus', (_event) => {
menuNavButton.setAttribute('aria-expanded', 'true');
});
} else {
memberTopMenu.addEventListener('keyup', (e) => {
if (e.key === 'Escape' && classList.contains('showing')) {
closeHeaderMenu(memberTopMenu, menuNavButton);
menuNavButton.focus();
}
});
}
memberTopMenu
.querySelector('.crayons-header__menu__dropdown')
.addEventListener('click', (event) => {
// There is a click event listener on the body and we do not want
// this click to be caught by it
event.stopPropagation();
// Close the menu if the user clicked or touched on mobile a link in the menu.
closeHeaderMenu(memberTopMenu, menuNavButton);
menuNavButton.focus();
});
document.addEventListener('click', (event) => {
if (event.target.closest('#member-menu-button') === menuNavButton) {
// The menu navigation button manages it's own click event.
return;
}
// Close the menu if the user clicked or touched on mobile a link in the menu.
closeHeaderMenu(memberTopMenu, menuNavButton);
});
const secondToLastNavLink = document.getElementById('second-last-nav-link');
document
.getElementById('last-nav-link')
.addEventListener('blur', (_event) => {
// When we tab out of the last link in the member menu, close
// the menu.
setTimeout(() => {
if (document.activeElement === secondToLastNavLink) {
return;
}
closeHeaderMenu(memberTopMenu, menuNavButton);
}, 10);
});
}
function toggleBurgerMenu() {
const { leftNavState = 'closed' } = document.body.dataset;
document.body.dataset.leftNavState =
leftNavState === 'open' ? 'closed' : 'open';
}
/**
* Gets a reference to InstantClick
*
* @param {number} [waitTime=2000] The amount of time to wait
* until giving up waiting for InstantClick to exist
*
* @returns {Promise<object>} The global instance of InstantClick.
*/
export async function getInstantClick(waitTime = 2000) {
return new Promise((resolve, reject) => {
const failTimer = setTimeout(() => {
clearInterval(timer);
reject(new Error('Unable to resolve InstantClick'));
}, waitTime);
const timer = setInterval(() => {
if (typeof InstantClick !== 'undefined') {
clearTimeout(failTimer);
clearInterval(timer);
resolve(InstantClick);
}
});
});
}
/**
* Initializes the hamburger menu for mobile navigation
*
* @param {HTMLElement[]} menuTriggers The menuTriggers include the hamburger to open the menu,
* the close icon to close the menu and the overlay to close the menu.
*/
export function initializeMobileMenu(menuTriggers) {
menuTriggers.forEach((trigger) => {
trigger.onclick = toggleBurgerMenu;
});
}
/**
* Sets the icon link visually for the current page if the current page
* is one of the main icon links of the top navigation.
*
* @param {string} currentPage
* @param {[string, HTMLElement][]} pageEntries
*/
export function setCurrentPageIconLink(currentPage, pageEntries) {
pageEntries
// Filter out nulls (means the user is logged out so most icons are not in the logged out view)
.filter(([, iconLink]) => iconLink)
.forEach(([page, iconLink]) => {
if (currentPage === page) {
iconLink.blur();
iconLink.setAttribute('aria-current', 'page');
} else {
iconLink.removeAttribute('aria-current');
}
});
}