diff --git a/src/js/FocusTracker.js b/src/js/FocusTracker.js new file mode 100644 index 0000000..453f8bb --- /dev/null +++ b/src/js/FocusTracker.js @@ -0,0 +1,100 @@ +'use strict' + +/** + * @constructor FocusTracker + * A custom focus tracker for a DOM element with complex internal DOM structure + * @param {[Object]} config A set of configurations for the FocusTracker + * {DOM Object} target * The DOM object to track (required) + * {Function} onFocus onFocus callback + * {Function} onBlur onBlur callback + * + * @return + */ + +export class FocusTracker { + constructor (config) { + this.target = config.target || null + if (!this.target) { + throw new Error('FocusTracker constructor called without a "target" to track.') + } + + this.onFocus = (typeof config.onFocus === 'function') ? config.onFocus : null + this.onBlur = (typeof config.onBlur === 'function') ? config.onBlur : null + this._onClick = this._onEvent.bind(this) + this._onKeyUp = function (event) { + if (event.which === 9 || event.keyCode === 9) { + this._onEvent(event) + } + }.bind(this) + + this.focusFlag = false + this.firstEventFlag = true + + /* + Adds required (click and keyup) event listeners to the 'document' object + to track the focus of the given 'target' + */ + if (this.onFocus || this.onBlur) { + document.addEventListener('click', this._onClick) + document.addEventListener('keyup', this._onKeyUp) + } + } + + /** + * Removes the event listeners on the 'document' object + * that were added to track the focus of the given 'target' + */ + destroy () { + document.removeEventListener('click', this._onClick) + document.removeEventListener('keyup', this._onKeyUp) + this._onEvent({ target: document.body }) // calling _onEvent with body element in the hope that the FocusTracker is added to an element inside the body tag + } + + /** + * Tracks the focus of the target and calls the onFocus and onBlur + * event callbacks if available. + * @param {Event} [event] The 'click' or 'keyup' event object, + * from the respective events set on + * document object + * @private + */ + + _onEvent (event) { + const target = event.target + let focusFlag + if (target === this.target) { + focusFlag = true + } else if (this.target.contains(target) || this.target.contains(document.activeElement)) { + focusFlag = true + } else { + focusFlag = false + } + + if (focusFlag) { + if (!this.focusFlag) { + // trigger the onFocus callback + if (this.onFocus) { + this.onFocus({ type: 'focus', target: this.target }) + } + this.focusFlag = true + } + } else { + if (this.focusFlag || this.firstEventFlag) { + // trigger the onBlur callback + if (this.onBlur) { + this.onBlur({ type: 'blur', target: this.target }) + } + this.focusFlag = false + + /* + When switching from one mode to another in the editor, the FocusTracker gets recreated. + At that time, this.focusFlag will be init to 'false' and will fail the above if condition, when blur occurs + this.firstEventFlag is added to overcome that issue + */ + if (this.firstEventFlag) { + this.firstEventFlag = false + } + } + } + } +} diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index 5de4c73..a11fc51 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -77,6 +77,14 @@ if (typeof Promise === 'undefined') { * Only applicable for * modes 'form', 'tree' and * 'view' + * {function} onFocus Callback method, triggered + * when the editor comes into focus, + * passing an object {type, target}, + * Applicable for all modes + * {function} onBlur Callback method, triggered + * when the editor goes out of focus, + * passing an object {type, target}, + * Applicable for all modes * {function} onClassName Callback method, triggered * when a Node DOM is rendered. Function returns * a css class name to be set on a node. @@ -170,6 +178,7 @@ JSONEditor.VALID_OPTIONS = [ 'onChange', 'onChangeJSON', 'onChangeText', 'onEditable', 'onError', 'onEvent', 'onModeChange', 'onNodeName', 'onValidate', 'onCreateMenu', 'onSelectionChange', 'onTextSelectionChange', 'onClassName', + 'onFocus', 'onBlur', 'colorPicker', 'onColorPicker', 'timestampTag', 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', diff --git a/src/js/previewmode.js b/src/js/previewmode.js index 42330c0..e61d02d 100644 --- a/src/js/previewmode.js +++ b/src/js/previewmode.js @@ -8,6 +8,7 @@ import { showSortModal } from './showSortModal' import { showTransformModal } from './showTransformModal' import { textModeMixins } from './textmode' import { DEFAULT_MODAL_ANCHOR, MAX_PREVIEW_CHARACTERS, PREVIEW_HISTORY_LIMIT, SIZE_LARGE } from './constants' +import { FocusTracker } from './FocusTracker' import { addClassName, debounce, @@ -78,6 +79,15 @@ previewmode.create = function (container, options = {}) { event.preventDefault() } + // setting the FocusTracker on 'this.frame' to track the editor's focus event + const focusTrackerConfig = { + target: this.frame, + onFocus: this.options.onFocus || null, + onBlur: this.options.onBlur || null + } + + this.frameFocusTracker = new FocusTracker(focusTrackerConfig) + this.content = document.createElement('div') this.content.className = 'jsoneditor-outer' @@ -408,6 +418,9 @@ previewmode.destroy = function () { this.history.clear() this.history = null + + // Removing the FocusTracker set to track the editor's focus event + this.frameFocusTracker.destroy() } /** diff --git a/src/js/textmode.js b/src/js/textmode.js index cda9ef5..8300726 100644 --- a/src/js/textmode.js +++ b/src/js/textmode.js @@ -8,6 +8,7 @@ import { ErrorTable } from './ErrorTable' import { validateCustom } from './validationUtils' import { showSortModal } from './showSortModal' import { showTransformModal } from './showTransformModal' +import { FocusTracker } from './FocusTracker' import { addClassName, debounce, @@ -144,6 +145,15 @@ textmode.create = function (container, options = {}) { } } + // setting the FocusTracker on 'this.frame' to track the editor's focus event + const focusTrackerConfig = { + target: this.frame, + onFocus: this.options.onFocus || null, + onBlur: this.options.onBlur || null + } + + this.frameFocusTracker = new FocusTracker(focusTrackerConfig) + // create sort button if (this.options.enableSort) { const sort = document.createElement('button') @@ -599,6 +609,9 @@ textmode.destroy = function () { this.textarea = null this._debouncedValidate = null + + // Removing the FocusTracker set to track the editor's focus event + this.frameFocusTracker.destroy() } /** diff --git a/src/js/treemode.js b/src/js/treemode.js index f64c5d1..6356d73 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -8,6 +8,7 @@ import { ContextMenu } from './ContextMenu' import { TreePath } from './TreePath' import { Node } from './Node' import { ModeSwitcher } from './ModeSwitcher' +import { FocusTracker } from './FocusTracker' import { addClassName, addEventListener, @@ -103,6 +104,9 @@ treemode.destroy = function () { this.modeSwitcher.destroy() this.modeSwitcher = null } + + // Removing the FocusTracker set to track the editor's focus event + this.frameFocusTracker.destroy() } /** @@ -888,6 +892,8 @@ treemode._createFrame = function () { // create the frame this.frame = document.createElement('div') this.frame.className = 'jsoneditor jsoneditor-mode-' + this.options.mode + // this.frame.setAttribute("tabindex","0"); + this.container.appendChild(this.frame) this.contentOuter = document.createElement('div') @@ -902,6 +908,16 @@ treemode._createFrame = function () { editor._onEvent(event) } } + + // setting the FocusTracker on 'this.frame' to track the editor's focus event + const focusTrackerConfig = { + target: this.frame, + onFocus: this.options.onFocus || null, + onBlur: this.options.onBlur || null + } + + this.frameFocusTracker = new FocusTracker(focusTrackerConfig) + this.frame.onclick = event => { const target = event.target// || event.srcElement; @@ -1498,12 +1514,21 @@ treemode._onKeyDown = function (event) { const metaKey = event.metaKey const shiftKey = event.shiftKey let handled = false + const currentTarget = this.focusTarget if (keynum === 9) { // Tab or Shift+Tab const me = this setTimeout(() => { - // select all text when moving focus to an editable div - selectContentEditable(me.focusTarget) + /* + - Checking for change in focusTarget + - Without the check, + pressing tab after reaching the final DOM element in the editor will + set the focus back to it than passing focus outside the editor + */ + if (me.focusTarget !== currentTarget) { + // select all text when moving focus to an editable div + selectContentEditable(me.focusTarget) + } }, 0) } diff --git a/test/test_build.html b/test/test_build.html index 95e468f..3e861db 100644 --- a/test/test_build.html +++ b/test/test_build.html @@ -57,6 +57,12 @@ onChangeText: function (text) { console.log('onChangeText', text); }, + onFocus: function(event) { + console.log("Focus : ",event); + }, + onBlur: function(event) { + console.log("Blur : ",event); + }, indentation: 4, escapeUnicode: true }; diff --git a/test/test_focus_tracker.html b/test/test_focus_tracker.html new file mode 100644 index 0000000..72bc259 --- /dev/null +++ b/test/test_focus_tracker.html @@ -0,0 +1,100 @@ + + +
+ + + + + + + + + +
+ Switch editor mode using the mode box.
+ Note that the mode can be changed programmatically as well using the method
+ editor.setMode(mode)
, try it in the console of your browser.
+