diff --git a/pd/nw/pd_canvas.js b/pd/nw/pd_canvas.js index ad1bf2fc89b9b3c33a74d89efd099a0fdeddc7cd..8a02095e61e327e2686011239de46a1f41785115 100644 --- a/pd/nw/pd_canvas.js +++ b/pd/nw/pd_canvas.js @@ -160,6 +160,8 @@ var canvas_events = (function() { last_draggable_y, // last y previous_state = "none", /* last state, excluding explicit 'none' */ match_words_state = false, + last_dropdown_menu_x, + last_dropdown_menu_y, last_search_term = "", svg_view = document.getElementById("patchsvg").viewBox.baseVal, textbox = function () { @@ -208,21 +210,44 @@ var canvas_events = (function() { svg.setAttribute("height", h); }, dropdown_index_to_pd = function(elem) { - pdgui.pdsend(elem.getAttribute("data-callback"), - elem.querySelector(".highlighted").getAttribute("data-index")); + var highlighted = elem.querySelector(".highlighted"); + if (highlighted) { + pdgui.pdsend(elem.getAttribute("data-callback"), + highlighted.getAttribute("data-index")); + } }, - dropdown_highlight_elem = function(elem) { + dropdown_clear_highlight = function() { var container = document.querySelector("#dropdown_list"), - li_array; - if (!elem.classList.contains("highlighted")) { li_array = container.querySelectorAll("li"); - Array.prototype.forEach.call(li_array, function(e) { - e.classList.remove("highlighted"); - }); + Array.prototype.forEach.call(li_array, function(e) { + e.classList.remove("highlighted"); + }); + }, + dropdown_highlight_elem = function(elem, scroll) { + var container = document.querySelector("#dropdown_list"); + if (!elem.classList.contains("highlighted")) { + dropdown_clear_highlight(); elem.classList.add("highlighted"); // Make sure the highlighted element is in view - container.scrollTop = elem.offsetTop + elem.offsetHeight - - container.clientHeight; + if (scroll) { + if (elem.offsetTop < container.scrollTop + || elem.offsetTop + elem.offsetHeight > + container.scrollTop + container.offsetHeight) { + if (scroll === "up") { + // we can't usse elem.scrollIntoView() here + // because it may also change the scrollbar on + // the document, which in turn could change the + // pageX/pageY, for the mousemove event, which + // in turn would make it impossible for us to + // filter out unnecessary mousemove calls + // elem.scrollIntoView(); + container.scrollTop = elem.offsetTop; + } else if (scroll === "down") { + container.scrollTop = elem.offsetTop + elem.offsetHeight + - container.clientHeight; + } + } + } } }, events = { @@ -494,13 +519,13 @@ var canvas_events = (function() { iemgui_label_mouseup: function(evt) { //pdgui.post("lifting the mousebutton on an iemgui label"); // Set last state (none doesn't count as a state) - //pdgui.post("previous state is " + canvas_events.get_previous_state()); + //pdgui.post("previous state is " + // + canvas_events.get_previous_state()); canvas_events[canvas_events.get_previous_state()](); }, dropdown_menu_keydown: function(evt) { var select_elem = document.querySelector("#dropdown_list"), li; - pdgui.post("keycode is " + evt.keyCode); switch(evt.keyCode) { case 13: case 32: @@ -510,34 +535,43 @@ var canvas_events = (function() { break; case 27: // escape select_elem.style.setProperty("display", "none"); - pdgui.post("canceled thing"); canvas_events.normal(); break; case 38: // up li = select_elem.querySelector(".highlighted"); li = li.previousElementSibling || li.parentElement.lastElementChild; - dropdown_highlight_elem(li); + dropdown_highlight_elem(li, "up"); + evt.preventDefault(); break; case 40: // down li = select_elem.querySelector(".highlighted"); li = li.nextElementSibling || li.parentElement.firstElementChild; - dropdown_highlight_elem(li); + dropdown_highlight_elem(li, "down"); + evt.preventDefault(); + break; default: } - }, dropdown_menu_keypress: function(evt) { var li_nodes = document.querySelectorAll("#dropdown_list li"), string_array = [], + highlighted, highlighted_index, match, offset; - highlighted_index = - +document.querySelector("#dropdown_list .highlighted") - .getAttribute("data-index"); - offset = highlighted_index + 1; + highlighted = document + .querySelector("#dropdown_list .highlighted"); + if (highlighted) { + highlighted_index = + +document.querySelector("#dropdown_list .highlighted") + .getAttribute("data-index"); + offset = highlighted_index + 1; + } else { + highlighted_index = 1; + offset = 2; + } Array.prototype.forEach.call(li_nodes, function(e, i, a) { var s = a[(i + offset) % a.length]; string_array.push(s.textContent.trim()); @@ -550,23 +584,49 @@ var canvas_events = (function() { if (match !== undefined) { match = (match + offset) % li_nodes.length; if (match !== highlighted_index) { - dropdown_highlight_elem(li_nodes[match]); + dropdown_highlight_elem(li_nodes[match], + match < highlighted_index ? "up" : "down"); } } }, dropdown_menu_mousedown: function(evt) { - var select_elem = document.querySelector("#dropdown_list"); + var select_elem = document.querySelector("#dropdown_list"), + in_dropdown = evt.target; + while (in_dropdown) { + if (in_dropdown.id === "dropdown_list") { + break; + } + in_dropdown = in_dropdown.parentNode; + } + // Allow scrollbar click and drag without closing the menu + if (in_dropdown && + evt.pageX - select_elem.offsetLeft > + select_elem.clientWidth) { + return; + } + // Special case for OSX, where the scrollbar doesn't take + // up any extra space + if (nw.process.platform === "darwin" + && (evt.target.id === "dropdown_list")) { + return; + } if (evt.target.parentNode && evt.target.parentNode.parentNode && evt.target.parentNode.parentNode.id === "dropdown_list") { dropdown_highlight_elem(evt.target); } + // This selects whatever item is highlighted even + // if we click outside the menu. Might be better to + // cancel in that case. dropdown_index_to_pd(select_elem); select_elem.style.setProperty("display", "none"); canvas_events.normal(); }, dropdown_menu_mouseup: function(evt) { var i, select_elem; + // This can be triggered if the user keeps the mouse down + // to highlight an element and releases the mouse button to + // choose that element if (evt.target.parentNode && evt.target.parentNode.parentNode && evt.target.parentNode.parentNode.id === "dropdown_list") { @@ -576,14 +636,33 @@ var canvas_events = (function() { canvas_events.normal(); } }, - dropdown_menu_mouseover: function(evt) { - var li_array; - if (evt.target.parentNode - && evt.target.parentNode.parentNode - && evt.target.parentNode.parentNode.id === "dropdown_list") { - dropdown_highlight_elem(evt.target); + dropdown_menu_wheel: function(evt) { + // Here we generate bogus mouse coords so that + // we can break through the filter below if we're + // using the mouse wheel to scroll in the list. + last_dropdown_menu_x = Number.MIN_VALUE; + last_dropdown_menu_y = Number.MIN_VALUE; + }, + dropdown_menu_mousemove: function(evt) { + // For whatever reason, Chromium decides to trigger the + // mousemove/mouseenter/mouseover events if the element + // underneath it changes (or for mousemove, if the element + // moves at all). Unfortunately that means we have to track + // the mouse position the entire time to filter out the + // true mousemove events from the ones Chromium generates + // when a scroll event changes the element under the mouse. + if (evt.pageX !== last_dropdown_menu_x + || evt.pageY !== last_dropdown_menu_y) { + if (evt.target.parentNode + && evt.target.parentNode.parentNode + && evt.target.parentNode.parentNode.id === "dropdown_list") { + dropdown_highlight_elem(evt.target); + } else { + dropdown_clear_highlight(); + } } - // hide the dropdown menu <div> thingy + last_dropdown_menu_x = evt.pageX; + last_dropdown_menu_y = evt.pageY; } }, utils = { @@ -840,9 +919,11 @@ var canvas_events = (function() { this.none(); document.addEventListener("mousedown", events.dropdown_menu_mousedown, false); document.addEventListener("mouseup", events.dropdown_menu_mouseup, false); - document.addEventListener("mouseover", events.dropdown_menu_mouseover, false); + document.addEventListener("mousemove", events.dropdown_menu_mousemove, false); document.addEventListener("keydown", events.dropdown_menu_keydown, false); document.addEventListener("keypress", events.dropdown_menu_keypress, false); + document.querySelector("#dropdown_list") + .addEventListener("wheel", events.dropdown_menu_wheel, false); }, search: function() { this.none(); diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js index 9b074b430a619ba28202d13124e24c3783f809bb..aa9c5161b6fb40069bd3decf021dfb02da9b1ce8 100644 --- a/pd/nw/pdgui.js +++ b/pd/nw/pdgui.js @@ -4596,7 +4596,13 @@ function dropdown_populate(cid, label_array, current_index) { } function gui_dropdown_activate(cid, obj_tag, tag, current_index, font_size, state, label_array) { - var g, select_elem, svg_view; + var g, select_elem, svg_view, g_bbox, + doc_height, // document height, excluding the scrollbar + menu_height, // height of the list of elements inside the div + div_y, // position of div containing the dropdown menu + div_max, // max height of the div + scroll_y, + offset_anchor; // top or bottom // Annoying: obj_tag is just the "x"-prepended hex value for the object, // and tag is the one from rtext_gettag that is used as our gobj id if (patchwin[cid]) { @@ -4604,21 +4610,54 @@ function gui_dropdown_activate(cid, obj_tag, tag, current_index, font_size, stat if (state !== 0) { svg_view = patchwin[cid].window.document.getElementById("patchsvg") .viewBox.baseVal; - dropdown_populate(cid, label_array, current_index); select_elem = patchwin[cid] .window.document.querySelector("#dropdown_list"); + dropdown_populate(cid, label_array, current_index); // stick the obj_tag in a data field select_elem.setAttribute("data-callback", obj_tag); - select_elem.style.setProperty("max-height", - (patchwin[cid].window.innerHeight - - g.getBoundingClientRect().bottom - 5) + "px" - ); + // display the menu so we can measure it + + g_bbox = g.getBoundingClientRect(); + // Measuring the document height is tricky-- the following + // method is the only reliable one I've found. And even here, + // if you display the select_elem as inline _before_ measuring + // the doc height, the result ends up being _smaller_. No idea. + doc_height = + patchwin[cid].window.document.documentElement + .clientHeight; + // Now let's display the select_elem div so we can measure it select_elem.style.setProperty("display", "inline"); + menu_height = select_elem.querySelector("ol") + .getBoundingClientRect().height; + scroll_y = patchwin[cid].window.scrollY; + // If the area below the object is smaller than 75px, then + // display the menu above the object. + // If the entire menu won't fit below the object but _will_ + // fit above it, display it above the object. + // If the menu needs a scrollbar, display it below the object + if (doc_height - g_bbox.bottom <= 75 + || (menu_height > doc_height - g_bbox.bottom + && menu_height <= g_bbox.top)) { + // menu on top + offset_anchor = "bottom"; + div_max = g_bbox.top - 2; + div_y = doc_height - (g_bbox.top + scroll_y); + } + else { + // menu on bottom (possibly with scrollbar) + offset_anchor = "top"; + div_max = doc_height - g_bbox.bottom - 2; + div_y = g_bbox.bottom + scroll_y; + } + // set a max-height to force scrollbar if needed + select_elem.style.setProperty("max-height", div_max + "px"); select_elem.style.setProperty("left", (elem_get_coords(g).x - svg_view.x) + "px"); - select_elem.style.setProperty("top", - (elem_get_coords(g).y + g.getBBox().height - svg_view.y) - + "px"); + // Remove "top" and "bottom" props to keep state clean + select_elem.style.removeProperty("top"); + select_elem.style.removeProperty("bottom"); + // Now position the div relative to either the "top" or "bottom" + select_elem.style.setProperty(offset_anchor, div_y + "px"); select_elem.style.setProperty("font-size", pd_fontsize_to_gui_fontsize(font_size) + "px"); select_elem.style.setProperty("min-width", g.getBBox().width + "px");