diff --git a/HISTORY.md b/HISTORY.md index 3c4f09d..56b5b39 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -30,6 +30,8 @@ https://github.com/josdejong/jsoneditor - Fixed #38: clear search results after a new JSON object is set. - Fixed #242: row stays highlighted when dragging outside editor. - Fixed quick-keys Shift+Alt+Arrows not registering actions in history. +- Fixed #104: context menus are now positioned relative to the elements of the + editor instead of an absolute position in the window. ## 2015-06-13, version 4.2.1 diff --git a/src/css/contextmenu.css b/src/css/contextmenu.css index 2e0303e..2d3e776 100644 --- a/src/css/contextmenu.css +++ b/src/css/contextmenu.css @@ -1,6 +1,12 @@ /* ContextMenu - main menu */ +div.jsoneditor-contextmenu-root { + position: relative; + width: 0; + height: 0; +} + div.jsoneditor-contextmenu { position: absolute; z-index: 99999; diff --git a/src/css/jsoneditor.css b/src/css/jsoneditor.css index 830ae9e..4d41bfd 100644 --- a/src/css/jsoneditor.css +++ b/src/css/jsoneditor.css @@ -142,7 +142,7 @@ div.jsoneditor-tree button.jsoneditor-contextmenu { div.jsoneditor-tree button.jsoneditor-contextmenu:hover, div.jsoneditor-tree button.jsoneditor-contextmenu:focus, -div.jsoneditor-tree button.jsoneditor-contextmenu.selected, +div.jsoneditor-tree button.jsoneditor-contextmenu.jsoneditor-selected, tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu { background-position: -48px -48px; } @@ -174,7 +174,7 @@ div.jsoneditor { width: 100%; height: 100%; - overflow: auto; + overflow: hidden; position: relative; padding: 0; line-height: 100%; @@ -197,8 +197,6 @@ div.jsoneditor-outer { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; - - overflow: hidden; } div.jsoneditor-tree { diff --git a/src/css/menu.css b/src/css/menu.css index 2b7ac61..6a4f44c 100644 --- a/src/css/menu.css +++ b/src/css/menu.css @@ -4,7 +4,6 @@ div.jsoneditor-menu { height: 35px; padding: 2px; margin: 0; - overflow: hidden; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; @@ -14,7 +13,8 @@ div.jsoneditor-menu { border-bottom: 1px solid #3883fa; } -div.jsoneditor-menu > button { +div.jsoneditor-menu > button, +div.jsoneditor-menu > div.jsoneditor-modes > button { width: 26px; height: 26px; margin: 2px; @@ -31,15 +31,19 @@ div.jsoneditor-menu > button { float: left; } -div.jsoneditor-menu > button:hover { +div.jsoneditor-menu > button:hover, +div.jsoneditor-menu > div.jsoneditor-modes > button:hover { background-color: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.4); } div.jsoneditor-menu > button:focus, -div.jsoneditor-menu > button:active { +div.jsoneditor-menu > button:active, +div.jsoneditor-menu > div.jsoneditor-modes > button:focus, +div.jsoneditor-menu > div.jsoneditor-modes > button:active { background-color: rgba(255,255,255,0.3); } -div.jsoneditor-menu > button:disabled { +div.jsoneditor-menu > button:disabled, +div.jsoneditor-menu > div.jsoneditor-modes > button:disabled { opacity: 0.5; } @@ -68,14 +72,20 @@ div.jsoneditor-menu > button.jsoneditor-format { background-position: -72px -120px; } -div.jsoneditor-menu > button.jsoneditor-modes { +div.jsoneditor-menu > div.jsoneditor-modes { + display: inline-block; + float: left; +} + +div.jsoneditor-menu > div.jsoneditor-modes > button { background-image: none; width: auto; padding-left: 6px; padding-right: 6px; } -div.jsoneditor-menu > button.jsoneditor-separator { +div.jsoneditor-menu > button.jsoneditor-separator, +div.jsoneditor-menu > div.jsoneditor-modes > button.jsoneditor-separator { margin-left: 10px; } @@ -98,5 +108,3 @@ div.jsoneditor-menu a.jsoneditor-poweredBy { top: 0; padding: 10px; } - -/* TODO: css for button:disabled is not supported by IE8 */ diff --git a/src/js/ContextMenu.js b/src/js/ContextMenu.js index edff281..01c7ff9 100644 --- a/src/js/ContextMenu.js +++ b/src/js/ContextMenu.js @@ -18,13 +18,18 @@ function ContextMenu (items, options) { this.items = items; this.eventListeners = {}; this.selection = undefined; // holds the selection before the menu was opened - this.visibleSubmenu = undefined; this.onClose = options ? options.close : undefined; + // create root element + var root = document.createElement('div'); + root.className = 'jsoneditor-contextmenu-root'; + dom.root = root; + // create a container element var menu = document.createElement('div'); menu.className = 'jsoneditor-contextmenu'; dom.menu = menu; + root.appendChild(menu); // create a list to hold the menu items var list = document.createElement('ul'); @@ -176,60 +181,65 @@ ContextMenu.visibleMenu = undefined; /** * Attach the menu to an anchor - * @param {HTMLElement} anchor + * @param {HTMLElement} anchor Anchor where the menu will be attached + * as sibling. + * @param {HTMLElement} [contentWindow] The DIV with with the (scrollable) contents */ -ContextMenu.prototype.show = function (anchor) { +ContextMenu.prototype.show = function (anchor, contentWindow) { this.hide(); - // calculate whether the menu fits below the anchor - var windowHeight = window.innerHeight, - windowScroll = (window.pageYOffset || document.scrollTop || 0), - windowBottom = windowHeight + windowScroll, - anchorHeight = anchor.offsetHeight, - menuHeight = this.maxHeight; + // determine whether to display the menu below or above the anchor + var showBelow = true; + if (contentWindow) { + var anchorRect = anchor.getBoundingClientRect(); + var contentRect = contentWindow.getBoundingClientRect(); + + if (anchorRect.bottom + this.maxHeight < contentRect.bottom) { + // fits below -> show below + } + else if (anchorRect.top - this.maxHeight > contentRect.top) { + // fits above -> show above + showBelow = false; + } + else { + // doesn't fit above nor below -> show below + } + } // position the menu - var left = util.getAbsoluteLeft(anchor); - var top = util.getAbsoluteTop(anchor); - if (top + anchorHeight + menuHeight < windowBottom) { + if (showBelow) { // display the menu below the anchor - this.dom.menu.style.left = left + 'px'; - this.dom.menu.style.top = (top + anchorHeight) + 'px'; + var anchorHeight = anchor.offsetHeight; + this.dom.menu.style.left = '0px'; + this.dom.menu.style.top = anchorHeight + 'px'; this.dom.menu.style.bottom = ''; } else { // display the menu above the anchor - this.dom.menu.style.left = left + 'px'; + this.dom.menu.style.left = '0px'; this.dom.menu.style.top = ''; - this.dom.menu.style.bottom = (windowHeight - top) + 'px'; + this.dom.menu.style.bottom = '0px'; } - // attach the menu to the document - document.body.appendChild(this.dom.menu); + // attach the menu to the parent of the anchor + var parent = anchor.parentNode; + parent.insertBefore(this.dom.root, parent.firstChild); // create and attach event listeners var me = this; var list = this.dom.list; - this.eventListeners.mousedown = util.addEventListener( - document, 'mousedown', function (event) { - // hide menu on click outside of the menu - var target = event.target; - if ((target != list) && !me._isChildOf(target, list)) { - me.hide(); - event.stopPropagation(); - event.preventDefault(); - } - }); - this.eventListeners.mousewheel = util.addEventListener( - document, 'mousewheel', function (event) { - // block scrolling when context menu is visible - event.stopPropagation(); - event.preventDefault(); - }); - this.eventListeners.keydown = util.addEventListener( - document, 'keydown', function (event) { - me._onKeyDown(event); - }); + this.eventListeners.mousedown = util.addEventListener(window, 'mousedown', function (event) { + // hide menu on click outside of the menu + var target = event.target; + if ((target != list) && !me._isChildOf(target, list)) { + me.hide(); + event.stopPropagation(); + event.preventDefault(); + } + }); + this.eventListeners.keydown = util.addEventListener(window, 'keydown', function (event) { + me._onKeyDown(event); + }); // move focus to the first button in the context menu this.selection = util.getSelection(); @@ -249,8 +259,8 @@ ContextMenu.prototype.show = function (anchor) { */ ContextMenu.prototype.hide = function () { // remove the menu from the DOM - if (this.dom.menu.parentNode) { - this.dom.menu.parentNode.removeChild(this.dom.menu); + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); if (this.onClose) { this.onClose(); } @@ -262,7 +272,7 @@ ContextMenu.prototype.hide = function () { if (this.eventListeners.hasOwnProperty(name)) { var fn = this.eventListeners[name]; if (fn) { - util.removeEventListener(document, name, fn); + util.removeEventListener(window, name, fn); } delete this.eventListeners[name]; } diff --git a/src/js/Node.js b/src/js/Node.js index e95a5b1..3993df8 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -2786,7 +2786,8 @@ Node.TYPE_TITLES = { /** * Show a contextmenu for this node - * @param {HTMLElement} anchor Anchor element to attache the context menu to. + * @param {HTMLElement} anchor Anchor element to attach the context menu to + * as sibling. * @param {function} [onClose] Callback method called when the context menu * is being closed. */ @@ -2996,7 +2997,7 @@ Node.prototype.showContextMenu = function (anchor, onClose) { } var menu = new ContextMenu(items, {close: onClose}); - menu.show(anchor); + menu.show(anchor, this.editor.content); }; /** diff --git a/src/js/appendNodeFactory.js b/src/js/appendNodeFactory.js index c3d4381..a2f219e 100644 --- a/src/js/appendNodeFactory.js +++ b/src/js/appendNodeFactory.js @@ -179,7 +179,7 @@ function appendNodeFactory(Node) { ]; var menu = new ContextMenu(items, {close: onClose}); - menu.show(anchor); + menu.show(anchor, this.editor.content); }; /** diff --git a/src/js/modeswitcher.js b/src/js/modeswitcher.js index ed4c1f6..b0d8a65 100644 --- a/src/js/modeswitcher.js +++ b/src/js/modeswitcher.js @@ -94,7 +94,12 @@ function createModeSwitcher(editor, modes, current) { menu.show(box); }; - return box; + var div = document.createElement('div'); + div.className = 'jsoneditor-modes'; + div.style.position = 'relative'; + div.appendChild(box); + + return div; } exports.create = createModeSwitcher; diff --git a/src/js/treemode.js b/src/js/treemode.js index 1f3360d..d39366d 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -653,7 +653,7 @@ treemode._onEvent = function (event) { if (node && node.selected) { if (event.type == 'click') { if (event.target == node.dom.menu) { - this.showContextMenu(event.target); + this.showContextMenu(event.target.parentNode); // stop propagation (else we will open the context menu of a single node) return; @@ -701,6 +701,10 @@ treemode._startDragDistance = function (event) { }; treemode._updateDragDistance = function (event) { + if (!this.dragDistanceEvent) { + this._startDragDistance(event); + } + var diffX = event.pageX - this.dragDistanceEvent.initialPageX; var diffY = event.pageY - this.dragDistanceEvent.initialPageY; @@ -1024,7 +1028,7 @@ treemode.showContextMenu = function (anchor, onClose) { }); var menu = new ContextMenu(items, {close: onClose}); - menu.show(anchor); + menu.show(anchor, this.content); };