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,
|
||||
'object': {'a': 'b', 'c': 'd'},
|
||||
'time': 1575599819000,
|
||||
'string': 'Hello World'
|
||||
'string': 'Hello World',
|
||||
'onlineDemo': 'https://jsoneditoronline.org/'
|
||||
}
|
||||
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 (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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue