pseudo-dom 0.1.0 → 0.3.0

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 (41) hide show
  1. package/README.md +626 -116
  2. package/browser/pseudo-dom.js +849 -407
  3. package/browser/pseudo-dom.min.js +1 -1
  4. package/dist/classes/PseudoEventListener.d.ts +25 -25
  5. package/dist/classes/PseudoEventListener.js +37 -34
  6. package/dist/classes/PseudoEventListener.min.js +1 -1
  7. package/dist/classes/PseudoHTMLDocument.d.ts +2 -2
  8. package/dist/classes/PseudoHTMLDocument.js +10 -21
  9. package/dist/classes/PseudoHTMLDocument.min.js +1 -1
  10. package/dist/classes/PseudoNodeList.js +14 -8
  11. package/dist/classes/PseudoNodeList.min.js +1 -1
  12. package/dist/factories/generateNode.d.ts +2 -10
  13. package/dist/factories/generateNode.js +9 -42
  14. package/dist/factories/generateNode.min.js +1 -1
  15. package/dist/functions/getParentNodes.d.ts +8 -6
  16. package/dist/functions/getParentNodes.js +16 -9
  17. package/dist/functions/getParentNodes.min.js +1 -1
  18. package/dist/functions/getParentNodesFromAttribute.d.ts +7 -7
  19. package/dist/functions/getParentNodesFromAttribute.js +21 -1
  20. package/dist/functions/getParentNodesFromAttribute.min.js +1 -1
  21. package/dist/functions.d.ts +1 -1
  22. package/dist/interfaces/PseudoEventTarget.d.ts +2 -2
  23. package/dist/services/DocumentFragmentService.d.ts +9 -0
  24. package/dist/services/DocumentFragmentService.js +16 -1
  25. package/dist/services/DocumentFragmentService.min.js +1 -1
  26. package/dist/services/DocumentService.d.ts +8 -0
  27. package/dist/services/DocumentService.js +15 -1
  28. package/dist/services/DocumentService.min.js +1 -1
  29. package/dist/services/ElementService.d.ts +6 -6
  30. package/dist/services/ElementService.js +28 -17
  31. package/dist/services/ElementService.min.js +1 -1
  32. package/dist/services/EventService.d.ts +16 -4
  33. package/dist/services/EventService.js +59 -31
  34. package/dist/services/EventService.min.js +1 -1
  35. package/dist/services/EventTargetService.d.ts +41 -9
  36. package/dist/services/EventTargetService.js +121 -52
  37. package/dist/services/EventTargetService.min.js +1 -1
  38. package/dist/services/NodeService.d.ts +31 -16
  39. package/dist/services/NodeService.js +90 -33
  40. package/dist/services/NodeService.min.js +1 -1
  41. package/package.json +2 -2
@@ -20,6 +20,13 @@
20
20
  * @property {boolean} isDefault
21
21
  */
22
22
  class PseudoEventListener {
23
+ /**
24
+ * @param {string} eventType The type of event this listens for
25
+ * @param {Object} [options] The capture, once and passive options
26
+ * @param {Function} handleEvent The function which is called with the event, already bound to what it should run as
27
+ * @param {Function} [originalCallback=handleEvent] The function (or object) which was given when registering, used to find this listener again
28
+ * @constructor
29
+ */
23
30
  constructor (eventType, {
24
31
  capture = false,
25
32
  once = false,
@@ -32,6 +39,7 @@
32
39
  }
33
40
  this.eventType = ''
34
41
  this.defaultListener = false
42
+ this.isRemoved = false
35
43
  this.eventOptions = {
36
44
  capture,
37
45
  once,
@@ -49,6 +57,11 @@
49
57
  return this.originalCallback
50
58
  }
51
59
 
60
+ /** Whether this listener listens in the capture phase (and at the target) rather than in the bubble phase. */
61
+ get capture () {
62
+ return this.eventOptions.capture
63
+ }
64
+
52
65
  get isDefault () {
53
66
  return this.defaultListener
54
67
  }
@@ -57,6 +70,20 @@
57
70
  return this.eventOptions.once
58
71
  }
59
72
 
73
+ /** Whether the listener promises not to prevent the default (preventDefault does nothing while it runs). */
74
+ get passive () {
75
+ return this.eventOptions.passive
76
+ }
77
+
78
+ /** Whether this listener has been removed, a removed listener does not run even if the event already started. */
79
+ get removed () {
80
+ return this.isRemoved
81
+ }
82
+
83
+ set removed (removed) {
84
+ this.isRemoved = removed
85
+ }
86
+
60
87
  /**
61
88
  * @method
62
89
  * @name PseudoEventListener#handleEvent
@@ -68,6 +95,7 @@
68
95
  }
69
96
 
70
97
  /**
98
+ * A capture listener runs while the event travels down to the target.
71
99
  * @method
72
100
  * @name PseudoEventListener#doCapturePhase
73
101
  * @param {PseudoEvent} event
@@ -78,6 +106,7 @@
78
106
  }
79
107
 
80
108
  /**
109
+ * Every listener of the target itself runs, capture listeners first.
81
110
  * @method
82
111
  * @name PseudoEventListener#doTargetPhase
83
112
  * @param {PseudoEvent} event
@@ -88,13 +117,14 @@
88
117
  }
89
118
 
90
119
  /**
120
+ * A listener which is not a capture listener runs while the event travels back up (when it bubbles).
91
121
  * @method
92
122
  * @name PseudoEventListener#doBubblePhase
93
123
  * @param {PseudoEvent} event
94
- * @returns {boolean|*}
124
+ * @returns {boolean}
95
125
  */
96
126
  doBubblePhase (event) {
97
- return event.eventPhase === EventService_1.EventService.BUBBLING_PHASE && (event.bubbles || !this.eventOptions.capture)
127
+ return event.eventPhase === EventService_1.EventService.BUBBLING_PHASE && !this.eventOptions.capture
98
128
  }
99
129
 
100
130
  /**
@@ -108,43 +138,16 @@
108
138
  }
109
139
 
110
140
  /**
111
- * @method
112
- * @name PseudoEventListener#skipDefault
113
- * @param {PseudoEvent} event
114
- * @returns {boolean|*}
115
- */
116
- skipDefault (event) {
117
- return this.isDefault && event.defaultPrevented
118
- }
119
-
120
- /**
121
- * @method
122
- * @name PseudoEventListener#stopPropagation
123
- * @param {PseudoEvent} event
124
- * @returns {boolean}
125
- */
126
- stopPropagation (event) {
127
- return !this.doTargetPhase(event) && event.inner.propagationStopped
128
- }
129
-
130
- /**
131
- * @method
132
- * @name PseudoEventListener#nonPassiveHalt
133
- * @param {PseudoEvent} event
134
- * @returns {boolean|*}
135
- */
136
- nonPassiveHalt (event) {
137
- return !this.eventOptions.passive && (this.skipDefault(event) || event.inner.immediatePropagationStopped || this.stopPropagation(event))
138
- }
139
-
140
- /**
141
+ * Whether this listener should not run for the event as it is now (it was removed, or it is for another phase).
142
+ * Stopping propagation is handled by the dispatching, since it stops other targets and not the listeners of the
143
+ * current one.
141
144
  * @method
142
145
  * @name PseudoEventListener#rejectEvent
143
146
  * @param {PseudoEvent} event
144
- * @returns {*|boolean}
147
+ * @returns {boolean}
145
148
  */
146
149
  rejectEvent (event) {
147
- return this.nonPassiveHalt(event) || this.skipPhase(event)
150
+ return this.isRemoved || this.skipPhase(event)
148
151
  }
149
152
  }
150
153
  exports.default = PseudoEventListener
