From 69fbe63f0e618cc5b0427afd83ac498c79092f99 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Wed, 23 Sep 2020 04:06:12 -0400 Subject: [PATCH] Multi-select mouse events now honor the event's window (#1098) * Multi-select mouse events now honor the event's window This prevents errors from opening JSONEditor in a child window (such as `window.open` or third-party libraries like react-new-window). * Add a demo of showing a JSONEditor in a child window * Minor spelling fixes * Further improvements to new window support Use `event.view` instead of the global `window`. Copy VanillaPicker styles in the example so that the color picker is correctly styled. * Add 'noopener' This helps security; otherwise, JavaScript running in the context of the new window can access the original window object via the `window.opener` property, even if it's a different origin. * Update modal handling for new windows There are two approaches we could take; we could use the existing modalAnchor property and make callers responsible for setting it, or we could modify the default handling to default to the node's container body. For now, I chose to make callers responsible. Rename showTransformModal's `anchor` parameter to `container`; as far as I can tell, `anchor` didn't work properly. --- examples/01_basic_usage.html | 3 +- examples/24_new_window.html | 87 ++++++++++++++++++++++++++++++++++++ src/js/Node.js | 14 +++--- src/js/previewmode.js | 2 +- src/js/textmode.js | 4 +- src/js/treemode.js | 20 +++++---- src/js/util.js | 16 +++++-- 7 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 examples/24_new_window.html diff --git a/examples/01_basic_usage.html b/examples/01_basic_usage.html index f763b1f..764e705 100644 --- a/examples/01_basic_usage.html +++ b/examples/01_basic_usage.html @@ -36,7 +36,8 @@ 'number': 123, 'object': {'a': 'b', 'c': 'd'}, 'time': 1575599819000, - 'string': 'Hello World' + 'string': 'Hello World', + 'onlineDemo': 'https://jsoneditoronline.org/' } editor.set(json) } diff --git a/examples/24_new_window.html b/examples/24_new_window.html new file mode 100644 index 0000000..d916715 --- /dev/null +++ b/examples/24_new_window.html @@ -0,0 +1,87 @@ + + + + JSONEditor | New window + + + + + + + +

+ + + +

