bee_api 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/bin/bee_api +84 -0
  2. data/lib/mdpreview.rb +80 -0
  3. data/lib/mdpreview/translator.rb +60 -0
  4. data/lib/mdpreview/version.rb +3 -0
  5. data/test/mdptest.rb +100 -0
  6. data/vendor/HISTORY.md +237 -0
  7. data/vendor/Jakefile.js +316 -0
  8. data/vendor/LICENSE +176 -0
  9. data/vendor/NOTICE +17 -0
  10. data/vendor/README.md +102 -0
  11. data/vendor/app/chrome/documentation.txt +12 -0
  12. data/vendor/app/chrome/manifest.json +24 -0
  13. data/vendor/app/web/ajax.js +43 -0
  14. data/vendor/app/web/app.css +292 -0
  15. data/vendor/app/web/app.js +377 -0
  16. data/vendor/app/web/beta/index.html +17 -0
  17. data/vendor/app/web/datapolicy.txt +48 -0
  18. data/vendor/app/web/doc/doc.css +60 -0
  19. data/vendor/app/web/doc/img/actions_menu.png +0 -0
  20. data/vendor/app/web/doc/img/button_actions_menu.png +0 -0
  21. data/vendor/app/web/doc/img/button_dragarea.png +0 -0
  22. data/vendor/app/web/doc/img/jsoneditor.png +0 -0
  23. data/vendor/app/web/doc/img/jsonformatter.png +0 -0
  24. data/vendor/app/web/doc/img/main_menu.png +0 -0
  25. data/vendor/app/web/doc/img/splitter.png +0 -0
  26. data/vendor/app/web/doc/index.html +201 -0
  27. data/vendor/app/web/favicon.ico +0 -0
  28. data/vendor/app/web/fileretriever.css +54 -0
  29. data/vendor/app/web/fileretriever.js +567 -0
  30. data/vendor/app/web/fileretriever.php +120 -0
  31. data/vendor/app/web/googlea47c4a0b36d11021.html +1 -0
  32. data/vendor/app/web/hash.js +133 -0
  33. data/vendor/app/web/img/description.txt +20 -0
  34. data/vendor/app/web/img/header_background.png +0 -0
  35. data/vendor/app/web/img/icon_128.png +0 -0
  36. data/vendor/app/web/img/icon_16.png +0 -0
  37. data/vendor/app/web/img/icon_gray.svg +151 -0
  38. data/vendor/app/web/img/icon_gray_16.svg +150 -0
  39. data/vendor/app/web/img/icon_orange.svg +151 -0
  40. data/vendor/app/web/img/logo.png +0 -0
  41. data/vendor/app/web/img/logo.xcf +0 -0
  42. data/vendor/app/web/img/logo_app.png +0 -0
  43. data/vendor/app/web/img/logo_app.xcf +0 -0
  44. data/vendor/app/web/index.html +191 -0
  45. data/vendor/app/web/notify.js +150 -0
  46. data/vendor/app/web/queryparams.js +71 -0
  47. data/vendor/app/web/robots.txt +0 -0
  48. data/vendor/app/web/splitter.js +179 -0
  49. data/vendor/app/web/test.html +224 -0
  50. data/vendor/component.json +33 -0
  51. data/vendor/docs/api.md +188 -0
  52. data/vendor/docs/usage.md +137 -0
  53. data/vendor/examples/01_basic_usage.html +45 -0
  54. data/vendor/examples/02_viewer.html +38 -0
  55. data/vendor/examples/03_switch_mode.html +98 -0
  56. data/vendor/examples/cur.file +1 -0
  57. data/vendor/examples/jquery.js +2 -0
  58. data/vendor/examples/meta.js +1 -0
  59. data/vendor/examples/requirejs_demo/requirejs_demo.html +19 -0
  60. data/vendor/examples/requirejs_demo/scripts/main.js +25 -0
  61. data/vendor/examples/requirejs_demo/scripts/require.js +35 -0
  62. data/vendor/img/jsoneditor-icons.png +0 -0
  63. data/vendor/jsoneditor-min.css +1 -0
  64. data/vendor/jsoneditor-min.js +34 -0
  65. data/vendor/jsoneditor.css +597 -0
  66. data/vendor/jsoneditor.js +6069 -0
  67. data/vendor/jsoneditor/css/contextmenu.css +219 -0
  68. data/vendor/jsoneditor/css/img/description.txt +13 -0
  69. data/vendor/jsoneditor/css/img/export.sh +16 -0
  70. data/vendor/jsoneditor/css/img/jsoneditor-icons.png +0 -0
  71. data/vendor/jsoneditor/css/img/jsoneditor-icons.svg +861 -0
  72. data/vendor/jsoneditor/css/jsoneditor.css +220 -0
  73. data/vendor/jsoneditor/css/menu.css +81 -0
  74. data/vendor/jsoneditor/css/searchbox.css +73 -0
  75. data/vendor/jsoneditor/js/appendnode.js +211 -0
  76. data/vendor/jsoneditor/js/contextmenu.js +440 -0
  77. data/vendor/jsoneditor/js/header.js +32 -0
  78. data/vendor/jsoneditor/js/highlighter.js +82 -0
  79. data/vendor/jsoneditor/js/history.js +218 -0
  80. data/vendor/jsoneditor/js/jsoneditor.js +206 -0
  81. data/vendor/jsoneditor/js/module.js +50 -0
  82. data/vendor/jsoneditor/js/node.js +2864 -0
  83. data/vendor/jsoneditor/js/searchbox.js +288 -0
  84. data/vendor/jsoneditor/js/texteditor.js +311 -0
  85. data/vendor/jsoneditor/js/treeeditor.js +770 -0
  86. data/vendor/jsoneditor/js/util.js +582 -0
  87. data/vendor/lib/ace/ace.js +11 -0
  88. data/vendor/lib/ace/mode-json.js +1 -0
  89. data/vendor/lib/ace/theme-jsoneditor.js +144 -0
  90. data/vendor/lib/ace/theme-textmate.js +163 -0
  91. data/vendor/lib/ace/worker-json.js +1 -0
  92. data/vendor/lib/jsonlint/README.md +62 -0
  93. data/vendor/lib/jsonlint/jsonlint.js +432 -0
  94. data/vendor/misc/screenshots/actionsmenu_640x400.png +0 -0
  95. data/vendor/misc/screenshots/codeeditor_640x400.png +0 -0
  96. data/vendor/misc/screenshots/description.json +17 -0
  97. data/vendor/misc/screenshots/jsoneditoronline.png +0 -0
  98. data/vendor/misc/screenshots/jsoneditoronline_640x400.png +0 -0
  99. data/vendor/misc/screenshots/search_640x400.png +0 -0
  100. data/vendor/misc/screenshots/small_tile.xcf +0 -0
  101. data/vendor/misc/screenshots/small_tile_440x280.png +0 -0
  102. data/vendor/misc/todo.txt +101 -0
  103. data/vendor/package.json +28 -0
  104. data/vendor/test/couchdbeditor.html +100 -0
  105. data/vendor/test/largefile.json +12605 -0
  106. data/vendor/test/test_ace.html +60 -0
  107. data/vendor/test/test_editable_div.html +449 -0
  108. metadata +154 -0
@@ -0,0 +1,50 @@
1
+
2
+ // module exports
3
+ var jsoneditor = {
4
+ 'JSONEditor': JSONEditor,
5
+ 'JSONFormatter': function () {
6
+ throw new Error('JSONFormatter is deprecated. ' +
7
+ 'Use JSONEditor with mode "text" or "code" instead');
8
+ },
9
+ 'util': util
10
+ };
11
+
12
+ /**
13
+ * load jsoneditor.css
14
+ */
15
+ var loadCss = function () {
16
+ // get the script location, and built the css file name from the js file name
17
+ // http://stackoverflow.com/a/2161748/1262753
18
+ var scripts = document.getElementsByTagName('script');
19
+ var jsFile = scripts[scripts.length-1].src.split('?')[0];
20
+ var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
21
+
22
+ // load css
23
+ var link = document.createElement('link');
24
+ link.type = 'text/css';
25
+ link.rel = 'stylesheet';
26
+ link.href = cssFile;
27
+ document.getElementsByTagName('head')[0].appendChild(link);
28
+ };
29
+
30
+ /**
31
+ * CommonJS module exports
32
+ */
33
+ if (typeof(module) != 'undefined' && typeof(exports) != 'undefined') {
34
+ loadCss();
35
+ module.exports = exports = jsoneditor;
36
+ }
37
+
38
+ /**
39
+ * AMD module exports
40
+ */
41
+ if (typeof(require) != 'undefined' && typeof(define) != 'undefined') {
42
+ define(function () {
43
+ loadCss();
44
+ return jsoneditor;
45
+ });
46
+ }
47
+ else {
48
+ // attach the module to the window, load as a regular javascript file
49
+ window['jsoneditor'] = jsoneditor;
50
+ }
@@ -0,0 +1,2864 @@
1
+ /**
2
+ * @constructor Node
3
+ * Create a new Node
4
+ * @param {TreeEditor} editor
5
+ * @param {Object} [params] Can contain parameters:
6
+ * {string} field
7
+ * {boolean} fieldEditable
8
+ * {*} value
9
+ * {String} type Can have values 'auto', 'array',
10
+ * 'object', or 'string'.
11
+ */
12
+ function Node (editor, params) {
13
+ /** @type {TreeEditor} */
14
+ this.editor = editor;
15
+ this.dom = {};
16
+ this.expanded = false;
17
+
18
+ if(params && (params instanceof Object)) {
19
+ this.setField(params.field, params.fieldEditable);
20
+ this.setValue(params.value, params.type);
21
+ }
22
+ else {
23
+ this.setField('');
24
+ this.setValue(null);
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Set parent node
30
+ * @param {Node} parent
31
+ */
32
+ Node.prototype.setParent = function(parent) {
33
+ this.parent = parent;
34
+ };
35
+
36
+ /**
37
+ * Set field
38
+ * @param {String} field
39
+ * @param {boolean} [fieldEditable]
40
+ */
41
+ Node.prototype.setField = function(field, fieldEditable) {
42
+ this.field = field;
43
+ this.fieldEditable = (fieldEditable == true);
44
+ };
45
+
46
+ /**
47
+ * Get field
48
+ * @return {String}
49
+ */
50
+ Node.prototype.getField = function() {
51
+ if (this.field === undefined) {
52
+ this._getDomField();
53
+ }
54
+
55
+ return this.field;
56
+ };
57
+
58
+ /**
59
+ * Set value. Value is a JSON structure or an element String, Boolean, etc.
60
+ * @param {*} value
61
+ * @param {String} [type] Specify the type of the value. Can be 'auto',
62
+ * 'array', 'object', or 'string'
63
+ */
64
+ Node.prototype.setValue = function(value, type) {
65
+ var childValue, child;
66
+
67
+ // first clear all current childs (if any)
68
+ var childs = this.childs;
69
+ if (childs) {
70
+ while (childs.length) {
71
+ this.removeChild(childs[0]);
72
+ }
73
+ }
74
+
75
+ // TODO: remove the DOM of this Node
76
+
77
+ this.type = this._getType(value);
78
+
79
+ // check if type corresponds with the provided type
80
+ if (type && type != this.type) {
81
+ if (type == 'string' && this.type == 'auto') {
82
+ this.type = type;
83
+ }
84
+ else {
85
+ throw new Error('Type mismatch: ' +
86
+ 'cannot cast value of type "' + this.type +
87
+ ' to the specified type "' + type + '"');
88
+ }
89
+ }
90
+
91
+ if (this.type == 'array') {
92
+ // array
93
+ this.childs = [];
94
+ for (var i = 0, iMax = value.length; i < iMax; i++) {
95
+ childValue = value[i];
96
+ if (childValue !== undefined && !(childValue instanceof Function)) {
97
+ // ignore undefined and functions
98
+ child = new Node(this.editor, {
99
+ 'value': childValue
100
+ });
101
+ this.appendChild(child);
102
+ }
103
+ }
104
+ this.value = '';
105
+ }
106
+ else if (this.type == 'object') {
107
+ // object
108
+ this.childs = [];
109
+ for (var childField in value) {
110
+ if (value.hasOwnProperty(childField)) {
111
+ childValue = value[childField];
112
+ if (childValue !== undefined && !(childValue instanceof Function)) {
113
+ // ignore undefined and functions
114
+ child = new Node(this.editor, {
115
+ 'field': childField,
116
+ 'value': childValue
117
+ });
118
+ this.appendChild(child);
119
+ }
120
+ }
121
+ }
122
+ this.value = '';
123
+ }
124
+ else {
125
+ // value
126
+ this.childs = undefined;
127
+ this.value = value;
128
+ /* TODO
129
+ if (typeof(value) == 'string') {
130
+ var escValue = JSON.stringify(value);
131
+ this.value = escValue.substring(1, escValue.length - 1);
132
+ util.log('check', value, this.value);
133
+ }
134
+ else {
135
+ this.value = value;
136
+ }
137
+ */
138
+ }
139
+ };
140
+
141
+ /**
142
+ * Get value. Value is a JSON structure
143
+ * @return {*} value
144
+ */
145
+ Node.prototype.getValue = function() {
146
+ //var childs, i, iMax;
147
+
148
+ if (this.type == 'array') {
149
+ var arr = [];
150
+ this.childs.forEach (function (child) {
151
+ arr.push(child.getValue());
152
+ });
153
+ return arr;
154
+ }
155
+ else if (this.type == 'object') {
156
+ var obj = {};
157
+ this.childs.forEach (function (child) {
158
+ obj[child.getField()] = child.getValue();
159
+ });
160
+ return obj;
161
+ }
162
+ else {
163
+ if (this.value === undefined) {
164
+ this._getDomValue();
165
+ }
166
+
167
+ return this.value;
168
+ }
169
+ };
170
+
171
+ /**
172
+ * Get the nesting level of this node
173
+ * @return {Number} level
174
+ */
175
+ Node.prototype.getLevel = function() {
176
+ return (this.parent ? this.parent.getLevel() + 1 : 0);
177
+ };
178
+
179
+ /**
180
+ * Create a clone of a node
181
+ * The complete state of a clone is copied, including whether it is expanded or
182
+ * not. The DOM elements are not cloned.
183
+ * @return {Node} clone
184
+ */
185
+ Node.prototype.clone = function() {
186
+ var clone = new Node(this.editor);
187
+ clone.type = this.type;
188
+ clone.field = this.field;
189
+ clone.fieldInnerText = this.fieldInnerText;
190
+ clone.fieldEditable = this.fieldEditable;
191
+ clone.value = this.value;
192
+ clone.valueInnerText = this.valueInnerText;
193
+ clone.expanded = this.expanded;
194
+
195
+ if (this.childs) {
196
+ // an object or array
197
+ var cloneChilds = [];
198
+ this.childs.forEach(function (child) {
199
+ var childClone = child.clone();
200
+ childClone.setParent(clone);
201
+ cloneChilds.push(childClone);
202
+ });
203
+ clone.childs = cloneChilds;
204
+ }
205
+ else {
206
+ // a value
207
+ clone.childs = undefined;
208
+ }
209
+
210
+ return clone;
211
+ };
212
+
213
+ /**
214
+ * Expand this node and optionally its childs.
215
+ * @param {boolean} [recurse] Optional recursion, true by default. When
216
+ * true, all childs will be expanded recursively
217
+ */
218
+ Node.prototype.expand = function(recurse) {
219
+ if (!this.childs) {
220
+ return;
221
+ }
222
+
223
+ // set this node expanded
224
+ this.expanded = true;
225
+ if (this.dom.expand) {
226
+ this.dom.expand.className = 'expanded';
227
+ }
228
+
229
+ this.showChilds();
230
+
231
+ if (recurse != false) {
232
+ this.childs.forEach(function (child) {
233
+ child.expand(recurse);
234
+ });
235
+ }
236
+ };
237
+
238
+ /**
239
+ * Collapse this node and optionally its childs.
240
+ * @param {boolean} [recurse] Optional recursion, true by default. When
241
+ * true, all childs will be collapsed recursively
242
+ */
243
+ Node.prototype.collapse = function(recurse) {
244
+ if (!this.childs) {
245
+ return;
246
+ }
247
+
248
+ this.hideChilds();
249
+
250
+ // collapse childs in case of recurse
251
+ if (recurse != false) {
252
+ this.childs.forEach(function (child) {
253
+ child.collapse(recurse);
254
+ });
255
+
256
+ }
257
+
258
+ // make this node collapsed
259
+ if (this.dom.expand) {
260
+ this.dom.expand.className = 'collapsed';
261
+ }
262
+ this.expanded = false;
263
+ };
264
+
265
+ /**
266
+ * Recursively show all childs when they are expanded
267
+ */
268
+ Node.prototype.showChilds = function() {
269
+ var childs = this.childs;
270
+ if (!childs) {
271
+ return;
272
+ }
273
+ if (!this.expanded) {
274
+ return;
275
+ }
276
+
277
+ var tr = this.dom.tr;
278
+ var table = tr ? tr.parentNode : undefined;
279
+ if (table) {
280
+ // show row with append button
281
+ var append = this.getAppend();
282
+ var nextTr = tr.nextSibling;
283
+ if (nextTr) {
284
+ table.insertBefore(append, nextTr);
285
+ }
286
+ else {
287
+ table.appendChild(append);
288
+ }
289
+
290
+ // show childs
291
+ this.childs.forEach(function (child) {
292
+ table.insertBefore(child.getDom(), append);
293
+ child.showChilds();
294
+ });
295
+ }
296
+ };
297
+
298
+ /**
299
+ * Hide the node with all its childs
300
+ */
301
+ Node.prototype.hide = function() {
302
+ var tr = this.dom.tr;
303
+ var table = tr ? tr.parentNode : undefined;
304
+ if (table) {
305
+ table.removeChild(tr);
306
+ }
307
+ this.hideChilds();
308
+ };
309
+
310
+
311
+ /**
312
+ * Recursively hide all childs
313
+ */
314
+ Node.prototype.hideChilds = function() {
315
+ var childs = this.childs;
316
+ if (!childs) {
317
+ return;
318
+ }
319
+ if (!this.expanded) {
320
+ return;
321
+ }
322
+
323
+ // hide append row
324
+ var append = this.getAppend();
325
+ if (append.parentNode) {
326
+ append.parentNode.removeChild(append);
327
+ }
328
+
329
+ // hide childs
330
+ this.childs.forEach(function (child) {
331
+ child.hide();
332
+ });
333
+ };
334
+
335
+
336
+ /**
337
+ * Add a new child to the node.
338
+ * Only applicable when Node value is of type array or object
339
+ * @param {Node} node
340
+ */
341
+ Node.prototype.appendChild = function(node) {
342
+ if (this._hasChilds()) {
343
+ // adjust the link to the parent
344
+ node.setParent(this);
345
+ node.fieldEditable = (this.type == 'object');
346
+ if (this.type == 'array') {
347
+ node.index = this.childs.length;
348
+ }
349
+ this.childs.push(node);
350
+
351
+ if (this.expanded) {
352
+ // insert into the DOM, before the appendRow
353
+ var newTr = node.getDom();
354
+ var appendTr = this.getAppend();
355
+ var table = appendTr ? appendTr.parentNode : undefined;
356
+ if (appendTr && table) {
357
+ table.insertBefore(newTr, appendTr);
358
+ }
359
+
360
+ node.showChilds();
361
+ }
362
+
363
+ this.updateDom({'updateIndexes': true});
364
+ node.updateDom({'recurse': true});
365
+ }
366
+ };
367
+
368
+
369
+ /**
370
+ * Move a node from its current parent to this node
371
+ * Only applicable when Node value is of type array or object
372
+ * @param {Node} node
373
+ * @param {Node} beforeNode
374
+ */
375
+ Node.prototype.moveBefore = function(node, beforeNode) {
376
+ if (this._hasChilds()) {
377
+ // create a temporary row, to prevent the scroll position from jumping
378
+ // when removing the node
379
+ var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;
380
+ if (tbody) {
381
+ var trTemp = document.createElement('tr');
382
+ trTemp.style.height = tbody.clientHeight + 'px';
383
+ tbody.appendChild(trTemp);
384
+ }
385
+
386
+ if (node.parent) {
387
+ node.parent.removeChild(node);
388
+ }
389
+
390
+ if (beforeNode instanceof AppendNode) {
391
+ this.appendChild(node);
392
+ }
393
+ else {
394
+ this.insertBefore(node, beforeNode);
395
+ }
396
+
397
+ if (tbody) {
398
+ tbody.removeChild(trTemp);
399
+ }
400
+ }
401
+ };
402
+
403
+ /**
404
+ * Move a node from its current parent to this node
405
+ * Only applicable when Node value is of type array or object.
406
+ * If index is out of range, the node will be appended to the end
407
+ * @param {Node} node
408
+ * @param {Number} index
409
+ */
410
+ Node.prototype.moveTo = function (node, index) {
411
+ if (node.parent == this) {
412
+ // same parent
413
+ var currentIndex = this.childs.indexOf(node);
414
+ if (currentIndex < index) {
415
+ // compensate the index for removal of the node itself
416
+ index++;
417
+ }
418
+ }
419
+
420
+ var beforeNode = this.childs[index] || this.append;
421
+ this.moveBefore(node, beforeNode);
422
+ };
423
+
424
+ /**
425
+ * Insert a new child before a given node
426
+ * Only applicable when Node value is of type array or object
427
+ * @param {Node} node
428
+ * @param {Node} beforeNode
429
+ */
430
+ Node.prototype.insertBefore = function(node, beforeNode) {
431
+ if (this._hasChilds()) {
432
+ if (beforeNode == this.append) {
433
+ // append to the child nodes
434
+
435
+ // adjust the link to the parent
436
+ node.setParent(this);
437
+ node.fieldEditable = (this.type == 'object');
438
+ this.childs.push(node);
439
+ }
440
+ else {
441
+ // insert before a child node
442
+ var index = this.childs.indexOf(beforeNode);
443
+ if (index == -1) {
444
+ throw new Error('Node not found');
445
+ }
446
+
447
+ // adjust the link to the parent
448
+ node.setParent(this);
449
+ node.fieldEditable = (this.type == 'object');
450
+ this.childs.splice(index, 0, node);
451
+ }
452
+
453
+ if (this.expanded) {
454
+ // insert into the DOM
455
+ var newTr = node.getDom();
456
+ var nextTr = beforeNode.getDom();
457
+ var table = nextTr ? nextTr.parentNode : undefined;
458
+ if (nextTr && table) {
459
+ table.insertBefore(newTr, nextTr);
460
+ }
461
+
462
+ node.showChilds();
463
+ }
464
+
465
+ this.updateDom({'updateIndexes': true});
466
+ node.updateDom({'recurse': true});
467
+ }
468
+ };
469
+
470
+ /**
471
+ * Insert a new child before a given node
472
+ * Only applicable when Node value is of type array or object
473
+ * @param {Node} node
474
+ * @param {Node} afterNode
475
+ */
476
+ Node.prototype.insertAfter = function(node, afterNode) {
477
+ if (this._hasChilds()) {
478
+ var index = this.childs.indexOf(afterNode);
479
+ var beforeNode = this.childs[index + 1];
480
+ if (beforeNode) {
481
+ this.insertBefore(node, beforeNode);
482
+ }
483
+ else {
484
+ this.appendChild(node);
485
+ }
486
+ }
487
+ };
488
+
489
+ /**
490
+ * Search in this node
491
+ * The node will be expanded when the text is found one of its childs, else
492
+ * it will be collapsed. Searches are case insensitive.
493
+ * @param {String} text
494
+ * @return {Node[]} results Array with nodes containing the search text
495
+ */
496
+ Node.prototype.search = function(text) {
497
+ var results = [];
498
+ var index;
499
+ var search = text ? text.toLowerCase() : undefined;
500
+
501
+ // delete old search data
502
+ delete this.searchField;
503
+ delete this.searchValue;
504
+
505
+ // search in field
506
+ if (this.field != undefined) {
507
+ var field = String(this.field).toLowerCase();
508
+ index = field.indexOf(search);
509
+ if (index != -1) {
510
+ this.searchField = true;
511
+ results.push({
512
+ 'node': this,
513
+ 'elem': 'field'
514
+ });
515
+ }
516
+
517
+ // update dom
518
+ this._updateDomField();
519
+ }
520
+
521
+ // search in value
522
+ if (this._hasChilds()) {
523
+ // array, object
524
+
525
+ // search the nodes childs
526
+ if (this.childs) {
527
+ var childResults = [];
528
+ this.childs.forEach(function (child) {
529
+ childResults = childResults.concat(child.search(text));
530
+ });
531
+ results = results.concat(childResults);
532
+ }
533
+
534
+ // update dom
535
+ if (search != undefined) {
536
+ var recurse = false;
537
+ if (childResults.length == 0) {
538
+ this.collapse(recurse);
539
+ }
540
+ else {
541
+ this.expand(recurse);
542
+ }
543
+ }
544
+ }
545
+ else {
546
+ // string, auto
547
+ if (this.value != undefined ) {
548
+ var value = String(this.value).toLowerCase();
549
+ index = value.indexOf(search);
550
+ if (index != -1) {
551
+ this.searchValue = true;
552
+ results.push({
553
+ 'node': this,
554
+ 'elem': 'value'
555
+ });
556
+ }
557
+ }
558
+
559
+ // update dom
560
+ this._updateDomValue();
561
+ }
562
+
563
+ return results;
564
+ };
565
+
566
+ /**
567
+ * Move the scroll position such that this node is in the visible area.
568
+ * The node will not get the focus
569
+ * @param {function(boolean)} [callback]
570
+ */
571
+ Node.prototype.scrollTo = function(callback) {
572
+ if (!this.dom.tr || !this.dom.tr.parentNode) {
573
+ // if the node is not visible, expand its parents
574
+ var parent = this.parent;
575
+ var recurse = false;
576
+ while (parent) {
577
+ parent.expand(recurse);
578
+ parent = parent.parent;
579
+ }
580
+ }
581
+
582
+ if (this.dom.tr && this.dom.tr.parentNode) {
583
+ this.editor.scrollTo(this.dom.tr.offsetTop, callback);
584
+ }
585
+ };
586
+
587
+
588
+ // stores the element name currently having the focus
589
+ Node.focusElement = undefined;
590
+
591
+ /**
592
+ * Set focus to this node
593
+ * @param {String} [elementName] The field name of the element to get the
594
+ * focus available values: 'drag', 'menu',
595
+ * 'expand', 'field', 'value' (default)
596
+ */
597
+ Node.prototype.focus = function(elementName) {
598
+ Node.focusElement = elementName;
599
+
600
+ if (this.dom.tr && this.dom.tr.parentNode) {
601
+ var dom = this.dom;
602
+
603
+ switch (elementName) {
604
+ case 'drag':
605
+ if (dom.drag) {
606
+ dom.drag.focus();
607
+ }
608
+ else {
609
+ dom.menu.focus();
610
+ }
611
+ break;
612
+
613
+ case 'menu':
614
+ dom.menu.focus();
615
+ break;
616
+
617
+ case 'expand':
618
+ if (this._hasChilds()) {
619
+ dom.expand.focus();
620
+ }
621
+ else if (dom.field && this.fieldEditable) {
622
+ dom.field.focus();
623
+ util.selectContentEditable(dom.field);
624
+ }
625
+ else if (dom.value && !this._hasChilds()) {
626
+ dom.value.focus();
627
+ util.selectContentEditable(dom.value);
628
+ }
629
+ else {
630
+ dom.menu.focus();
631
+ }
632
+ break;
633
+
634
+ case 'field':
635
+ if (dom.field && this.fieldEditable) {
636
+ dom.field.focus();
637
+ util.selectContentEditable(dom.field);
638
+ }
639
+ else if (dom.value && !this._hasChilds()) {
640
+ dom.value.focus();
641
+ util.selectContentEditable(dom.value);
642
+ }
643
+ else if (this._hasChilds()) {
644
+ dom.expand.focus();
645
+ }
646
+ else {
647
+ dom.menu.focus();
648
+ }
649
+ break;
650
+
651
+ case 'value':
652
+ default:
653
+ if (dom.value && !this._hasChilds()) {
654
+ dom.value.focus();
655
+ util.selectContentEditable(dom.value);
656
+ }
657
+ else if (dom.field && this.fieldEditable) {
658
+ dom.field.focus();
659
+ util.selectContentEditable(dom.field);
660
+ }
661
+ else if (this._hasChilds()) {
662
+ dom.expand.focus();
663
+ }
664
+ else {
665
+ dom.menu.focus();
666
+ }
667
+ break;
668
+ }
669
+ }
670
+ };
671
+
672
+ /**
673
+ * Select all text in an editable div after a delay of 0 ms
674
+ * @param {Element} editableDiv
675
+ */
676
+ Node.select = function(editableDiv) {
677
+ setTimeout(function () {
678
+ util.selectContentEditable(editableDiv);
679
+ }, 0);
680
+ };
681
+
682
+ /**
683
+ * Update the values from the DOM field and value of this node
684
+ */
685
+ Node.prototype.blur = function() {
686
+ // retrieve the actual field and value from the DOM.
687
+ this._getDomValue(false);
688
+ this._getDomField(false);
689
+ };
690
+
691
+ /**
692
+ * Duplicate given child node
693
+ * new structure will be added right before the cloned node
694
+ * @param {Node} node the childNode to be duplicated
695
+ * @return {Node} clone the clone of the node
696
+ * @private
697
+ */
698
+ Node.prototype._duplicate = function(node) {
699
+ var clone = node.clone();
700
+
701
+ /* TODO: adjust the field name (to prevent equal field names)
702
+ if (this.type == 'object') {
703
+ }
704
+ */
705
+
706
+ this.insertAfter(clone, node);
707
+
708
+ return clone;
709
+ };
710
+
711
+ /**
712
+ * Check if given node is a child. The method will check recursively to find
713
+ * this node.
714
+ * @param {Node} node
715
+ * @return {boolean} containsNode
716
+ */
717
+ Node.prototype.containsNode = function(node) {
718
+ if (this == node) {
719
+ return true;
720
+ }
721
+
722
+ var childs = this.childs;
723
+ if (childs) {
724
+ // TODO: use the js5 Array.some() here?
725
+ for (var i = 0, iMax = childs.length; i < iMax; i++) {
726
+ if (childs[i].containsNode(node)) {
727
+ return true;
728
+ }
729
+ }
730
+ }
731
+
732
+ return false;
733
+ };
734
+
735
+ /**
736
+ * Move given node into this node
737
+ * @param {Node} node the childNode to be moved
738
+ * @param {Node} beforeNode node will be inserted before given
739
+ * node. If no beforeNode is given,
740
+ * the node is appended at the end
741
+ * @private
742
+ */
743
+ Node.prototype._move = function(node, beforeNode) {
744
+ if (node == beforeNode) {
745
+ // nothing to do...
746
+ return;
747
+ }
748
+
749
+ // check if this node is not a child of the node to be moved here
750
+ if (node.containsNode(this)) {
751
+ throw new Error('Cannot move a field into a child of itself');
752
+ }
753
+
754
+ // remove the original node
755
+ if (node.parent) {
756
+ node.parent.removeChild(node);
757
+ }
758
+
759
+ // create a clone of the node
760
+ var clone = node.clone();
761
+ node.clearDom();
762
+
763
+ // insert or append the node
764
+ if (beforeNode) {
765
+ this.insertBefore(clone, beforeNode);
766
+ }
767
+ else {
768
+ this.appendChild(clone);
769
+ }
770
+
771
+ /* TODO: adjust the field name (to prevent equal field names)
772
+ if (this.type == 'object') {
773
+ }
774
+ */
775
+ };
776
+
777
+ /**
778
+ * Remove a child from the node.
779
+ * Only applicable when Node value is of type array or object
780
+ * @param {Node} node The child node to be removed;
781
+ * @return {Node | undefined} node The removed node on success,
782
+ * else undefined
783
+ */
784
+ Node.prototype.removeChild = function(node) {
785
+ if (this.childs) {
786
+ var index = this.childs.indexOf(node);
787
+
788
+ if (index != -1) {
789
+ node.hide();
790
+
791
+ // delete old search results
792
+ delete node.searchField;
793
+ delete node.searchValue;
794
+
795
+ var removedNode = this.childs.splice(index, 1)[0];
796
+
797
+ this.updateDom({'updateIndexes': true});
798
+
799
+ return removedNode;
800
+ }
801
+ }
802
+
803
+ return undefined;
804
+ };
805
+
806
+ /**
807
+ * Remove a child node node from this node
808
+ * This method is equal to Node.removeChild, except that _remove firex an
809
+ * onChange event.
810
+ * @param {Node} node
811
+ * @private
812
+ */
813
+ Node.prototype._remove = function (node) {
814
+ this.removeChild(node);
815
+ };
816
+
817
+ /**
818
+ * Change the type of the value of this Node
819
+ * @param {String} newType
820
+ */
821
+ Node.prototype.changeType = function (newType) {
822
+ var oldType = this.type;
823
+
824
+ if (oldType == newType) {
825
+ // type is not changed
826
+ return;
827
+ }
828
+
829
+ if ((newType == 'string' || newType == 'auto') &&
830
+ (oldType == 'string' || oldType == 'auto')) {
831
+ // this is an easy change
832
+ this.type = newType;
833
+ }
834
+ else {
835
+ // change from array to object, or from string/auto to object/array
836
+ var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
837
+ var lastTr;
838
+ if (this.expanded) {
839
+ lastTr = this.getAppend();
840
+ }
841
+ else {
842
+ lastTr = this.getDom();
843
+ }
844
+ var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;
845
+
846
+ // hide current field and all its childs
847
+ this.hide();
848
+ this.clearDom();
849
+
850
+ // adjust the field and the value
851
+ this.type = newType;
852
+
853
+ // adjust childs
854
+ if (newType == 'object') {
855
+ if (!this.childs) {
856
+ this.childs = [];
857
+ }
858
+
859
+ this.childs.forEach(function (child, index) {
860
+ child.clearDom();
861
+ delete child.index;
862
+ child.fieldEditable = true;
863
+ if (child.field == undefined) {
864
+ child.field = '';
865
+ }
866
+ });
867
+
868
+ if (oldType == 'string' || oldType == 'auto') {
869
+ this.expanded = true;
870
+ }
871
+ }
872
+ else if (newType == 'array') {
873
+ if (!this.childs) {
874
+ this.childs = [];
875
+ }
876
+
877
+ this.childs.forEach(function (child, index) {
878
+ child.clearDom();
879
+ child.fieldEditable = false;
880
+ child.index = index;
881
+ });
882
+
883
+ if (oldType == 'string' || oldType == 'auto') {
884
+ this.expanded = true;
885
+ }
886
+ }
887
+ else {
888
+ this.expanded = false;
889
+ }
890
+
891
+ // create new DOM
892
+ if (table) {
893
+ if (nextTr) {
894
+ table.insertBefore(this.getDom(), nextTr);
895
+ }
896
+ else {
897
+ table.appendChild(this.getDom());
898
+ }
899
+ }
900
+ this.showChilds();
901
+ }
902
+
903
+ if (newType == 'auto' || newType == 'string') {
904
+ // cast value to the correct type
905
+ if (newType == 'string') {
906
+ this.value = String(this.value);
907
+ }
908
+ else {
909
+ this.value = this._stringCast(String(this.value));
910
+ }
911
+
912
+ this.focus();
913
+ }
914
+
915
+ this.updateDom({'updateIndexes': true});
916
+ };
917
+
918
+ /**
919
+ * Retrieve value from DOM
920
+ * @param {boolean} [silent] If true (default), no errors will be thrown in
921
+ * case of invalid data
922
+ * @private
923
+ */
924
+ Node.prototype._getDomValue = function(silent) {
925
+ if (this.dom.value && this.type != 'array' && this.type != 'object') {
926
+ this.valueInnerText = util.getInnerText(this.dom.value);
927
+ }
928
+
929
+ if (this.valueInnerText != undefined) {
930
+ try {
931
+ // retrieve the value
932
+ var value;
933
+ if (this.type == 'string') {
934
+ value = this._unescapeHTML(this.valueInnerText);
935
+ }
936
+ else {
937
+ var str = this._unescapeHTML(this.valueInnerText);
938
+ value = this._stringCast(str);
939
+ }
940
+ if (value !== this.value) {
941
+ var oldValue = this.value;
942
+ this.value = value;
943
+ this.editor._onAction('editValue', {
944
+ 'node': this,
945
+ 'oldValue': oldValue,
946
+ 'newValue': value,
947
+ 'oldSelection': this.editor.selection,
948
+ 'newSelection': this.editor.getSelection()
949
+ });
950
+ }
951
+ }
952
+ catch (err) {
953
+ this.value = undefined;
954
+ // TODO: sent an action with the new, invalid value?
955
+ if (silent != true) {
956
+ throw err;
957
+ }
958
+ }
959
+ }
960
+ };
961
+
962
+ /**
963
+ * Update dom value:
964
+ * - the text color of the value, depending on the type of the value
965
+ * - the height of the field, depending on the width
966
+ * - background color in case it is empty
967
+ * @private
968
+ */
969
+ Node.prototype._updateDomValue = function () {
970
+ var domValue = this.dom.value;
971
+ if (domValue) {
972
+ // set text color depending on value type
973
+ // TODO: put colors in css
974
+ var v = this.value;
975
+ var t = (this.type == 'auto') ? typeof(v) : this.type;
976
+ var isUrl = (t == 'string' && util.isUrl(v));
977
+ var color = '';
978
+ if (isUrl && !this.editor.mode.edit) {
979
+ color = '';
980
+ }
981
+ else if (t == 'string') {
982
+ color = 'green';
983
+ }
984
+ else if (t == 'number') {
985
+ color = 'red';
986
+ }
987
+ else if (t == 'boolean') {
988
+ color = 'orange';
989
+ }
990
+ else if (this._hasChilds()) {
991
+ // note: typeof(null)=="object", therefore check this.type instead of t
992
+ color = '';
993
+ }
994
+ else if (v === null) {
995
+ color = '#004ED0'; // blue
996
+ }
997
+ else {
998
+ // invalid value
999
+ color = 'black';
1000
+ }
1001
+ domValue.style.color = color;
1002
+
1003
+ // make backgound color lightgray when empty
1004
+ var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
1005
+ if (isEmpty) {
1006
+ util.addClassName(domValue, 'empty');
1007
+ }
1008
+ else {
1009
+ util.removeClassName(domValue, 'empty');
1010
+ }
1011
+
1012
+ // underline url
1013
+ if (isUrl) {
1014
+ util.addClassName(domValue, 'url');
1015
+ }
1016
+ else {
1017
+ util.removeClassName(domValue, 'url');
1018
+ }
1019
+
1020
+ // update title
1021
+ if (t == 'array' || t == 'object') {
1022
+ var count = this.childs ? this.childs.length : 0;
1023
+ domValue.title = this.type + ' containing ' + count + ' items';
1024
+ }
1025
+ else if (t == 'string' && util.isUrl(v)) {
1026
+ if (this.editor.mode.edit) {
1027
+ domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
1028
+ }
1029
+ }
1030
+ else {
1031
+ domValue.title = '';
1032
+ }
1033
+
1034
+ // highlight when there is a search result
1035
+ if (this.searchValueActive) {
1036
+ util.addClassName(domValue, 'highlight-active');
1037
+ }
1038
+ else {
1039
+ util.removeClassName(domValue, 'highlight-active');
1040
+ }
1041
+ if (this.searchValue) {
1042
+ util.addClassName(domValue, 'highlight');
1043
+ }
1044
+ else {
1045
+ util.removeClassName(domValue, 'highlight');
1046
+ }
1047
+
1048
+ // strip formatting from the contents of the editable div
1049
+ util.stripFormatting(domValue);
1050
+ }
1051
+ };
1052
+
1053
+ /**
1054
+ * Update dom field:
1055
+ * - the text color of the field, depending on the text
1056
+ * - the height of the field, depending on the width
1057
+ * - background color in case it is empty
1058
+ * @private
1059
+ */
1060
+ Node.prototype._updateDomField = function () {
1061
+ var domField = this.dom.field;
1062
+ if (domField) {
1063
+ // make backgound color lightgray when empty
1064
+ var isEmpty = (String(this.field) == '' && this.parent.type != 'array');
1065
+ if (isEmpty) {
1066
+ util.addClassName(domField, 'empty');
1067
+ }
1068
+ else {
1069
+ util.removeClassName(domField, 'empty');
1070
+ }
1071
+
1072
+ // highlight when there is a search result
1073
+ if (this.searchFieldActive) {
1074
+ util.addClassName(domField, 'highlight-active');
1075
+ }
1076
+ else {
1077
+ util.removeClassName(domField, 'highlight-active');
1078
+ }
1079
+ if (this.searchField) {
1080
+ util.addClassName(domField, 'highlight');
1081
+ }
1082
+ else {
1083
+ util.removeClassName(domField, 'highlight');
1084
+ }
1085
+
1086
+ // strip formatting from the contents of the editable div
1087
+ util.stripFormatting(domField);
1088
+ }
1089
+ };
1090
+
1091
+ /**
1092
+ * Retrieve field from DOM
1093
+ * @param {boolean} [silent] If true (default), no errors will be thrown in
1094
+ * case of invalid data
1095
+ * @private
1096
+ */
1097
+ Node.prototype._getDomField = function(silent) {
1098
+ if (this.dom.field && this.fieldEditable) {
1099
+ this.fieldInnerText = util.getInnerText(this.dom.field);
1100
+ }
1101
+
1102
+ if (this.fieldInnerText != undefined) {
1103
+ try {
1104
+ var field = this._unescapeHTML(this.fieldInnerText);
1105
+
1106
+ if (field !== this.field) {
1107
+ var oldField = this.field;
1108
+ this.field = field;
1109
+ this.editor._onAction('editField', {
1110
+ 'node': this,
1111
+ 'oldValue': oldField,
1112
+ 'newValue': field,
1113
+ 'oldSelection': this.editor.selection,
1114
+ 'newSelection': this.editor.getSelection()
1115
+ });
1116
+ }
1117
+ }
1118
+ catch (err) {
1119
+ this.field = undefined;
1120
+ // TODO: sent an action here, with the new, invalid value?
1121
+ if (silent != true) {
1122
+ throw err;
1123
+ }
1124
+ }
1125
+ }
1126
+ };
1127
+
1128
+ /**
1129
+ * Clear the dom of the node
1130
+ */
1131
+ Node.prototype.clearDom = function() {
1132
+ // TODO: hide the node first?
1133
+ //this.hide();
1134
+ // TODO: recursively clear dom?
1135
+
1136
+ this.dom = {};
1137
+ };
1138
+
1139
+ /**
1140
+ * Get the HTML DOM TR element of the node.
1141
+ * The dom will be generated when not yet created
1142
+ * @return {Element} tr HTML DOM TR Element
1143
+ */
1144
+ Node.prototype.getDom = function() {
1145
+ var dom = this.dom;
1146
+ if (dom.tr) {
1147
+ return dom.tr;
1148
+ }
1149
+
1150
+ // create row
1151
+ dom.tr = document.createElement('tr');
1152
+ dom.tr.node = this;
1153
+
1154
+ if (this.editor.mode.edit) {
1155
+ // create draggable area
1156
+ var tdDrag = document.createElement('td');
1157
+ if (this.parent) {
1158
+ var domDrag = document.createElement('button');
1159
+ dom.drag = domDrag;
1160
+ domDrag.className = 'dragarea';
1161
+ domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';
1162
+ tdDrag.appendChild(domDrag);
1163
+ }
1164
+ dom.tr.appendChild(tdDrag);
1165
+
1166
+ // create context menu
1167
+ var tdMenu = document.createElement('td');
1168
+ var menu = document.createElement('button');
1169
+ dom.menu = menu;
1170
+ menu.className = 'contextmenu';
1171
+ menu.title = 'Click to open the actions menu (Ctrl+M)';
1172
+ tdMenu.appendChild(dom.menu);
1173
+ dom.tr.appendChild(tdMenu);
1174
+ }
1175
+
1176
+ // create tree and field
1177
+ var tdField = document.createElement('td');
1178
+ dom.tr.appendChild(tdField);
1179
+ dom.tree = this._createDomTree();
1180
+ tdField.appendChild(dom.tree);
1181
+
1182
+ this.updateDom({'updateIndexes': true});
1183
+
1184
+ return dom.tr;
1185
+ };
1186
+
1187
+ /**
1188
+ * DragStart event, fired on mousedown on the dragarea at the left side of a Node
1189
+ * @param {Event} event
1190
+ * @private
1191
+ */
1192
+ Node.prototype._onDragStart = function (event) {
1193
+ event = event || window.event;
1194
+
1195
+ var node = this;
1196
+ if (!this.mousemove) {
1197
+ this.mousemove = util.addEventListener(document, 'mousemove',
1198
+ function (event) {
1199
+ node._onDrag(event);
1200
+ });
1201
+ }
1202
+
1203
+ if (!this.mouseup) {
1204
+ this.mouseup = util.addEventListener(document, 'mouseup',
1205
+ function (event ) {
1206
+ node._onDragEnd(event);
1207
+ });
1208
+ }
1209
+
1210
+ this.editor.highlighter.lock();
1211
+ this.drag = {
1212
+ 'oldCursor': document.body.style.cursor,
1213
+ 'startParent': this.parent,
1214
+ 'startIndex': this.parent.childs.indexOf(this),
1215
+ 'mouseX': util.getMouseX(event),
1216
+ 'level': this.getLevel()
1217
+ };
1218
+ document.body.style.cursor = 'move';
1219
+
1220
+ util.preventDefault(event);
1221
+ };
1222
+
1223
+ /**
1224
+ * Drag event, fired when moving the mouse while dragging a Node
1225
+ * @param {Event} event
1226
+ * @private
1227
+ */
1228
+ Node.prototype._onDrag = function (event) {
1229
+ // TODO: this method has grown too large. Split it in a number of methods
1230
+ event = event || window.event;
1231
+ var mouseY = util.getMouseY(event);
1232
+ var mouseX = util.getMouseX(event);
1233
+
1234
+ var trThis, trPrev, trNext, trFirst, trLast, trRoot;
1235
+ var nodePrev, nodeNext;
1236
+ var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;
1237
+ var moved = false;
1238
+
1239
+ // TODO: add an ESC option, which resets to the original position
1240
+
1241
+ // move up/down
1242
+ trThis = this.dom.tr;
1243
+ topThis = util.getAbsoluteTop(trThis);
1244
+ heightThis = trThis.offsetHeight;
1245
+ if (mouseY < topThis) {
1246
+ // move up
1247
+ trPrev = trThis;
1248
+ do {
1249
+ trPrev = trPrev.previousSibling;
1250
+ nodePrev = Node.getNodeFromTarget(trPrev);
1251
+ topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
1252
+ }
1253
+ while (trPrev && mouseY < topPrev);
1254
+
1255
+ if (nodePrev && !nodePrev.parent) {
1256
+ nodePrev = undefined;
1257
+ }
1258
+
1259
+ if (!nodePrev) {
1260
+ // move to the first node
1261
+ trRoot = trThis.parentNode.firstChild;
1262
+ trPrev = trRoot ? trRoot.nextSibling : undefined;
1263
+ nodePrev = Node.getNodeFromTarget(trPrev);
1264
+ if (nodePrev == this) {
1265
+ nodePrev = undefined;
1266
+ }
1267
+ }
1268
+
1269
+ if (nodePrev) {
1270
+ // check if mouseY is really inside the found node
1271
+ trPrev = nodePrev.dom.tr;
1272
+ topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
1273
+ if (mouseY > topPrev + heightThis) {
1274
+ nodePrev = undefined;
1275
+ }
1276
+ }
1277
+
1278
+ if (nodePrev) {
1279
+ nodePrev.parent.moveBefore(this, nodePrev);
1280
+ moved = true;
1281
+ }
1282
+ }
1283
+ else {
1284
+ // move down
1285
+ trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;
1286
+ trFirst = trLast ? trLast.nextSibling : undefined;
1287
+ if (trFirst) {
1288
+ topFirst = util.getAbsoluteTop(trFirst);
1289
+ trNext = trFirst;
1290
+ do {
1291
+ nodeNext = Node.getNodeFromTarget(trNext);
1292
+ if (trNext) {
1293
+ bottomNext = trNext.nextSibling ?
1294
+ util.getAbsoluteTop(trNext.nextSibling) : 0;
1295
+ heightNext = trNext ? (bottomNext - topFirst) : 0;
1296
+
1297
+ if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {
1298
+ // We are about to remove the last child of this parent,
1299
+ // which will make the parents appendNode visible.
1300
+ topThis += 24 - 1;
1301
+ // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.
1302
+ }
1303
+ }
1304
+
1305
+ trNext = trNext.nextSibling;
1306
+ }
1307
+ while (trNext && mouseY > topThis + heightNext);
1308
+
1309
+ if (nodeNext && nodeNext.parent) {
1310
+ // calculate the desired level
1311
+ var diffX = (mouseX - this.drag.mouseX);
1312
+ var diffLevel = Math.round(diffX / 24 / 2);
1313
+ var level = this.drag.level + diffLevel; // desired level
1314
+ var levelNext = nodeNext.getLevel(); // level to be
1315
+
1316
+ // find the best fitting level (move upwards over the append nodes)
1317
+ trPrev = nodeNext.dom.tr.previousSibling;
1318
+ while (levelNext < level && trPrev) {
1319
+ nodePrev = Node.getNodeFromTarget(trPrev);
1320
+ if (nodePrev == this || nodePrev._isChildOf(this)) {
1321
+ // neglect itself and its childs
1322
+ }
1323
+ else if (nodePrev instanceof AppendNode) {
1324
+ var childs = nodePrev.parent.childs;
1325
+ if (childs.length > 1 ||
1326
+ (childs.length == 1 && childs[0] != this)) {
1327
+ // non-visible append node of a list of childs
1328
+ // consisting of not only this node (else the
1329
+ // append node will change into a visible "empty"
1330
+ // text when removing this node).
1331
+ nodeNext = Node.getNodeFromTarget(trPrev);
1332
+ levelNext = nodeNext.getLevel();
1333
+ }
1334
+ else {
1335
+ break;
1336
+ }
1337
+ }
1338
+ else {
1339
+ break;
1340
+ }
1341
+
1342
+ trPrev = trPrev.previousSibling;
1343
+ }
1344
+
1345
+ // move the node when its position is changed
1346
+ if (trLast.nextSibling != nodeNext.dom.tr) {
1347
+ nodeNext.parent.moveBefore(this, nodeNext);
1348
+ moved = true;
1349
+ }
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ if (moved) {
1355
+ // update the dragging parameters when moved
1356
+ this.drag.mouseX = mouseX;
1357
+ this.drag.level = this.getLevel();
1358
+ }
1359
+
1360
+ // auto scroll when hovering around the top of the editor
1361
+ this.editor.startAutoScroll(mouseY);
1362
+
1363
+ util.preventDefault(event);
1364
+ };
1365
+
1366
+ /**
1367
+ * Drag event, fired on mouseup after having dragged a node
1368
+ * @param {Event} event
1369
+ * @private
1370
+ */
1371
+ Node.prototype._onDragEnd = function (event) {
1372
+ event = event || window.event;
1373
+
1374
+ var params = {
1375
+ 'node': this,
1376
+ 'startParent': this.drag.startParent,
1377
+ 'startIndex': this.drag.startIndex,
1378
+ 'endParent': this.parent,
1379
+ 'endIndex': this.parent.childs.indexOf(this)
1380
+ };
1381
+ if ((params.startParent != params.endParent) ||
1382
+ (params.startIndex != params.endIndex)) {
1383
+ // only register this action if the node is actually moved to another place
1384
+ this.editor._onAction('moveNode', params);
1385
+ }
1386
+
1387
+ document.body.style.cursor = this.drag.oldCursor;
1388
+ this.editor.highlighter.unlock();
1389
+ delete this.drag;
1390
+
1391
+ if (this.mousemove) {
1392
+ util.removeEventListener(document, 'mousemove', this.mousemove);
1393
+ delete this.mousemove;}
1394
+ if (this.mouseup) {
1395
+ util.removeEventListener(document, 'mouseup', this.mouseup);
1396
+ delete this.mouseup;
1397
+ }
1398
+
1399
+ // Stop any running auto scroll
1400
+ this.editor.stopAutoScroll();
1401
+
1402
+ util.preventDefault(event);
1403
+ };
1404
+
1405
+ /**
1406
+ * Test if this node is a child of an other node
1407
+ * @param {Node} node
1408
+ * @return {boolean} isChild
1409
+ * @private
1410
+ */
1411
+ Node.prototype._isChildOf = function (node) {
1412
+ var n = this.parent;
1413
+ while (n) {
1414
+ if (n == node) {
1415
+ return true;
1416
+ }
1417
+ n = n.parent;
1418
+ }
1419
+
1420
+ return false;
1421
+ };
1422
+
1423
+ /**
1424
+ * Create an editable field
1425
+ * @return {Element} domField
1426
+ * @private
1427
+ */
1428
+ Node.prototype._createDomField = function () {
1429
+ return document.createElement('div');
1430
+ };
1431
+
1432
+ /**
1433
+ * Set highlighting for this node and all its childs.
1434
+ * Only applied to the currently visible (expanded childs)
1435
+ * @param {boolean} highlight
1436
+ */
1437
+ Node.prototype.setHighlight = function (highlight) {
1438
+ if (this.dom.tr) {
1439
+ this.dom.tr.className = (highlight ? 'highlight' : '');
1440
+
1441
+ if (this.append) {
1442
+ this.append.setHighlight(highlight);
1443
+ }
1444
+
1445
+ if (this.childs) {
1446
+ this.childs.forEach(function (child) {
1447
+ child.setHighlight(highlight);
1448
+ });
1449
+ }
1450
+ }
1451
+ };
1452
+
1453
+ /**
1454
+ * Update the value of the node. Only primitive types are allowed, no Object
1455
+ * or Array is allowed.
1456
+ * @param {String | Number | Boolean | null} value
1457
+ */
1458
+ Node.prototype.updateValue = function (value) {
1459
+ this.value = value;
1460
+ this.updateDom();
1461
+ };
1462
+
1463
+ /**
1464
+ * Update the field of the node.
1465
+ * @param {String} field
1466
+ */
1467
+ Node.prototype.updateField = function (field) {
1468
+ this.field = field;
1469
+ this.updateDom();
1470
+ };
1471
+
1472
+ /**
1473
+ * Update the HTML DOM, optionally recursing through the childs
1474
+ * @param {Object} [options] Available parameters:
1475
+ * {boolean} [recurse] If true, the
1476
+ * DOM of the childs will be updated recursively.
1477
+ * False by default.
1478
+ * {boolean} [updateIndexes] If true, the childs
1479
+ * indexes of the node will be updated too. False by
1480
+ * default.
1481
+ */
1482
+ Node.prototype.updateDom = function (options) {
1483
+ // update level indentation
1484
+ var domTree = this.dom.tree;
1485
+ if (domTree) {
1486
+ domTree.style.marginLeft = this.getLevel() * 24 + 'px';
1487
+ }
1488
+
1489
+ // update field
1490
+ var domField = this.dom.field;
1491
+ if (domField) {
1492
+ if (this.fieldEditable == true) {
1493
+ // parent is an object
1494
+ domField.contentEditable = this.editor.mode.edit;
1495
+ domField.spellcheck = false;
1496
+ domField.className = 'field';
1497
+ }
1498
+ else {
1499
+ // parent is an array this is the root node
1500
+ domField.className = 'readonly';
1501
+ }
1502
+
1503
+ var field;
1504
+ if (this.index != undefined) {
1505
+ field = this.index;
1506
+ }
1507
+ else if (this.field != undefined) {
1508
+ field = this.field;
1509
+ }
1510
+ else if (this._hasChilds()) {
1511
+ field = this.type;
1512
+ }
1513
+ else {
1514
+ field = '';
1515
+ }
1516
+ domField.innerHTML = this._escapeHTML(field);
1517
+ }
1518
+
1519
+ // update value
1520
+ var domValue = this.dom.value;
1521
+ if (domValue) {
1522
+ var count = this.childs ? this.childs.length : 0;
1523
+ if (this.type == 'array') {
1524
+ domValue.innerHTML = '[' + count + ']';
1525
+ }
1526
+ else if (this.type == 'object') {
1527
+ domValue.innerHTML = '{' + count + '}';
1528
+ }
1529
+ else {
1530
+ domValue.innerHTML = this._escapeHTML(this.value);
1531
+ }
1532
+ }
1533
+
1534
+ // update field and value
1535
+ this._updateDomField();
1536
+ this._updateDomValue();
1537
+
1538
+ // update childs indexes
1539
+ if (options && options.updateIndexes == true) {
1540
+ // updateIndexes is true or undefined
1541
+ this._updateDomIndexes();
1542
+ }
1543
+
1544
+ if (options && options.recurse == true) {
1545
+ // recurse is true or undefined. update childs recursively
1546
+ if (this.childs) {
1547
+ this.childs.forEach(function (child) {
1548
+ child.updateDom(options);
1549
+ });
1550
+ }
1551
+ }
1552
+
1553
+ // update row with append button
1554
+ if (this.append) {
1555
+ this.append.updateDom();
1556
+ }
1557
+ };
1558
+
1559
+ /**
1560
+ * Update the DOM of the childs of a node: update indexes and undefined field
1561
+ * names.
1562
+ * Only applicable when structure is an array or object
1563
+ * @private
1564
+ */
1565
+ Node.prototype._updateDomIndexes = function () {
1566
+ var domValue = this.dom.value;
1567
+ var childs = this.childs;
1568
+ if (domValue && childs) {
1569
+ if (this.type == 'array') {
1570
+ childs.forEach(function (child, index) {
1571
+ child.index = index;
1572
+ var childField = child.dom.field;
1573
+ if (childField) {
1574
+ childField.innerHTML = index;
1575
+ }
1576
+ });
1577
+ }
1578
+ else if (this.type == 'object') {
1579
+ childs.forEach(function (child) {
1580
+ if (child.index != undefined) {
1581
+ delete child.index;
1582
+
1583
+ if (child.field == undefined) {
1584
+ child.field = '';
1585
+ }
1586
+ }
1587
+ });
1588
+ }
1589
+ }
1590
+ };
1591
+
1592
+ /**
1593
+ * Create an editable value
1594
+ * @private
1595
+ */
1596
+ Node.prototype._createDomValue = function () {
1597
+ var domValue;
1598
+
1599
+ if (this.type == 'array') {
1600
+ domValue = document.createElement('div');
1601
+ domValue.className = 'readonly';
1602
+ domValue.innerHTML = '[...]';
1603
+ }
1604
+ else if (this.type == 'object') {
1605
+ domValue = document.createElement('div');
1606
+ domValue.className = 'readonly';
1607
+ domValue.innerHTML = '{...}';
1608
+ }
1609
+ else {
1610
+ if (!this.editor.mode.edit && util.isUrl(this.value)) {
1611
+ // create a link in case of read-only editor and value containing an url
1612
+ domValue = document.createElement('a');
1613
+ domValue.className = 'value';
1614
+ domValue.href = this.value;
1615
+ domValue.target = '_blank';
1616
+ domValue.innerHTML = this._escapeHTML(this.value);
1617
+ }
1618
+ else {
1619
+ // create and editable or read-only div
1620
+ domValue = document.createElement('div');
1621
+ domValue.contentEditable = !this.editor.mode.view;
1622
+ domValue.spellcheck = false;
1623
+ domValue.className = 'value';
1624
+ domValue.innerHTML = this._escapeHTML(this.value);
1625
+ }
1626
+ }
1627
+
1628
+ return domValue;
1629
+ };
1630
+
1631
+ /**
1632
+ * Create an expand/collapse button
1633
+ * @return {Element} expand
1634
+ * @private
1635
+ */
1636
+ Node.prototype._createDomExpandButton = function () {
1637
+ // create expand button
1638
+ var expand = document.createElement('button');
1639
+ if (this._hasChilds()) {
1640
+ expand.className = this.expanded ? 'expanded' : 'collapsed';
1641
+ expand.title =
1642
+ 'Click to expand/collapse this field (Ctrl+E). \n' +
1643
+ 'Ctrl+Click to expand/collapse including all childs.';
1644
+ }
1645
+ else {
1646
+ expand.className = 'invisible';
1647
+ expand.title = '';
1648
+ }
1649
+
1650
+ return expand;
1651
+ };
1652
+
1653
+
1654
+ /**
1655
+ * Create a DOM tree element, containing the expand/collapse button
1656
+ * @return {Element} domTree
1657
+ * @private
1658
+ */
1659
+ Node.prototype._createDomTree = function () {
1660
+ var dom = this.dom;
1661
+ var domTree = document.createElement('table');
1662
+ var tbody = document.createElement('tbody');
1663
+ domTree.style.borderCollapse = 'collapse'; // TODO: put in css
1664
+ domTree.appendChild(tbody);
1665
+ var tr = document.createElement('tr');
1666
+ tbody.appendChild(tr);
1667
+
1668
+ // create expand button
1669
+ var tdExpand = document.createElement('td');
1670
+ tdExpand.className = 'tree';
1671
+ tr.appendChild(tdExpand);
1672
+ dom.expand = this._createDomExpandButton();
1673
+ tdExpand.appendChild(dom.expand);
1674
+ dom.tdExpand = tdExpand;
1675
+
1676
+ // create the field
1677
+ var tdField = document.createElement('td');
1678
+ tdField.className = 'tree';
1679
+ tr.appendChild(tdField);
1680
+ dom.field = this._createDomField();
1681
+ tdField.appendChild(dom.field);
1682
+ dom.tdField = tdField;
1683
+
1684
+ // create a separator
1685
+ var tdSeparator = document.createElement('td');
1686
+ tdSeparator.className = 'tree';
1687
+ tr.appendChild(tdSeparator);
1688
+ if (this.type != 'object' && this.type != 'array') {
1689
+ tdSeparator.appendChild(document.createTextNode(':'));
1690
+ tdSeparator.className = 'separator';
1691
+ }
1692
+ dom.tdSeparator = tdSeparator;
1693
+
1694
+ // create the value
1695
+ var tdValue = document.createElement('td');
1696
+ tdValue.className = 'tree';
1697
+ tr.appendChild(tdValue);
1698
+ dom.value = this._createDomValue();
1699
+ tdValue.appendChild(dom.value);
1700
+ dom.tdValue = tdValue;
1701
+
1702
+ return domTree;
1703
+ };
1704
+
1705
+ /**
1706
+ * Handle an event. The event is catched centrally by the editor
1707
+ * @param {Event} event
1708
+ */
1709
+ Node.prototype.onEvent = function (event) {
1710
+ var type = event.type,
1711
+ target = event.target || event.srcElement,
1712
+ dom = this.dom,
1713
+ node = this,
1714
+ focusNode,
1715
+ expandable = this._hasChilds();
1716
+
1717
+ // check if mouse is on menu or on dragarea.
1718
+ // If so, highlight current row and its childs
1719
+ if (target == dom.drag || target == dom.menu) {
1720
+ if (type == 'mouseover') {
1721
+ this.editor.highlighter.highlight(this);
1722
+ }
1723
+ else if (type == 'mouseout') {
1724
+ this.editor.highlighter.unhighlight();
1725
+ }
1726
+ }
1727
+
1728
+ // drag events
1729
+ if (type == 'mousedown' && target == dom.drag) {
1730
+ this._onDragStart(event);
1731
+ }
1732
+
1733
+ // context menu events
1734
+ if (type == 'click' && target == dom.menu) {
1735
+ var highlighter = node.editor.highlighter;
1736
+ highlighter.highlight(node);
1737
+ highlighter.lock();
1738
+ util.addClassName(dom.menu, 'selected');
1739
+ this.showContextMenu(dom.menu, function () {
1740
+ util.removeClassName(dom.menu, 'selected');
1741
+ highlighter.unlock();
1742
+ highlighter.unhighlight();
1743
+ });
1744
+ }
1745
+
1746
+ // expand events
1747
+ if (type == 'click' && target == dom.expand) {
1748
+ if (expandable) {
1749
+ var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
1750
+ this._onExpand(recurse);
1751
+ }
1752
+ }
1753
+
1754
+ // value events
1755
+ var domValue = dom.value;
1756
+ if (target == domValue) {
1757
+ //noinspection FallthroughInSwitchStatementJS
1758
+ switch (type) {
1759
+ case 'focus':
1760
+ focusNode = this;
1761
+ break;
1762
+
1763
+ case 'blur':
1764
+ case 'change':
1765
+ this._getDomValue(true);
1766
+ this._updateDomValue();
1767
+ if (this.value) {
1768
+ domValue.innerHTML = this._escapeHTML(this.value);
1769
+ }
1770
+ break;
1771
+
1772
+ case 'input':
1773
+ this._getDomValue(true);
1774
+ this._updateDomValue();
1775
+ break;
1776
+
1777
+ case 'keydown':
1778
+ case 'mousedown':
1779
+ this.editor.selection = this.editor.getSelection();
1780
+ break;
1781
+
1782
+ case 'click':
1783
+ if (event.ctrlKey && this.editor.mode.edit) {
1784
+ if (util.isUrl(this.value)) {
1785
+ window.open(this.value, '_blank');
1786
+ }
1787
+ }
1788
+ break;
1789
+
1790
+ case 'keyup':
1791
+ this._getDomValue(true);
1792
+ this._updateDomValue();
1793
+ break;
1794
+
1795
+ case 'cut':
1796
+ case 'paste':
1797
+ setTimeout(function () {
1798
+ node._getDomValue(true);
1799
+ node._updateDomValue();
1800
+ }, 1);
1801
+ break;
1802
+ }
1803
+ }
1804
+
1805
+ // field events
1806
+ var domField = dom.field;
1807
+ if (target == domField) {
1808
+ switch (type) {
1809
+ case 'focus':
1810
+ focusNode = this;
1811
+ break;
1812
+
1813
+ case 'blur':
1814
+ case 'change':
1815
+ this._getDomField(true);
1816
+ this._updateDomField();
1817
+ if (this.field) {
1818
+ domField.innerHTML = this._escapeHTML(this.field);
1819
+ }
1820
+ break;
1821
+
1822
+ case 'input':
1823
+ this._getDomField(true);
1824
+ this._updateDomField();
1825
+ break;
1826
+
1827
+ case 'keydown':
1828
+ case 'mousedown':
1829
+ this.editor.selection = this.editor.getSelection();
1830
+ break;
1831
+
1832
+ case 'keyup':
1833
+ this._getDomField(true);
1834
+ this._updateDomField();
1835
+ break;
1836
+
1837
+ case 'cut':
1838
+ case 'paste':
1839
+ setTimeout(function () {
1840
+ node._getDomField(true);
1841
+ node._updateDomField();
1842
+ }, 1);
1843
+ break;
1844
+ }
1845
+ }
1846
+
1847
+ // focus
1848
+ // when clicked in whitespace left or right from the field or value, set focus
1849
+ var domTree = dom.tree;
1850
+ if (target == domTree.parentNode) {
1851
+ switch (type) {
1852
+ case 'click':
1853
+ var left = (event.offsetX != undefined) ?
1854
+ (event.offsetX < (this.getLevel() + 1) * 24) :
1855
+ (util.getMouseX(event) < util.getAbsoluteLeft(dom.tdSeparator));// for FF
1856
+ if (left || expandable) {
1857
+ // node is expandable when it is an object or array
1858
+ if (domField) {
1859
+ util.setEndOfContentEditable(domField);
1860
+ domField.focus();
1861
+ }
1862
+ }
1863
+ else {
1864
+ if (domValue) {
1865
+ util.setEndOfContentEditable(domValue);
1866
+ domValue.focus();
1867
+ }
1868
+ }
1869
+ break;
1870
+ }
1871
+ }
1872
+ if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||
1873
+ target == dom.tdSeparator) {
1874
+ switch (type) {
1875
+ case 'click':
1876
+ if (domField) {
1877
+ util.setEndOfContentEditable(domField);
1878
+ domField.focus();
1879
+ }
1880
+ break;
1881
+ }
1882
+ }
1883
+
1884
+ if (type == 'keydown') {
1885
+ this.onKeyDown(event);
1886
+ }
1887
+ };
1888
+
1889
+ /**
1890
+ * Key down event handler
1891
+ * @param {Event} event
1892
+ */
1893
+ Node.prototype.onKeyDown = function (event) {
1894
+ var keynum = event.which || event.keyCode;
1895
+ var target = event.target || event.srcElement;
1896
+ var ctrlKey = event.ctrlKey;
1897
+ var shiftKey = event.shiftKey;
1898
+ var altKey = event.altKey;
1899
+ var handled = false;
1900
+ var prevNode, nextNode, nextDom, nextDom2;
1901
+
1902
+ // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
1903
+ if (keynum == 13) { // Enter
1904
+ if (target == this.dom.value) {
1905
+ if (!this.editor.mode.edit || event.ctrlKey) {
1906
+ if (util.isUrl(this.value)) {
1907
+ window.open(this.value, '_blank');
1908
+ handled = true;
1909
+ }
1910
+ }
1911
+ }
1912
+ else if (target == this.dom.expand) {
1913
+ var expandable = this._hasChilds();
1914
+ if (expandable) {
1915
+ var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
1916
+ this._onExpand(recurse);
1917
+ target.focus();
1918
+ handled = true;
1919
+ }
1920
+ }
1921
+ }
1922
+ else if (keynum == 68) { // D
1923
+ if (ctrlKey) { // Ctrl+D
1924
+ this._onDuplicate();
1925
+ handled = true;
1926
+ }
1927
+ }
1928
+ else if (keynum == 69) { // E
1929
+ if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E
1930
+ this._onExpand(shiftKey); // recurse = shiftKey
1931
+ target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
1932
+ handled = true;
1933
+ }
1934
+ }
1935
+ else if (keynum == 77) { // M
1936
+ if (ctrlKey) { // Ctrl+M
1937
+ this.showContextMenu(target);
1938
+ handled = true;
1939
+ }
1940
+ }
1941
+ else if (keynum == 46) { // Del
1942
+ if (ctrlKey) { // Ctrl+Del
1943
+ this._onRemove();
1944
+ handled = true;
1945
+ }
1946
+ }
1947
+ else if (keynum == 45) { // Ins
1948
+ if (ctrlKey && !shiftKey) { // Ctrl+Ins
1949
+ this._onInsertBefore();
1950
+ handled = true;
1951
+ }
1952
+ else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins
1953
+ this._onInsertAfter();
1954
+ handled = true;
1955
+ }
1956
+ }
1957
+ else if (keynum == 35) { // End
1958
+ if (altKey) { // Alt+End
1959
+ // find the last node
1960
+ var lastNode = this._lastNode();
1961
+ if (lastNode) {
1962
+ lastNode.focus(Node.focusElement || this._getElementName(target));
1963
+ }
1964
+ handled = true;
1965
+ }
1966
+ }
1967
+ else if (keynum == 36) { // Home
1968
+ if (altKey) { // Alt+Home
1969
+ // find the first node
1970
+ var firstNode = this._firstNode();
1971
+ if (firstNode) {
1972
+ firstNode.focus(Node.focusElement || this._getElementName(target));
1973
+ }
1974
+ handled = true;
1975
+ }
1976
+ }
1977
+ else if (keynum == 37) { // Arrow Left
1978
+ if (altKey && !shiftKey) { // Alt + Arrow Left
1979
+ // move to left element
1980
+ var prevElement = this._previousElement(target);
1981
+ if (prevElement) {
1982
+ this.focus(this._getElementName(prevElement));
1983
+ }
1984
+ handled = true;
1985
+ }
1986
+ else if (altKey && shiftKey) { // Alt + Shift Arrow left
1987
+ if (this.expanded) {
1988
+ var appendDom = this.getAppend();
1989
+ nextDom = appendDom ? appendDom.nextSibling : undefined;
1990
+ }
1991
+ else {
1992
+ var dom = this.getDom();
1993
+ nextDom = dom.nextSibling;
1994
+ }
1995
+ if (nextDom) {
1996
+ nextNode = Node.getNodeFromTarget(nextDom);
1997
+ nextDom2 = nextDom.nextSibling;
1998
+ nextNode2 = Node.getNodeFromTarget(nextDom2);
1999
+ if (nextNode && nextNode instanceof AppendNode &&
2000
+ !(this.parent.childs.length == 1) &&
2001
+ nextNode2 && nextNode2.parent) {
2002
+ nextNode2.parent.moveBefore(this, nextNode2);
2003
+ this.focus(Node.focusElement || this._getElementName(target));
2004
+ }
2005
+ }
2006
+ }
2007
+ }
2008
+ else if (keynum == 38) { // Arrow Up
2009
+ if (altKey && !shiftKey) { // Alt + Arrow Up
2010
+ // find the previous node
2011
+ prevNode = this._previousNode();
2012
+ if (prevNode) {
2013
+ prevNode.focus(Node.focusElement || this._getElementName(target));
2014
+ }
2015
+ handled = true;
2016
+ }
2017
+ else if (altKey && shiftKey) { // Alt + Shift + Arrow Up
2018
+ // find the previous node
2019
+ prevNode = this._previousNode();
2020
+ if (prevNode && prevNode.parent) {
2021
+ prevNode.parent.moveBefore(this, prevNode);
2022
+ this.focus(Node.focusElement || this._getElementName(target));
2023
+ }
2024
+ handled = true;
2025
+ }
2026
+ }
2027
+ else if (keynum == 39) { // Arrow Right
2028
+ if (altKey && !shiftKey) { // Alt + Arrow Right
2029
+ // move to right element
2030
+ var nextElement = this._nextElement(target);
2031
+ if (nextElement) {
2032
+ this.focus(this._getElementName(nextElement));
2033
+ }
2034
+ handled = true;
2035
+ }
2036
+ else if (altKey && shiftKey) { // Alt + Shift Arrow Right
2037
+ dom = this.getDom();
2038
+ var prevDom = dom.previousSibling;
2039
+ if (prevDom) {
2040
+ prevNode = Node.getNodeFromTarget(prevDom);
2041
+ if (prevNode && prevNode.parent &&
2042
+ (prevNode instanceof AppendNode)
2043
+ && !prevNode.isVisible()) {
2044
+ prevNode.parent.moveBefore(this, prevNode);
2045
+ this.focus(Node.focusElement || this._getElementName(target));
2046
+ }
2047
+ }
2048
+ }
2049
+ }
2050
+ else if (keynum == 40) { // Arrow Down
2051
+ if (altKey && !shiftKey) { // Alt + Arrow Down
2052
+ // find the next node
2053
+ nextNode = this._nextNode();
2054
+ if (nextNode) {
2055
+ nextNode.focus(Node.focusElement || this._getElementName(target));
2056
+ }
2057
+ handled = true;
2058
+ }
2059
+ else if (altKey && shiftKey) { // Alt + Shift + Arrow Down
2060
+ // find the 2nd next node and move before that one
2061
+ if (this.expanded) {
2062
+ nextNode = this.append ? this.append._nextNode() : undefined;
2063
+ }
2064
+ else {
2065
+ nextNode = this._nextNode();
2066
+ }
2067
+ nextDom = nextNode ? nextNode.getDom() : undefined;
2068
+ if (this.parent.childs.length == 1) {
2069
+ nextDom2 = nextDom;
2070
+ }
2071
+ else {
2072
+ nextDom2 = nextDom ? nextDom.nextSibling : undefined;
2073
+ }
2074
+ var nextNode2 = Node.getNodeFromTarget(nextDom2);
2075
+ if (nextNode2 && nextNode2.parent) {
2076
+ nextNode2.parent.moveBefore(this, nextNode2);
2077
+ this.focus(Node.focusElement || this._getElementName(target));
2078
+ }
2079
+ handled = true;
2080
+ }
2081
+ }
2082
+
2083
+ if (handled) {
2084
+ util.preventDefault(event);
2085
+ util.stopPropagation(event);
2086
+ }
2087
+ };
2088
+
2089
+ /**
2090
+ * Handle the expand event, when clicked on the expand button
2091
+ * @param {boolean} recurse If true, child nodes will be expanded too
2092
+ * @private
2093
+ */
2094
+ Node.prototype._onExpand = function (recurse) {
2095
+ if (recurse) {
2096
+ // Take the table offline
2097
+ var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
2098
+ var frame = table.parentNode;
2099
+ var scrollTop = frame.scrollTop;
2100
+ frame.removeChild(table);
2101
+ }
2102
+
2103
+ if (this.expanded) {
2104
+ this.collapse(recurse);
2105
+ }
2106
+ else {
2107
+ this.expand(recurse);
2108
+ }
2109
+
2110
+ if (recurse) {
2111
+ // Put the table online again
2112
+ frame.appendChild(table);
2113
+ frame.scrollTop = scrollTop;
2114
+ }
2115
+ };
2116
+
2117
+ /**
2118
+ * Remove this node
2119
+ * @private
2120
+ */
2121
+ Node.prototype._onRemove = function() {
2122
+ this.editor.highlighter.unhighlight();
2123
+ var childs = this.parent.childs;
2124
+ var index = childs.indexOf(this);
2125
+
2126
+ // adjust the focus
2127
+ var oldSelection = this.editor.getSelection();
2128
+ if (childs[index + 1]) {
2129
+ childs[index + 1].focus();
2130
+ }
2131
+ else if (childs[index - 1]) {
2132
+ childs[index - 1].focus();
2133
+ }
2134
+ else {
2135
+ this.parent.focus();
2136
+ }
2137
+ var newSelection = this.editor.getSelection();
2138
+
2139
+ // remove the node
2140
+ this.parent._remove(this);
2141
+
2142
+ // store history action
2143
+ this.editor._onAction('removeNode', {
2144
+ 'node': this,
2145
+ 'parent': this.parent,
2146
+ 'index': index,
2147
+ 'oldSelection': oldSelection,
2148
+ 'newSelection': newSelection
2149
+ });
2150
+ };
2151
+
2152
+ /**
2153
+ * Duplicate this node
2154
+ * @private
2155
+ */
2156
+ Node.prototype._onDuplicate = function() {
2157
+ var oldSelection = this.editor.getSelection();
2158
+ var clone = this.parent._duplicate(this);
2159
+ clone.focus();
2160
+ var newSelection = this.editor.getSelection();
2161
+
2162
+ this.editor._onAction('duplicateNode', {
2163
+ 'node': this,
2164
+ 'clone': clone,
2165
+ 'parent': this.parent,
2166
+ 'oldSelection': oldSelection,
2167
+ 'newSelection': newSelection
2168
+ });
2169
+ };
2170
+
2171
+ /**
2172
+ * Handle insert before event
2173
+ * @param {String} [field]
2174
+ * @param {*} [value]
2175
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
2176
+ * @private
2177
+ */
2178
+ Node.prototype._onInsertBefore = function (field, value, type) {
2179
+ var oldSelection = this.editor.getSelection();
2180
+
2181
+ var newNode = new Node(this.editor, {
2182
+ 'field': (field != undefined) ? field : '',
2183
+ 'value': (value != undefined) ? value : '',
2184
+ 'type': type
2185
+ });
2186
+ newNode.expand(true);
2187
+ this.parent.insertBefore(newNode, this);
2188
+ this.editor.highlighter.unhighlight();
2189
+ newNode.focus('field');
2190
+ var newSelection = this.editor.getSelection();
2191
+
2192
+ this.editor._onAction('insertBeforeNode', {
2193
+ 'node': newNode,
2194
+ 'beforeNode': this,
2195
+ 'parent': this.parent,
2196
+ 'oldSelection': oldSelection,
2197
+ 'newSelection': newSelection
2198
+ });
2199
+ };
2200
+
2201
+ /**
2202
+ * Handle insert after event
2203
+ * @param {String} [field]
2204
+ * @param {*} [value]
2205
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
2206
+ * @private
2207
+ */
2208
+ Node.prototype._onInsertAfter = function (field, value, type) {
2209
+ var oldSelection = this.editor.getSelection();
2210
+
2211
+ var newNode = new Node(this.editor, {
2212
+ 'field': (field != undefined) ? field : '',
2213
+ 'value': (value != undefined) ? value : '',
2214
+ 'type': type
2215
+ });
2216
+ newNode.expand(true);
2217
+ this.parent.insertAfter(newNode, this);
2218
+ this.editor.highlighter.unhighlight();
2219
+ newNode.focus('field');
2220
+ var newSelection = this.editor.getSelection();
2221
+
2222
+ this.editor._onAction('insertAfterNode', {
2223
+ 'node': newNode,
2224
+ 'afterNode': this,
2225
+ 'parent': this.parent,
2226
+ 'oldSelection': oldSelection,
2227
+ 'newSelection': newSelection
2228
+ });
2229
+ };
2230
+
2231
+ /**
2232
+ * Handle append event
2233
+ * @param {String} [field]
2234
+ * @param {*} [value]
2235
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
2236
+ * @private
2237
+ */
2238
+ Node.prototype._onAppend = function (field, value, type) {
2239
+ var oldSelection = this.editor.getSelection();
2240
+
2241
+ var newNode = new Node(this.editor, {
2242
+ 'field': (field != undefined) ? field : '',
2243
+ 'value': (value != undefined) ? value : '',
2244
+ 'type': type
2245
+ });
2246
+ newNode.expand(true);
2247
+ this.parent.appendChild(newNode);
2248
+ this.editor.highlighter.unhighlight();
2249
+ newNode.focus('field');
2250
+ var newSelection = this.editor.getSelection();
2251
+
2252
+ this.editor._onAction('appendNode', {
2253
+ 'node': newNode,
2254
+ 'parent': this.parent,
2255
+ 'oldSelection': oldSelection,
2256
+ 'newSelection': newSelection
2257
+ });
2258
+ };
2259
+
2260
+ /**
2261
+ * Change the type of the node's value
2262
+ * @param {String} newType
2263
+ * @private
2264
+ */
2265
+ Node.prototype._onChangeType = function (newType) {
2266
+ var oldType = this.type;
2267
+ if (newType != oldType) {
2268
+ var oldSelection = this.editor.getSelection();
2269
+ this.changeType(newType);
2270
+ var newSelection = this.editor.getSelection();
2271
+
2272
+ this.editor._onAction('changeType', {
2273
+ 'node': this,
2274
+ 'oldType': oldType,
2275
+ 'newType': newType,
2276
+ 'oldSelection': oldSelection,
2277
+ 'newSelection': newSelection
2278
+ });
2279
+ }
2280
+ };
2281
+
2282
+ /**
2283
+ * Sort the childs of the node. Only applicable when the node has type 'object'
2284
+ * or 'array'.
2285
+ * @param {String} direction Sorting direction. Available values: "asc", "desc"
2286
+ * @private
2287
+ */
2288
+ Node.prototype._onSort = function (direction) {
2289
+ if (this._hasChilds()) {
2290
+ var order = (direction == 'desc') ? -1 : 1;
2291
+ var prop = (this.type == 'array') ? 'value': 'field';
2292
+ this.hideChilds();
2293
+
2294
+ var oldChilds = this.childs;
2295
+ var oldSort = this.sort;
2296
+
2297
+ // copy the array (the old one will be kept for an undo action
2298
+ this.childs = this.childs.concat();
2299
+
2300
+ // sort the arrays
2301
+ this.childs.sort(function (a, b) {
2302
+ if (a[prop] > b[prop]) return order;
2303
+ if (a[prop] < b[prop]) return -order;
2304
+ return 0;
2305
+ });
2306
+ this.sort = (order == 1) ? 'asc' : 'desc';
2307
+
2308
+ this.editor._onAction('sort', {
2309
+ 'node': this,
2310
+ 'oldChilds': oldChilds,
2311
+ 'oldSort': oldSort,
2312
+ 'newChilds': this.childs,
2313
+ 'newSort': this.sort
2314
+ });
2315
+
2316
+ this.showChilds();
2317
+ }
2318
+ };
2319
+
2320
+ /**
2321
+ * Create a table row with an append button.
2322
+ * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable
2323
+ */
2324
+ Node.prototype.getAppend = function () {
2325
+ if (!this.append) {
2326
+ this.append = new AppendNode(this.editor);
2327
+ this.append.setParent(this);
2328
+ }
2329
+ return this.append.getDom();
2330
+ };
2331
+
2332
+ /**
2333
+ * Find the node from an event target
2334
+ * @param {Node} target
2335
+ * @return {Node | undefined} node or undefined when not found
2336
+ * @static
2337
+ */
2338
+ Node.getNodeFromTarget = function (target) {
2339
+ while (target) {
2340
+ if (target.node) {
2341
+ return target.node;
2342
+ }
2343
+ target = target.parentNode;
2344
+ }
2345
+
2346
+ return undefined;
2347
+ };
2348
+
2349
+ /**
2350
+ * Get the previously rendered node
2351
+ * @return {Node | null} previousNode
2352
+ * @private
2353
+ */
2354
+ Node.prototype._previousNode = function () {
2355
+ var prevNode = null;
2356
+ var dom = this.getDom();
2357
+ if (dom && dom.parentNode) {
2358
+ // find the previous field
2359
+ var prevDom = dom;
2360
+ do {
2361
+ prevDom = prevDom.previousSibling;
2362
+ prevNode = Node.getNodeFromTarget(prevDom);
2363
+ }
2364
+ while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));
2365
+ }
2366
+ return prevNode;
2367
+ };
2368
+
2369
+ /**
2370
+ * Get the next rendered node
2371
+ * @return {Node | null} nextNode
2372
+ * @private
2373
+ */
2374
+ Node.prototype._nextNode = function () {
2375
+ var nextNode = null;
2376
+ var dom = this.getDom();
2377
+ if (dom && dom.parentNode) {
2378
+ // find the previous field
2379
+ var nextDom = dom;
2380
+ do {
2381
+ nextDom = nextDom.nextSibling;
2382
+ nextNode = Node.getNodeFromTarget(nextDom);
2383
+ }
2384
+ while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));
2385
+ }
2386
+
2387
+ return nextNode;
2388
+ };
2389
+
2390
+ /**
2391
+ * Get the first rendered node
2392
+ * @return {Node | null} firstNode
2393
+ * @private
2394
+ */
2395
+ Node.prototype._firstNode = function () {
2396
+ var firstNode = null;
2397
+ var dom = this.getDom();
2398
+ if (dom && dom.parentNode) {
2399
+ var firstDom = dom.parentNode.firstChild;
2400
+ firstNode = Node.getNodeFromTarget(firstDom);
2401
+ }
2402
+
2403
+ return firstNode;
2404
+ };
2405
+
2406
+ /**
2407
+ * Get the last rendered node
2408
+ * @return {Node | null} lastNode
2409
+ * @private
2410
+ */
2411
+ Node.prototype._lastNode = function () {
2412
+ var lastNode = null;
2413
+ var dom = this.getDom();
2414
+ if (dom && dom.parentNode) {
2415
+ var lastDom = dom.parentNode.lastChild;
2416
+ lastNode = Node.getNodeFromTarget(lastDom);
2417
+ while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {
2418
+ lastDom = lastDom.previousSibling;
2419
+ lastNode = Node.getNodeFromTarget(lastDom);
2420
+ }
2421
+ }
2422
+ return lastNode;
2423
+ };
2424
+
2425
+ /**
2426
+ * Get the next element which can have focus.
2427
+ * @param {Element} elem
2428
+ * @return {Element | null} nextElem
2429
+ * @private
2430
+ */
2431
+ Node.prototype._previousElement = function (elem) {
2432
+ var dom = this.dom;
2433
+ // noinspection FallthroughInSwitchStatementJS
2434
+ switch (elem) {
2435
+ case dom.value:
2436
+ if (this.fieldEditable) {
2437
+ return dom.field;
2438
+ }
2439
+ // intentional fall through
2440
+ case dom.field:
2441
+ if (this._hasChilds()) {
2442
+ return dom.expand;
2443
+ }
2444
+ // intentional fall through
2445
+ case dom.expand:
2446
+ return dom.menu;
2447
+ case dom.menu:
2448
+ if (dom.drag) {
2449
+ return dom.drag;
2450
+ }
2451
+ // intentional fall through
2452
+ default:
2453
+ return null;
2454
+ }
2455
+ };
2456
+
2457
+ /**
2458
+ * Get the next element which can have focus.
2459
+ * @param {Element} elem
2460
+ * @return {Element | null} nextElem
2461
+ * @private
2462
+ */
2463
+ Node.prototype._nextElement = function (elem) {
2464
+ var dom = this.dom;
2465
+ // noinspection FallthroughInSwitchStatementJS
2466
+ switch (elem) {
2467
+ case dom.drag:
2468
+ return dom.menu;
2469
+ case dom.menu:
2470
+ if (this._hasChilds()) {
2471
+ return dom.expand;
2472
+ }
2473
+ // intentional fall through
2474
+ case dom.expand:
2475
+ if (this.fieldEditable) {
2476
+ return dom.field;
2477
+ }
2478
+ // intentional fall through
2479
+ case dom.field:
2480
+ if (!this._hasChilds()) {
2481
+ return dom.value;
2482
+ }
2483
+ default:
2484
+ return null;
2485
+ }
2486
+ };
2487
+
2488
+ /**
2489
+ * Get the dom name of given element. returns null if not found.
2490
+ * For example when element == dom.field, "field" is returned.
2491
+ * @param {Element} element
2492
+ * @return {String | null} elementName Available elements with name: 'drag',
2493
+ * 'menu', 'expand', 'field', 'value'
2494
+ * @private
2495
+ */
2496
+ Node.prototype._getElementName = function (element) {
2497
+ var dom = this.dom;
2498
+ for (var name in dom) {
2499
+ if (dom.hasOwnProperty(name)) {
2500
+ if (dom[name] == element) {
2501
+ return name;
2502
+ }
2503
+ }
2504
+ }
2505
+ return null;
2506
+ };
2507
+
2508
+ /**
2509
+ * Test if this node has childs. This is the case when the node is an object
2510
+ * or array.
2511
+ * @return {boolean} hasChilds
2512
+ * @private
2513
+ */
2514
+ Node.prototype._hasChilds = function () {
2515
+ return this.type == 'array' || this.type == 'object';
2516
+ };
2517
+
2518
+ // titles with explanation for the different types
2519
+ Node.TYPE_TITLES = {
2520
+ 'auto': 'Field type "auto". ' +
2521
+ 'The field type is automatically determined from the value ' +
2522
+ 'and can be a string, number, boolean, or null.',
2523
+ 'object': 'Field type "object". ' +
2524
+ 'An object contains an unordered set of key/value pairs.',
2525
+ 'array': 'Field type "array". ' +
2526
+ 'An array contains an ordered collection of values.',
2527
+ 'string': 'Field type "string". ' +
2528
+ 'Field type is not determined from the value, ' +
2529
+ 'but always returned as string.'
2530
+ };
2531
+
2532
+ /**
2533
+ * Show a contextmenu for this node
2534
+ * @param {HTMLElement} anchor Anchor element to attache the context menu to.
2535
+ * @param {function} [onClose] Callback method called when the context menu
2536
+ * is being closed.
2537
+ */
2538
+ Node.prototype.showContextMenu = function (anchor, onClose) {
2539
+ var node = this;
2540
+ var titles = Node.TYPE_TITLES;
2541
+ var items = [];
2542
+
2543
+ items.push({
2544
+ 'text': 'Type',
2545
+ 'title': 'Change the type of this field',
2546
+ 'className': 'type-' + this.type,
2547
+ 'submenu': [
2548
+ {
2549
+ 'text': 'Auto',
2550
+ 'className': 'type-auto' +
2551
+ (this.type == 'auto' ? ' selected' : ''),
2552
+ 'title': titles.auto,
2553
+ 'click': function () {
2554
+ node._onChangeType('auto');
2555
+ }
2556
+ },
2557
+ {
2558
+ 'text': 'Array',
2559
+ 'className': 'type-array' +
2560
+ (this.type == 'array' ? ' selected' : ''),
2561
+ 'title': titles.array,
2562
+ 'click': function () {
2563
+ node._onChangeType('array');
2564
+ }
2565
+ },
2566
+ {
2567
+ 'text': 'Object',
2568
+ 'className': 'type-object' +
2569
+ (this.type == 'object' ? ' selected' : ''),
2570
+ 'title': titles.object,
2571
+ 'click': function () {
2572
+ node._onChangeType('object');
2573
+ }
2574
+ },
2575
+ {
2576
+ 'text': 'String',
2577
+ 'className': 'type-string' +
2578
+ (this.type == 'string' ? ' selected' : ''),
2579
+ 'title': titles.string,
2580
+ 'click': function () {
2581
+ node._onChangeType('string');
2582
+ }
2583
+ }
2584
+ ]
2585
+ });
2586
+
2587
+ if (this._hasChilds()) {
2588
+ var direction = ((this.sort == 'asc') ? 'desc': 'asc');
2589
+ items.push({
2590
+ 'text': 'Sort',
2591
+ 'title': 'Sort the childs of this ' + this.type,
2592
+ 'className': 'sort-' + direction,
2593
+ 'click': function () {
2594
+ node._onSort(direction);
2595
+ },
2596
+ 'submenu': [
2597
+ {
2598
+ 'text': 'Ascending',
2599
+ 'className': 'sort-asc',
2600
+ 'title': 'Sort the childs of this ' + this.type + ' in ascending order',
2601
+ 'click': function () {
2602
+ node._onSort('asc');
2603
+ }
2604
+ },
2605
+ {
2606
+ 'text': 'Descending',
2607
+ 'className': 'sort-desc',
2608
+ 'title': 'Sort the childs of this ' + this.type +' in descending order',
2609
+ 'click': function () {
2610
+ node._onSort('desc');
2611
+ }
2612
+ }
2613
+ ]
2614
+ });
2615
+ }
2616
+
2617
+ if (this.parent && this.parent._hasChilds()) {
2618
+ // create a separator
2619
+ items.push({
2620
+ 'type': 'separator'
2621
+ });
2622
+
2623
+ // create append button (for last child node only)
2624
+ var childs = node.parent.childs;
2625
+ if (node == childs[childs.length - 1]) {
2626
+ items.push({
2627
+ 'text': 'Append',
2628
+ 'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
2629
+ 'submenuTitle': 'Select the type of the field to be appended',
2630
+ 'className': 'append',
2631
+ 'click': function () {
2632
+ node._onAppend('', '', 'auto');
2633
+ },
2634
+ 'submenu': [
2635
+ {
2636
+ 'text': 'Auto',
2637
+ 'className': 'type-auto',
2638
+ 'title': titles.auto,
2639
+ 'click': function () {
2640
+ node._onAppend('', '', 'auto');
2641
+ }
2642
+ },
2643
+ {
2644
+ 'text': 'Array',
2645
+ 'className': 'type-array',
2646
+ 'title': titles.array,
2647
+ 'click': function () {
2648
+ node._onAppend('', []);
2649
+ }
2650
+ },
2651
+ {
2652
+ 'text': 'Object',
2653
+ 'className': 'type-object',
2654
+ 'title': titles.object,
2655
+ 'click': function () {
2656
+ node._onAppend('', {});
2657
+ }
2658
+ },
2659
+ {
2660
+ 'text': 'String',
2661
+ 'className': 'type-string',
2662
+ 'title': titles.string,
2663
+ 'click': function () {
2664
+ node._onAppend('', '', 'string');
2665
+ }
2666
+ }
2667
+ ]
2668
+ });
2669
+ }
2670
+
2671
+ // create insert button
2672
+ items.push({
2673
+ 'text': 'Insert',
2674
+ 'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
2675
+ 'submenuTitle': 'Select the type of the field to be inserted',
2676
+ 'className': 'insert',
2677
+ 'click': function () {
2678
+ node._onInsertBefore('', '', 'auto');
2679
+ },
2680
+ 'submenu': [
2681
+ {
2682
+ 'text': 'Auto',
2683
+ 'className': 'type-auto',
2684
+ 'title': titles.auto,
2685
+ 'click': function () {
2686
+ node._onInsertBefore('', '', 'auto');
2687
+ }
2688
+ },
2689
+ {
2690
+ 'text': 'Array',
2691
+ 'className': 'type-array',
2692
+ 'title': titles.array,
2693
+ 'click': function () {
2694
+ node._onInsertBefore('', []);
2695
+ }
2696
+ },
2697
+ {
2698
+ 'text': 'Object',
2699
+ 'className': 'type-object',
2700
+ 'title': titles.object,
2701
+ 'click': function () {
2702
+ node._onInsertBefore('', {});
2703
+ }
2704
+ },
2705
+ {
2706
+ 'text': 'String',
2707
+ 'className': 'type-string',
2708
+ 'title': titles.string,
2709
+ 'click': function () {
2710
+ node._onInsertBefore('', '', 'string');
2711
+ }
2712
+ }
2713
+ ]
2714
+ });
2715
+
2716
+ // create duplicate button
2717
+ items.push({
2718
+ 'text': 'Duplicate',
2719
+ 'title': 'Duplicate this field (Ctrl+D)',
2720
+ 'className': 'duplicate',
2721
+ 'click': function () {
2722
+ node._onDuplicate();
2723
+ }
2724
+ });
2725
+
2726
+ // create remove button
2727
+ items.push({
2728
+ 'text': 'Remove',
2729
+ 'title': 'Remove this field (Ctrl+Del)',
2730
+ 'className': 'remove',
2731
+ 'click': function () {
2732
+ node._onRemove();
2733
+ }
2734
+ });
2735
+ }
2736
+
2737
+ var menu = new ContextMenu(items, {close: onClose});
2738
+ menu.show(anchor);
2739
+ };
2740
+
2741
+ /**
2742
+ * get the type of a value
2743
+ * @param {*} value
2744
+ * @return {String} type Can be 'object', 'array', 'string', 'auto'
2745
+ * @private
2746
+ */
2747
+ Node.prototype._getType = function(value) {
2748
+ if (value instanceof Array) {
2749
+ return 'array';
2750
+ }
2751
+ if (value instanceof Object) {
2752
+ return 'object';
2753
+ }
2754
+ if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {
2755
+ return 'string';
2756
+ }
2757
+
2758
+ return 'auto';
2759
+ };
2760
+
2761
+ /**
2762
+ * cast contents of a string to the correct type. This can be a string,
2763
+ * a number, a boolean, etc
2764
+ * @param {String} str
2765
+ * @return {*} castedStr
2766
+ * @private
2767
+ */
2768
+ Node.prototype._stringCast = function(str) {
2769
+ var lower = str.toLowerCase(),
2770
+ num = Number(str), // will nicely fail with '123ab'
2771
+ numFloat = parseFloat(str); // will nicely fail with ' '
2772
+
2773
+ if (str == '') {
2774
+ return '';
2775
+ }
2776
+ else if (lower == 'null') {
2777
+ return null;
2778
+ }
2779
+ else if (lower == 'true') {
2780
+ return true;
2781
+ }
2782
+ else if (lower == 'false') {
2783
+ return false;
2784
+ }
2785
+ else if (!isNaN(num) && !isNaN(numFloat)) {
2786
+ return num;
2787
+ }
2788
+ else {
2789
+ return str;
2790
+ }
2791
+ };
2792
+
2793
+ /**
2794
+ * escape a text, such that it can be displayed safely in an HTML element
2795
+ * @param {String} text
2796
+ * @return {String} escapedText
2797
+ * @private
2798
+ */
2799
+ Node.prototype._escapeHTML = function (text) {
2800
+ var htmlEscaped = String(text)
2801
+ .replace(/</g, '&lt;')
2802
+ .replace(/>/g, '&gt;')
2803
+ .replace(/ /g, ' &nbsp;') // replace double space with an nbsp and space
2804
+ .replace(/^ /, '&nbsp;') // space at start
2805
+ .replace(/ $/, '&nbsp;'); // space at end
2806
+
2807
+ var json = JSON.stringify(htmlEscaped);
2808
+ return json.substring(1, json.length - 1);
2809
+ };
2810
+
2811
+ /**
2812
+ * unescape a string.
2813
+ * @param {String} escapedText
2814
+ * @return {String} text
2815
+ * @private
2816
+ */
2817
+ Node.prototype._unescapeHTML = function (escapedText) {
2818
+ var json = '"' + this._escapeJSON(escapedText) + '"';
2819
+ var htmlEscaped = util.parse(json);
2820
+ return htmlEscaped
2821
+ .replace(/&lt;/g, '<')
2822
+ .replace(/&gt;/g, '>')
2823
+ .replace(/&nbsp;/g, ' ');
2824
+ };
2825
+
2826
+ /**
2827
+ * escape a text to make it a valid JSON string. The method will:
2828
+ * - replace unescaped double quotes with '\"'
2829
+ * - replace unescaped backslash with '\\'
2830
+ * - replace returns with '\n'
2831
+ * @param {String} text
2832
+ * @return {String} escapedText
2833
+ * @private
2834
+ */
2835
+ Node.prototype._escapeJSON = function (text) {
2836
+ // TODO: replace with some smart regex (only when a new solution is faster!)
2837
+ var escaped = '';
2838
+ var i = 0, iMax = text.length;
2839
+ while (i < iMax) {
2840
+ var c = text.charAt(i);
2841
+ if (c == '\n') {
2842
+ escaped += '\\n';
2843
+ }
2844
+ else if (c == '\\') {
2845
+ escaped += c;
2846
+ i++;
2847
+
2848
+ c = text.charAt(i);
2849
+ if ('"\\/bfnrtu'.indexOf(c) == -1) {
2850
+ escaped += '\\'; // no valid escape character
2851
+ }
2852
+ escaped += c;
2853
+ }
2854
+ else if (c == '"') {
2855
+ escaped += '\\"';
2856
+ }
2857
+ else {
2858
+ escaped += c;
2859
+ }
2860
+ i++;
2861
+ }
2862
+
2863
+ return escaped;
2864
+ };