manhattan-manage
Version:
Support for the manhattan web framework's manage UI
391 lines (331 loc) • 10.3 kB
JavaScript
import * as $ from 'manhattan-essentials'
// -- Classes --
/**
* A class to manage a set of navigation items.
*/
class NavMgr {
constructor() {
// A map of navigation items added to the navigation manager
this._navItems = {}
// A map of defined nav groups
this._navGroups = {}
}
/**
* Add a navigation item to the navigation manager.
*/
add(navItem) {
this._navItems[navItem.name] = navItem
}
/**
* Add a navigation group to the navigation manager (associated with the
* manager).
*/
addGroup(groupName, names) {
this._navGroups[groupName] = names
}
/**
* Close the named list of navigation items.
*/
close(names) {
const flatNames = this._expandNames(names)
for (let name of flatNames) {
this._navItems[name].open = false
}
}
/**
* Close all navigation items (associated with the manager), optionally
* exclude any of the named navigation items in the `exclude` list.
*/
closeAll(exclude=[]) {
const flatExclude = this._expandNames(exclude)
for (let name in this._navItems) {
if (exclude.indexOf(name) === -1) {
this._navItems[name].open = false
}
}
}
// -- Private methods --
/**
* Flattern a list of nav and/or group names into a list of nav names
* only.
*/
_expandNames(names) {
const expandedNames = []
for (let name of names) {
if (this._navItems[name]) {
expandedNames.push(name)
}
if (this._navGroups[name]) {
expandedNames.push(...this._navGroups[name])
}
}
return expandedNames
}
}
/**
* A UI component for navigation.
*/
class NavItem {
constructor(name, navElm, handleElm, targetElm, css) {
// The name of the navigation item
this._name = name
// The navigation element
this._navElm = navElm
// The element that triggers the navigation item to open/close
this._handleElm = handleElm
// The element that the open/close css class will be applied to
this._targetElm = targetElm
// The CSS class that will be applied to the target element when the
// navigation item is open.
this._css = css
// Set up toggle event for the navigation item
$.listen(
handleElm,
{
'click': (event) => {
if (event.buttons === 0) {
event.stopPropagation()
this.open = !this.open
}
}
}
)
}
get name() {
return this._name
}
get open() {
return this._targetElm.classList.contains(this._css)
}
set open(value) {
if (value) {
this._targetElm.classList.add(this._css)
$.dispatch(this._navElm, 'opened')
} else {
this._targetElm.classList.remove(this._css)
$.dispatch(this._navElm, 'closed')
}
}
}
// -- Utils --
/**
* Update the height of the primary navigation so that it appears to travel
* with the page as it scrolls and sticks to the top of the page.
*/
function updatePrimaryNavHeight() {
const {top} = $.one('.mh-frame__inner').getBoundingClientRect()
if (top > 0) {
$.one('.mh-nav').style.maxHeight = `calc(100% - ${top}px)`
} else {
$.one('.mh-nav').style.maxHeight = '100%'
}
}
// -- Initializer
export function init() {
const navMgr = new NavMgr()
// Primary navigation
const primaryNavElm = $.one('.mh-nav__section--prime')
if (primaryNavElm) {
// Set up the primary navigation item
const primaryNav = new NavItem(
'primary',
primaryNavElm,
$.one('.mh-handle', primaryNavElm),
$.one('.mh-frame'),
'mh-frame--prime-nav-open'
)
navMgr.add(primaryNav)
$.listen(
primaryNavElm,
{
'opened': (event) => {
if (primaryNavElm === event.target) {
navMgr.closeAll(['primary'])
}
}
}
)
// Listen for resize and scroll events and update the height of
// primary navigation to match.
let primaryNavTimeout = null
$.listen(
window,
{
'resize scroll': () => {
clearTimeout(primaryNavTimeout)
primaryNavTimeout = setTimeout(updatePrimaryNavHeight, 50)
}
}
)
updatePrimaryNavHeight()
}
// Sub-navigation
for (let [i, subNavElm]
of $.many('.mh-nav-item--has-children').entries()) {
// Set up the sub-navigation item
let subNav = new NavItem(
`subNav_${i}`,
subNavElm,
subNavElm,
subNavElm,
'mh-nav-item--open'
)
navMgr.add(subNav)
$.listen(
subNavElm,
{
'opened': (event) => {
if (subNavElm === event.target) {
navMgr.closeAll(['primary', `subNav_${i}`])
}
}
}
)
}
// User navigation
const userNavElm = $.one('.mh-nav__section--user')
if (userNavElm) {
// Set up the user navigation item
const userNav = new NavItem(
'user',
userNavElm,
$.one('.mh-handle', userNavElm),
$.one('.mh-frame'),
'mh-frame--user-nav-open'
)
navMgr.add(userNav)
$.listen(
userNavElm,
{
'opened': (event) => {
if (userNavElm === event.target) {
navMgr.closeAll(['user'])
}
}
}
)
}
// Actions
const actionsElm = $.one('.mh-actions')
if (actionsElm) {
// Set up the action navigation item
const actionsNav = new NavItem(
'actions',
actionsElm,
$.one('.mh-actions__handle', actionsElm),
actionsElm,
'mh-actions--open'
)
navMgr.add(actionsNav)
$.listen(
actionsElm,
{
'closed': (event) => {
const actionElms = $
.many('.mh-actions__link--has-sub-actions')
for (const actionElm of actionElms) {
actionElm
.classList
.remove('mh-actions__link--sub-actions-open')
}
},
'opened': (event) => {
if (actionsElm === event.target) {
navMgr.closeAll(['actions'])
}
}
}
)
for (const actionElm of $.many('.mh-actions__link--has-sub-actions')) {
// Set up sub-action menus
$.listen(
actionElm,
{
'click': (event) => {
event.stopPropagation()
event
.currentTarget
.classList
.toggle('mh-actions__link--sub-actions-open')
if (event
.currentTarget
.classList
.contains('mh-actions__link--sub-actions-open')) {
// Close any other sub nav that is open
const actionElms = $
.many('.mh-actions__link--has-sub-actions')
for (const element of actionElms) {
if (element !== event.currentTarget) {
element
.classList
.remove('mh-actions__link'
+ '--sub-actions-open')
}
}
navMgr.close(['filter'])
}
}
}
)
}
}
// Filter
const filterElm = $.one('.mh-filter-adv')
if (filterElm) {
// Set up the filter navigation item
const filterNav = new NavItem(
'filter',
filterElm,
$.one('.mh-filter-adv__handle', filterElm),
filterElm,
'mh-filter-adv--open'
)
navMgr.add(filterNav)
$.listen(
filterElm,
{
'opened': (event) => {
if (filterElm === event.target) {
navMgr.close(['actions'])
}
}
}
)
$.listen(
$.one('.mh-filter-adv__fields', filterElm),
{
'click': (ev) => {
ev.stopPropagation()
}
}
)
$.listen(
$.one('.mh-filter-adv__close', filterElm),
{
'click': (ev) => {
navMgr.close(['filter'])
}
}
)
}
// Close actions and filter if the user clicks the body
$.listen(
document.body,
{
'click': (event) => {
if ('navSupressClick' in document.body.dataset) {
delete document.body.dataset.navSupressClick
return
}
if (!event.defaultPrevented) {
navMgr.close(['actions', 'filter'])
}
}
}
)
if (window.innerWidth < 1080) {
// On narrow devices we don't want the user nav to be open when the
// page loads so we ensure it's closed it initially.
navMgr.close(['user'])
}
}