+ + + + diff --git a/src/js/Node.js b/src/js/Node.js index eba6ee3..111f017 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -2574,7 +2574,7 @@ export class Node { // if read-only, we use the regular click behavior of an anchor if (isUrl(this.value)) { event.preventDefault() - window.open(this.value, '_blank') + window.open(this.value, '_blank', 'noopener') } } break @@ -2729,7 +2729,7 @@ export class Node { if (target === this.dom.value) { if (!this.editable.value || event.ctrlKey) { if (isUrl(this.value)) { - window.open(this.value, '_blank') + window.open(this.value, '_blank', 'noopener') handled = true } } @@ -3917,7 +3917,7 @@ export class Node { const json = this.getValue() showTransformModal({ - anchor: modalAnchor || DEFAULT_MODAL_ANCHOR, + container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, @@ -4116,13 +4116,13 @@ Node.onDragStart = (nodes, event) => { const offsetY = getAbsoluteTop(draggedNode.dom.tr) - getAbsoluteTop(firstNode.dom.tr) if (!editor.mousemove) { - editor.mousemove = addEventListener(window, 'mousemove', event => { + editor.mousemove = addEventListener(event.view, 'mousemove', event => { Node.onDrag(nodes, event) }) } if (!editor.mouseup) { - editor.mouseup = addEventListener(window, 'mouseup', event => { + editor.mouseup = addEventListener(event.view, 'mouseup', event => { Node.onDragEnd(nodes, event) }) } @@ -4378,11 +4378,11 @@ Node.onDragEnd = (nodes, event) => { delete editor.drag if (editor.mousemove) { - removeEventListener(window, 'mousemove', editor.mousemove) + removeEventListener(event.view, 'mousemove', editor.mousemove) delete editor.mousemove } if (editor.mouseup) { - removeEventListener(window, 'mouseup', editor.mouseup) + removeEventListener(event.view, 'mouseup', editor.mouseup) delete editor.mouseup } diff --git a/src/js/previewmode.js b/src/js/previewmode.js index 49b29e0..4ea20f9 100644 --- a/src/js/previewmode.js +++ b/src/js/previewmode.js @@ -398,7 +398,7 @@ previewmode._showTransformModal = function () { this._renderPreview() // update array count showTransformModal({ - anchor: modalAnchor || DEFAULT_MODAL_ANCHOR, + container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, diff --git a/src/js/textmode.js b/src/js/textmode.js index b4d34ee..c77a94d 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -244,7 +244,7 @@ textmode.create = function (container, options = {}) { // TODO: this anchor falls below the margin of the content, // therefore the normal a.href does not work. We use a click event // for now, but this should be fixed. - window.open(poweredBy.href, poweredBy.target) + window.open(poweredBy.href, poweredBy.target, 'noopener') } this.menu.appendChild(poweredBy) } @@ -484,7 +484,7 @@ textmode._showTransformModal = function () { const json = this.get() showTransformModal({ - anchor: modalAnchor || DEFAULT_MODAL_ANCHOR, + container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, diff --git a/src/js/treemode.js b/src/js/treemode.js index 10f7adc..4cbfb8c 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -25,7 +25,8 @@ import { repair, selectContentEditable, setSelectionOffset, - isValidationErrorChanged + isValidationErrorChanged, + getWindow } from './util' import { autocomplete } from './autocomplete' import { setLanguage, setLanguages, translate } from './i18n' @@ -136,7 +137,7 @@ treemode._setOptions = function (options) { // when there is not enough space below, and there is enough space above const pickerHeight = 300 // estimated height of the color picker const top = parent.getBoundingClientRect().top - const windowHeight = window.innerHeight + const windowHeight = getWindow(parent).innerHeight const showOnTop = ((windowHeight - top) < pickerHeight && top > pickerHeight) new VanillaPicker({ @@ -1280,7 +1281,7 @@ treemode._updateDragDistance = function (event) { /** * Start multi selection of nodes by dragging the mouse - * @param event + * @param {MouseEvent} event * @private */ treemode._onMultiSelectStart = function (event) { @@ -1302,12 +1303,12 @@ treemode._onMultiSelectStart = function (event) { const editor = this if (!this.mousemove) { - this.mousemove = addEventListener(window, 'mousemove', event => { + this.mousemove = addEventListener(event.view, 'mousemove', event => { editor._onMultiSelect(event) }) } if (!this.mouseup) { - this.mouseup = addEventListener(window, 'mouseup', event => { + this.mouseup = addEventListener(event.view, 'mouseup', event => { editor._onMultiSelectEnd(event) }) } @@ -1317,7 +1318,7 @@ treemode._onMultiSelectStart = function (event) { /** * Multiselect nodes by dragging - * @param event + * @param {MouseEvent} event * @private */ treemode._onMultiSelect = function (event) { @@ -1360,9 +1361,10 @@ treemode._onMultiSelect = function (event) { /** * End of multiselect nodes by dragging + * @param {MouseEvent} event * @private */ -treemode._onMultiSelectEnd = function () { +treemode._onMultiSelectEnd = function (event) { // set focus to the context menu button of the first node if (this.multiselection.nodes[0]) { this.multiselection.nodes[0].dom.menu.focus() @@ -1373,11 +1375,11 @@ treemode._onMultiSelectEnd = function () { // cleanup global event listeners if (this.mousemove) { - removeEventListener(window, 'mousemove', this.mousemove) + removeEventListener(event.view, 'mousemove', this.mousemove) delete this.mousemove } if (this.mouseup) { - removeEventListener(window, 'mouseup', this.mouseup) + removeEventListener(event.view, 'mouseup', this.mouseup) delete this.mouseup } } diff --git a/src/js/util.js b/src/js/util.js index 65dbdf5..bc02850 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -455,6 +455,16 @@ export function isArray (obj) { return Object.prototype.toString.call(obj) === '[object Array]' } +/** + * Gets a DOM element's Window. This is normally just the global `window` + * variable, but if we opened a child window, it may be different. + * @param {HTMLElement} element + * @return {Window} + */ +export function getWindow (element) { + return element.ownerDocument.defaultView; +} + /** * Retrieve the absolute left value of a DOM element * @param {Element} elem A dom element, for example a div @@ -789,7 +799,7 @@ export function isFirefox () { var _ieVersion = -1 /** - * Add and event listener. Works for all browsers + * Add an event listener. Works for all browsers * @param {Element} element An html element * @param {string} action The action, for example "click", * without the prefix "on" @@ -1139,7 +1149,7 @@ export function getInputSelection (el) { } /** - * Returns the index for certaion position in text element + * Returns the index for certain position in text element * @param {DOMElement} el A dom element of a textarea or input text. * @param {Number} row row value, > 0, if exceeds rows number - last row will be returned * @param {Number} column column value, > 0, if exceeds column length - end of column will be returned @@ -1499,7 +1509,7 @@ export function contains (array, item) { } /** - * Checkes if validation has changed from the previous execution + * Checks if validation has changed from the previous execution * @param {Array} currErr current validation errors * @param {Array} prevErr previous validation errors */