bee_api 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };