diff --git a/src/UI/UIContextMenu.js b/src/UI/UIContextMenu.js index 9b5a174e..140b2436 100644 --- a/src/UI/UIContextMenu.js +++ b/src/UI/UIContextMenu.js @@ -17,6 +17,335 @@ * along with this program. If not, see . */ + +/** + * 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: $.noop, + 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; + }; + + $menu.on('mouseenter', function(e) { + if($menu.find('.context-menu-item-active').length === 0 && $menu.find('.has-open-context-menu-submenu').length === 0) + activeRow = null; + }) + /** + * Hook up initial menu events + */ + $menu + .mouseleave(mouseleaveMenu) + .find(options.rowSelector) + .mouseenter(mouseenterRow) + .mouseleave(mouseleaveRow) + .click(clickRow); + + $(document).mousemove(mousemoveDocument); + + }; +})(jQuery); + function UIContextMenu(options){ $('.window-active .window-app-iframe').css('pointer-events', 'none'); @@ -184,8 +513,9 @@ function UIContextMenu(options){ // just passing over the item, the submenu doesn't open immediately. let submenu_delay_timer; - // Initialize the menuAim plugin (../libs/jquery.menu-aim.js) + // Initialize the menuAim plugin $(contextMenu).menuAim({ + rowSelector: ".context-menu-item", submenuSelector: ".context-menu-item-submenu", submenuDirection: function(){ // If not submenu @@ -198,6 +528,10 @@ function UIContextMenu(options){ } } }, + enter: function (e) { + // activate items + // this.activate(e); + }, // activates item when mouse enters depending on mouse position and direction activate: function (e) { // activate items @@ -258,6 +592,7 @@ function UIContextMenu(options){ // remove `has-open-context-menu-submenu` class from the parent menu item $(e).removeClass('has-open-context-menu-submenu'); } + } }); @@ -307,6 +642,14 @@ function UIContextMenu(options){ return false; }) + $(contextMenu).on("mouseleave", function (e) { + $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); + clearTimeout(submenu_delay_timer); + }) + + $(contextMenu).on("mouseenter", function (e) { + }) + return { cancel: (cancel_options) => { cancel_options_ = cancel_options; @@ -344,7 +687,14 @@ $(document).on('mouseenter', '.context-menu', function(e){ }) $(document).on('mouseenter', '.context-menu-item', function(e){ - select_ctxmenu_item(this); + // select_ctxmenu_item(this); }) +$(document).on('mouseenter', '.context-menu-divider', function(e){ + // unselect all items + $(this).siblings('.context-menu-item:not(.has-open-context-menu-submenu)').removeClass('context-menu-item-active'); +}) + +$(document) + export default UIContextMenu; \ No newline at end of file diff --git a/src/lib/jquery.menu-aim.js b/src/lib/jquery.menu-aim.js deleted file mode 100644 index b8dadf9b..00000000 --- a/src/lib/jquery.menu-aim.js +++ /dev/null @@ -1,323 +0,0 @@ -/** - * 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: $.noop, - 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 39303fc6..1b8b0e5d 100644 --- a/src/static-assets.js +++ b/src/static-assets.js @@ -27,7 +27,6 @@ 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`,