From a5b84b9d0f913c3df7ce00ae126fe6b2ebe00496 Mon Sep 17 00:00:00 2001 From: acer Date: Mon, 12 Oct 2020 01:17:56 -0400 Subject: [PATCH] Rewrite menu from scratch to support mobile screen sizes and keyboard controls --- index.html | 6 +- src/css/menu.css | 347 +++++++-------- src/js/config-menu.js | 832 +++++++++++++++++++++++++++--------- src/js/core/base-gui.js | 43 +- src/js/core/gui/gui-menu.js | 374 +++++++++++++++- 5 files changed, 1161 insertions(+), 441 deletions(-) diff --git a/index.html b/index.html index 8ea2757..f37db63 100644 --- a/index.html +++ b/index.html @@ -84,9 +84,11 @@
- +
- + diff --git a/src/css/menu.css b/src/css/menu.css index 0cb9bad..64db173 100644 --- a/src/css/menu.css +++ b/src/css/menu.css @@ -1,222 +1,193 @@ -.mobile_menu{ - display:none; - position: absolute; - width:100%; - top: 0; +:root { + --menu-dropdown-background-color: #ffffff; + --menu-dropdown-border-color: #5680C1; + --menu-dropdown-text-color: #2d2b2b; + --menu-dropdown-text-muted-color: #aaaaaa; + --menu-dropdown-hover-background-color: #E4EBF8; + --menu-dropdown-hover-text-color: #2d2d2d; + --menu-dropdown-divider-color: #e5e5e5; } -.left_mobile_menu, .right_mobile_menu{ - position:absolute; - width:50px; - height:50px; - background: url("images/sprites.png") no-repeat 11px -86px; - filter: invert(1); - display:block; - top:0; - z-index:200; - border:0; - outline:0; - cursor: pointer; -} -.left_mobile_menu{left:0;} -.right_mobile_menu{right:0;} -.ddsmoothmenu{ - position:fixed; - top:0; - left:0; - width:100%; - font:12px Arial,sans-serif; - background: #2D2D2D; - background: var(--background-color-menu); - width: 100%; - padding-left:10px; - z-index:100; -} -.ddsmoothmenu ul{ - z-index:100; - margin: 0; +.sr_only { + position: absolute; + width: 1px; + height: 1px; padding: 0; - list-style-type: none; - height:30px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } -.ddsmoothmenu ul li{ - position: relative; - display: inline-block; - float: left; - color: #2d2b2b; - height:100%; + +.main_menu { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; } -.ddsmoothmenu ul ul li a{ - width:100%; +.main_menu > ul.menu_bar { + display: flex; + flex-direction: row; + list-style: none; + padding: 0; + margin: 0; + height: 30px; + padding-left: 10px; + background: var(--background-color-menu); } -.ddsmoothmenu .rightarrowclass{ - display:none !important; +.main_menu > ul.menu_bar > li { + padding: 0; + overflow: hidden; + height: 100%; } -.ddsmoothmenu ul li a{ - display: inline-block; - color: #cccccc; +.main_menu > ul.menu_bar > li > a { + display: flex; + align-items: center; + font-size: 12px; color: var(--text-color-menu); text-decoration: none; - text-align:center; - padding: 7px 10px 8px 10px !important; + padding: 0 10px; + height: 100%; } -.ddsmoothmenu ul ul li a{ - padding-right: 25px !important; +.main_menu > ul.menu_bar > li > a::-moz-focus-inner { + border: 0; } -.ddsmoothmenu ul li a.selected{ - background-color: #FFFFFF !important; - color: #2d2b2b; +.main_menu > ul.menu_bar > li > a:focus { + outline: none; + box-shadow: 0 -3px var(--menu-dropdown-background-color) inset; } -.ddsmoothmenu ul li ul li a.selected{ - background-color:#E4EBF8 !important; +.main_menu > ul.menu_bar > li > a:hover { + background: var(--menu-dropdown-hover-background-color); + box-shadow: none; + color: var(--menu-dropdown-hover-text-color); } -.ddsmoothmenu ul li a:hover{ - background-color: #E4EBF8; - color: #2D2D2D; +.main_menu > ul.menu_bar > li > a[aria-expanded="true"] { + background: var(--menu-dropdown-background-color); + box-shadow: none; + color: var(--menu-dropdown-text-color); } -.ddsmoothmenu .hide_ul{ - position: absolute; - left: -3000px; - display: none; - visibility: hidden; - border:1px solid #5680C1; - border-top:0px; +.main_menu > ul.menu_bar > li > a > * { + pointer-events: none; } -.ddsmoothmenu ul li ul{ - position: absolute; - left: -3000px; - display: none; - visibility: hidden; - border:1px solid #5680C1; - border-top:0px; - margin-left: -1px; - height:auto; - min-width:140px; - width:auto !important; - top:30px !important; + +.main_menu > ul.menu_dropdown { + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + list-style: none; + padding: 0; + margin: 0; overflow-x: hidden; overflow-y: auto; - max-height: calc(100vh - 60px); + min-width: 150px; + box-shadow: 0 0 0 1px var(--menu-dropdown-border-color); + background: var(--menu-dropdown-background-color); } -.ddsmoothmenu ul li ul.expanded{ - overflow: visible; - max-height: none; +.main_menu > ul.menu_dropdown > li { + padding: 0; } -.ddsmoothmenu ul li ul li{ - display: list-item; - background: #ffffff; - float: none; - height:auto; - width:100%; -} -.ddsmoothmenu ul li ul li a{ - text-align:left; -} -.ddsmoothmenu ul li ul li ul{ - top: 0; - border-top:1px solid #5680C1; -} -.ddsmoothmenu ul li ul li a{ - padding-left: 5px; - padding-right:5px; +.main_menu > ul.menu_dropdown > li > hr { + background: none; + border: 1px solid var(--menu-dropdown-divider-color); + border-bottom: none; margin: 0; - color: #2D2D2D; - white-space: nowrap; } -.ddsmoothmenu ul li ul li ul{ - top:0 !important; +.main_menu > ul.menu_dropdown > li > a { + display: flex; + flex-direction: row; + align-items: center; + position: relative; + height: 30px; + padding: 0 10px; + font-size: 12px; + line-height: 30px; + text-decoration: none; + color: var(--menu-dropdown-text-color); } -.ddsmoothmenu .downarrowclass{ +.main_menu > ul.menu_dropdown > li > ::-moz-focus-inner { + border: 0; +} +.main_menu > ul.menu_dropdown > li > a:focus { + outline: none; + box-shadow: 0 0 0 2px var(--menu-dropdown-hover-background-color) inset; +} +.main_menu > ul.menu_dropdown > li > a:hover { + background: var(--menu-dropdown-hover-background-color); + box-shadow: none; + color: var(--menu-dropdown-hover-text-color); +} +.main_menu > ul.menu_dropdown > li > a[aria-expanded="true"] { + background: var(--menu-dropdown-hover-background-color); + box-shadow: none; + color: var(--menu-dropdown-hover-text-color); +} +.main_menu > ul.menu_dropdown > li > a[aria-haspopup="true"]::after { position: absolute; - top: 12px; - right: 7px; -} -.ddsmoothmenu .ddshadow{ - position: absolute; - left: 0; - top: 0; - width: 0; - height: 0; - background-color: #ccc; -} -.ddsmoothmenu .mid-line{ - background-color:#ff0000; - border-top:1px solid #e5e5e5; - font-size:0; - padding:0 8px 0 8px; -} -.ddsmoothmenu ul li ul li.more > a{ - position:relative; -} -.ddsmoothmenu ul li ul li.more > a:before{ - position: absolute; - content:">"; + content: ">"; right: 9px; width: 5px; - height: 14px; transform: scaleY(2); color: #808080; } -.ddsmoothmenu ul li ul li ul{ - left: calc(100% + 1px) !important; +.main_menu > ul.menu_dropdown > li > a[aria-haspopup="true"] > .name { + margin-right: 8px; } -.ddsmoothmenu .dots::after{ - content: " ..."; +.main_menu > ul.menu_dropdown > li > a[target="_blank"]::after { + content: ""; + width: 10px; + height: 10px; + margin-left: 5px; + background: url(images/sprites.png) no-repeat -700px 0; + opacity: 0.3; } -.ddsmoothmenu a[data-key]:after{ - position: absolute; - content: attr(data-key) " "; - color: #aaa; - font-size: 12px; - margin-left: 8px; - right:10px; +.main_menu > ul.menu_dropdown > li > a > * { + pointer-events: none; +} +.main_menu > ul.menu_dropdown > li > a > .name { + flex-grow: 1; + overflow: hidden; + white-space: nowrap; +} +.main_menu > ul.menu_dropdown > li > a > .shortcut { + flex-shrink: 1; + color: var(--menu-dropdown-text-muted-color); } -@media screen and (max-width:700px){ - .mobile_menu{ - display:block; - } - .left_mobile_menu{ - display:none; - } - .ddsmoothmenu{ - height:50px; - } - .ddsmoothmenu ul{ - width: calc(100% - 50px); - height:50px; - } - .ddsmoothmenu > ul > li > a{ - height:50px; - padding-top: 15px !important; - } - .ddsmoothmenu ul li ul{ - top:50px !important; - } - .ddsmoothmenu ul li ul li{ - height:auto; - } - .ddsmoothmenu ul li ul li a{ - height:30px; - } +.mobile_menu { + display: none; + position: absolute; + width: 100%; + top: 0; } -@media screen and (max-width:550px){ - .ddsmoothmenu{ - padding-left:0; - } - .ddsmoothmenu ul{ - width: calc(100% - 50px); - } - .ddsmoothmenu > ul > li{ - width: calc(100% / 7); - } - .ddsmoothmenu > ul > li > a{ - width:100%; - padding-left: 3px !important; - padding-right: 3px !important; - overflow: hidden; - } - .left_mobile_menu{ - display:block; - } +.left_mobile_menu, .right_mobile_menu { + position: absolute; + width: 50px; + height: 50px; + background: url("images/sprites.png") no-repeat 11px -86px; + filter: invert(1); + display: block; + top: 0; + z-index: 200; + border: 0; + outline: 0; + cursor: pointer; } +.left_mobile_menu { left:0; } +.right_mobile_menu { right:0; } + +@media screen and (max-width:700px) { + .mobile_menu { + display: block; + } + .left_mobile_menu { + display: none; + } + .main_menu > ul.menu_bar { + height: 50px; + padding-left: 0; + padding-right: 50px; + } +} \ No newline at end of file diff --git a/src/js/config-menu.js b/src/js/config-menu.js index 9203c22..48ac212 100644 --- a/src/js/config-menu.js +++ b/src/js/config-menu.js @@ -1,204 +1,630 @@ -var menu_template = ` - -`; +const menuDefinition = [ + { + name: 'File', + children: [ + { + name: 'New', + target: 'file/new.new' + }, + { + divider: true + }, + { + name: 'Open', + children: [ + { + name: 'Open File', + shortcut: 'Drag&Drop', + target: 'file/open.open_file' + }, + { + name: 'Open Directory', + target: 'file/open.open_dir' + }, + { + name: 'Open from Webcam', + target: 'file/open.open_webcam' + }, + { + name: 'Open URL', + target: 'file/open.open_url' + }, + { + name: 'Open Data URL', + target: 'file/open.open_data_url' + }, + { + name: 'Open Test Template', + target: 'file/open.open_template_test' + } + ] + }, + { + name: 'Search Images', + ellipsis: true, + target: 'file/search.search' + }, + { + divider: true + }, + { + name: 'Save As', + ellipsis: true, + shortcut: 'S', + target: 'file/save.save' + }, + { + name: 'Save As Data URL', + ellipsis: true, + target: 'file/save.save_data_url' + }, + { + name: 'Print', + ellipsis: true, + shortcut: 'Ctrl-P', + target: 'file/print.print' + }, + { + divider: true + }, + { + name: 'Quick Save', + shortcut: 'F9', + target: 'file/quicksave.quicksave' + }, + { + name: 'Quick Load', + shortcut: 'F10', + target: 'file/quickload.quickload' + } + ] + }, + { + name: 'Edit', + children: [ + { + name: 'Undo', + target: 'edit/undo.undo' + }, + { + divider: true + }, + { + name: 'Delete Selection', + shortcut: 'Del', + target: 'edit/selection.delete' + }, + { + name: 'Copy Selection', + target: 'layer/new.new_selection' + }, + { + name: 'Paste', + shortcut: 'Ctrl+V', + target: 'edit/paste.paste' + }, + { + divider: true + }, + { + name: 'Select All', + target: 'edit/selection.select_all' + } + ] + }, + { + name: 'Image', + children: [ + { + name: 'Information', + ellipsis: true, + target: 'image/information.information' + }, + { + name: 'Size', + ellipsis: true, + target: 'image/size.size' + }, + { + name: 'Trim', + ellipsis: true, + shortcut: 'T', + target: 'image/trim.trim' + }, + { + name: 'Zoom', + children: [ + { + name: 'Zoom In', + target: 'image/zoom.in' + }, + { + name: 'Zoom Out', + target: 'image/zoom.out' + }, + { + divider: true + }, + { + name: 'Original Size', + target: 'image/zoom.original' + }, + { + name: 'Fit Window', + target: 'image/zoom.auto' + } + ] + }, + { + divider: true + }, + { + name: 'Resize', + ellipsis: true, + target: 'image/resize.resize' + }, + { + name: 'Rotate', + ellipsis: true, + target: 'image/rotate.rotate' + }, + { + name: 'Flip', + children: [ + { + name: 'Vertical', + target: 'image/flip.vertical' + }, + { + name: 'Horizontal', + target: 'image/flip.horizontal' + } + ] + }, + { + name: 'Translate', + ellipsis: true, + target: 'image/translate.translate' + }, + { + name: 'Opacity', + ellipsis: true, + target: 'image/opacity.opacity' + }, + { + divider: true + }, + { + name: 'Color Correction', + ellipsis: true, + target: 'image/color_corrections.color_corrections' + }, + { + name: 'Auto Adjust Colors', + target: 'image/auto_adjust.auto_adjust' + }, + { + name: 'Decrease Color Depth', + target: 'image/decrease_colors.decrease_colors' + }, + { + name: 'Color Palette', + ellipsis: true, + target: 'image/palette.palette' + }, + { + name: 'Grid', + ellipsis: true, + shortcut: 'G', + target: 'image/grid.grid' + }, + { + divider: true + }, + { + name: 'Histogram', + ellipsis: true, + shortcut: 'H', + target: 'image/histogram.histogram' + } + ] + }, + { + name: 'Layers', + children: [ + { + name: 'New', + shortcut: 'N', + target: 'layer/new.new' + }, + { + name: 'New from Selection', + target: 'layer/new.new_selection' + }, + { + divider: true + }, + { + name: 'Duplicate', + target: 'layer/duplicate.duplicate' + }, + { + name: 'Show / Hide', + target: 'layer/visibility.toggle' + }, + { + name: 'Delete', + target: 'layer/delete.delete' + }, + { + name: 'Convert to Raster', + target: 'layer/raster.raster' + }, + { + divider: true + }, + { + name: 'Move', + children: [ + { + name: 'Up', + target: 'layer/move.up' + }, + { + name: 'Down', + target: 'layer/move.down' + } + ] + }, + { + name: 'Composition', + ellipsis: true, + target: 'layer/composition.composition' + }, + { + name: 'Rename', + ellipsis: true, + target: 'layer/rename.rename' + }, + { + name: 'Clear', + target: 'layer/clear.clear' + }, + { + divider: true + }, + { + name: 'Differences Down', + target: 'layer/differences.differences' + }, + { + name: 'Merge Down', + target: 'layer/merge.merge' + }, + { + name: 'Flatten Image', + target: 'layer/flatten.flatten' + } + ] + }, + { + name: 'Effects', + children: [ + { + name: 'CSS Filters', + children: [ + { + name: 'Gaussian Blur', + ellipsis: true, + target: 'effects/blur.blur' + }, + { + name: 'Brightness', + ellipsis: true, + target: 'effects/brightness.brightness' + }, + { + name: 'Contrast', + ellipsis: true, + target: 'effects/contrast.contrast' + }, + { + name: 'Grayscale', + ellipsis: true, + target: 'effects/grayscale.grayscale' + }, + { + name: 'Hue Rotate', + ellipsis: true, + target: 'effects/hue_rotate.hue_rotate' + }, + { + name: 'Negative', + ellipsis: true, + target: 'effects/negative.negative' + }, + { + name: 'Saturate', + ellipsis: true, + target: 'effects/saturate.saturate' + }, + { + name: 'Sepia', + ellipsis: true, + target: 'effects/sepia.sepia' + }, + { + name: 'Shadow', + ellipsis: true, + target: 'effects/shadow.shadow' + }, + ] + }, + { + name: 'Black and White', + ellipsis: true, + target: 'effects/black_and_white.black_and_white' + }, + { + name: 'Blueprint', + ellipsis: true, + target: 'effects/blueprint.blueprint' + }, + { + name: 'Box Blur', + ellipsis: true, + target: 'effects/box_blur.box_blur' + }, + { + name: 'Denoise', + ellipsis: true, + target: 'effects/denoise.denoise' + }, + { + name: 'Dither', + ellipsis: true, + target: 'effects/dither.dither' + }, + { + name: 'Dot Screen', + ellipsis: true, + target: 'effects/dot_screen.dot_screen' + }, + { + name: 'Edge', + ellipsis: true, + target: 'effects/edge.edge' + }, + { + name: 'Emboss', + ellipsis: true, + target: 'effects/emboss.emboss' + }, + { + name: 'Enrich', + ellipsis: true, + target: 'effects/enrich.enrich' + }, + { + name: 'Grains', + ellipsis: true, + target: 'effects/grains.grains' + }, + { + name: 'Heatmap', + ellipsis: true, + target: 'effects/heatmap.heatmap' + }, + { + name: 'Mosaic', + ellipsis: true, + target: 'effects/mosaic.mosaic' + }, + { + name: 'Night Vision', + ellipsis: true, + target: 'effects/night_vision.night_vision' + }, + { + name: 'Oil', + ellipsis: true, + target: 'effects/oil.oil' + }, + { + name: 'Pencil', + ellipsis: true, + target: 'effects/pencil.pencil' + }, + { + name: 'Sharpen', + ellipsis: true, + target: 'effects/sharpen.sharpen' + }, + { + name: 'Solarize', + ellipsis: true, + target: 'effects/solarize.solarize' + }, + { + name: 'Tilt Shift', + ellipsis: true, + target: 'effects/tilt_shift.tilt_shift' + }, + { + name: 'Vignette', + ellipsis: true, + target: 'effects/vignette.vignette' + }, + { + name: 'Vibrance', + ellipsis: true, + target: 'effects/vibrance.vibrance' + }, + { + name: 'Vintage', + ellipsis: true, + target: 'effects/vintage.vintage' + }, + { + name: 'Zoom Blur', + ellipsis: true, + target: 'effects/zoom_blur.zoom_blur' + } + ] + }, + { + name: 'Tools', + children: [ + { + name: 'Borders', + ellipsis: true, + target: 'tools/borders.borders' + }, + { + name: 'Sprites', + target: 'tools/sprites.sprites' + }, + { + name: 'Key-Points', + target: 'tools/keypoints.keypoints' + }, + { + name: 'Content Fill', + ellipsis: true, + target: 'tools/content_fill.content_fill' + }, + { + divider: true + }, + { + name: 'Color to Alpha', + ellipsis: true, + target: 'tools/color_to_alpha.color_to_alpha' + }, + { + name: 'Color Zoom', + ellipsis: true, + target: 'tools/color_zoom.color_zoom' + }, + { + name: 'Replace Color', + ellipsis: true, + target: 'tools/replace_color.replace_color' + }, + { + name: 'Restore Alpha', + ellipsis: true, + target: 'tools/restore_alpha.restore_alpha' + }, + { + name: 'External', + children: [ + { + name: 'TINYPNG - Compress PNG and JPEG', + href: 'https://tinypng.com' + }, + { + name: 'REMOVE.BG - Remove Image Background', + href: 'https://www.remove.bg' + }, + { + name: 'PNGTOSVG - Convert Image to SVG', + href: 'https://www.pngtosvg.com' + }, + { + name: 'SQUOOSH - Compress and Compare Images', + href: 'https://squoosh.app' + } + ] + }, + { + divider: true + }, + { + name: 'Settings', + ellipsis: true, + target: 'tools/settings.settings' + } + ] + }, + { + name: 'Help', + children: [ + { + name: 'Keyboard Shortcuts', + ellipsis: true, + target: 'help/shortcuts.shortcuts' + }, + { + name: 'Report Issues', + href: 'https://github.com/viliusle/miniPaint/issues' + }, + { + name: 'Language', + children: [ + { + name: 'English', + target: 'help/translate.translate.en' + }, + { + divider: true + }, + { + name: '简体中文', + target: 'help/translate.translate.zh' + }, + { + name: 'Español', + target: 'help/translate.translate.es' + }, + { + name: 'Français', + target: 'help/translate.translate.fr' + }, + { + name: 'Deutsch', + target: 'help/translate.translate.de' + }, + { + name: 'Italiano', + target: 'help/translate.translate.it' + }, + { + name: '日本語', + target: 'help/translate.translate.ja' + }, + { + name: '한국어', + target: 'help/translate.translate.ko' + }, + { + name: 'Lietuvių', + target: 'help/translate.translate.lt' + }, + { + name: 'Português', + target: 'help/translate.translate.pt' + }, + { + name: 'русский язык', + target: 'help/translate.translate.ru' + }, + { + name: 'Türkçe', + target: 'help/translate.translate.tr' + } + ] + }, + { + divider: true + }, + { + name: 'About', + target: 'help/about.about' + } + ] + } +]; -export default menu_template; \ No newline at end of file + +export default menuDefinition; \ No newline at end of file diff --git a/src/js/core/base-gui.js b/src/js/core/base-gui.js index f507d65..29b2a0f 100644 --- a/src/js/core/base-gui.js +++ b/src/js/core/base-gui.js @@ -124,34 +124,23 @@ class Base_gui_class { var _this = this; //menu events - var targets = document.querySelectorAll('#main_menu a'); - for (var i = 0; i < targets.length; i++) { - if (targets[i].dataset.target == undefined) - continue; - targets[i].addEventListener('click', function (event) { - var parts = this.dataset.target.split('.'); - var module = parts[0]; - var function_name = parts[1]; - var param = parts[2]; + this.GUI_menu.on('select_target', (target) => { + var parts = target.split('.'); + var module = parts[0]; + var function_name = parts[1]; + var param = parts[2]; - //close menu - var menu = document.querySelector('#main_menu .selected'); - if (menu != undefined) { - menu.click(); - } - - //call module - if (_this.modules[module] == undefined) { - alertify.error('Modules class not found: ' + module); - return; - } - if (_this.modules[module][function_name] == undefined) { - alertify.error('Module function not found. ' + module + '.' + function_name); - return; - } - _this.modules[module][function_name](param); - }); - } + //call module + if (this.modules[module] == undefined) { + alertify.error('Modules class not found: ' + module); + return; + } + if (this.modules[module][function_name] == undefined) { + alertify.error('Module function not found. ' + module + '.' + function_name); + return; + } + this.modules[module][function_name](param); + }); //registerToggleAbility var targets = document.querySelectorAll('.toggle'); diff --git a/src/js/core/gui/gui-menu.js b/src/js/core/gui/gui-menu.js index 3a353fa..beca954 100644 --- a/src/js/core/gui/gui-menu.js +++ b/src/js/core/gui/gui-menu.js @@ -4,37 +4,369 @@ */ import config from './../../config.js'; -import menu_template from './../../config-menu.js'; -import ddsmoothmenu from './../../libs/menu.js'; +import menuDefinition from './../../config-menu.js'; /** * class responsible for rendering main menu */ class GUI_menu_class { + constructor() { + this.eventSubscriptions = {}; + this.dropdownMaxHeightMargin = 15; + this.menuContainer = null; + this.menuBarNode = null; + this.lastFocusedMenuBarLink = 0; + this.dropdownStack = []; + } + render_main() { - document.getElementById('main_menu').innerHTML = menu_template; - ddsmoothmenu.init({ - mainmenuid: "main_menu", - method: 'toggle', //'hover' (default) or 'toggle' - contentsource: "markup", + this.menuContainer = document.getElementById('main_menu'); + + let menuTemplate = ''; + + this.menuContainer.innerHTML = menuTemplate; + this.menuBarNode = this.menuContainer.querySelector('[role="menubar"]'); + + this.menuContainer.addEventListener('click', (event) => { return this.on_click_menu(event); }, true); + this.menuContainer.addEventListener('keydown', (event) => { return this.on_key_down_menu(event); }, true); + this.menuBarNode.addEventListener('focus', (event) => { return this.on_focus_menu_bar(event); }); + this.menuBarNode.addEventListener('blur', (event) => { return this.on_blur_menu_bar(event); }); + this.menuBarNode.querySelectorAll('a').forEach((link) => { + link.addEventListener('focus', (event) => { return this.on_focus_menu_bar_link(event); }); + }); + document.body.addEventListener('mousedown', (event) => { return this.on_mouse_down_body(event); }, true); + document.body.addEventListener('touchstart', (event) => { return this.on_mouse_down_body(event); }, true); + window.addEventListener('resize', (event) => { return this.on_resize_window(event); }, true) + } + + on(eventName, callback) { + if (!this.eventSubscriptions[eventName]) { + this.eventSubscriptions[eventName] = []; + } + if (!this.eventSubscriptions[eventName].includes(callback)) { + this.eventSubscriptions[eventName].push(callback); + } + } + + emit(eventName, payload) { + if (this.eventSubscriptions[eventName]) { + for (let callback of this.eventSubscriptions[eventName]) { + callback(payload); + } + } + } + + generate_menu_bar_item_template(definition, index) { + return ` +
  • + +
  • + `.trim(); + } + + generate_menu_dropdown_item_template(definition, level, index) { + if (definition.divider) { + return ` +
  • +
    +
  • + `.trim(); + } else { + return ` +
  • + + ${ definition.name }${ definition.ellipsis ? ' ...' : '' } + ${ !!definition.shortcut ? ` + Shortcut Key: ${ definition.shortcut } + ` : `` } + +
  • + `.trim(); + } + } + + on_mouse_down_body(event) { + const target = event.touches ? event.touches[0].target : event.target; + + // Clicked outside of menu; close dropdowns. + if (target && !this.menuContainer.contains(target)) { + this.close_child_dropdowns(0); + } + } + + on_focus_menu_bar(event) { + if (document.activeElement === this.menuBarNode) { + let lastFocusedLink = this.menuBarNode.querySelector(`[data-index="${ this.lastFocusedMenuBarLink }"]`); + if (!lastFocusedLink) { + lastFocusedLink = this.menuBarNode.querySelector('a'); + } + lastFocusedLink.focus(); + } + } + + on_focus_menu_bar_link(event) { + this.lastFocusedMenuBarLink = parseInt(event.target.getAttribute('data-index'), 10) || 0; + } + + on_blur_menu_bar(event) { + // TODO + } + + on_key_down_menu(event) { + const key = event.key; + const activeElement = document.activeElement; + + if (activeElement && activeElement.tagName === 'A') { + const linkLevel = parseInt(activeElement.getAttribute('data-level'), 10) || 0; + const linkIndex = parseInt(activeElement.getAttribute('data-index'), 10) || 0; + const menuParent = activeElement.closest('ul'); + if (linkLevel === 0) { + if (['Right', 'ArrowRight'].includes(event.key)) { + let nextLink = menuParent.querySelector(`[data-index="${ linkIndex + 1 }"]`); + if (!nextLink) { + nextLink = menuParent.querySelector(`[data-index="0"]`); + } + nextLink.focus(); + } + else if (['Left', 'ArrowLeft'].includes(event.key)) { + let previousLink = menuParent.querySelector(`[data-index="${ linkIndex - 1 }"]`); + if (!previousLink) { + previousLink = menuParent.querySelector(`[data-index="${ menuParent.querySelectorAll('[data-index]').length - 1 }"]`); + } + previousLink.focus(); + } + else if (['Down', 'ArrowDown'].includes(event.key)) { + if (activeElement.getAttribute('aria-haspopup') === 'true') { + event.preventDefault(); + activeElement.click(); + } + } + else if ([' ', 'Enter'].includes(event.key)) { + event.preventDefault(); + activeElement.click(); + } + } else { + if (['Up', 'ArrowUp'].includes(event.key)) { + event.preventDefault(); + let previousLink = menuParent.querySelector(`[data-index="${ linkIndex - 1 }"]`); + if (!previousLink) { + previousLink = menuParent.querySelector(`[data-index="${ linkIndex - 2 }"]`); // Skip dividers + } + if (!previousLink) { + previousLink = menuParent.querySelector(`[data-index="${ this.dropdownStack[linkLevel - 1].children.length - 1 }"]`); + } + previousLink.focus(); + } + else if (['Down', 'ArrowDown'].includes(event.key)) { + event.preventDefault(); + let nextLink = menuParent.querySelector(`[data-index="${ linkIndex + 1 }"]`); + if (!nextLink) { + nextLink = menuParent.querySelector(`[data-index="${ linkIndex + 2 }"]`); // Skip dividers + } + if (!nextLink) { + nextLink = menuParent.querySelector(`[data-index="0"]`); + } + nextLink.focus(); + } + else if (['Right', 'ArrowRight'].includes(event.key)) { + const menuBarLinkIndex = parseInt(this.dropdownStack[0].opener.getAttribute('data-index'), 10) || 0; + let nextLink = this.menuBarNode.querySelector(`[data-index="${ menuBarLinkIndex + 1 }"]`); + if (!nextLink) { + nextLink = this.menuBarNode.querySelector(`[data-index="0"]`); + } + nextLink.click(); + } + else if (['Left', 'ArrowLeft'].includes(event.key)) { + const menuBarLinkIndex = parseInt(this.dropdownStack[0].opener.getAttribute('data-index'), 10) || 0; + let previousLink = this.menuBarNode.querySelector(`[data-index="${ menuBarLinkIndex - 1 }"]`); + if (!previousLink) { + previousLink = this.menuBarNode.querySelector(`[data-index="${ this.menuBarNode.querySelectorAll('[data-index]').length - 1 }"]`); + } + previousLink.click(); + } + else if ([' ', 'Enter'].includes(event.key)) { + event.preventDefault(); + activeElement.click(); + } + else if (['Esc', 'Escape'].includes(event.key)) { + const opener = this.dropdownStack[linkLevel - 1].opener; + opener.click(); + opener.focus(); + } + } + } + } + + on_click_menu(event) { + const target = event.target.closest('a'); + + // Any link in the menu is clicked. + if (target && target.tagName === 'A') { + const hasPopup = target.getAttribute('aria-haspopup') === 'true'; + if (hasPopup) { + this.toggle_dropdown(target, event.isTrusted); + } else { + this.trigger_link(target); + } + } else { + this.close_child_dropdowns(0); + } + } + + on_resize_window(event) { + if (this.dropdownStack.length > 0) { + this.position_dropdowns(); + } + } + + toggle_dropdown(opener, isTrusted) { + const linkLevel = parseInt(opener.getAttribute('data-level'), 10) || 0; + const linkIndex = parseInt(opener.getAttribute('data-index'), 10) || 0; + if (opener.getAttribute('aria-expanded') === 'true') { + this.close_child_dropdowns(linkLevel); + } else { + const parentList = opener.closest('ul'); + parentList.querySelectorAll('a').forEach((item) => { + item.setAttribute('aria-expanded', 'false'); + }); + opener.setAttribute('aria-expanded', true); + this.create_dropdown(opener, linkLevel, linkIndex, !isTrusted); + } + } + + trigger_link(link) { + const level = parseInt(link.getAttribute('data-level'), 10) || 0; + const index = parseInt(link.getAttribute('data-index'), 10) || 0; + + // Find link definition + let children = menuDefinition; + for (let i = 0; i < level; i++) { + const childIndex = this.dropdownStack[i] != null ? this.dropdownStack[i].index : index; + children = children[childIndex].children; + } + let definition = children[index]; + + // Close the dropdown + this.close_child_dropdowns(0); + + // Emit callback events for triggered links + if (definition.target) { + this.emit('select_target', definition.target); + } + else if (definition.href) { + this.emit('select_href', definition.href); + } + } + + close_child_dropdowns(level) { + for (let i = this.dropdownStack.length - 1; i >= 0; i--) { + if (i >= level) { + this.dropdownStack[i].element.parentNode.removeChild(this.dropdownStack[i].element); + this.dropdownStack[i].opener.setAttribute('aria-expanded', false); + } + } + this.dropdownStack = this.dropdownStack.slice(0, level); + } + + create_dropdown(opener, level, index, focusAfterCreation) { + this.close_child_dropdowns(level); + + // Find child list in the menu definition + let children = menuDefinition; + for (let i = 0; i <= level; i++) { + const childIndex = this.dropdownStack[i] != null ? this.dropdownStack[i].index : index; + children = children[childIndex].children; + } + + // Create the dropdown element, place it in DOM & position it + let dropdownElement = document.createElement('ul'); + dropdownElement.className = 'menu_dropdown'; + dropdownElement.role = 'menu'; + dropdownElement.setAttribute('aria-labelledby', 'main_menu_' + level + '_' + index); + let dropdownTemplate = ''; + for (let i = 0; i < children.length; i++) { + dropdownTemplate += this.generate_menu_dropdown_item_template(children[i], level + 1, i); + } + dropdownElement.innerHTML = dropdownTemplate; + + this.menuContainer.appendChild(dropdownElement); + + if (focusAfterCreation) { + dropdownElement.querySelector('a').focus(); + } + + this.dropdownStack.push({ + children, + opener, + index, + element: dropdownElement }); - // Additional logic for ddsmoothmenu library: - // Add CSS class to primary dropdown to identify when to toggle scrolling for mobile. - document.getElementById('main_menu').addEventListener('click', (e) => { - const target = e.target; - if (!target || !target.parentNode) return; - if (target.parentNode.classList.contains('more')) { - var parentList = target.closest('ul'); - var wasSelected = target.classList.contains('selected'); - setTimeout(() => { - parentList.classList.toggle('expanded', !wasSelected); - }, 1); - } else if (target.tagName === 'A' && target.matches('#main_menu > ul > li > a')) { - target.nextElementSibling.classList.remove('expanded'); + this.position_dropdowns(); + } + + position_dropdowns() { + const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); + const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); + + let topNavHeight = 0; + for (let level = 0; level < this.dropdownStack.length; level++) { + const dropdownElement = this.dropdownStack[level].element; + const openerRect = this.dropdownStack[level].opener.getBoundingClientRect(); + + topNavHeight = openerRect.height; + const dropdownMaxHeight = vh - topNavHeight - this.dropdownMaxHeightMargin; + dropdownElement.style.maxHeight = dropdownMaxHeight + 'px'; + const dropdownRect = dropdownElement.getBoundingClientRect(); + + if (level === 0) { + dropdownElement.style.top = (openerRect.y + openerRect.height) + 'px'; + + let left = openerRect.x; + if (left + dropdownRect.width > vw) { + left = openerRect.x + openerRect.width - dropdownRect.width; + } + if (left + dropdownRect.width > vw) { + left = vw - dropdownRect.width; + } + if (left < 0) { + left = 0; + } + dropdownElement.style.left = left + 'px'; + } else { + let top = openerRect.y; + if (top + dropdownRect.height > vh - this.dropdownMaxHeightMargin) { + top = vh - this.dropdownMaxHeightMargin - dropdownRect.height; + } + dropdownElement.style.top = top + 'px'; + + let left = openerRect.x + openerRect.width + 1; + if (left + dropdownRect.width > vw) { + left = openerRect.x - dropdownRect.width - 1; + } + if (left < 0) { + if (openerRect.x + (openerRect.width / 2) > vw / 2) { + left = 1; + } else { + left = vw - dropdownRect.width - 1; + if (left < 0) { + left = 1; + } + } + } + dropdownElement.style.left = left + 'px'; } - }, true); + } } }