From 39e73b3f4d5d76a1dfc8fb0a90e7fcba0c0c5ed7 Mon Sep 17 00:00:00 2001 From: ayamoosa Date: Wed, 13 Mar 2024 08:13:52 -0700 Subject: [PATCH 1/4] added menu-aim plugin --- src/lib/jquery.menu-aim.js | 322 +++++++++++++++++++++++++++++++++++++ src/static-assets.js | 1 + 2 files changed, 323 insertions(+) create mode 100644 src/lib/jquery.menu-aim.js diff --git a/src/lib/jquery.menu-aim.js b/src/lib/jquery.menu-aim.js new file mode 100644 index 000000000..1039f5c13 --- /dev/null +++ b/src/lib/jquery.menu-aim.js @@ -0,0 +1,322 @@ +/** + * menu-aim is a jQuery plugin for dropdown menus that can differentiate + * between a user trying hover over a dropdown item vs trying to navigate into + * a submenu's contents. + * + * menu-aim assumes that you have are using a menu with submenus that expand + * to the menu's right. It will fire events when the user's mouse enters a new + * dropdown item *and* when that item is being intentionally hovered over. + * + * __________________________ + * | Monkeys >| Gorilla | + * | Gorillas >| Content | + * | Chimps >| Here | + * |___________|____________| + * + * In the above example, "Gorillas" is selected and its submenu content is + * being shown on the right. Imagine that the user's cursor is hovering over + * "Gorillas." When they move their mouse into the "Gorilla Content" area, they + * may briefly hover over "Chimps." This shouldn't close the "Gorilla Content" + * area. + * + * This problem is normally solved using timeouts and delays. menu-aim tries to + * solve this by detecting the direction of the user's mouse movement. This can + * make for quicker transitions when navigating up and down the menu. The + * experience is hopefully similar to amazon.com/'s "Shop by Department" + * dropdown. + * + * Use like so: + * + * $("#menu").menuAim({ + * activate: $.noop, // fired on row activation + * deactivate: $.noop // fired on row deactivation + * }); + * + * ...to receive events when a menu's row has been purposefully (de)activated. + * + * The following options can be passed to menuAim. All functions execute with + * the relevant row's HTML element as the execution context ('this'): + * + * .menuAim({ + * // Function to call when a row is purposefully activated. Use this + * // to show a submenu's content for the activated row. + * activate: function() {}, + * + * // Function to call when a row is deactivated. + * deactivate: function() {}, + * + * // Function to call when mouse enters a menu row. Entering a row + * // does not mean the row has been activated, as the user may be + * // mousing over to a submenu. + * enter: function() {}, + * + * // Function to call when mouse exits a menu row. + * exit: function() {}, + * + * // Selector for identifying which elements in the menu are rows + * // that can trigger the above events. Defaults to "> li". + * rowSelector: "> li", + * + * // You may have some menu rows that aren't submenus and therefore + * // shouldn't ever need to "activate." If so, filter submenu rows w/ + * // this selector. Defaults to "*" (all elements). + * submenuSelector: "*", + * + * // Direction the submenu opens relative to the main menu. Can be + * // left, right, above, or below. Defaults to "right". + * submenuDirection: "right" + * }); + * + * https://github.com/kamens/jQuery-menu-aim +*/ +(function($) { + + $.fn.menuAim = function(opts) { + // Initialize menu-aim for all elements in jQuery collection + this.each(function() { + init.call(this, opts); + }); + + return this; + }; + + function init(opts) { + var $menu = $(this), + activeRow = null, + mouseLocs = [], + lastDelayLoc = null, + timeoutId = null, + options = $.extend({ + rowSelector: "> li", + submenuSelector: "*", + submenuDirection: "right", + tolerance: 75, // bigger = more forgivey when entering submenu + enter: $.noop, + exit: $.noop, + activate: $.noop, + deactivate: $.noop, + exitMenu: $.noop + }, opts); + + var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track + DELAY = 300; // ms delay when user appears to be entering submenu + + /** + * Keep track of the last few locations of the mouse. + */ + var mousemoveDocument = function(e) { + mouseLocs.push({x: e.pageX, y: e.pageY}); + + if (mouseLocs.length > MOUSE_LOCS_TRACKED) { + mouseLocs.shift(); + } + }; + + /** + * Cancel possible row activations when leaving the menu entirely + */ + var mouseleaveMenu = function() { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // If exitMenu is supplied and returns true, deactivate the + // currently active row on menu exit. + if (options.exitMenu(this)) { + if (activeRow) { + options.deactivate(activeRow); + } + + activeRow = null; + } + }; + + /** + * Trigger a possible row activation whenever entering a new row. + */ + var mouseenterRow = function() { + if (timeoutId) { + // Cancel any previous activation delays + clearTimeout(timeoutId); + } + + options.enter(this); + possiblyActivate(this); + }, + mouseleaveRow = function() { + options.exit(this); + }; + + /* + * Immediately activate a row if the user clicks on it. + */ + var clickRow = function() { + activate(this); + }; + + /** + * Activate a menu row. + */ + var activate = function(row) { + if (row == activeRow) { + return; + } + + if (activeRow) { + options.deactivate(activeRow); + } + + options.activate(row); + activeRow = row; + }; + + /** + * Possibly activate a menu row. If mouse movement indicates that we + * shouldn't activate yet because user may be trying to enter + * a submenu's content, then delay and check again later. + */ + var possiblyActivate = function(row) { + var delay = activationDelay(); + + if (delay) { + timeoutId = setTimeout(function() { + possiblyActivate(row); + }, delay); + } else { + activate(row); + } + }; + + /** + * Return the amount of time that should be used as a delay before the + * currently hovered row is activated. + * + * Returns 0 if the activation should happen immediately. Otherwise, + * returns the number of milliseconds that should be delayed before + * checking again to see if the row should be activated. + */ + var activationDelay = function() { + if (!activeRow || !$(activeRow).is(options.submenuSelector)) { + // If there is no other submenu row already active, then + // go ahead and activate immediately. + return 0; + } + + var offset = $menu.offset(), + upperLeft = { + x: offset.left, + y: offset.top - options.tolerance + }, + upperRight = { + x: offset.left + $menu.outerWidth(), + y: upperLeft.y + }, + lowerLeft = { + x: offset.left, + y: offset.top + $menu.outerHeight() + options.tolerance + }, + lowerRight = { + x: offset.left + $menu.outerWidth(), + y: lowerLeft.y + }, + loc = mouseLocs[mouseLocs.length - 1], + prevLoc = mouseLocs[0]; + + if (!loc) { + return 0; + } + + if (!prevLoc) { + prevLoc = loc; + } + + if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || + prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { + // If the previous mouse location was outside of the entire + // menu's bounds, immediately activate. + return 0; + } + + if (lastDelayLoc && + loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) { + // If the mouse hasn't moved since the last time we checked + // for activation status, immediately activate. + return 0; + } + + // Detect if the user is moving towards the currently activated + // submenu. + // + // If the mouse is heading relatively clearly towards + // the submenu's content, we should wait and give the user more + // time before activating a new row. If the mouse is heading + // elsewhere, we can immediately activate a new row. + // + // We detect this by calculating the slope formed between the + // current mouse location and the upper/lower right points of + // the menu. We do the same for the previous mouse location. + // If the current mouse location's slopes are + // increasing/decreasing appropriately compared to the + // previous's, we know the user is moving toward the submenu. + // + // Note that since the y-axis increases as the cursor moves + // down the screen, we are looking for the slope between the + // cursor and the upper right corner to decrease over time, not + // increase (somewhat counterintuitively). + function slope(a, b) { + return (b.y - a.y) / (b.x - a.x); + }; + + var decreasingCorner = upperRight, + increasingCorner = lowerRight; + + // Our expectations for decreasing or increasing slope values + // depends on which direction the submenu opens relative to the + // main menu. By default, if the menu opens on the right, we + // expect the slope between the cursor and the upper right + // corner to decrease over time, as explained above. If the + // submenu opens in a different direction, we change our slope + // expectations. + if (options.submenuDirection == "left") { + decreasingCorner = lowerLeft; + increasingCorner = upperLeft; + } else if (options.submenuDirection == "below") { + decreasingCorner = lowerRight; + increasingCorner = lowerLeft; + } else if (options.submenuDirection == "above") { + decreasingCorner = upperLeft; + increasingCorner = upperRight; + } + + var decreasingSlope = slope(loc, decreasingCorner), + increasingSlope = slope(loc, increasingCorner), + prevDecreasingSlope = slope(prevLoc, decreasingCorner), + prevIncreasingSlope = slope(prevLoc, increasingCorner); + + if (decreasingSlope < prevDecreasingSlope && + increasingSlope > prevIncreasingSlope) { + // Mouse is moving from previous location towards the + // currently activated submenu. Delay before activating a + // new menu row, because user may be moving into submenu. + lastDelayLoc = loc; + return DELAY; + } + + lastDelayLoc = null; + return 0; + }; + + /** + * Hook up initial menu events + */ + $menu + .mouseleave(mouseleaveMenu) + .find(options.rowSelector) + .mouseenter(mouseenterRow) + .mouseleave(mouseleaveRow) + .click(clickRow); + + $(document).mousemove(mousemoveDocument); + + }; +})(jQuery); \ No newline at end of file diff --git a/src/static-assets.js b/src/static-assets.js index 32e5cf7ee..da8d50a02 100644 --- a/src/static-assets.js +++ b/src/static-assets.js @@ -27,6 +27,7 @@ const lib_paths =[ `/lib/jquery-ui-1.13.2/jquery-ui.min.js`, `/lib/lodash@4.17.21.min.js`, `/lib/jquery.dragster.js`, + '/lib/jquery.menu-aim.js', `/lib/html-entities.js`, `/lib/timeago.min.js`, `/lib/iro.min.js`, From 99ab5cec5b59876e99eae1dc3f4aaa93942df80b Mon Sep 17 00:00:00 2001 From: ayamoosa Date: Wed, 13 Mar 2024 09:21:18 -0700 Subject: [PATCH 2/4] works on right side --- src/UI/UIContextMenu.js | 119 +++++++++++++++++++++++++++---------- src/lib/jquery.menu-aim.js | 2 +- 2 files changed, 89 insertions(+), 32 deletions(-) diff --git a/src/UI/UIContextMenu.js b/src/UI/UIContextMenu.js index a6168eda4..bb86aa230 100644 --- a/src/UI/UIContextMenu.js +++ b/src/UI/UIContextMenu.js @@ -149,42 +149,99 @@ function UIContextMenu(options){ return false; }); - // when mouse is over an item - $(contextMenu).find('.context-menu-item').on('mouseover', function (e) { - // mark other items as inactive - $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); - // mark this item as active - $(this).addClass('context-menu-item-active'); - // close any submenu that doesn't belong to this item - $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); - // mark this context menu as active - $(contextMenu).addClass('context-menu-active'); - }) + $(contextMenu).menuAim({ + submenuDirection: function (e){ + // if submenu will open to the left of menu item + if (e.getBoundingClientRect().left + e.getBoundingClientRect().width > window.innerWidth) { + return 'left'; + } else { + return 'right'; + } + }, + activate: function (e) { + + console.log('activate', e) + + // mark other items as inactive + $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); + // mark this item as active + $(e).addClass('context-menu-item-active'); + // close any submenu that doesn't belong to this item + $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); + // mark this context menu as active + $(contextMenu).addClass('context-menu-active'); + + + // activate submenu + // open submenu if applicable + if($(e).hasClass('context-menu-item-submenu')){ + let item_rect_box = e.getBoundingClientRect(); + // open submenu only if it's not already open + if($(`.context-menu[data-id="${menu_id}-${$(e).attr('data-action')}"]`).length === 0){ + // close other submenus + $(`.context-menu[parent-element-id="${menu_id}"]`).remove(); + // open the new submenu + UIContextMenu({ + items: options.items[parseInt($(e).attr('data-action'))].items, + parent_id: menu_id, + is_submenu: true, + id: menu_id + '-' + $(e).attr('data-action'), + position:{ + top: item_rect_box.top - 5, + left: x_pos + item_rect_box.width + 15, + } + }) + } + } + }, + deactivate: function (e) { + console.log('deactivate') + //deactivate submenu + if($(e).hasClass('context-menu-item-submenu')){ + $(`.context-menu[data-id="${menu_id}-${$(e).attr('data-action')}"]`).remove(); + } + } + }); + + + + + // // when mouse is over an item + // $(contextMenu).find('.context-menu-item').on('mouseover', function (e) { + // // mark other items as inactive + // $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); + // // mark this item as active + // $(this).addClass('context-menu-item-active'); + // // close any submenu that doesn't belong to this item + // $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); + // // mark this context menu as active + // $(contextMenu).addClass('context-menu-active'); + // }) // open submenu if applicable - $(`#context-menu-${menu_id} > li.context-menu-item-submenu`).on('mouseover', function (e) { + // $(`#context-menu-${menu_id} > li.context-menu-item-submenu`).on('mouseover', function (e) { - // open submenu only if it's not already open - if($(`.context-menu[data-id="${menu_id}-${$(this).attr('data-action')}"]`).length === 0){ - let item_rect_box = this.getBoundingClientRect(); + // // open submenu only if it's not already open + // if($(`.context-menu[data-id="${menu_id}-${$(this).attr('data-action')}"]`).length === 0){ + // let item_rect_box = this.getBoundingClientRect(); - // close other submenus - $(`.context-menu[parent-element-id="${menu_id}"]`).remove(); + // // close other submenus + // $(`.context-menu[parent-element-id="${menu_id}"]`).remove(); - // open the new submenu - UIContextMenu({ - items: options.items[parseInt($(this).attr('data-action'))].items, - parent_id: menu_id, - is_submenu: true, - id: menu_id + '-' + $(this).attr('data-action'), - position:{ - top: item_rect_box.top - 5, - left: x_pos + item_rect_box.width + 15, - } - }) - } - return false; - }); + // // open the new submenu + // UIContextMenu({ + // items: options.items[parseInt($(this).attr('data-action'))].items, + // parent_id: menu_id, + // is_submenu: true, + // id: menu_id + '-' + $(this).attr('data-action'), + // position:{ + // top: item_rect_box.top - 5, + // left: x_pos + item_rect_box.width + 15, + // } + // }) + // } + // return false; + // }); // useful in cases such as where a menue item is over a window, this prevents from the mousedown event // reaching the window underneath diff --git a/src/lib/jquery.menu-aim.js b/src/lib/jquery.menu-aim.js index 1039f5c13..0c2f82bad 100644 --- a/src/lib/jquery.menu-aim.js +++ b/src/lib/jquery.menu-aim.js @@ -89,7 +89,7 @@ options = $.extend({ rowSelector: "> li", submenuSelector: "*", - submenuDirection: "right", + submenuDirection: $.noop, tolerance: 75, // bigger = more forgivey when entering submenu enter: $.noop, exit: $.noop, From f88cfc750f80e3f0ccca45662b23fc2bbe3d40d6 Mon Sep 17 00:00:00 2001 From: ayamoosa Date: Fri, 15 Mar 2024 09:15:40 -0700 Subject: [PATCH 3/4] need to work on submenuDirection reactivity --- src/UI/UIContextMenu.js | 44 ++++++++++++++++++++++++++++++-------- src/lib/jquery.menu-aim.js | 7 +++--- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/UI/UIContextMenu.js b/src/UI/UIContextMenu.js index bb86aa230..c4852dcc2 100644 --- a/src/UI/UIContextMenu.js +++ b/src/UI/UIContextMenu.js @@ -149,23 +149,45 @@ function UIContextMenu(options){ return false; }); + // initialize menuAim plugin $(contextMenu).menuAim({ - submenuDirection: function (e){ - // if submenu will open to the left of menu item - if (e.getBoundingClientRect().left + e.getBoundingClientRect().width > window.innerWidth) { - return 'left'; - } else { - return 'right'; - } + submenuDirection: function(){ + //if this is not a submenu + // if(!options.is_submenu){ + // if submenu is going to be on the right of the main menu + if(x_pos + $(contextMenu).width() + 10 > window.innerWidth){ + console.log('right') + return "right"; + } else { + console.log('left') + return "left"; + } + // } }, activate: function (e) { + + //if submenu opens on right make submenuDirection right + // if($(contextMenu).offset().left + $(contextMenu).width() + 10 > window.innerWidth){ + // $(contextMenu).menuAim({ + // submenuDirection: 'right' + // }); + // } else { + // $(contextMenu).menuAim({ + // submenuDirection: 'left' + // }); + // } + // hover over an item + + + + let item = $(e).closest('.context-menu-item'); console.log('activate', e) // mark other items as inactive $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); // mark this item as active - $(e).addClass('context-menu-item-active'); + $(item).addClass('context-menu-item-active'); // close any submenu that doesn't belong to this item $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); // mark this context menu as active @@ -173,6 +195,7 @@ function UIContextMenu(options){ // activate submenu + // open submenu if applicable if($(e).hasClass('context-menu-item-submenu')){ let item_rect_box = e.getBoundingClientRect(); @@ -193,6 +216,7 @@ function UIContextMenu(options){ }) } } + }, deactivate: function (e) { console.log('deactivate') @@ -284,4 +308,6 @@ window.select_ctxmenu_item = function ($ctxmenu_item){ $($ctxmenu_item).addClass('context-menu-item-active'); } -export default UIContextMenu; \ No newline at end of file +export default UIContextMenu; + + diff --git a/src/lib/jquery.menu-aim.js b/src/lib/jquery.menu-aim.js index 0c2f82bad..471053400 100644 --- a/src/lib/jquery.menu-aim.js +++ b/src/lib/jquery.menu-aim.js @@ -166,6 +166,7 @@ options.deactivate(activeRow); } + options.activate(row); activeRow = row; }; @@ -277,13 +278,13 @@ // corner to decrease over time, as explained above. If the // submenu opens in a different direction, we change our slope // expectations. - if (options.submenuDirection == "left") { + if (options.submenuDirection() == "left") { decreasingCorner = lowerLeft; increasingCorner = upperLeft; - } else if (options.submenuDirection == "below") { + } else if (options.submenuDirection() == "below") { decreasingCorner = lowerRight; increasingCorner = lowerLeft; - } else if (options.submenuDirection == "above") { + } else if (options.submenuDirection() == "above") { decreasingCorner = upperLeft; increasingCorner = upperRight; } From 8ec0e63d7b3ffca72b346e06c5b247896766e8ff Mon Sep 17 00:00:00 2001 From: ayamoosa Date: Fri, 15 Mar 2024 12:31:40 -0700 Subject: [PATCH 4/4] fixed left/right issue. Feature is ready --- src/UI/UIContextMenu.js | 78 ++++++----------------------------------- 1 file changed, 10 insertions(+), 68 deletions(-) diff --git a/src/UI/UIContextMenu.js b/src/UI/UIContextMenu.js index c4852dcc2..ce0ab8f80 100644 --- a/src/UI/UIContextMenu.js +++ b/src/UI/UIContextMenu.js @@ -145,45 +145,28 @@ function UIContextMenu(options){ $(contextMenu).remove(); }); } - return false; }); - // initialize menuAim plugin + // initialize menuAim plugin (../libs/jquery.menu-aim.js) $(contextMenu).menuAim({ submenuDirection: function(){ - //if this is not a submenu - // if(!options.is_submenu){ - // if submenu is going to be on the right of the main menu - if(x_pos + $(contextMenu).width() + 10 > window.innerWidth){ - console.log('right') + //if not submenu + if(!options.is_submenu){ + // if submenu left postiton is greater than main menu left position + if($(contextMenu).offset().left + 2 * $(contextMenu).width() + 15 < window.innerWidth ){ return "right"; } else { - console.log('left') return "left"; } - // } + } }, + //activates item when mouse enters depending in mouse position and direction activate: function (e) { - - //if submenu opens on right make submenuDirection right - // if($(contextMenu).offset().left + $(contextMenu).width() + 10 > window.innerWidth){ - // $(contextMenu).menuAim({ - // submenuDirection: 'right' - // }); - // } else { - // $(contextMenu).menuAim({ - // submenuDirection: 'left' - // }); - // } - // hover over an item - - + //activate items let item = $(e).closest('.context-menu-item'); - console.log('activate', e) - // mark other items as inactive $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); // mark this item as active @@ -195,7 +178,6 @@ function UIContextMenu(options){ // activate submenu - // open submenu if applicable if($(e).hasClass('context-menu-item-submenu')){ let item_rect_box = e.getBoundingClientRect(); @@ -216,8 +198,8 @@ function UIContextMenu(options){ }) } } - }, + //deactivates row when mouse leavess deactivate: function (e) { console.log('deactivate') //deactivate submenu @@ -227,47 +209,7 @@ function UIContextMenu(options){ } }); - - - - // // when mouse is over an item - // $(contextMenu).find('.context-menu-item').on('mouseover', function (e) { - // // mark other items as inactive - // $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); - // // mark this item as active - // $(this).addClass('context-menu-item-active'); - // // close any submenu that doesn't belong to this item - // $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); - // // mark this context menu as active - // $(contextMenu).addClass('context-menu-active'); - // }) - - // open submenu if applicable - // $(`#context-menu-${menu_id} > li.context-menu-item-submenu`).on('mouseover', function (e) { - - // // open submenu only if it's not already open - // if($(`.context-menu[data-id="${menu_id}-${$(this).attr('data-action')}"]`).length === 0){ - // let item_rect_box = this.getBoundingClientRect(); - - // // close other submenus - // $(`.context-menu[parent-element-id="${menu_id}"]`).remove(); - - // // open the new submenu - // UIContextMenu({ - // items: options.items[parseInt($(this).attr('data-action'))].items, - // parent_id: menu_id, - // is_submenu: true, - // id: menu_id + '-' + $(this).attr('data-action'), - // position:{ - // top: item_rect_box.top - 5, - // left: x_pos + item_rect_box.width + 15, - // } - // }) - // } - // return false; - // }); - - // useful in cases such as where a menue item is over a window, this prevents from the mousedown event + // useful in cases such as where a menu item is over a window, this prevents from the mousedown event // reaching the window underneath $(`#context-menu-${menu_id} > li:not(.context-menu-item-disabled)`).on('mousedown', function (e) { e.preventDefault();