@@ -152,13 +155,6 @@
152
155
  2: [function (require, module, exports) {
153
156
  'use strict'
154
157
 
155
- const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
156
- return mod && mod.__esModule
157
- ? mod
158
- : {
159
- default: mod
160
- }
161
- }
162
158
  Object.defineProperty(exports, '__esModule', {
163
159
  value: true
164
160
  })
@@ -172,8 +168,6 @@
172
168
  * @type {PseudoHTMLElement}
173
169
  */
174
170
  const HTMLElementService_1 = require('../services/HTMLElementService')
175
- const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
176
- const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
177
171
  /**
178
172
  * Simulate the behaviour of the HTMLDocument Class when there is no DOM available.
179
173
  * @author Joshua Heagle <joshuaheagle@gmail.com>
@@ -181,7 +175,7 @@
181
175
  * @augments PseudoHTMLElement
182
176
  * @property {PseudoHTMLElement} head - A reference to the Head child element
183
177
  * @property {PseudoHTMLElement} body - A reference to the Body child element
184
- * @property {function} createElement - Generate a new PseudoHTMLElement with parent of document
178
+ * @property {function} createElement - Generate a new PseudoHTMLElement (which is not in the document until it is appended)
185
179
  */
186
180
  class PseudoHTMLDocument extends HTMLElementService_1.HTMLElementService {
187
181
  /**
@@ -191,43 +185,41 @@
191
185
  constructor () {
192
186
  super()
193
187
  const html = new HTMLElementService_1.HTMLElementService({
194
- tagName: 'html',
195
- parent: this
188
+ tagName: 'html'
196
189
  })
190
+ this.appendChild(html)
197
191
  /**
198
192
  * Create document head element
199
193
  * @type {PseudoHTMLElement}
200
194
  */
201
195
  this.head = new HTMLElementService_1.HTMLElementService({
202
- tagName: 'head',
203
- parent: html
196
+ tagName: 'head'
204
197
  })
198
+ html.appendChild(this.head)
205
199
  /**
206
200
  * Create document body element
207
201
  * @type {PseudoHTMLElement}
208
202
  */
209
203
  this.body = new HTMLElementService_1.HTMLElementService({
210
- tagName: 'body',
211
- parent: html
204
+ tagName: 'body'
212
205
  })
213
- html.children = (0, generateNodeList_1.default)(TreeLinker_1.TreeLinker.fromArray([this.head, this.body]).head)
206
+ html.appendChild(this.body)
214
207
  }
215
208
 
216
209
  /**
217
- * Create and return a PseudoHTMLElement
210
+ * Create and return a PseudoHTMLElement, which is not added to the document until it is appended somewhere
218
211
  * @param {string} tagName - Tag Name is a string representing the type of Dom element this represents
219
212
  * @returns {PseudoHTMLElement}
220
213
  */
221
214
  createElement (tagName = 'div') {
222
- const returnElement = new HTMLElementService_1.HTMLElementService({
215
+ // Like the DOM, the new element is not added anywhere: it has no parent until it is appended
216
+ return new HTMLElementService_1.HTMLElementService({
223
217
  tagName
224
218
  })
225
- returnElement.parent = this
226
- return returnElement
227
219
  }
228
220
  }
229
221
  exports.default = PseudoHTMLDocument
230
- }, { '../factories/generateNodeList': 5, '../services/HTMLElementService': 14, 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker': 24 }],
222
+ }, { '../services/HTMLElementService': 14 }],
231
223
  3: [function (require, module, exports) {
232
224
  'use strict'
233
225
 
@@ -255,16 +247,22 @@
255
247
  * @returns {Iterator}
256
248
  */
257
249
  [Symbol.iterator] () {
258
- const linkers = super[Symbol.iterator]()
250
+ // Walk the nodes of this list only (the linkers of a child list have no children of their own)
251
+ let current = this.first
259
252
  return {
260
253
  next: () => {
261
- const result = linkers.next()
262
- return result.done
263
- ? result
264
- : {
265
- done: false,
266
- value: result.value.data
267
- }
254
+ if (current === null) {
255
+ return {
256
+ done: true,
257
+ value: undefined
258
+ }
259
+ }
260
+ const result = {
261
+ done: false,
262
+ value: current.data
263
+ }
264
+ current = current.next
265
+ return result
268
266
  }
269
267
  }
270
268
  }
@@ -380,31 +378,58 @@
380
378
  6: [function (require, module, exports) {
381
379
  'use strict'
382
380
 
383
- const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
384
- return mod && mod.__esModule
385
- ? mod
386
- : {
387
- default: mod
388
- }
389
- }
390
381
  Object.defineProperty(exports, '__esModule', {
391
382
  value: true
392
383
  })
393
- const getParentNodesFromAttribute_1 = __importDefault(require('./getParentNodesFromAttribute'))
394
- const getParentNodes = node => (0, getParentNodesFromAttribute_1.default)('', false, node)
384
+ /**
385
+ * Get all of the ancestors of a node, starting with the root of the tree and ending with the node's own parent (the
386
+ * order in which an event travels down through them). A node which has no parent has no ancestors.
387
+ * @function getParentNodes
388
+ * @param {PseudoEventTarget|PseudoNode|*} node The node to find the ancestors of
389
+ * @returns {Array<PseudoNode>}
390
+ */
391
+ const getParentNodes = node => {
392
+ const parents = []
393
+ let current = node && node.parentNode ? node.parentNode : null
394
+ while (current) {
395
+ parents.unshift(current)
396
+ current = current.parentNode
397
+ }
398
+ return parents
399
+ }
395
400
  exports.default = getParentNodes
396
- }, { './getParentNodesFromAttribute': 7 }],
401
+ }, {}],
397
402
  7: [function (require, module, exports) {
398
403
  'use strict'
399
404
 
405
+ require('core-js/modules/esnext.iterator.constructor.js')
406
+ require('core-js/modules/esnext.iterator.filter.js')
407
+ const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
408
+ return mod && mod.__esModule
409
+ ? mod
410
+ : {
411
+ default: mod
412
+ }
413
+ }
400
414
  Object.defineProperty(exports, '__esModule', {
401
415
  value: true
402
416
  })
417
+ const getParentNodes_1 = __importDefault(require('./getParentNodes'))
418
+ /**
419
+ * A selector function for retrieving existing parent PseudoNode from the given child item.
420
+ * This function will check all the parents starting from node, and scan the attributes
421
+ * property for matches. The return array contains all matching parent ancestors, starting with the root of the tree.
422
+ * @function getParentNodesFromAttribute
423
+ * @param {string} attr The property to compare on each ancestor (a missing property counts as false)
424
+ * @param {boolean|number|string} value The value the property must have
425
+ * @param {PseudoEventTarget|PseudoNode|*} node The node to find the matching ancestors of
426
+ * @returns {Array.<PseudoNode>}
427
+ */
403
428
  const getParentNodesFromAttribute = (attr, value, node) => {
404
- return Object.keys(node.parentNode).length ? (node.parentNode[attr] || false) === value ? getParentNodesFromAttribute(attr, value, node.parentNode).concat([node.parentNode]) : getParentNodesFromAttribute(attr, value, node.parentNode) : []
429
+ return (0, getParentNodes_1.default)(node).filter(parent => (parent[attr] || false) === value)
405
430
  }
406
431
  exports.default = getParentNodesFromAttribute
407
- }, {}],
432
+ }, { './getParentNodes': 6, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.filter.js': 131 }],
408
433
  8: [function (require, module, exports) {
409
434
  'use strict'
410
435
 
@@ -679,8 +704,7 @@
679
704
  value: true
680
705
  })
681
706
  exports.ElementService = void 0
682
- const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
683
- const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
707
+ const EventService_1 = require('./EventService')
684
708
  const NodeService_1 = require('./NodeService')
685
709
  const AttrService_1 = require('./AttrService')
686
710
  const DOMTokenListService_1 = require('./DOMTokenListService')
@@ -706,8 +730,8 @@
706
730
  * @param {Object} [settings={}]
707
731
  * @param {string} [settings.tagName=''] The name of the tag this element represents
708
732
  * @param {Array<{name: string, value: *}>} [settings.attributes=[]] The attributes (also assigned as properties) to start with
709
- * @param {PseudoNode|null} [settings.parent=null] The parent node
710
- * @param {Array} [settings.children=[]] The values or nodes to start as children
733
+ * @param {PseudoNode|null} [settings.parent=null] The node to add this element to as its last child
734
+ * @param {Array<PseudoNode>} [settings.children=[]] The nodes to start as children
711
735
  * @constructor
712
736
  */
713
737
  constructor ({
@@ -717,9 +741,8 @@
717
741
  children = []
718
742
  } = {}) {
719
743
  super()
744
+ this.defaultEventApplied = false
720
745
  this.tokenList = new DOMTokenListService_1.DOMTokenListService()
721
- this.parent = parent
722
- this.children = (0, generateNodeList_1.default)(TreeLinker_1.TreeLinker.fromArray(children).head)
723
746
  this.tag = tagName
724
747
  this.attributeList = attributes.concat([{
725
748
  name: 'className',
@@ -740,6 +763,15 @@
740
763
  }) => {
741
764
  this[name] = value
742
765
  })
766
+ children.forEach(child => {
767
+ if (!child || typeof child.nodeType !== 'number') {
768
+ throw new TypeError('The children of an element must be nodes.')
769
+ }
770
+ this.appendChild(child)
771
+ })
772
+ if (parent) {
773
+ parent.appendChild(this)
774
+ }
743
775
  }
744
776
 
745
777
  get tagName () {
@@ -775,34 +807,38 @@
775
807
  */
776
808
  applyDefaultEvent () {
777
809
  let callback = event => undefined
810
+ if (this.defaultEventApplied) {
811
+ return callback
812
+ }
778
813
  switch (this.tagName) {
779
- case 'form':
780
- this.addEventListener('submit', callback)
781
- break
782
814
  case 'button':
783
815
  case 'input':
784
816
  if (/^(submit|image)$/i.test(this.type || '')) {
817
+ // Clicking a submit button submits the form it is in: the form gets a submit event, which can be cancelled
785
818
  callback = event => {
786
819
  const forms = (0, getParentNodesFromAttribute_1.default)('tagName', 'form', this)
787
820
  if (forms.length) {
788
- forms[0].submit()
821
+ forms[forms.length - 1].dispatchEvent(new EventService_1.EventService('submit', {
822
+ bubbles: true,
823
+ cancelable: true
824
+ }))
789
825
  }
790
826
  }
791
827
  super.setDefaultEvent('click', callback)
828
+ this.defaultEventApplied = true
792
829
  }
793
830
  }
794
831
  return callback
795
832
  }
796
833
 
797
834
  /**
798
- *
799
- * @param {PseudoNode|ElementService} childElement
800
- * @returns {PseudoNode}
835
+ * An element which is added as a child gets its default events (for example a submit button submits its form).
836
+ * @param {NodeService} child The node which was inserted
801
837
  */
802
- appendChild (childElement) {
803
- super.appendChild(childElement)
804
- childElement.applyDefaultEvent()
805
- return childElement
838
+ childInserted (child) {
839
+ if (typeof child.applyDefaultEvent === 'function') {
840
+ child.applyDefaultEvent()
841
+ }
806
842
  }
807
843
 
808
844
  /**
@@ -866,7 +902,7 @@
866
902
  }
867
903
  }
868
904
  exports.ElementService = ElementService
869
- }, { '../factories/generateNodeList': 5, '../functions/getParentNodesFromAttribute': 7, './AttrService': 9, './DOMTokenListService': 10, './NamedNodeMapService': 15, './NodeService': 16, 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker': 24, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.find.js': 132, 'core-js/modules/esnext.iterator.for-each.js': 133, 'core-js/modules/esnext.iterator.map.js': 134, 'core-js/modules/esnext.iterator.some.js': 136 }],
905
+ }, { '../functions/getParentNodesFromAttribute': 7, './AttrService': 9, './DOMTokenListService': 10, './EventService': 12, './NamedNodeMapService': 15, './NodeService': 16, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.find.js': 132, 'core-js/modules/esnext.iterator.for-each.js': 133, 'core-js/modules/esnext.iterator.map.js': 134, 'core-js/modules/esnext.iterator.some.js': 136 }],
870
906
  12: [function (require, module, exports) {
871
907
  'use strict'
872
908
 
@@ -875,18 +911,10 @@
875
911
  * @author Joshua Heagle <joshuaheagle@gmail.com>
876
912
  * @version 1.0.0
877
913
  */
878
- const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
879
- return mod && mod.__esModule
880
- ? mod
881
- : {
882
- default: mod
883
- }
884
- }
885
914
  Object.defineProperty(exports, '__esModule', {
886
915
  value: true
887
916
  })
888
917
  exports.EventService = void 0
889
- const getParentNodes_1 = __importDefault(require('../functions/getParentNodes'))
890
918
  /**
891
919
  * Simulate the behaviour of the Event Class when there is no DOM available.
892
920
  * @author Joshua Heagle <joshuaheagle@gmail.com>
@@ -921,20 +949,20 @@
921
949
  *
922
950
  * @param {string} typeArg
923
951
  * @param {Object} [eventOptions={}]
924
- * @param {boolean} [eventOptions.bubbles=true]
925
- * @param {boolean} [eventOptions.cancelable=true]
926
- * @param {boolean} [eventOptions.composed=true]
952
+ * @param {boolean} [eventOptions.bubbles=false]
953
+ * @param {boolean} [eventOptions.cancelable=false]
954
+ * @param {boolean} [eventOptions.composed=false]
927
955
  * @constructor
928
956
  */
929
957
  constructor (typeArg = '', {
930
- bubbles = true,
931
- cancelable = true,
932
- composed = true
958
+ bubbles = false,
959
+ cancelable = false,
960
+ composed = false
933
961
  } = {}) {
934
962
  this.properties = {
935
- bubbles: true,
936
- cancelable: true,
937
- composed: true,
963
+ bubbles: false,
964
+ cancelable: false,
965
+ composed: false,
938
966
  currentTarget: null,
939
967
  defaultPrevented: false,
940
968
  immediatePropagationStopped: false,
@@ -943,7 +971,10 @@
943
971
  target: null,
944
972
  timeStamp: Math.floor(Date.now() / 1000),
945
973
  type: '',
946
- isTrusted: true
974
+ isTrusted: true,
975
+ dispatching: false,
976
+ inPassiveListener: false,
977
+ path: []
947
978
  }
948
979
  this.setReadOnlyProperties({
949
980
  type: typeArg,
@@ -1000,12 +1031,21 @@
1000
1031
  get inner () {
1001
1032
  const self = this
1002
1033
  return {
1034
+ get currentTarget () {
1035
+ return self.properties.currentTarget
1036
+ },
1003
1037
  set currentTarget (target) {
1004
1038
  self.properties.currentTarget = target
1005
1039
  },
1040
+ get eventPhase () {
1041
+ return self.properties.eventPhase
1042
+ },
1006
1043
  set eventPhase (phase) {
1007
1044
  self.properties.eventPhase = phase
1008
1045
  },
1046
+ get target () {
1047
+ return self.properties.target
1048
+ },
1009
1049
  set target (target) {
1010
1050
  self.properties.target = target
1011
1051
  },
@@ -1014,6 +1054,35 @@
1014
1054
  },
1015
1055
  get propagationStopped () {
1016
1056
  return self.properties.propagationStopped
1057
+ },
1058
+ get dispatching () {
1059
+ return self.properties.dispatching
1060
+ },
1061
+ set dispatching (dispatching) {
1062
+ self.properties.dispatching = dispatching
1063
+ },
1064
+ get inPassiveListener () {
1065
+ return self.properties.inPassiveListener
1066
+ },
1067
+ set inPassiveListener (passive) {
1068
+ self.properties.inPassiveListener = passive
1069
+ },
1070
+ get path () {
1071
+ return self.properties.path
1072
+ },
1073
+ set path (path) {
1074
+ self.properties.path = path
1075
+ },
1076
+ finishDispatch () {
1077
+ self.setReadOnlyProperties({
1078
+ currentTarget: null,
1079
+ eventPhase: EventService.NONE,
1080
+ path: [],
1081
+ dispatching: false,
1082
+ inPassiveListener: false,
1083
+ propagationStopped: false,
1084
+ immediatePropagationStopped: false
1085
+ })
1017
1086
  }
1018
1087
  }
1019
1088
  }
@@ -1024,16 +1093,8 @@
1024
1093
  * @returns {Array.<PseudoEventTarget>}
1025
1094
  */
1026
1095
  composedPath () {
1027
- switch (this.eventPhase) {
1028
- case EventService.CAPTURING_PHASE:
1029
- return (0, getParentNodes_1.default)(this.target)
1030
- case EventService.BUBBLING_PHASE:
1031
- return (0, getParentNodes_1.default)(this.target).slice().reverse()
1032
- case EventService.AT_TARGET:
1033
- return [this.target]
1034
- default:
1035
- return []
1036
- }
1096
+ // While the event is being dispatched this is every target it travels through, the target first and the root last
1097
+ return this.properties.dispatching ? this.properties.path.slice() : []
1037
1098
  }
1038
1099
 
1039
1100
  /**
@@ -1042,9 +1103,12 @@
1042
1103
  * @returns {null}
1043
1104
  */
1044
1105
  preventDefault () {
1045
- this.setReadOnlyProperties({
1046
- defaultPrevented: true
1047
- })
1106
+ // Only an event which can be cancelled can be prevented, and a passive listener cannot prevent the default
1107
+ if (this.cancelable && !this.properties.inPassiveListener) {
1108
+ this.setReadOnlyProperties({
1109
+ defaultPrevented: true
1110
+ })
1111
+ }
1048
1112
  return null
1049
1113
  }
1050
1114
 
@@ -1084,7 +1148,7 @@
1084
1148
  EventService.CAPTURING_PHASE = 1
1085
1149
  EventService.AT_TARGET = 2
1086
1150
  EventService.BUBBLING_PHASE = 3
1087
- }, { '../functions/getParentNodes': 6 }],
1151
+ }, {}],
1088
1152
  13: [function (require, module, exports) {
1089
1153
  'use strict'
1090
1154
 
@@ -1092,6 +1156,8 @@
1092
1156
  require('core-js/modules/esnext.iterator.filter.js')
1093
1157
  require('core-js/modules/esnext.iterator.find.js')
1094
1158
  require('core-js/modules/esnext.iterator.for-each.js')
1159
+ require('core-js/modules/esnext.iterator.map.js')
1160
+ require('core-js/modules/esnext.iterator.some.js')
1095
1161
  const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
1096
1162
  return mod && mod.__esModule
1097
1163
  ? mod
@@ -1109,9 +1175,13 @@
1109
1175
  */
1110
1176
  const EventService_1 = require('./EventService')
1111
1177
  const PseudoEventListener_1 = __importDefault(require('../classes/PseudoEventListener'))
1178
+ const getParentNodes_1 = __importDefault(require('../functions/getParentNodes'))
1112
1179
  const LinkedList_1 = require('collect-your-stuff/dist/collections/linked-list/LinkedList')
1113
1180
  /**
1114
1181
  * Simulate the behaviour of the EventTarget Class when there is no DOM available.
1182
+ * Dispatching an event sends it through the tree the way the DOM does: down from the root to the target (capture
1183
+ * listeners), to the target itself, then back up to the root (the listeners which are not capture listeners, when the
1184
+ * event bubbles).
1115
1185
  * @author Joshua Heagle <joshuaheagle@gmail.com>
1116
1186
  * @class
1117
1187
  * @property {Object.<string, Array.<PseudoEventListener>>} listeners
@@ -1141,33 +1211,51 @@
1141
1211
  }
1142
1212
 
1143
1213
  /**
1144
- * Run each of the listeners registered on this target for the type of the event.
1145
- * Listeners which do not apply to the event's phase are skipped, running stops once immediate propagation is stopped,
1146
- * and listeners added or removed while running do not change which ones run for this event.
1147
- * @param {EventService} event
1148
- * @returns {*} true when there was nothing registered, otherwise the last value returned from a handler (null when none ran)
1214
+ * Run the listeners registered on this target for the type of the event which apply to the phase the event is in
1215
+ * (at the target, the capture listeners run before the others). Listeners which are added while this runs do not run
1216
+ * for this event, and listeners which are removed while it runs no longer do. Running stops as soon as immediate
1217
+ * propagation is stopped. A listener which throws does not stop the others.
1218
+ * @param {EventService} event The event, which is at a phase and has a current target
1219
+ * @returns {Array<*>} The errors which the listeners threw
1149
1220
  */
1150
1221
  runEvents (event) {
1222
+ const errors = []
1151
1223
  if (!(event.type in this.listeners)) {
1152
- return true
1224
+ return errors
1153
1225
  }
1154
- const listeners = this.listeners[event.type]
1155
- let eventReturn = null
1156
- // Work from a copy of the linkers so that removing a listener (for example a once listener) does not disturb the walk
1157
- for (const linker of Array.from(listeners)) {
1158
- const listener = linker.data
1226
+ const listeners = Array.from(this.listeners[event.type]).map(linker => linker.data)
1227
+ // At the target the capture listeners come first, otherwise the order is the order they were added
1228
+ const ordered = event.eventPhase === EventService_1.EventService.AT_TARGET ? listeners.filter(listener => listener.capture).concat(listeners.filter(listener => !listener.capture)) : listeners
1229
+ for (const listener of ordered) {
1159
1230
  if (event.inner.immediatePropagationStopped) {
1160
1231
  break
1161
1232
  }
1162
1233
  if (listener.rejectEvent(event)) {
1163
1234
  continue
1164
1235
  }
1165
- eventReturn = listener.handleEvent(event)
1166
1236
  if (listener.once) {
1167
- listeners.remove(linker)
1237
+ this.removeListener(event.type, listener)
1238
+ }
1239
+ event.inner.inPassiveListener = listener.passive
1240
+ try {
1241
+ listener.handleEvent(event)
1242
+ } catch (error) {
1243
+ errors.push(error)
1168
1244
  }
1245
+ event.inner.inPassiveListener = false
1169
1246
  }
1170
- return eventReturn
1247
+ return errors
1248
+ }
1249
+
1250
+ /**
1251
+ * Take a listener out of the registered listeners, so that it does not run again.
1252
+ * @param {string} type
1253
+ * @param {PseudoEventListener} listener
1254
+ */
1255
+ removeListener (type, listener) {
1256
+ listener.removed = true
1257
+ const registered = this.listeners[type]
1258
+ Array.from(registered).filter(linker => linker.data === listener).forEach(linker => registered.remove(linker))
1171
1259
  }
1172
1260
 
1173
1261
  /**
@@ -1180,48 +1268,34 @@
1180
1268
  this.defaultEvent[type] = callback
1181
1269
  }
1182
1270
 
1183
- runDefaultEvent (event) {
1184
- if (event.defaultPrevented) {
1185
- return false
1186
- }
1187
- this.defaultEvent[event.type](event)
1188
- return true
1189
- }
1190
-
1191
- startEvents (eventType) {
1192
- const event = new EventService_1.EventService(eventType)
1193
- event.inner.target = this;
1194
- [EventService_1.EventService.CAPTURING_PHASE, EventService_1.EventService.AT_TARGET, EventService_1.EventService.BUBBLING_PHASE].forEach(phase => {
1195
- let continueEvents = null
1196
- if (phase === EventService_1.EventService.AT_TARGET || !event.inner.propagationStopped) {
1197
- event.inner.eventPhase = phase
1198
- event.composedPath().forEach(target => {
1199
- event.inner.currentTarget = target
1200
- continueEvents = event.currentTarget.runEvents(event)
1201
- })
1202
- }
1203
- if (event.eventPhase === EventService_1.EventService.AT_TARGET && typeof continueEvents !== 'boolean' && this.defaultEvent[eventType]) {
1204
- this.runDefaultEvent(event)
1205
- }
1206
- })
1207
- return true
1208
- }
1209
-
1271
+ /**
1272
+ * Registers an event handler of a specific event type. Adding the same handler again for the same type and phase does
1273
+ * nothing, like the DOM.
1274
+ * @param {string} type The type of event to listen for
1275
+ * @param {Function|Object} callback The function to call (or an object with a handleEvent function)
1276
+ * @param {Object|boolean} [useCapture=false] Listen while the event travels down to the target (true), or an object with capture, once and passive
1277
+ */
1210
1278
  addEventListener (type, callback, useCapture = false) {
1211
1279
  let options = {
1212
1280
  capture: false,
1213
1281
  once: false,
1214
1282
  passive: false
1215
1283
  }
1216
- if (typeof useCapture === 'object') {
1284
+ if (typeof useCapture === 'object' && useCapture !== null) {
1217
1285
  // Originally useCapture was a single boolean flag, later optional other flags can be used
1218
1286
  // Here we take all the given flags from the object and assign them as the options
1219
1287
  options = Object.assign(options, useCapture)
1220
1288
  } else {
1221
- options.capture = useCapture
1289
+ options.capture = !!useCapture
1222
1290
  }
1223
- const listener = new PseudoEventListener_1.default(type, options, (callback.handleEvent || callback).bind(this), callback)
1224
1291
  const listeners = this.listenersFor(type)
1292
+ const alreadyAdded = Array.from(listeners).some(linker => linker.data.callback === callback && linker.data.capture === options.capture)
1293
+ if (alreadyAdded) {
1294
+ return
1295
+ }
1296
+ // A function runs with this target as this, an object runs its handleEvent as itself
1297
+ const handler = typeof callback === 'function' ? callback.bind(this) : callback.handleEvent.bind(callback)
1298
+ const listener = new PseudoEventListener_1.default(type, options, handler, callback)
1225
1299
  // Listeners run in the order they were added, except that listeners which are not defaults always come before the defaults
1226
1300
  const firstDefault = Array.from(listeners).find(linker => linker.data.isDefault)
1227
1301
  if (firstDefault && !listener.isDefault) {
@@ -1231,25 +1305,84 @@
1231
1305
  }
1232
1306
  }
1233
1307
 
1234
- removeEventListener (type, callback) {
1308
+ /**
1309
+ * Removes an event listener, the one which was added with the same type, handler and phase.
1310
+ * @param {string} type The type of event
1311
+ * @param {Function|Object} callback The handler which was added
1312
+ * @param {Object|boolean} [options=false] Whether the listener was a capture listener (true), or an object with capture
1313
+ */
1314
+ removeEventListener (type, callback, options = false) {
1235
1315
  if (!(type in this.listeners)) {
1236
1316
  return
1237
1317
  }
1238
- const listeners = this.listeners[type]
1239
- Array.from(listeners).filter(linker => !linker.data.isDefault && linker.data.callback === callback).forEach(linker => listeners.remove(linker))
1318
+ const capture = typeof options === 'object' && options !== null ? !!options.capture : !!options
1319
+ Array.from(this.listeners[type]).map(linker => linker.data).filter(listener => !listener.isDefault && listener.callback === callback && listener.capture === capture).forEach(listener => this.removeListener(type, listener))
1240
1320
  }
1241
1321
 
1242
- dispatchEvent (event, target = this) {
1243
- event.inner.target = target
1244
- if (!(event.type in this.listeners)) {
1245
- return true
1322
+ /**
1323
+ * Dispatches an event to this target and through the tree: capture listeners of the ancestors from the root down,
1324
+ * then the listeners of this target, then (when the event bubbles) the other listeners of the ancestors from the
1325
+ * parent up to the root. stopPropagation() stops it reaching further targets, stopImmediatePropagation() also stops
1326
+ * the remaining listeners of the current target. Afterwards, unless the default was prevented, the default action
1327
+ * of this target (see setDefaultEvent) runs. The event can be dispatched again afterwards.
1328
+ * @param {EventService} event The event to dispatch
1329
+ * @returns {boolean} False when the event was cancelable and a listener prevented the default, otherwise true
1330
+ * @throws {Error} When the event is already being dispatched, or (after the whole dispatch has finished) the error
1331
+ * which a listener threw (an error with all of them in its errors property when several did)
1332
+ */
1333
+ dispatchEvent (event) {
1334
+ if (event.inner.dispatching) {
1335
+ throw new Error('The event is already being dispatched.')
1336
+ }
1337
+ event.inner.dispatching = true
1338
+ event.inner.target = this
1339
+ // The ancestors, the root first, which can have listeners
1340
+ const ancestors = (0, getParentNodes_1.default)(this).filter(node => node instanceof EventTargetService)
1341
+ event.inner.path = [this].concat(ancestors.slice().reverse())
1342
+ const errors = []
1343
+ const visit = (target, phase) => {
1344
+ event.inner.eventPhase = phase
1345
+ event.inner.currentTarget = target
1346
+ errors.push(...target.runEvents(event))
1347
+ }
1348
+ for (const ancestor of ancestors) {
1349
+ if (event.inner.propagationStopped) {
1350
+ break
1351
+ }
1352
+ visit(ancestor, EventService_1.EventService.CAPTURING_PHASE)
1353
+ }
1354
+ if (!event.inner.propagationStopped) {
1355
+ visit(this, EventService_1.EventService.AT_TARGET)
1356
+ }
1357
+ if (event.bubbles) {
1358
+ for (const ancestor of ancestors.slice().reverse()) {
1359
+ if (event.inner.propagationStopped) {
1360
+ break
1361
+ }
1362
+ visit(ancestor, EventService_1.EventService.BUBBLING_PHASE)
1363
+ }
1364
+ }
1365
+ event.inner.finishDispatch()
1366
+ if (!event.defaultPrevented && typeof this.defaultEvent[event.type] === 'function') {
1367
+ try {
1368
+ this.defaultEvent[event.type](event)
1369
+ } catch (error) {
1370
+ errors.push(error)
1371
+ }
1372
+ }
1373
+ if (errors.length === 1) {
1374
+ throw errors[0]
1375
+ }
1376
+ if (errors.length > 1) {
1377
+ throw Object.assign(new Error(`${errors.length} listeners threw an error while dispatching the ${event.type} event.`), {
1378
+ errors
1379
+ })
1246
1380
  }
1247
- this.runEvents(event)
1248
1381
  return !event.defaultPrevented
1249
1382
  }
1250
1383
  }
1251
1384
  exports.default = EventTargetService
1252
- }, { '../classes/PseudoEventListener': 1, './EventService': 12, 'collect-your-stuff/dist/collections/linked-list/LinkedList': 21, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.filter.js': 131, 'core-js/modules/esnext.iterator.find.js': 132, 'core-js/modules/esnext.iterator.for-each.js': 133 }],
1385
+ }, { '../classes/PseudoEventListener': 1, '../functions/getParentNodes': 6, './EventService': 12, 'collect-your-stuff/dist/collections/linked-list/LinkedList': 21, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.filter.js': 131, 'core-js/modules/esnext.iterator.find.js': 132, 'core-js/modules/esnext.iterator.for-each.js': 133, 'core-js/modules/esnext.iterator.map.js': 134, 'core-js/modules/esnext.iterator.some.js': 136 }],
1253
1386
  14: [function (require, module, exports) {
1254
1387
  'use strict'
1255
1388
 
@@ -1402,8 +1535,6 @@
1402
1535
  16: [function (require, module, exports) {
1403
1536
  'use strict'
1404
1537
 
1405
- require('core-js/modules/esnext.iterator.constructor.js')
1406
- require('core-js/modules/esnext.iterator.for-each.js')
1407
1538
  const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
1408
1539
  return mod && mod.__esModule
1409
1540
  ? mod
@@ -1421,6 +1552,7 @@
1421
1552
  * @version 1.0.0
1422
1553
  */
1423
1554
  const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
1555
+ const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
1424
1556
  const EventTargetService_1 = __importDefault(require('./EventTargetService'))
1425
1557
  /**
1426
1558
  * Simulate the behaviour of the Node Class when there is no DOM available.
@@ -1443,8 +1575,7 @@
1443
1575
  this.nodeNameValue = ''
1444
1576
  this.children = (0, generateNodeList_1.default)()
1445
1577
  this.parent = null
1446
- this.next = null
1447
- this.prev = null
1578
+ this.listLinker = null
1448
1579
  }
1449
1580
 
1450
1581
  get baseURI () {
@@ -1468,7 +1599,7 @@
1468
1599
  }
1469
1600
 
1470
1601
  get nextSibling () {
1471
- return this.isConnected ? this.next : null
1602
+ return this.listLinker && this.listLinker.next ? this.listLinker.next.data : null
1472
1603
  }
1473
1604
 
1474
1605
  get nodeName () {
@@ -1500,7 +1631,7 @@
1500
1631
  }
1501
1632
 
1502
1633
  get previousSibling () {
1503
- return this.isConnected ? this.prev : null
1634
+ return this.listLinker && this.listLinker.prev ? this.listLinker.prev.data : null
1504
1635
  }
1505
1636
 
1506
1637
  get textContent () {
@@ -1512,15 +1643,20 @@
1512
1643
  }
1513
1644
 
1514
1645
  /**
1515
- *
1516
- * @param {PseudoNode} childNode
1517
- * @returns {PseudoNode}
1646
+ * Add a node as the last child of this node (a node which is already in a tree is moved).
1647
+ * @param {PseudoNode} childNode The node to add
1648
+ * @returns {PseudoNode} The added node
1518
1649
  */
1519
1650
  appendChild (childNode) {
1520
- this.children.append(childNode)
1521
- return childNode
1651
+ return this.insertBefore(childNode, null)
1522
1652
  }
1523
1653
 
1654
+ /**
1655
+ * Called each time a node has been inserted as a child of this node, so that nodes which need to react to children
1656
+ * (for example elements applying default events) can do so.
1657
+ * @param {NodeService} child The node which was inserted
1658
+ */
1659
+ childInserted (child) {}
1524
1660
  /**
1525
1661
  * Not implemented yet.
1526
1662
  * @throws {Error}
@@ -1538,11 +1674,19 @@
1538
1674
  }
1539
1675
 
1540
1676
  /**
1541
- * Not implemented yet.
1542
- * @throws {Error}
1677
+ * Check whether a node is this node or one of its descendants.
1678
+ * @param {PseudoNode|null} otherNode The node to look for
1679
+ * @returns {boolean}
1543
1680
  */
1544
1681
  contains (otherNode) {
1545
- throw new Error('NodeService.contains() is not implemented yet.')
1682
+ let current = otherNode
1683
+ while (current) {
1684
+ if (current === this) {
1685
+ return true
1686
+ }
1687
+ current = current.parentNode
1688
+ }
1689
+ return false
1546
1690
  }
1547
1691
 
1548
1692
  getRootNode (options = {
@@ -1556,11 +1700,44 @@
1556
1700
  }
1557
1701
 
1558
1702
  /**
1559
- * Not implemented yet.
1560
- * @throws {Error}
1703
+ * Insert a node as a child of this node, before the given child (or at the end when there is none). A node which is
1704
+ * already in a tree is moved, and the children of a document fragment are moved in order.
1705
+ * @param {PseudoNode} newNode The node to insert
1706
+ * @param {PseudoNode|null} [referenceNode=null] The child of this node to insert before, or null to insert at the end
1707
+ * @returns {PseudoNode} The inserted node
1708
+ * @throws {Error} When the reference node is not a child of this node, or the new node is this node or contains it
1561
1709
  */
1562
- insertBefore (newNode, referenceNode) {
1563
- throw new Error('NodeService.insertBefore() is not implemented yet.')
1710
+ insertBefore (newNode, referenceNode = null) {
1711
+ if (referenceNode !== null && referenceNode.parentNode !== this) {
1712
+ throw new Error('The node before which the new node is to be inserted is not a child of this node.')
1713
+ }
1714
+ if (newNode === referenceNode) {
1715
+ // Inserting a node before itself leaves it where it is
1716
+ return newNode
1717
+ }
1718
+ if (typeof newNode.contains === 'function' && newNode.contains(this)) {
1719
+ throw new Error('The new node cannot be inserted into itself or one of its own descendants.')
1720
+ }
1721
+ if (newNode.nodeType === NodeService.DOCUMENT_FRAGMENT_NODE) {
1722
+ // The children of a fragment are inserted (moved) in order, and the fragment is left empty
1723
+ while (newNode.firstChild) {
1724
+ this.insertBefore(newNode.firstChild, referenceNode)
1725
+ }
1726
+ return newNode
1727
+ }
1728
+ if (newNode.parentNode) {
1729
+ // A node can only be in one place, so it is moved from where it was
1730
+ newNode.parentNode.removeChild(newNode)
1731
+ }
1732
+ const linker = new TreeLinker_1.TreeLinker({
1733
+ data: newNode
1734
+ })
1735
+ this.children.insertBefore(referenceNode ? referenceNode.listLinker : null, linker)
1736
+ const inserted = newNode
1737
+ inserted.parent = this
1738
+ inserted.listLinker = linker
1739
+ this.childInserted(inserted)
1740
+ return newNode
1564
1741
  }
1565
1742
 
1566
1743
  isDefaultNamespace (namespaceURI) {
@@ -1589,31 +1766,44 @@
1589
1766
 
1590
1767
  normalize () {}
1591
1768
  /**
1592
- * Remove the given child from this node.
1593
- * @param {PseudoNode} childElement The child node, or its TreeLinker from the children list
1594
- * @returns {PseudoNode}
1769
+ * Remove a child from this node, it no longer has a parent or siblings afterwards.
1770
+ * @param {PseudoNode} childElement The child node to remove
1771
+ * @returns {PseudoNode} The removed node
1595
1772
  * @throws {Error} When the node is not a child of this node
1596
1773
  */
1597
1774
  removeChild (childElement) {
1598
- let found = null
1599
- this.children.forEach(linker => {
1600
- if (found === null && (linker === childElement || linker.data === childElement)) {
1601
- found = linker
1602
- }
1603
- })
1604
- if (found === null) {
1775
+ if (!childElement || childElement.parentNode !== this) {
1605
1776
  throw new Error('The node to be removed is not a child of this node.')
1606
1777
  }
1607
- this.children.remove(found)
1608
- return found.data
1778
+ const removed = childElement
1779
+ this.children.remove(removed.listLinker)
1780
+ removed.parent = null
1781
+ removed.listLinker = null
1782
+ return childElement
1609
1783
  }
1610
1784
 
1611
1785
  /**
1612
- * Not implemented yet.
1613
- * @throws {Error}
1786
+ * Replace a child of this node with another node (which is moved if it is already in a tree).
1787
+ * @param {PseudoNode} newChild The node which takes the place
1788
+ * @param {PseudoNode} oldChild The child of this node to replace
1789
+ * @returns {PseudoNode} The replaced node
1790
+ * @throws {Error} When the old node is not a child of this node
1614
1791
  */
1615
1792
  replaceChild (newChild, oldChild) {
1616
- throw new Error('NodeService.replaceChild() is not implemented yet.')
1793
+ if (!oldChild || oldChild.parentNode !== this) {
1794
+ throw new Error('The node to be replaced is not a child of this node.')
1795
+ }
1796
+ if (newChild === oldChild) {
1797
+ return oldChild
1798
+ }
1799
+ // The new node goes where the old one was, which is before the old node's next sibling (unless that is the new node)
1800
+ let reference = oldChild.nextSibling
1801
+ if (reference === newChild) {
1802
+ reference = newChild.nextSibling
1803
+ }
1804
+ this.removeChild(oldChild)
1805
+ this.insertBefore(newChild, reference)
1806
+ return oldChild
1617
1807
  }
1618
1808
  }
1619
1809
  exports.NodeService = NodeService
@@ -1630,7 +1820,7 @@
1630
1820
  NodeService.DOCUMENT_TYPE_NODE = 10
1631
1821
  NodeService.DOCUMENT_FRAGMENT_NODE = 11
1632
1822
  NodeService.NOTATION_NODE = 12
1633
- }, { '../factories/generateNodeList': 5, './EventTargetService': 13, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.for-each.js': 133 }],
1823
+ }, { '../factories/generateNodeList': 5, './EventTargetService': 13, 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker': 24 }],
1634
1824
  17: [function (require, module, exports) {
1635
1825
  'use strict'
1636
1826
 
@@ -1649,7 +1839,9 @@
1649
1839
  * @param {*} [data=null] The data to be stored in this element.
1650
1840
  */
1651
1841
  constructor (data = null) {
1842
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1652
1843
  this.classType = ArrayElement
1844
+ /** The data stored in this element. */
1653
1845
  this.data = null
1654
1846
  this.data = data
1655
1847
  }
@@ -1662,8 +1854,8 @@
1662
1854
  */
1663
1855
  exports.ArrayElement = ArrayElement
1664
1856
  ArrayElement.make = (element, classType = ArrayElement) => {
1665
- if (typeof element !== 'object') {
1666
- // It is not an object, so instantiate the Element with element as the data
1857
+ if (element === null || typeof element !== 'object') {
1858
+ // It is not an object (or it is null), so instantiate the Element with element as the data
1667
1859
  return new classType(element)
1668
1860
  }
1669
1861
  if (element.classType) {
@@ -1719,14 +1911,32 @@
1719
1911
  class Arrayable {
1720
1912
  /**
1721
1913
  * Create the new Arrayable instance, configure the Arrayable class.
1914
+ * @param {ArrayElement} [elementClass=ArrayElement] The class used to wrap given data as elements.
1722
1915
  */
1723
1916
  constructor (elementClass = _ArrayElement.ArrayElement) {
1917
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1724
1918
  this.classType = Arrayable
1919
+ /** The array which stores the elements of this Arrayable. */
1725
1920
  this.innerList = []
1921
+ /** Whether the inner list has been initialized (it can only be initialized once). */
1726
1922
  this.initialized = false
1727
1923
  this.elementClass = elementClass
1728
1924
  }
1729
1925
 
1926
+ /**
1927
+ * Find the position of an element which must be in this list.
1928
+ * @param {ArrayElement} node The element to find
1929
+ * @returns {number}
1930
+ * @throws {Error} When the element is not in this list
1931
+ */
1932
+ indexOfElement (node) {
1933
+ const index = this.innerList.indexOf(node)
1934
+ if (index < 0) {
1935
+ throw new Error('The reference element is not in this list.')
1936
+ }
1937
+ return index
1938
+ }
1939
+
1730
1940
  /**
1731
1941
  * Initialize the inner list, should only run once.
1732
1942
  * @param {Array<ArrayElement>} initialList Give the array of elements to start in this Arrayable.
@@ -1743,7 +1953,7 @@
1743
1953
  }
1744
1954
 
1745
1955
  /**
1746
- * Retrieve a copy of the innerList used.
1956
+ * Retrieve the innerList used (the list itself, not a copy).
1747
1957
  * @returns {Array<ArrayElement>}
1748
1958
  */
1749
1959
  get list () {
@@ -1752,18 +1962,18 @@
1752
1962
 
1753
1963
  /**
1754
1964
  * Retrieve the first Element from the Arrayable
1755
- * @returns {ArrayElement}
1965
+ * @returns {ArrayElement|null} The first element, or null when the Arrayable is empty
1756
1966
  */
1757
1967
  get first () {
1758
- return this.innerList[0]
1968
+ return this.length ? this.innerList[0] : null
1759
1969
  }
1760
1970
 
1761
1971
  /**
1762
1972
  * Retrieve the last Element from the Arrayable
1763
- * @returns {ArrayElement}
1973
+ * @returns {ArrayElement|null} The last element, or null when the Arrayable is empty
1764
1974
  */
1765
1975
  get last () {
1766
- return this.innerList[this.length - 1]
1976
+ return this.length ? this.innerList[this.length - 1] : null
1767
1977
  }
1768
1978
 
1769
1979
  /**
@@ -1776,25 +1986,29 @@
1776
1986
 
1777
1987
  /**
1778
1988
  * Insert a new node (or data) after a node.
1779
- * @param {ArrayElement|*} node The existing node as reference
1989
+ * @param {ArrayElement|null} node The existing node as reference, or null to insert at the start of the list
1780
1990
  * @param {ArrayElement|*} newNode The new node to go after the existing node
1781
1991
  * @returns {Arrayable}
1992
+ * @throws {Error} When the reference node is not in this list
1782
1993
  */
1783
1994
  insertAfter (node, newNode) {
1784
- const insertAt = this.innerList.indexOf(node)
1785
- this.innerList.splice(insertAt + 1, 0, this.elementClass.make(newNode))
1995
+ // With no reference element, the new one goes after nothing: at the start of the list
1996
+ const insertAt = node === null || typeof node === 'undefined' ? -1 : this.indexOfElement(node)
1997
+ this.innerList.splice(insertAt + 1, 0, this.elementClass.make(newNode, this.elementClass))
1786
1998
  return this
1787
1999
  }
1788
2000
 
1789
2001
  /**
1790
2002
  * Insert a new node (or data) before a node.
1791
- * @param {ArrayElement|*} node The existing node as reference
2003
+ * @param {ArrayElement|null} node The existing node as reference, or null to insert at the end of the list
1792
2004
  * @param {ArrayElement|*} newNode The new node to go before the existing node
1793
2005
  * @returns {Arrayable}
2006
+ * @throws {Error} When the reference node is not in this list
1794
2007
  */
1795
2008
  insertBefore (node, newNode) {
1796
- const insertAt = this.innerList.indexOf(node)
1797
- this.innerList.splice(insertAt, 0, this.elementClass.make(newNode))
2009
+ // With no reference element, the new one goes before nothing: at the end of the list
2010
+ const insertAt = node === null || typeof node === 'undefined' ? this.length : this.indexOfElement(node)
2011
+ this.innerList.splice(insertAt, 0, this.elementClass.make(newNode, this.elementClass))
1798
2012
  return this
1799
2013
  }
1800
2014
 
@@ -1805,6 +2019,11 @@
1805
2019
  * @returns {Arrayable}
1806
2020
  */
1807
2021
  append (node, after = this.last) {
2022
+ if (after === this.last) {
2023
+ // Adding to the end does not need to search for where that is
2024
+ this.innerList.push(this.elementClass.make(node, this.elementClass))
2025
+ return this
2026
+ }
1808
2027
  return this.insertAfter(after, node)
1809
2028
  }
1810
2029
 
@@ -1815,16 +2034,24 @@
1815
2034
  * @returns {Arrayable}
1816
2035
  */
1817
2036
  prepend (node, before = this.first) {
2037
+ if (before === this.first) {
2038
+ // Adding to the start does not need to search for where that is
2039
+ this.innerList.unshift(this.elementClass.make(node, this.elementClass))
2040
+ return this
2041
+ }
1818
2042
  return this.insertBefore(before, node)
1819
2043
  }
1820
2044
 
1821
2045
  /**
1822
2046
  * Remove an element from this arrayable.
1823
2047
  * @param {ArrayElement} node The node we wish to remove (and it will be returned after removal)
1824
- * @return {ArrayElement}
2048
+ * @return {ArrayElement|null} The removed node, or null when it was not in this list (nothing is removed)
1825
2049
  */
1826
2050
  remove (node) {
1827
2051
  const deleteAt = this.innerList.indexOf(node)
2052
+ if (deleteAt < 0) {
2053
+ return null
2054
+ }
1828
2055
  this.innerList.splice(deleteAt, 1)
1829
2056
  return node
1830
2057
  }
@@ -1904,7 +2131,7 @@
1904
2131
  class DoubleLinker {
1905
2132
  /**
1906
2133
  * Create the new DoubleLinker instance, provide the data and optionally the next and prev references.
1907
- * @param {Object} [nodeData={}]
2134
+ * @param {Object} [nodeData={}] The settings for the new linker.
1908
2135
  * @param {*} [nodeData.data=null] The data to be stored in this linker
1909
2136
  * @param {DoubleLinker|null} [nodeData.next=null] The reference to the next linker if any
1910
2137
  * @param {DoubleLinker|null} [nodeData.prev=null] The reference to the previous linker if any
@@ -1914,9 +2141,13 @@
1914
2141
  next = null,
1915
2142
  prev = null
1916
2143
  } = {}) {
2144
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1917
2145
  this.classType = DoubleLinker
2146
+ /** The data stored in this linker. */
1918
2147
  this.data = null
2148
+ /** The linker after this one, or null when this is the last. */
1919
2149
  this.next = null
2150
+ /** The linker before this one, or null when this is the first. */
1920
2151
  this.prev = null
1921
2152
  this.data = data
1922
2153
  this.next = next
@@ -1984,11 +2215,19 @@
1984
2215
  class DoublyLinkedList {
1985
2216
  /**
1986
2217
  * Create the new DoublyLinkedList instance.
2218
+ * @param {DoubleLinker} [linkerClass=DoubleLinker] The class used to wrap given data as linkers.
1987
2219
  */
1988
2220
  constructor (linkerClass = _DoubleLinker.DoubleLinker) {
2221
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
1989
2222
  this.classType = DoublyLinkedList
2223
+ /** A linker of the list (null when the list is empty); the head is found by walking back from it. */
1990
2224
  this.innerList = null
2225
+ /** Whether the inner list has been initialized (it can only be initialized once). */
1991
2226
  this.initialized = false
2227
+ /** The last linker, remembered so that adding to the end does not need to walk the whole list (null when not known yet). */
2228
+ this.tailCache = null
2229
+ /** The number of linkers, kept up to date by the list's own methods so that the length does not need to walk the whole list (null when not known yet). */
2230
+ this.countCache = null
1992
2231
  this.linkerClass = linkerClass
1993
2232
  }
1994
2233
 
@@ -1998,11 +2237,12 @@
1998
2237
  * @return {DoublyLinkedList}
1999
2238
  */
2000
2239
  initialize (initialList) {
2240
+ // Borrowed from LinkedList, which types its return as a LinkedList although it returns whatever list called it
2001
2241
  return _LinkedList.LinkedList.prototype.initialize.call(this, initialList)
2002
2242
  }
2003
2243
 
2004
2244
  /**
2005
- * Retrieve a copy of the innerList used.
2245
+ * Retrieve the innerList used (the list itself, not a copy).
2006
2246
  * @returns {DoubleLinker}
2007
2247
  */
2008
2248
  get list () {
@@ -2014,91 +2254,122 @@
2014
2254
  * @returns {DoubleLinker}
2015
2255
  */
2016
2256
  get first () {
2017
- return this.reset()
2257
+ let head = this.innerList
2258
+ if (head === null) {
2259
+ return null
2260
+ }
2261
+ // innerList is normally the head already, walking back also finds anything linked on before it outside of this list
2262
+ while (head.prev !== null) {
2263
+ head = head.prev
2264
+ }
2265
+ this.innerList = head
2266
+ return head
2018
2267
  }
2019
2268
 
2020
2269
  /**
2021
- * Retrieve the last DoubleLinker in the list.
2270
+ * Retrieve the last DoubleLinker in the list. The end is remembered, so this does not walk the list.
2022
2271
  * @returns {DoubleLinker}
2023
2272
  */
2024
2273
  get last () {
2025
- let tail = this.innerList
2026
- if (tail === null) {
2274
+ if (this.innerList === null) {
2027
2275
  return null
2028
2276
  }
2029
- let next = tail.next
2030
- while (next !== null) {
2031
- tail = next
2032
- next = tail.next
2277
+ let tail = this.tailCache !== null ? this.tailCache : this.innerList
2278
+ // The remembered tail is normally the end already, walking on from it also finds anything linked on outside of this list
2279
+ while (tail.next !== null) {
2280
+ tail = tail.next
2033
2281
  }
2282
+ this.tailCache = tail
2034
2283
  return tail
2035
2284
  }
2036
2285
 
2037
2286
  /**
2038
- * Return the length of the list.
2287
+ * Return the length of the list. It is kept up to date by the list's own methods, so this does not walk the list
2288
+ * (call reset() after linkers were changed directly).
2039
2289
  * @returns {number}
2040
2290
  */
2041
2291
  get length () {
2042
- let current = this.first
2043
- let length = 0
2044
- while (current !== null) {
2045
- ++length
2046
- current = current.next
2292
+ if (this.countCache === null) {
2293
+ this.reset()
2047
2294
  }
2048
- return length
2295
+ return this.countCache
2049
2296
  }
2050
2297
 
2051
2298
  /**
2052
2299
  * Insert a new node (or data) after a node.
2053
- * @param {DoubleLinker|*} node The existing node as reference
2300
+ * @param {DoubleLinker|*} node The existing node as reference (which must be in this list, this is not checked), or null to insert at the start of the list
2054
2301
  * @param {DoubleLinker|*} newNode The new node to go after the existing node
2055
2302
  * @returns {DoublyLinkedList}
2056
2303
  */
2057
2304
  insertAfter (node, newNode) {
2058
- newNode = this.linkerClass.make(newNode)
2059
- if (node !== null) {
2305
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2306
+ if (node === null || typeof node === 'undefined') {
2307
+ // After nothing means at the start of the list
2308
+ const head = this.first
2309
+ newNode.prev = null
2310
+ newNode.next = head
2311
+ if (head) {
2312
+ head.prev = newNode
2313
+ } else {
2314
+ this.tailCache = newNode
2315
+ }
2316
+ this.innerList = newNode
2317
+ } else {
2060
2318
  // Ensure the next reference of this node is assigned to the new node
2061
2319
  newNode.next = node.next
2062
2320
  // Ensure this node is assigned as the prev reference of the new node
2063
2321
  newNode.prev = node
2064
2322
  // Then set this node's next reference to the new node
2065
2323
  node.next = newNode
2324
+ if (newNode.next) {
2325
+ // Update the next reference to ensure circular reference for prev points to the new node
2326
+ newNode.next.prev = newNode
2327
+ } else {
2328
+ this.tailCache = newNode
2329
+ }
2066
2330
  }
2067
- if (newNode.next) {
2068
- // Update the next reference to ensure circular reference for prev points to the new node
2069
- newNode.next.prev = newNode
2070
- }
2071
- if (!this.length) {
2072
- this.innerList = newNode
2331
+ if (this.countCache !== null) {
2332
+ ++this.countCache
2073
2333
  }
2074
- this.reset()
2075
2334
  return this
2076
2335
  }
2077
2336
 
2078
2337
  /**
2079
2338
  * Insert a new node (or data) before a node.
2080
- * @param {DoubleLinker|*} node The existing node as reference
2339
+ * @param {DoubleLinker|*} node The existing node as reference (which must be in this list, this is not checked), or null to insert at the end of the list
2081
2340
  * @param {DoubleLinker|*} newNode The new node to go before the existing node
2082
2341
  * @returns {DoublyLinkedList}
2083
2342
  */
2084
2343
  insertBefore (node, newNode) {
2085
- newNode = this.linkerClass.make(newNode)
2086
- if (node !== null) {
2344
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2345
+ if (node === null || typeof node === 'undefined') {
2346
+ // Before nothing means at the end of the list
2347
+ const tail = this.last
2348
+ newNode.next = null
2349
+ newNode.prev = tail
2350
+ if (tail === null) {
2351
+ this.innerList = newNode
2352
+ } else {
2353
+ tail.next = newNode
2354
+ }
2355
+ this.tailCache = newNode
2356
+ } else {
2087
2357
  // The new node will reference this prev node as prev
2088
2358
  newNode.prev = node.prev
2089
2359
  // The new node will reference this node as next
2090
2360
  newNode.next = node
2091
2361
  // This prev will reference the new node
2092
2362
  node.prev = newNode
2363
+ if (newNode.prev) {
2364
+ // Update the prev reference to ensure circular reference for next points to the new node
2365
+ newNode.prev.next = newNode
2366
+ } else {
2367
+ this.innerList = newNode
2368
+ }
2093
2369
  }
2094
- if (newNode.prev) {
2095
- // Update the prev reference to ensure circular reference for next points to the new node
2096
- newNode.prev.next = newNode
2370
+ if (this.countCache !== null) {
2371
+ ++this.countCache
2097
2372
  }
2098
- if (!this.length) {
2099
- this.innerList = newNode
2100
- }
2101
- this.reset()
2102
2373
  return this
2103
2374
  }
2104
2375
 
@@ -2128,7 +2399,7 @@
2128
2399
  * @return {DoubleLinker}
2129
2400
  */
2130
2401
  remove (node) {
2131
- if (node === null) {
2402
+ if (node === null || typeof node === 'undefined') {
2132
2403
  return null
2133
2404
  }
2134
2405
  if (node.prev) {
@@ -2144,35 +2415,47 @@
2144
2415
  if (this.innerList === node) {
2145
2416
  this.innerList = node.next || node.prev || null
2146
2417
  }
2147
- // Update head reference
2148
- this.reset()
2418
+ if (this.tailCache === node) {
2419
+ this.tailCache = node.prev
2420
+ }
2421
+ if (this.innerList === null) {
2422
+ this.tailCache = null
2423
+ }
2424
+ if (this.countCache !== null) {
2425
+ --this.countCache
2426
+ }
2149
2427
  return node
2150
2428
  }
2151
2429
 
2152
2430
  /**
2153
- * Refresh all references and return head reference.
2154
- * @return {DoubleLinker}
2431
+ * Refresh all references (the head, the end and the length) by walking the list once, and return the head. The list's
2432
+ * own methods keep these up to date, so this is only needed after linkers were changed directly.
2433
+ * @return {DoubleLinker|null}
2155
2434
  */
2156
2435
  reset () {
2157
2436
  // Start at the pointer for the list
2158
2437
  let pointer = this.innerList
2159
2438
  if (pointer === null) {
2439
+ this.countCache = 0
2440
+ this.tailCache = null
2160
2441
  return null
2161
2442
  }
2162
- let next = pointer.next
2163
- // Follow references till the end
2164
- while (next !== null) {
2165
- pointer = next
2166
- next = pointer.next
2443
+ // Follow references back to the beginning
2444
+ while (pointer.prev !== null) {
2445
+ pointer = pointer.prev
2167
2446
  }
2168
- let prev = pointer.prev
2169
- // From final reference, follow references back to the beginning
2170
- while (prev !== null) {
2171
- pointer = prev
2172
- prev = pointer.prev
2173
- }
2174
- // All the live references should have been found, and we are pointing to the true head
2447
+ // We are pointing to the true head, now count along to the end to find the tail and the length
2175
2448
  this.innerList = pointer
2449
+ let count = 0
2450
+ let tail = pointer
2451
+ let current = pointer
2452
+ while (current !== null) {
2453
+ ++count
2454
+ tail = current
2455
+ current = current.next
2456
+ }
2457
+ this.countCache = count
2458
+ this.tailCache = tail
2176
2459
  return pointer
2177
2460
  }
2178
2461
 
@@ -2208,6 +2491,7 @@
2208
2491
  * Be able to run forEach on this DoublyLinkedList to iterate over the DoubleLinker Items.
2209
2492
  * @param {forEachCallback} callback The function to call for-each double linker
2210
2493
  * @param {DoublyLinkedList} thisArg Optional, 'this' reference
2494
+ * @return {DoublyLinkedList} The list which was iterated.
2211
2495
  */
2212
2496
  forEach (callback, thisArg = this) {
2213
2497
  return _LinkedList.LinkedList.prototype.forEach.call(this, callback, thisArg)
@@ -2251,11 +2535,19 @@
2251
2535
  class LinkedList {
2252
2536
  /**
2253
2537
  * Create the new LinkedList instance.
2538
+ * @param {Linker} [linkerClass=Linker] The class used to wrap given data as linkers.
2254
2539
  */
2255
2540
  constructor (linkerClass = _Linker.Linker) {
2541
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2256
2542
  this.classType = LinkedList
2543
+ /** The first linker of the list (null when the list is empty), from which the whole list is reached. */
2257
2544
  this.innerList = null
2545
+ /** Whether the inner list has been initialized (it can only be initialized once). */
2258
2546
  this.initialized = false
2547
+ /** The last linker, remembered so that adding to the end does not need to walk the whole list (null when not known yet). */
2548
+ this.tailCache = null
2549
+ /** The number of linkers, kept up to date by the list's own methods so that the length does not need to walk the whole list (null when not known yet). */
2550
+ this.countCache = null
2259
2551
  this.linkerClass = linkerClass
2260
2552
  }
2261
2553
 
@@ -2265,11 +2557,12 @@
2265
2557
  * @return {LinkedList}
2266
2558
  */
2267
2559
  initialize (initialList) {
2560
+ // Borrowed from Arrayable, which types its return as an Arrayable although it returns whatever list called it
2268
2561
  return _Arrayable.Arrayable.prototype.initialize.call(this, initialList)
2269
2562
  }
2270
2563
 
2271
2564
  /**
2272
- * Retrieve a copy of the innerList used.
2565
+ * Retrieve the innerList used (the list itself, not a copy).
2273
2566
  * @returns {Linker}
2274
2567
  */
2275
2568
  get list () {
@@ -2285,78 +2578,100 @@
2285
2578
  }
2286
2579
 
2287
2580
  /**
2288
- * Retrieve the last Linker in the list.
2581
+ * Retrieve the last Linker in the list. The end is remembered, so this does not walk the list.
2289
2582
  * @returns {Linker}
2290
2583
  */
2291
2584
  get last () {
2292
- let tail = this.innerList
2293
- if (tail === null) {
2585
+ if (this.innerList === null) {
2294
2586
  return null
2295
2587
  }
2296
- let next = tail.next
2297
- while (next !== null) {
2298
- tail = next
2299
- next = tail.next
2588
+ let tail = this.tailCache !== null ? this.tailCache : this.innerList
2589
+ // The remembered tail is normally the end already, walking on from it also finds anything linked on outside of this list
2590
+ while (tail.next !== null) {
2591
+ tail = tail.next
2300
2592
  }
2593
+ this.tailCache = tail
2301
2594
  return tail
2302
2595
  }
2303
2596
 
2304
2597
  /**
2305
- * Return the length of the list.
2598
+ * Return the length of the list. It is kept up to date by the list's own methods, so this does not walk the list
2599
+ * (call reset() after linkers were changed directly).
2306
2600
  * @returns {number}
2307
2601
  */
2308
2602
  get length () {
2309
- let current = this.first
2310
- let length = 0
2311
- while (current !== null) {
2312
- ++length
2313
- current = current.next
2603
+ if (this.countCache === null) {
2604
+ this.reset()
2314
2605
  }
2315
- return length
2606
+ return this.countCache
2316
2607
  }
2317
2608
 
2318
2609
  /**
2319
2610
  * Insert a new node (or data) after a node.
2320
- * @param {Linker|*} node The existing node as reference
2611
+ * @param {Linker|*} node The existing node as reference, or null to insert at the start of the list
2321
2612
  * @param {Linker|*} newNode The new node to go after the existing node
2322
2613
  * @returns {LinkedList}
2323
2614
  */
2324
2615
  insertAfter (node, newNode) {
2325
- newNode = this.linkerClass.make(newNode)
2326
- if (node !== null) {
2327
- // Ensure the next reference of this node is assigned to the new node
2616
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2617
+ if (node === null || typeof node === 'undefined') {
2618
+ // After nothing means at the start of the list
2619
+ newNode.next = this.innerList
2620
+ if (this.innerList === null) {
2621
+ this.tailCache = newNode
2622
+ }
2623
+ this.innerList = newNode
2624
+ } else {
2328
2625
  newNode.next = node.next
2329
- // Then set this node's next reference to the new node
2330
2626
  node.next = newNode
2627
+ if (newNode.next === null) {
2628
+ this.tailCache = newNode
2629
+ }
2331
2630
  }
2332
- if (!this.length) {
2333
- this.innerList = newNode
2631
+ if (this.countCache !== null) {
2632
+ ++this.countCache
2334
2633
  }
2335
2634
  return this
2336
2635
  }
2337
2636
 
2338
2637
  /**
2339
2638
  * Insert a new node (or data) before a node.
2340
- * @param {Linker|*} node The existing node as reference
2639
+ * @param {Linker|*} node The existing node as reference, or null to insert at the end of the list
2341
2640
  * @param {Linker|*} newNode The new node to go before the existing node
2342
2641
  * @returns {LinkedList}
2642
+ * @throws {Error} When the reference node is not in this list
2343
2643
  */
2344
2644
  insertBefore (node, newNode) {
2345
- newNode = this.linkerClass.make(newNode)
2346
- let prevNode = null
2347
- let currentNode = this.first
2348
- while (currentNode !== node) {
2349
- prevNode = currentNode
2350
- currentNode = currentNode.next
2351
- }
2352
- // The new node will reference this node as next
2353
- newNode.next = node
2354
- if (prevNode) {
2355
- // Ensure the next reference of the previous node is assigned to the new node
2356
- prevNode.next = newNode
2645
+ newNode = this.linkerClass.make(newNode, this.linkerClass)
2646
+ if (node === null || typeof node === 'undefined') {
2647
+ // Before nothing means at the end of the list
2648
+ const tail = this.last
2649
+ newNode.next = null
2650
+ if (tail === null) {
2651
+ this.innerList = newNode
2652
+ } else {
2653
+ tail.next = newNode
2654
+ }
2655
+ this.tailCache = newNode
2656
+ } else {
2657
+ let prevNode = null
2658
+ let currentNode = this.first
2659
+ while (currentNode !== null && currentNode !== node) {
2660
+ prevNode = currentNode
2661
+ currentNode = currentNode.next
2662
+ }
2663
+ if (currentNode === null) {
2664
+ throw new Error('The reference node is not in this list.')
2665
+ }
2666
+ newNode.next = node
2667
+ if (prevNode) {
2668
+ prevNode.next = newNode
2669
+ } else {
2670
+ this.innerList = newNode
2671
+ }
2357
2672
  }
2358
- if (node === this.first || node === null) {
2359
- this.innerList = newNode
2673
+ if (this.countCache !== null) {
2674
+ ++this.countCache
2360
2675
  }
2361
2676
  return this
2362
2677
  }
@@ -2384,26 +2699,58 @@
2384
2699
  /**
2385
2700
  * Remove a linker from this linked list.
2386
2701
  * @param {Linker} node The node we wish to remove (and it will be returned after removal)
2387
- * @return {Linker}
2702
+ * @return {Linker|null} The removed node, or null when it was not in this list (nothing is removed)
2388
2703
  */
2389
2704
  remove (node) {
2705
+ if (node === null || typeof node === 'undefined') {
2706
+ return null
2707
+ }
2390
2708
  let prevNode = null
2391
2709
  let currentNode = this.first
2392
- while (currentNode !== node) {
2710
+ while (currentNode !== null && currentNode !== node) {
2393
2711
  prevNode = currentNode
2394
2712
  currentNode = currentNode.next
2395
2713
  }
2714
+ if (currentNode === null) {
2715
+ // The node is not in this list, so there is nothing to remove
2716
+ return null
2717
+ }
2396
2718
  if (prevNode) {
2397
- // Ensure the next reference of the previous node skips over the removed node
2398
2719
  prevNode.next = node.next
2399
- }
2400
- if (node === this.first && node !== null) {
2401
- // Update list head to point to next if it was this node
2720
+ } else {
2402
2721
  this.innerList = node.next
2403
2722
  }
2723
+ if (this.tailCache === node) {
2724
+ this.tailCache = prevNode
2725
+ }
2726
+ if (this.innerList === null) {
2727
+ this.tailCache = null
2728
+ }
2729
+ if (this.countCache !== null) {
2730
+ --this.countCache
2731
+ }
2404
2732
  return node
2405
2733
  }
2406
2734
 
2735
+ /**
2736
+ * Refresh the remembered end and length of the list by walking it once. The list's own methods keep these up to date,
2737
+ * so this is only needed after linkers were changed directly (for example by setting next on a linker).
2738
+ * @return {Linker|null} The first linker of the list
2739
+ */
2740
+ reset () {
2741
+ let count = 0
2742
+ let tail = null
2743
+ let current = this.innerList
2744
+ while (current !== null) {
2745
+ ++count
2746
+ tail = current
2747
+ current = current.next
2748
+ }
2749
+ this.countCache = count
2750
+ this.tailCache = tail
2751
+ return this.innerList
2752
+ }
2753
+
2407
2754
  /**
2408
2755
  * Retrieve a Linker item from this list by numeric index, otherwise return null.
2409
2756
  * @param {number} index The integer number for retrieving a node by position.
@@ -2486,7 +2833,7 @@
2486
2833
  class Linker {
2487
2834
  /**
2488
2835
  * Create the new Linker instance, provide the data and optionally give the next Linker.
2489
- * @param {Object} [nodeData={}]
2836
+ * @param {Object} [nodeData={}] The settings for the new linker.
2490
2837
  * @param {*} [nodeData.data=null] The data to be stored in this linker
2491
2838
  * @param {Linker|null} [nodeData.next=null] The reference to the next linker if any
2492
2839
  */
@@ -2494,8 +2841,11 @@
2494
2841
  data = null,
2495
2842
  next = null
2496
2843
  } = {}) {
2844
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2497
2845
  this.classType = Linker
2846
+ /** The data stored in this linker. */
2498
2847
  this.data = null
2848
+ /** The linker after this one, or null when this is the last. */
2499
2849
  this.next = null
2500
2850
  this.data = data
2501
2851
  this.next = next
@@ -2509,8 +2859,8 @@
2509
2859
  */
2510
2860
  exports.Linker = Linker
2511
2861
  Linker.make = (linker, classType = Linker) => {
2512
- if (typeof linker !== 'object') {
2513
- // It is not an object, so instantiate the Linker with element as the data
2862
+ if (linker === null || typeof linker !== 'object') {
2863
+ // It is not an object (or it is null), so instantiate the Linker with element as the data
2514
2864
  return new classType({
2515
2865
  data: linker
2516
2866
  })
@@ -2519,7 +2869,8 @@
2519
2869
  // Already valid Linker, return as-is
2520
2870
  return linker
2521
2871
  }
2522
- if (!linker.data) {
2872
+ if (!('data' in linker)) {
2873
+ // Not the settings for a linker (which would have data, even if it is falsy), so it is the data itself
2523
2874
  linker = {
2524
2875
  data: linker
2525
2876
  }
@@ -2533,7 +2884,7 @@
2533
2884
  * @param {IsLinker} [classType=Linker] Provide the type of IsLinker to use.
2534
2885
  * @returns {{head: Linker, tail: Linker}}
2535
2886
  */
2536
- Linker.fromArray = (values, classType = Linker) => values.reduce((references, linker) => {
2887
+ Linker.fromArray = (values = [], classType = Linker) => values.reduce((references, linker) => {
2537
2888
  const newLinker = classType.make(linker, classType)
2538
2889
  if (references.head === null) {
2539
2890
  // Initialize the head and tail with the new node
@@ -2558,6 +2909,8 @@
2558
2909
  value: true
2559
2910
  })
2560
2911
  exports.LinkedTreeList = void 0
2912
+ require('core-js/modules/esnext.iterator.constructor.js')
2913
+ require('core-js/modules/esnext.iterator.for-each.js')
2561
2914
  const _TreeLinker = require('./TreeLinker')
2562
2915
  const _TreeLinkerIterator = require('../../recipes/TreeLinkerIterator')
2563
2916
  const _DoublyLinkedList = require('../doubly-linked-list/DoublyLinkedList')
@@ -2568,6 +2921,13 @@
2568
2921
  * @memberOf module:collect-your-stuff
2569
2922
  */
2570
2923
 
2924
+ /**
2925
+ * Use one of the accessors of DoublyLinkedList (which keeps track of the head, tail and length) for a LinkedTreeList.
2926
+ * @param {string} name The accessor to use
2927
+ * @param {LinkedTreeList} list The list to use it on
2928
+ * @returns {*}
2929
+ */
2930
+ const borrowedGetter = (name, list) => Object.getOwnPropertyDescriptor(_DoublyLinkedList.DoublyLinkedList.prototype, name).get.call(list)
2571
2931
  /**
2572
2932
  * LinkedTreeList represents a collection stored with a root and spreading in branching (tree) formation.
2573
2933
  * @extends DoublyLinkedList
@@ -2575,11 +2935,21 @@
2575
2935
  class LinkedTreeList {
2576
2936
  /**
2577
2937
  * Create the new LinkedTreeList instance, configure the list class.
2938
+ * @param {TreeLinker} [linkerClass=TreeLinker] The class used to wrap given data as tree linkers.
2578
2939
  */
2579
2940
  constructor (linkerClass = _TreeLinker.TreeLinker) {
2941
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2580
2942
  this.classType = LinkedTreeList
2943
+ /** A linker of the list (null when the list is empty); the head is found by walking back from it. */
2581
2944
  this.innerList = null
2945
+ /** Whether the inner list has been initialized (it can only be initialized once). */
2582
2946
  this.initialized = false
2947
+ /** The last linker, remembered so that adding to the end does not need to walk the whole list (null when not known yet). */
2948
+ this.tailCache = null
2949
+ /** The number of linkers, kept up to date by the list's own methods so that the length does not need to walk the whole list (null when not known yet). */
2950
+ this.countCache = null
2951
+ /** The node these linkers are the children of, remembered so that it is known even while the list is empty (undefined until it is known). */
2952
+ this.ownerNode = undefined
2583
2953
  this.linkerClass = linkerClass
2584
2954
  }
2585
2955
 
@@ -2599,7 +2969,7 @@
2599
2969
  }
2600
2970
 
2601
2971
  /**
2602
- * Retrieve a copy of the innerList used.
2972
+ * Retrieve the innerList used (the list itself, not a copy).
2603
2973
  * @returns {TreeLinker}
2604
2974
  */
2605
2975
  get list () {
@@ -2611,57 +2981,46 @@
2611
2981
  * @returns {TreeLinker}
2612
2982
  */
2613
2983
  get first () {
2614
- return this.reset()
2984
+ return borrowedGetter('first', this)
2615
2985
  }
2616
2986
 
2617
2987
  /**
2618
- * Retrieve the last TreeLinker in the list.
2988
+ * Retrieve the last TreeLinker in the list. The end is remembered, so this does not walk the list.
2619
2989
  * @returns {TreeLinker}
2620
2990
  */
2621
2991
  get last () {
2622
- let tail = this.innerList
2623
- if (tail === null) {
2624
- return null
2625
- }
2626
- let next = tail.next
2627
- while (next !== null) {
2628
- tail = next
2629
- next = tail.next
2630
- }
2631
- return tail
2992
+ return borrowedGetter('last', this)
2632
2993
  }
2633
2994
 
2634
2995
  /**
2635
- * Return the length of the list.
2996
+ * Return the length of the list. It is kept up to date by the list's own methods, so this does not walk the list
2997
+ * (call reset() after linkers were changed directly).
2636
2998
  * @returns {number}
2637
2999
  */
2638
3000
  get length () {
2639
- let current = this.first
2640
- let length = 0
2641
- while (current !== null) {
2642
- ++length
2643
- current = current.next
2644
- }
2645
- return length
3001
+ return borrowedGetter('length', this)
2646
3002
  }
2647
3003
 
2648
3004
  /**
2649
- * Get the parent of this tree list.
2650
- * @return {TreeLinker}
3005
+ * Get the parent of this tree list: the node these linkers are the children of (remembered even while the list is
3006
+ * empty), or null for the linkers at the top of a tree.
3007
+ * @return {TreeLinker|null}
2651
3008
  */
2652
3009
  get parent () {
2653
- const first = this.first
2654
- if (first === null) {
2655
- return null
3010
+ if (this.ownerNode !== undefined) {
3011
+ return this.ownerNode
2656
3012
  }
2657
- return this.first.parent
3013
+ const first = this.first
3014
+ return first === null ? null : first.parent
2658
3015
  }
2659
3016
 
2660
3017
  /**
2661
- * Set the parent of this tree list
2662
- * @param {TreeLinker} parent The new node to use as the parent for this group of children
3018
+ * Set the parent of this tree list: every linker in it gets the node as its parent, and the node gets this list as its
3019
+ * children. Linkers added to the list later get this parent too.
3020
+ * @param {TreeLinker|null} parent The new node to use as the parent for this group of children
2663
3021
  */
2664
3022
  set parent (parent) {
3023
+ this.ownerNode = parent
2665
3024
  let current = this.first
2666
3025
  while (current !== null) {
2667
3026
  current.parent = parent
@@ -2691,34 +3050,57 @@
2691
3050
 
2692
3051
  /**
2693
3052
  * Set the children on a parent item.
2694
- * @param {TreeLinker} item The TreeLinker node that will be the parent of the children
2695
- * @param {LinkedTreeList} children The LinkedTreeList which has the child nodes to use
3053
+ * @param {TreeLinker} item The TreeLinker node (one of the linkers of this list) that will be the parent of the children
3054
+ * @param {LinkedTreeList|null} [children=null] The LinkedTreeList which has the child nodes to use, or null to remove the children of the item
3055
+ * @throws {Error} When the item is not one of the linkers of this list
2696
3056
  */
2697
3057
  setChildren (item, children = null) {
2698
- if (Array.from(this).indexOf(item) < 0) {
2699
- console.error('item is not a child of this')
3058
+ // The item must be one of the linkers of this list (only the siblings are checked, not the whole tree)
3059
+ let isChild = false
3060
+ this.forEach(linker => {
3061
+ if (linker === item) {
3062
+ isChild = true
3063
+ }
3064
+ })
3065
+ if (!isChild) {
3066
+ throw new Error('The item is not one of the linkers of this list.')
3067
+ }
3068
+ if (children === null || typeof children === 'undefined') {
3069
+ item.children = null
3070
+ return
2700
3071
  }
2701
3072
  children.parent = item
2702
3073
  }
2703
3074
 
2704
3075
  /**
2705
- * Insert a new node (or data) after a node.
2706
- * @param {TreeLinker|*} node The existing node as reference
3076
+ * Make a linker of the given node (or data) and make this list's parent its parent.
3077
+ * @param {TreeLinker|*} newNode The node (or data) which is being added to this list
3078
+ * @returns {TreeLinker}
3079
+ */
3080
+ adopt (newNode) {
3081
+ const linker = this.linkerClass.make(newNode, this.linkerClass)
3082
+ linker.parent = this.parent
3083
+ return linker
3084
+ }
3085
+
3086
+ /**
3087
+ * Insert a new node (or data) after a node. The new node gets the parent of this list.
3088
+ * @param {TreeLinker|*} node The existing node as reference, or null to insert at the start of the list
2707
3089
  * @param {TreeLinker|*} newNode The new node to go after the existing node
2708
3090
  * @returns {LinkedTreeList}
2709
3091
  */
2710
3092
  insertAfter (node, newNode) {
2711
- return _DoublyLinkedList.DoublyLinkedList.prototype.insertAfter.call(this, node, newNode)
3093
+ return _DoublyLinkedList.DoublyLinkedList.prototype.insertAfter.call(this, node, this.adopt(newNode))
2712
3094
  }
2713
3095
 
2714
3096
  /**
2715
- * Insert a new node (or data) before a node.
2716
- * @param {TreeLinker|*} node The existing node as reference
3097
+ * Insert a new node (or data) before a node. The new node gets the parent of this list.
3098
+ * @param {TreeLinker|*} node The existing node as reference, or null to insert at the end of the list
2717
3099
  * @param {TreeLinker|*} newNode The new node to go before the existing node
2718
3100
  * @returns {LinkedTreeList}
2719
3101
  */
2720
3102
  insertBefore (node, newNode) {
2721
- return _DoublyLinkedList.DoublyLinkedList.prototype.insertBefore.call(this, node, newNode)
3103
+ return _DoublyLinkedList.DoublyLinkedList.prototype.insertBefore.call(this, node, this.adopt(newNode))
2722
3104
  }
2723
3105
 
2724
3106
  /**
@@ -2742,16 +3124,24 @@
2742
3124
  }
2743
3125
 
2744
3126
  /**
2745
- * Remove a linker from this linked list.
3127
+ * Remove a linker from this linked list. The removed node no longer has a parent.
2746
3128
  * @param {TreeLinker} node The node we wish to remove (and it will be returned after removal)
2747
- * @return {TreeLinker}
3129
+ * @return {TreeLinker|null} The removed node, or null when there was nothing to remove
2748
3130
  */
2749
3131
  remove (node) {
2750
- return _DoublyLinkedList.DoublyLinkedList.prototype.remove.call(this, node)
3132
+ const owner = this.parent
3133
+ const removed = _DoublyLinkedList.DoublyLinkedList.prototype.remove.call(this, node)
3134
+ if (removed && removed.parent === owner) {
3135
+ // Remember whose children these are (the list may now be empty), the removed node no longer has that parent
3136
+ this.ownerNode = owner
3137
+ removed.parent = null
3138
+ }
3139
+ return removed
2751
3140
  }
2752
3141
 
2753
3142
  /**
2754
- * Refresh all references and return head reference.
3143
+ * Refresh all references (the head, the end and the length) by walking the list once, and return the head. The
3144
+ * list's own methods keep these up to date, so this is only needed after linkers were changed directly.
2755
3145
  * @return {TreeLinker}
2756
3146
  */
2757
3147
  reset () {
@@ -2771,6 +3161,7 @@
2771
3161
  * Be able to run forEach on this LinkedTreeList to iterate over the TreeLinker Items.
2772
3162
  * @param {forEachCallback} callback The function to call for-each tree node
2773
3163
  * @param {LinkedTreeList} thisArg Optional, 'this' reference
3164
+ * @return {LinkedTreeList} The list which was iterated.
2774
3165
  */
2775
3166
  forEach (callback, thisArg = this) {
2776
3167
  let index = 0
@@ -2784,12 +3175,14 @@
2784
3175
  }
2785
3176
 
2786
3177
  /**
2787
- * Be able to iterate over this class.
3178
+ * Be able to iterate over this class: the linkers of this list and everything below them (left-first). It stays within
3179
+ * this list (it does not start at, or climb up to, the parents), use the parseTree service to parse a whole tree.
2788
3180
  * @returns {Iterator}
2789
3181
  */
2790
3182
  [Symbol.iterator] () {
2791
- const root = this.rootParent
2792
- return new _TreeLinkerIterator.TreeLinkerIterator(root)
3183
+ // The linkers of this list and everything below them, left-first. It stays within this list: it does not start at,
3184
+ // or climb up to, the parents (use the parseTree service to parse a whole tree)
3185
+ return new _TreeLinkerIterator.TreeLinkerIterator(this.first, this.parent)
2793
3186
  }
2794
3187
  }
2795
3188
  /**
@@ -2804,7 +3197,7 @@
2804
3197
  const list = new classType(linkerClass)
2805
3198
  return list.initialize(linkerClass.fromArray(values).head)
2806
3199
  }
2807
- }, { '../../recipes/TreeLinkerIterator': 28, '../doubly-linked-list/DoublyLinkedList': 20, './TreeLinker': 24 }],
3200
+ }, { '../../recipes/TreeLinkerIterator': 28, '../doubly-linked-list/DoublyLinkedList': 20, './TreeLinker': 24, 'core-js/modules/esnext.iterator.constructor.js': 130, 'core-js/modules/esnext.iterator.for-each.js': 133 }],
2808
3201
  24: [function (require, module, exports) {
2809
3202
  'use strict'
2810
3203
 
@@ -2823,7 +3216,7 @@
2823
3216
  class TreeLinker {
2824
3217
  /**
2825
3218
  * Create the new TreeLinker instance, provide the data and optionally set references for next, prev, parent, or children.
2826
- * @param {Object} [settings={}]
3219
+ * @param {Object} [settings={}] The settings for the new tree node.
2827
3220
  * @param {*} [settings.data=null] The data to be stored in this tree node
2828
3221
  * @param {TreeLinker} [settings.next=null] The reference to the next linker if any
2829
3222
  * @param {TreeLinker} [settings.prev=null] The reference to the previous linker if any
@@ -2839,11 +3232,17 @@
2839
3232
  parent = null,
2840
3233
  listClass = _LinkedTreeList.LinkedTreeList
2841
3234
  } = {}) {
3235
+ /** The class used to create this instance, so that it can be recognized as valid without an instanceof check. */
2842
3236
  this.classType = TreeLinker
3237
+ /** The data stored in this tree node. */
2843
3238
  this.data = null
3239
+ /** The sibling after this node, or null when this is the last child. */
2844
3240
  this.next = null
3241
+ /** The sibling before this node, or null when this is the first child. */
2845
3242
  this.prev = null
3243
+ /** The node this node is a child of, or null for a root node. */
2846
3244
  this.parent = null
3245
+ /** The list of the children of this node, or null when it has none. */
2847
3246
  this.children = null
2848
3247
  this.data = data
2849
3248
  this.next = next
@@ -2853,7 +3252,9 @@
2853
3252
  }
2854
3253
 
2855
3254
  /**
2856
- * Create the children for this tree from an array.
3255
+ * Create the children for this tree from an array. Each child becomes a tree linker with this node as its parent: an
3256
+ * existing linker is kept as it is, an object with a data property gives the settings of the linker, and anything
3257
+ * else is the data of the linker.
2857
3258
  * @param {Array|null} children Provide an array of data / linker references to be children of this tree node.
2858
3259
  * @param {IsArrayable<IsTreeNode>} listClass Give the type of list to use for storing the children
2859
3260
  * @return {LinkedTreeList|null}
@@ -2862,10 +3263,17 @@
2862
3263
  if (children === null) {
2863
3264
  return null
2864
3265
  }
2865
- // Creates a linked-tree-list to store the children.
2866
- return listClass.fromArray(children.map(child => Object.assign({}, child, {
2867
- parent: this
2868
- })), this.classType)
3266
+ // Every child is made into a tree linker (an existing one is kept as it is, and a plain value is the data) and is
3267
+ // given this node as its parent
3268
+ const nodes = children.map(child => {
3269
+ const linker = this.classType.make(child, this.classType)
3270
+ linker.parent = this
3271
+ return linker
3272
+ })
3273
+ // Creates a linked-tree-list to store the children, which remembers this node as its parent even when it is empty
3274
+ const list = listClass.fromArray(nodes, this.classType)
3275
+ list.parent = this
3276
+ return list
2869
3277
  }
2870
3278
  }
2871
3279
  /**
@@ -2897,11 +3305,21 @@
2897
3305
  * Class ArrayIterator returns the next value when using elements of array type list.
2898
3306
  */
2899
3307
  class ArrayIterator {
3308
+ /**
3309
+ * Create an iterator over the given array.
3310
+ * @param {Array<IsElement>} innerList The elements to iterate over.
3311
+ * @param {number} [index=0] The position to start from.
3312
+ */
2900
3313
  constructor (innerList, index = 0) {
2901
3314
  this.innerList = innerList
2902
3315
  this.index = index
2903
3316
  }
2904
3317
 
3318
+ /**
3319
+ * Get the next element, moving the iterator forward.
3320
+ * @param {*} [value] Not used, present to match the Iterator interface.
3321
+ * @return {IteratorResult<IsElement>} The next element, or done when there are no more.
3322
+ */
2905
3323
  next (value) {
2906
3324
  if (this.index < this.innerList.length) {
2907
3325
  return {
@@ -2928,10 +3346,19 @@
2928
3346
  * Class DoubleLinkerIterator returns the next value when using linkers of linked type lists.
2929
3347
  */
2930
3348
  class DoubleLinkerIterator {
3349
+ /**
3350
+ * Create an iterator starting at the given item.
3351
+ * @param {IsDoubleLinker} current The item to start from.
3352
+ */
2931
3353
  constructor (current) {
2932
3354
  this.current = current
2933
3355
  }
2934
3356
 
3357
+ /**
3358
+ * Get the current item and move on to the following one.
3359
+ * @param {*} [value] Not used, present to match the Iterator interface.
3360
+ * @return {IteratorResult<IsDoubleLinker>} The current item, or done when there are no more.
3361
+ */
2935
3362
  next (value) {
2936
3363
  const result = {
2937
3364
  value: this.current,
@@ -2954,10 +3381,19 @@
2954
3381
  * Class LinkerIterator returns the next value when using linkers of linked type lists.
2955
3382
  */
2956
3383
  class LinkerIterator {
3384
+ /**
3385
+ * Create an iterator starting at the given item.
3386
+ * @param {IsLinker} current The item to start from.
3387
+ */
2957
3388
  constructor (current) {
2958
3389
  this.current = current
2959
3390
  }
2960
3391
 
3392
+ /**
3393
+ * Get the current item and move on to the following one.
3394
+ * @param {*} [value] Not used, present to match the Iterator interface.
3395
+ * @return {IteratorResult<IsLinker>} The current item, or done when there are no more.
3396
+ */
2961
3397
  next (value) {
2962
3398
  const result = {
2963
3399
  value: this.current,
@@ -2981,16 +3417,27 @@
2981
3417
  * Class TreeLinkerIterator returns the next value taking a left-first approach down a tree.
2982
3418
  */
2983
3419
  class TreeLinkerIterator {
2984
- constructor (current) {
3420
+ /**
3421
+ * Create an iterator starting at the given item.
3422
+ * @param {IsTreeNode} current The item to start from.
3423
+ * @param {IsTreeNode|null} [boundaryParent] The parent of the nodes to stay within (null for the top of a tree), the whole tree when not given.
3424
+ */
3425
+ constructor (current, boundaryParent) {
2985
3426
  this.current = current
3427
+ this.boundaryParent = boundaryParent
2986
3428
  }
2987
3429
 
3430
+ /**
3431
+ * Get the current item and move on to the following one (left-first, down each branch).
3432
+ * @param {*} [value] Not used, present to match the Iterator interface.
3433
+ * @return {IteratorResult<IsTreeNode>} The current item, or done when there are no more.
3434
+ */
2988
3435
  next (value) {
2989
3436
  const result = {
2990
3437
  value: this.current,
2991
3438
  done: !this.current
2992
3439
  }
2993
- this.current = (0, _parseTreeNext.parseTreeNext)(this.current)
3440
+ this.current = (0, _parseTreeNext.parseTreeNext)(this.current, this.boundaryParent)
2994
3441
  return result
2995
3442
  }
2996
3443
  }
@@ -3012,36 +3459,31 @@
3012
3459
  * 5. Repeat 3
3013
3460
  * 6. If no next child, return to parent and repeat 3
3014
3461
  * 7. Stop at root (next is null and parent is null
3462
+ * A boundary can be given to parse only part of a tree: going back up to the parents stops at the boundary, so the
3463
+ * parsing stays within the nodes whose parent is the boundary (and everything below them).
3015
3464
  * @param {IsTreeNode} treeNode Provide a node in a tree and get the next node (left-first approach)
3465
+ * @param {IsTreeNode|null} [boundaryParent] The parent of the nodes to stay within, null for the nodes at the top of a tree. When it is not given the whole tree is parsed.
3016
3466
  * @returns {IsTreeNode|null}
3017
3467
  */
3018
- const parseTreeNext = treeNode => {
3468
+ const parseTreeNext = (treeNode, boundaryParent) => {
3019
3469
  if (!treeNode) {
3020
3470
  return null
3021
3471
  }
3022
- let test = null
3023
3472
  if (treeNode.children && treeNode.children.length) {
3024
- // Go down the left side of the tree
3025
- test = treeNode.children.first
3026
- }
3027
- if (!test) {
3028
- // Reached the bottom, go the next node on the right
3029
- test = treeNode.next
3030
- }
3031
- if (!test && treeNode.parent) {
3032
- // No more child nodes, return to parent and check parent sibling on the right
3033
- let parentNext = treeNode.parent.next
3034
- let parent = treeNode.parent
3035
- while (parent && !parentNext) {
3036
- parentNext = parent.next
3037
- // Keep checking parent next, until there are no more parents, or we find the parent sibling
3038
- parent = parent.parent
3039
- }
3040
- // This may be the parent sibling, or it could be null indicating we are done
3041
- test = parentNext
3042
- }
3043
- // Finally, either use the node we found, or it may be null
3044
- return test
3473
+ return treeNode.children.first
3474
+ }
3475
+ if (treeNode.next) {
3476
+ return treeNode.next
3477
+ }
3478
+ // Nothing more below or beside this node, so go back up until there is a node which has a next (or the boundary)
3479
+ let parent = treeNode.parent
3480
+ while (parent && parent !== boundaryParent) {
3481
+ if (parent.next) {
3482
+ return parent.next
3483
+ }
3484
+ parent = parent.parent
3485
+ }
3486
+ return null
3045
3487
  }
3046
3488
  exports.parseTreeNext = parseTreeNext
3047
3489
  }, {}],