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.
This commit is contained in:
parent
f3694c3126
commit
69fbe63f0e
|
@ -36,7 +36,8 @@
|
||||||
'number': 123,
|
'number': 123,
|
||||||
'object': {'a': 'b', 'c': 'd'},
|
'object': {'a': 'b', 'c': 'd'},
|
||||||
'time': 1575599819000,
|
'time': 1575599819000,
|
||||||
'string': 'Hello World'
|
'string': 'Hello World',
|
||||||
|
'onlineDemo': 'https://jsoneditoronline.org/'
|
||||||
}
|
}
|
||||||
editor.set(json)
|
editor.set(json)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>JSONEditor | New window</title>
|
||||||
|
|
||||||
|
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||||
|
<script src="../dist/jsoneditor.js"></script>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
#jsoneditor {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
<button id="openNewEditor">Open Editor in New Window</button>
|
||||||
|
<button id="setJSON">Set JSON</button>
|
||||||
|
<button id="getJSON">Get JSON</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let editor
|
||||||
|
|
||||||
|
function openNewEditor() {
|
||||||
|
const child = window.open("", "_blank", "width=400,height=400")
|
||||||
|
child.document.title = 'JSONEditor | New window'
|
||||||
|
child.onunload = function () {
|
||||||
|
editor = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the necessary styles available within the child window
|
||||||
|
// for JSONEditor
|
||||||
|
const baseUrl = window.location.href.slice(0, window.location.href.lastIndexOf('/'))
|
||||||
|
const jsonEditorStyles = child.document.createElement("link")
|
||||||
|
jsonEditorStyles.setAttribute("href", baseUrl + "/../dist/jsoneditor.css")
|
||||||
|
jsonEditorStyles.setAttribute("rel", "stylesheet")
|
||||||
|
child.document.head.append(jsonEditorStyles)
|
||||||
|
// for vanilla-picker
|
||||||
|
const colorPickerStyles = JSONEditor.VanillaPicker.StyleElement.cloneNode(true)
|
||||||
|
child.document.head.append(colorPickerStyles)
|
||||||
|
|
||||||
|
const container = child.document.createElement("div")
|
||||||
|
child.document.body.append(container)
|
||||||
|
|
||||||
|
// create the editor
|
||||||
|
const options = {
|
||||||
|
// Show sort and transform modals in the child window, not the parent.
|
||||||
|
modalAnchor: child.document.body
|
||||||
|
}
|
||||||
|
editor = new JSONEditor(container, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new window
|
||||||
|
document.getElementById('openNewEditor').onclick = openNewEditor
|
||||||
|
|
||||||
|
// set json
|
||||||
|
document.getElementById('setJSON').onclick = function () {
|
||||||
|
if (!editor) {
|
||||||
|
openNewEditor()
|
||||||
|
}
|
||||||
|
const json = {
|
||||||
|
'array': [1, 2, 3],
|
||||||
|
'boolean': true,
|
||||||
|
'color': '#82b92c',
|
||||||
|
'null': null,
|
||||||
|
'number': 123,
|
||||||
|
'object': {'a': 'b', 'c': 'd'},
|
||||||
|
'time': 1575599819000,
|
||||||
|
'string': 'Hello World'
|
||||||
|
}
|
||||||
|
editor.set(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get json
|
||||||
|
document.getElementById('getJSON').onclick = function () {
|
||||||
|
if (!editor) {
|
||||||
|
alert('No editor is open')
|
||||||
|
} else {
|
||||||
|
const json = editor.get()
|
||||||
|
alert(JSON.stringify(json, null, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -2574,7 +2574,7 @@ export class Node {
|
||||||
// if read-only, we use the regular click behavior of an anchor
|
// if read-only, we use the regular click behavior of an anchor
|
||||||
if (isUrl(this.value)) {
|
if (isUrl(this.value)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
window.open(this.value, '_blank')
|
window.open(this.value, '_blank', 'noopener')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -2729,7 +2729,7 @@ export class Node {
|
||||||
if (target === this.dom.value) {
|
if (target === this.dom.value) {
|
||||||
if (!this.editable.value || event.ctrlKey) {
|
if (!this.editable.value || event.ctrlKey) {
|
||||||
if (isUrl(this.value)) {
|
if (isUrl(this.value)) {
|
||||||
window.open(this.value, '_blank')
|
window.open(this.value, '_blank', 'noopener')
|
||||||
handled = true
|
handled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3917,7 +3917,7 @@ export class Node {
|
||||||
const json = this.getValue()
|
const json = this.getValue()
|
||||||
|
|
||||||
showTransformModal({
|
showTransformModal({
|
||||||
anchor: modalAnchor || DEFAULT_MODAL_ANCHOR,
|
container: modalAnchor || DEFAULT_MODAL_ANCHOR,
|
||||||
json,
|
json,
|
||||||
queryDescription, // can be undefined
|
queryDescription, // can be undefined
|
||||||
createQuery,
|
createQuery,
|
||||||
|
@ -4116,13 +4116,13 @@ Node.onDragStart = (nodes, event) => {
|
||||||
const offsetY = getAbsoluteTop(draggedNode.dom.tr) - getAbsoluteTop(firstNode.dom.tr)
|
const offsetY = getAbsoluteTop(draggedNode.dom.tr) - getAbsoluteTop(firstNode.dom.tr)
|
||||||
|
|
||||||
if (!editor.mousemove) {
|
if (!editor.mousemove) {
|
||||||
editor.mousemove = addEventListener(window, 'mousemove', event => {
|
editor.mousemove = addEventListener(event.view, 'mousemove', event => {
|
||||||
Node.onDrag(nodes, event)
|
Node.onDrag(nodes, event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editor.mouseup) {
|
if (!editor.mouseup) {
|
||||||
editor.mouseup = addEventListener(window, 'mouseup', event => {
|
editor.mouseup = addEventListener(event.view, 'mouseup', event => {
|
||||||
Node.onDragEnd(nodes, event)
|
Node.onDragEnd(nodes, event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -4378,11 +4378,11 @@ Node.onDragEnd = (nodes, event) => {
|
||||||
delete editor.drag
|
delete editor.drag
|
||||||
|
|
||||||
if (editor.mousemove) {
|
if (editor.mousemove) {
|
||||||
removeEventListener(window, 'mousemove', editor.mousemove)
|
removeEventListener(event.view, 'mousemove', editor.mousemove)
|
||||||
delete editor.mousemove
|
delete editor.mousemove
|
||||||
}
|
}
|
||||||
if (editor.mouseup) {
|
if (editor.mouseup) {
|
||||||
removeEventListener(window, 'mouseup', editor.mouseup)
|
removeEventListener(event.view, 'mouseup', editor.mouseup)
|
||||||
delete editor.mouseup
|
delete editor.mouseup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -398,7 +398,7 @@ previewmode._showTransformModal = function () {
|
||||||
this._renderPreview() // update array count
|
this._renderPreview() // update array count
|
||||||
|
|
||||||
showTransformModal({
|
showTransformModal({
|
||||||
anchor: modalAnchor || DEFAULT_MODAL_ANCHOR,
|
container: modalAnchor || DEFAULT_MODAL_ANCHOR,
|
||||||
json,
|
json,
|
||||||
queryDescription, // can be undefined
|
queryDescription, // can be undefined
|
||||||
createQuery,
|
createQuery,
|
||||||
|
|
|
@ -244,7 +244,7 @@ textmode.create = function (container, options = {}) {
|
||||||
// TODO: this anchor falls below the margin of the content,
|
// TODO: this anchor falls below the margin of the content,
|
||||||
// therefore the normal a.href does not work. We use a click event
|
// therefore the normal a.href does not work. We use a click event
|
||||||
// for now, but this should be fixed.
|
// for now, but this should be fixed.
|
||||||
window.open(poweredBy.href, poweredBy.target)
|
window.open(poweredBy.href, poweredBy.target, 'noopener')
|
||||||
}
|
}
|
||||||
this.menu.appendChild(poweredBy)
|
this.menu.appendChild(poweredBy)
|
||||||
}
|
}
|
||||||
|
@ -484,7 +484,7 @@ textmode._showTransformModal = function () {
|
||||||
const json = this.get()
|
const json = this.get()
|
||||||
|
|
||||||
showTransformModal({
|
showTransformModal({
|
||||||
anchor: modalAnchor || DEFAULT_MODAL_ANCHOR,
|
container: modalAnchor || DEFAULT_MODAL_ANCHOR,
|
||||||
json,
|
json,
|
||||||
queryDescription, // can be undefined
|
queryDescription, // can be undefined
|
||||||
createQuery,
|
createQuery,
|
||||||
|
|
|
@ -25,7 +25,8 @@ import {
|
||||||
repair,
|
repair,
|
||||||
selectContentEditable,
|
selectContentEditable,
|
||||||
setSelectionOffset,
|
setSelectionOffset,
|
||||||
isValidationErrorChanged
|
isValidationErrorChanged,
|
||||||
|
getWindow
|
||||||
} from './util'
|
} from './util'
|
||||||
import { autocomplete } from './autocomplete'
|
import { autocomplete } from './autocomplete'
|
||||||
import { setLanguage, setLanguages, translate } from './i18n'
|
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
|
// when there is not enough space below, and there is enough space above
|
||||||
const pickerHeight = 300 // estimated height of the color picker
|
const pickerHeight = 300 // estimated height of the color picker
|
||||||
const top = parent.getBoundingClientRect().top
|
const top = parent.getBoundingClientRect().top
|
||||||
const windowHeight = window.innerHeight
|
const windowHeight = getWindow(parent).innerHeight
|
||||||
const showOnTop = ((windowHeight - top) < pickerHeight && top > pickerHeight)
|
const showOnTop = ((windowHeight - top) < pickerHeight && top > pickerHeight)
|
||||||
|
|
||||||
new VanillaPicker({
|
new VanillaPicker({
|
||||||
|
@ -1280,7 +1281,7 @@ treemode._updateDragDistance = function (event) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start multi selection of nodes by dragging the mouse
|
* Start multi selection of nodes by dragging the mouse
|
||||||
* @param event
|
* @param {MouseEvent} event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
treemode._onMultiSelectStart = function (event) {
|
treemode._onMultiSelectStart = function (event) {
|
||||||
|
@ -1302,12 +1303,12 @@ treemode._onMultiSelectStart = function (event) {
|
||||||
|
|
||||||
const editor = this
|
const editor = this
|
||||||
if (!this.mousemove) {
|
if (!this.mousemove) {
|
||||||
this.mousemove = addEventListener(window, 'mousemove', event => {
|
this.mousemove = addEventListener(event.view, 'mousemove', event => {
|
||||||
editor._onMultiSelect(event)
|
editor._onMultiSelect(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!this.mouseup) {
|
if (!this.mouseup) {
|
||||||
this.mouseup = addEventListener(window, 'mouseup', event => {
|
this.mouseup = addEventListener(event.view, 'mouseup', event => {
|
||||||
editor._onMultiSelectEnd(event)
|
editor._onMultiSelectEnd(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1317,7 +1318,7 @@ treemode._onMultiSelectStart = function (event) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multiselect nodes by dragging
|
* Multiselect nodes by dragging
|
||||||
* @param event
|
* @param {MouseEvent} event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
treemode._onMultiSelect = function (event) {
|
treemode._onMultiSelect = function (event) {
|
||||||
|
@ -1360,9 +1361,10 @@ treemode._onMultiSelect = function (event) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End of multiselect nodes by dragging
|
* End of multiselect nodes by dragging
|
||||||
|
* @param {MouseEvent} event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
treemode._onMultiSelectEnd = function () {
|
treemode._onMultiSelectEnd = function (event) {
|
||||||
// set focus to the context menu button of the first node
|
// set focus to the context menu button of the first node
|
||||||
if (this.multiselection.nodes[0]) {
|
if (this.multiselection.nodes[0]) {
|
||||||
this.multiselection.nodes[0].dom.menu.focus()
|
this.multiselection.nodes[0].dom.menu.focus()
|
||||||
|
@ -1373,11 +1375,11 @@ treemode._onMultiSelectEnd = function () {
|
||||||
|
|
||||||
// cleanup global event listeners
|
// cleanup global event listeners
|
||||||
if (this.mousemove) {
|
if (this.mousemove) {
|
||||||
removeEventListener(window, 'mousemove', this.mousemove)
|
removeEventListener(event.view, 'mousemove', this.mousemove)
|
||||||
delete this.mousemove
|
delete this.mousemove
|
||||||
}
|
}
|
||||||
if (this.mouseup) {
|
if (this.mouseup) {
|
||||||
removeEventListener(window, 'mouseup', this.mouseup)
|
removeEventListener(event.view, 'mouseup', this.mouseup)
|
||||||
delete this.mouseup
|
delete this.mouseup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -455,6 +455,16 @@ export function isArray (obj) {
|
||||||
return Object.prototype.toString.call(obj) === '[object Array]'
|
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
|
* Retrieve the absolute left value of a DOM element
|
||||||
* @param {Element} elem A dom element, for example a div
|
* @param {Element} elem A dom element, for example a div
|
||||||
|
@ -789,7 +799,7 @@ export function isFirefox () {
|
||||||
var _ieVersion = -1
|
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 {Element} element An html element
|
||||||
* @param {string} action The action, for example "click",
|
* @param {string} action The action, for example "click",
|
||||||
* without the prefix "on"
|
* 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 {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} 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
|
* @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} currErr current validation errors
|
||||||
* @param {Array} prevErr previous validation errors
|
* @param {Array} prevErr previous validation errors
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue