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
@@ -4,6 +4,8 @@ require('core-js/modules/esnext.iterator.constructor.js')
4
4
  require('core-js/modules/esnext.iterator.filter.js')
5
5
  require('core-js/modules/esnext.iterator.find.js')
6
6
  require('core-js/modules/esnext.iterator.for-each.js')
7
+ require('core-js/modules/esnext.iterator.map.js')
8
+ require('core-js/modules/esnext.iterator.some.js')
7
9
  const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
8
10
  return mod && mod.__esModule
9
11
  ? mod
@@ -21,9 +23,13 @@ Object.defineProperty(exports, '__esModule', {
21
23
  */
22
24
  const EventService_1 = require('./EventService')
23
25
  const PseudoEventListener_1 = __importDefault(require('../classes/PseudoEventListener'))
26
+ const getParentNodes_1 = __importDefault(require('../functions/getParentNodes'))
24
27
  const LinkedList_1 = require('collect-your-stuff/dist/collections/linked-list/LinkedList')
25
28
  /**
26
29
  * Simulate the behaviour of the EventTarget Class when there is no DOM available.
30
+ * Dispatching an event sends it through the tree the way the DOM does: down from the root to the target (capture
31
+ * listeners), to the target itself, then back up to the root (the listeners which are not capture listeners, when the
32
+ * event bubbles).
27
33
  * @author Joshua Heagle <joshuaheagle@gmail.com>
28
34
  * @class
29
35
  * @property {Object.<string, Array.<PseudoEventListener>>} listeners
@@ -53,33 +59,51 @@ class EventTargetService {
53
59
  }
54
60
 
55
61
  /**
56
- * Run each of the listeners registered on this target for the type of the event.
57
- * Listeners which do not apply to the event's phase are skipped, running stops once immediate propagation is stopped,
58
- * and listeners added or removed while running do not change which ones run for this event.
59
- * @param {EventService} event
60
- * @returns {*} true when there was nothing registered, otherwise the last value returned from a handler (null when none ran)
62
+ * Run the listeners registered on this target for the type of the event which apply to the phase the event is in
63
+ * (at the target, the capture listeners run before the others). Listeners which are added while this runs do not run
64
+ * for this event, and listeners which are removed while it runs no longer do. Running stops as soon as immediate
65
+ * propagation is stopped. A listener which throws does not stop the others.
66
+ * @param {EventService} event The event, which is at a phase and has a current target
67
+ * @returns {Array<*>} The errors which the listeners threw
61
68
  */
62
69
  runEvents (event) {
70
+ const errors = []
63
71
  if (!(event.type in this.listeners)) {
64
- return true
72
+ return errors
65
73
  }
66
- const listeners = this.listeners[event.type]
67
- let eventReturn = null
68
- // Work from a copy of the linkers so that removing a listener (for example a once listener) does not disturb the walk
69
- for (const linker of Array.from(listeners)) {
70
- const listener = linker.data
74
+ const listeners = Array.from(this.listeners[event.type]).map(linker => linker.data)
75
+ // At the target the capture listeners come first, otherwise the order is the order they were added
76
+ const ordered = event.eventPhase === EventService_1.EventService.AT_TARGET ? listeners.filter(listener => listener.capture).concat(listeners.filter(listener => !listener.capture)) : listeners
77
+ for (const listener of ordered) {
71
78
  if (event.inner.immediatePropagationStopped) {
72
79
  break
73
80
  }
74
81
  if (listener.rejectEvent(event)) {
75
82
  continue
76
83
  }
77
- eventReturn = listener.handleEvent(event)
78
84
  if (listener.once) {
79
- listeners.remove(linker)
85
+ this.removeListener(event.type, listener)
80
86
  }
87
+ event.inner.inPassiveListener = listener.passive
88
+ try {
89
+ listener.handleEvent(event)
90
+ } catch (error) {
91
+ errors.push(error)
92
+ }
93
+ event.inner.inPassiveListener = false
81
94
  }
82
- return eventReturn
95
+ return errors
96
+ }
97
+
98
+ /**
99
+ * Take a listener out of the registered listeners, so that it does not run again.
100
+ * @param {string} type
101
+ * @param {PseudoEventListener} listener
102
+ */
103
+ removeListener (type, listener) {
104
+ listener.removed = true
105
+ const registered = this.listeners[type]
106
+ Array.from(registered).filter(linker => linker.data === listener).forEach(linker => registered.remove(linker))
83
107
  }
84
108
 
85
109
  /**
@@ -92,48 +116,34 @@ class EventTargetService {
92
116
  this.defaultEvent[type] = callback
93
117
  }
94
118
 
95
- runDefaultEvent (event) {
96
- if (event.defaultPrevented) {
97
- return false
98
- }
99
- this.defaultEvent[event.type](event)
100
- return true
101
- }
102
-
103
- startEvents (eventType) {
104
- const event = new EventService_1.EventService(eventType)
105
- event.inner.target = this;
106
- [EventService_1.EventService.CAPTURING_PHASE, EventService_1.EventService.AT_TARGET, EventService_1.EventService.BUBBLING_PHASE].forEach(phase => {
107
- let continueEvents = null
108
- if (phase === EventService_1.EventService.AT_TARGET || !event.inner.propagationStopped) {
109
- event.inner.eventPhase = phase
110
- event.composedPath().forEach(target => {
111
- event.inner.currentTarget = target
112
- continueEvents = event.currentTarget.runEvents(event)
113
- })
114
- }
115
- if (event.eventPhase === EventService_1.EventService.AT_TARGET && typeof continueEvents !== 'boolean' && this.defaultEvent[eventType]) {
116
- this.runDefaultEvent(event)
117
- }
118
- })
119
- return true
120
- }
121
-
119
+ /**
120
+ * Registers an event handler of a specific event type. Adding the same handler again for the same type and phase does
121
+ * nothing, like the DOM.
122
+ * @param {string} type The type of event to listen for
123
+ * @param {Function|Object} callback The function to call (or an object with a handleEvent function)
124
+ * @param {Object|boolean} [useCapture=false] Listen while the event travels down to the target (true), or an object with capture, once and passive
125
+ */
122
126
  addEventListener (type, callback, useCapture = false) {
123
127
  let options = {
124
128
  capture: false,
125
129
  once: false,
126
130
  passive: false
127
131
  }
128
- if (typeof useCapture === 'object') {
132
+ if (typeof useCapture === 'object' && useCapture !== null) {
129
133
  // Originally useCapture was a single boolean flag, later optional other flags can be used
130
134
  // Here we take all the given flags from the object and assign them as the options
131
135
  options = Object.assign(options, useCapture)
132
136
  } else {
133
- options.capture = useCapture
137
+ options.capture = !!useCapture
134
138
  }
135
- const listener = new PseudoEventListener_1.default(type, options, (callback.handleEvent || callback).bind(this), callback)
136
139
  const listeners = this.listenersFor(type)
140
+ const alreadyAdded = Array.from(listeners).some(linker => linker.data.callback === callback && linker.data.capture === options.capture)
141
+ if (alreadyAdded) {
142
+ return
143
+ }
144
+ // A function runs with this target as this, an object runs its handleEvent as itself
145
+ const handler = typeof callback === 'function' ? callback.bind(this) : callback.handleEvent.bind(callback)
146
+ const listener = new PseudoEventListener_1.default(type, options, handler, callback)
137
147
  // Listeners run in the order they were added, except that listeners which are not defaults always come before the defaults
138
148
  const firstDefault = Array.from(listeners).find(linker => linker.data.isDefault)
139
149
  if (firstDefault && !listener.isDefault) {
@@ -143,20 +153,79 @@ class EventTargetService {
143
153
  }
144
154
  }
145
155
 
146
- removeEventListener (type, callback) {
156
+ /**
157
+ * Removes an event listener, the one which was added with the same type, handler and phase.
158
+ * @param {string} type The type of event
159
+ * @param {Function|Object} callback The handler which was added
160
+ * @param {Object|boolean} [options=false] Whether the listener was a capture listener (true), or an object with capture
161
+ */
162
+ removeEventListener (type, callback, options = false) {
147
163
  if (!(type in this.listeners)) {
148
164
  return
149
165
  }
150
- const listeners = this.listeners[type]
151
- Array.from(listeners).filter(linker => !linker.data.isDefault && linker.data.callback === callback).forEach(linker => listeners.remove(linker))
166
+ const capture = typeof options === 'object' && options !== null ? !!options.capture : !!options
167
+ 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))
152
168
  }
153
169
 
154
- dispatchEvent (event, target = this) {
155
- event.inner.target = target
156
- if (!(event.type in this.listeners)) {
157
- return true
170
+ /**
171
+ * Dispatches an event to this target and through the tree: capture listeners of the ancestors from the root down,
172
+ * then the listeners of this target, then (when the event bubbles) the other listeners of the ancestors from the
173
+ * parent up to the root. stopPropagation() stops it reaching further targets, stopImmediatePropagation() also stops
174
+ * the remaining listeners of the current target. Afterwards, unless the default was prevented, the default action
175
+ * of this target (see setDefaultEvent) runs. The event can be dispatched again afterwards.
176
+ * @param {EventService} event The event to dispatch
177
+ * @returns {boolean} False when the event was cancelable and a listener prevented the default, otherwise true
178
+ * @throws {Error} When the event is already being dispatched, or (after the whole dispatch has finished) the error
179
+ * which a listener threw (an error with all of them in its errors property when several did)
180
+ */
181
+ dispatchEvent (event) {
182
+ if (event.inner.dispatching) {
183
+ throw new Error('The event is already being dispatched.')
184
+ }
185
+ event.inner.dispatching = true
186
+ event.inner.target = this
187
+ // The ancestors, the root first, which can have listeners
188
+ const ancestors = (0, getParentNodes_1.default)(this).filter(node => node instanceof EventTargetService)
189
+ event.inner.path = [this].concat(ancestors.slice().reverse())
190
+ const errors = []
191
+ const visit = (target, phase) => {
192
+ event.inner.eventPhase = phase
193
+ event.inner.currentTarget = target
194
+ errors.push(...target.runEvents(event))
195
+ }
196
+ for (const ancestor of ancestors) {
197
+ if (event.inner.propagationStopped) {
198
+ break
199
+ }
200
+ visit(ancestor, EventService_1.EventService.CAPTURING_PHASE)
201
+ }
202
+ if (!event.inner.propagationStopped) {
203
+ visit(this, EventService_1.EventService.AT_TARGET)
204
+ }
205
+ if (event.bubbles) {
206
+ for (const ancestor of ancestors.slice().reverse()) {
207
+ if (event.inner.propagationStopped) {
208
+ break
209
+ }
210
+ visit(ancestor, EventService_1.EventService.BUBBLING_PHASE)
211
+ }
212
+ }
213
+ event.inner.finishDispatch()
214
+ if (!event.defaultPrevented && typeof this.defaultEvent[event.type] === 'function') {
215
+ try {
216
+ this.defaultEvent[event.type](event)
217
+ } catch (error) {
218
+ errors.push(error)
219
+ }
220
+ }
221
+ if (errors.length === 1) {
222
+ throw errors[0]
223
+ }
224
+ if (errors.length > 1) {
225
+ throw Object.assign(new Error(`${errors.length} listeners threw an error while dispatching the ${event.type} event.`), {
226
+ errors
227
+ })
158
228
  }
159
- this.runEvents(event)
160
229
  return !event.defaultPrevented
161
230
  }
162
231
  }
@@ -1 +1 @@
1
- "use strict";require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.for-each.js");var __importDefault=function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0});const EventService_1=require("./EventService"),PseudoEventListener_1=__importDefault(require("../classes/PseudoEventListener")),LinkedList_1=require("collect-your-stuff/dist/collections/linked-list/LinkedList");class EventTargetService{constructor(){this.listeners={},this.defaultEvent={}}listenersFor(e){return e in this.listeners||(this.listeners[e]=new LinkedList_1.LinkedList),this.listeners[e]}runEvents(e){if(!(e.type in this.listeners))return!0;const t=this.listeners[e.type];let r=null;for(const n of Array.from(t)){const s=n.data;if(e.inner.immediatePropagationStopped)break;s.rejectEvent(e)||(r=s.handleEvent(e),s.once&&t.remove(n))}return r}setDefaultEvent(e,t){this.listenersFor(e),this.defaultEvent[e]=t}runDefaultEvent(e){return!e.defaultPrevented&&(this.defaultEvent[e.type](e),!0)}startEvents(e){const t=new EventService_1.EventService(e);return t.inner.target=this,[EventService_1.EventService.CAPTURING_PHASE,EventService_1.EventService.AT_TARGET,EventService_1.EventService.BUBBLING_PHASE].forEach(r=>{let n=null;r!==EventService_1.EventService.AT_TARGET&&t.inner.propagationStopped||(t.inner.eventPhase=r,t.composedPath().forEach(e=>{t.inner.currentTarget=e,n=t.currentTarget.runEvents(t)})),t.eventPhase===EventService_1.EventService.AT_TARGET&&"boolean"!=typeof n&&this.defaultEvent[e]&&this.runDefaultEvent(t)}),!0}addEventListener(e,t,r=!1){let n={capture:!1,once:!1,passive:!1};"object"==typeof r?n=Object.assign(n,r):n.capture=r;const s=new PseudoEventListener_1.default(e,n,(t.handleEvent||t).bind(this),t),i=this.listenersFor(e),o=Array.from(i).find(e=>e.data.isDefault);o&&!s.isDefault?i.insertBefore(o,s):i.append(s)}removeEventListener(e,t){if(!(e in this.listeners))return;const r=this.listeners[e];Array.from(r).filter(e=>!e.data.isDefault&&e.data.callback===t).forEach(e=>r.remove(e))}dispatchEvent(e,t=this){return e.inner.target=t,!(e.type in this.listeners)||(this.runEvents(e),!e.defaultPrevented)}}exports.default=EventTargetService;
1
+ "use strict";require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.filter.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.for-each.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.some.js");var __importDefault=function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0});const EventService_1=require("./EventService"),PseudoEventListener_1=__importDefault(require("../classes/PseudoEventListener")),getParentNodes_1=__importDefault(require("../functions/getParentNodes")),LinkedList_1=require("collect-your-stuff/dist/collections/linked-list/LinkedList");class EventTargetService{constructor(){this.listeners={},this.defaultEvent={}}listenersFor(e){return e in this.listeners||(this.listeners[e]=new LinkedList_1.LinkedList),this.listeners[e]}runEvents(e){const t=[];if(!(e.type in this.listeners))return t;const r=Array.from(this.listeners[e.type]).map(e=>e.data),n=e.eventPhase===EventService_1.EventService.AT_TARGET?r.filter(e=>e.capture).concat(r.filter(e=>!e.capture)):r;for(const r of n){if(e.inner.immediatePropagationStopped)break;if(!r.rejectEvent(e)){r.once&&this.removeListener(e.type,r),e.inner.inPassiveListener=r.passive;try{r.handleEvent(e)}catch(e){t.push(e)}e.inner.inPassiveListener=!1}}return t}removeListener(e,t){t.removed=!0;const r=this.listeners[e];Array.from(r).filter(e=>e.data===t).forEach(e=>r.remove(e))}setDefaultEvent(e,t){this.listenersFor(e),this.defaultEvent[e]=t}addEventListener(e,t,r=!1){let n={capture:!1,once:!1,passive:!1};"object"==typeof r&&null!==r?n=Object.assign(n,r):n.capture=!!r;const i=this.listenersFor(e);if(Array.from(i).some(e=>e.data.callback===t&&e.data.capture===n.capture))return;const s="function"==typeof t?t.bind(this):t.handleEvent.bind(t),o=new PseudoEventListener_1.default(e,n,s,t),a=Array.from(i).find(e=>e.data.isDefault);a&&!o.isDefault?i.insertBefore(a,o):i.append(o)}removeEventListener(e,t,r=!1){if(!(e in this.listeners))return;const n="object"==typeof r&&null!==r?!!r.capture:!!r;Array.from(this.listeners[e]).map(e=>e.data).filter(e=>!e.isDefault&&e.callback===t&&e.capture===n).forEach(t=>this.removeListener(e,t))}dispatchEvent(e){if(e.inner.dispatching)throw new Error("The event is already being dispatched.");e.inner.dispatching=!0,e.inner.target=this;const t=(0,getParentNodes_1.default)(this).filter(e=>e instanceof EventTargetService);e.inner.path=[this].concat(t.slice().reverse());const r=[],n=(t,n)=>{e.inner.eventPhase=n,e.inner.currentTarget=t,r.push(...t.runEvents(e))};for(const r of t){if(e.inner.propagationStopped)break;n(r,EventService_1.EventService.CAPTURING_PHASE)}if(e.inner.propagationStopped||n(this,EventService_1.EventService.AT_TARGET),e.bubbles)for(const r of t.slice().reverse()){if(e.inner.propagationStopped)break;n(r,EventService_1.EventService.BUBBLING_PHASE)}if(e.inner.finishDispatch(),!e.defaultPrevented&&"function"==typeof this.defaultEvent[e.type])try{this.defaultEvent[e.type](e)}catch(e){r.push(e)}if(1===r.length)throw r[0];if(r.length>1)throw Object.assign(new Error(`${r.length} listeners threw an error while dispatching the ${e.type} event.`),{errors:r});return!e.defaultPrevented}}exports.default=EventTargetService;
@@ -1,5 +1,6 @@
1
1
  import { PseudoNodeList } from '../classes/PseudoNodeList';
2
2
  import { LinkedTreeList } from 'collect-your-stuff/dist/collections/linked-tree-list/LinkedTreeList';
3
+ import { TreeLinker } from 'collect-your-stuff/dist/collections/linked-tree-list/TreeLinker';
3
4
  import { PseudoNode } from '../interfaces/PseudoNode';
4
5
  import EventTargetService from './EventTargetService';
5
6
  import { PseudoElement } from '../interfaces/PseudoElement';
@@ -33,8 +34,8 @@ export declare class NodeService extends EventTargetService implements PseudoNod
33
34
  protected nodeNameValue: string;
34
35
  private nodeValueStore;
35
36
  private textContentStore;
36
- private next;
37
- private prev;
37
+ /** The linker which holds this node in the children list of its parent, from which its siblings are found (null while it has no parent). */
38
+ protected listLinker: TreeLinker | null;
38
39
  /**
39
40
  *
40
41
  * @constructor
@@ -57,11 +58,17 @@ export declare class NodeService extends EventTargetService implements PseudoNod
57
58
  get textContent(): string | null;
58
59
  set textContent(text: string | null);
59
60
  /**
60
- *
61
- * @param {PseudoNode} childNode
62
- * @returns {PseudoNode}
61
+ * Add a node as the last child of this node (a node which is already in a tree is moved).
62
+ * @param {PseudoNode} childNode The node to add
63
+ * @returns {PseudoNode} The added node
63
64
  */
64
65
  appendChild(childNode: PseudoNode): PseudoNode;
66
+ /**
67
+ * Called each time a node has been inserted as a child of this node, so that nodes which need to react to children
68
+ * (for example elements applying default events) can do so.
69
+ * @param {NodeService} child The node which was inserted
70
+ */
71
+ protected childInserted(child: NodeService): void;
65
72
  /**
66
73
  * Not implemented yet.
67
74
  * @throws {Error}
@@ -73,19 +80,24 @@ export declare class NodeService extends EventTargetService implements PseudoNod
73
80
  */
74
81
  compareDocumentPosition(otherNode: PseudoNode): number;
75
82
  /**
76
- * Not implemented yet.
77
- * @throws {Error}
83
+ * Check whether a node is this node or one of its descendants.
84
+ * @param {PseudoNode|null} otherNode The node to look for
85
+ * @returns {boolean}
78
86
  */
79
- contains(otherNode: PseudoNode): boolean;
87
+ contains(otherNode: PseudoNode | null): boolean;
80
88
  getRootNode(options?: {
81
89
  composed: boolean;
82
90
  }): PseudoNode;
83
91
  hasChildNodes(): boolean;
84
92
  /**
85
- * Not implemented yet.
86
- * @throws {Error}
93
+ * 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
94
+ * already in a tree is moved, and the children of a document fragment are moved in order.
95
+ * @param {PseudoNode} newNode The node to insert
96
+ * @param {PseudoNode|null} [referenceNode=null] The child of this node to insert before, or null to insert at the end
97
+ * @returns {PseudoNode} The inserted node
98
+ * @throws {Error} When the reference node is not a child of this node, or the new node is this node or contains it
87
99
  */
88
- insertBefore(newNode: PseudoNode, referenceNode: PseudoNode | null): PseudoNode | PseudoDocumentFragment;
100
+ insertBefore(newNode: PseudoNode, referenceNode?: PseudoNode | null): PseudoNode | PseudoDocumentFragment;
89
101
  isDefaultNamespace(namespaceURI: string | null): boolean;
90
102
  /**
91
103
  * Not implemented yet.
@@ -97,15 +109,18 @@ export declare class NodeService extends EventTargetService implements PseudoNod
97
109
  lookupNamespaceURI(prefix: string): string | null;
98
110
  normalize(): void;
99
111
  /**
100
- * Remove the given child from this node.
101
- * @param {PseudoNode} childElement The child node, or its TreeLinker from the children list
102
- * @returns {PseudoNode}
112
+ * Remove a child from this node, it no longer has a parent or siblings afterwards.
113
+ * @param {PseudoNode} childElement The child node to remove
114
+ * @returns {PseudoNode} The removed node
103
115
  * @throws {Error} When the node is not a child of this node
104
116
  */
105
117
  removeChild(childElement: PseudoNode): PseudoNode;
106
118
  /**
107
- * Not implemented yet.
108
- * @throws {Error}
119
+ * Replace a child of this node with another node (which is moved if it is already in a tree).
120
+ * @param {PseudoNode} newChild The node which takes the place
121
+ * @param {PseudoNode} oldChild The child of this node to replace
122
+ * @returns {PseudoNode} The replaced node
123
+ * @throws {Error} When the old node is not a child of this node
109
124
  */
110
125
  replaceChild(newChild: PseudoNode, oldChild: PseudoNode): PseudoNode;
111
126
  }
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- require('core-js/modules/esnext.iterator.constructor.js')
4
- require('core-js/modules/esnext.iterator.for-each.js')
5
3
  const __importDefault = void 0 && (void 0).__importDefault || function (mod) {
6
4
  return mod && mod.__esModule
7
5
  ? mod
@@ -19,6 +17,7 @@ exports.NodeService = void 0
19
17
  * @version 1.0.0
20
18
  */
21
19
  const generateNodeList_1 = __importDefault(require('../factories/generateNodeList'))
20
+ const TreeLinker_1 = require('collect-your-stuff/dist/collections/linked-tree-list/TreeLinker')
22
21
  const EventTargetService_1 = __importDefault(require('./EventTargetService'))
23
22
  /**
24
23
  * Simulate the behaviour of the Node Class when there is no DOM available.
@@ -41,8 +40,7 @@ class NodeService extends EventTargetService_1.default {
41
40
  this.nodeNameValue = ''
42
41
  this.children = (0, generateNodeList_1.default)()
43
42
  this.parent = null
44
- this.next = null
45
- this.prev = null
43
+ this.listLinker = null
46
44
  }
47
45
 
48
46
  get baseURI () {
@@ -66,7 +64,7 @@ class NodeService extends EventTargetService_1.default {
66
64
  }
67
65
 
68
66
  get nextSibling () {
69
- return this.isConnected ? this.next : null
67
+ return this.listLinker && this.listLinker.next ? this.listLinker.next.data : null
70
68
  }
71
69
 
72
70
  get nodeName () {
@@ -98,7 +96,7 @@ class NodeService extends EventTargetService_1.default {
98
96
  }
99
97
 
100
98
  get previousSibling () {
101
- return this.isConnected ? this.prev : null
99
+ return this.listLinker && this.listLinker.prev ? this.listLinker.prev.data : null
102
100
  }
103
101
 
104
102
  get textContent () {
@@ -110,15 +108,20 @@ class NodeService extends EventTargetService_1.default {
110
108
  }
111
109
 
112
110
  /**
113
- *
114
- * @param {PseudoNode} childNode
115
- * @returns {PseudoNode}
111
+ * Add a node as the last child of this node (a node which is already in a tree is moved).
112
+ * @param {PseudoNode} childNode The node to add
113
+ * @returns {PseudoNode} The added node
116
114
  */
117
115
  appendChild (childNode) {
118
- this.children.append(childNode)
119
- return childNode
116
+ return this.insertBefore(childNode, null)
120
117
  }
121
118
 
119
+ /**
120
+ * Called each time a node has been inserted as a child of this node, so that nodes which need to react to children
121
+ * (for example elements applying default events) can do so.
122
+ * @param {NodeService} child The node which was inserted
123
+ */
124
+ childInserted (child) {}
122
125
  /**
123
126
  * Not implemented yet.
124
127
  * @throws {Error}
@@ -136,11 +139,19 @@ class NodeService extends EventTargetService_1.default {
136
139
  }
137
140
 
138
141
  /**
139
- * Not implemented yet.
140
- * @throws {Error}
142
+ * Check whether a node is this node or one of its descendants.
143
+ * @param {PseudoNode|null} otherNode The node to look for
144
+ * @returns {boolean}
141
145
  */
142
146
  contains (otherNode) {
143
- throw new Error('NodeService.contains() is not implemented yet.')
147
+ let current = otherNode
148
+ while (current) {
149
+ if (current === this) {
150
+ return true
151
+ }
152
+ current = current.parentNode
153
+ }
154
+ return false
144
155
  }
145
156
 
146
157
  getRootNode (options = {
@@ -154,11 +165,44 @@ class NodeService extends EventTargetService_1.default {
154
165
  }
155
166
 
156
167
  /**
157
- * Not implemented yet.
158
- * @throws {Error}
168
+ * 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
169
+ * already in a tree is moved, and the children of a document fragment are moved in order.
170
+ * @param {PseudoNode} newNode The node to insert
171
+ * @param {PseudoNode|null} [referenceNode=null] The child of this node to insert before, or null to insert at the end
172
+ * @returns {PseudoNode} The inserted node
173
+ * @throws {Error} When the reference node is not a child of this node, or the new node is this node or contains it
159
174
  */
160
- insertBefore (newNode, referenceNode) {
161
- throw new Error('NodeService.insertBefore() is not implemented yet.')
175
+ insertBefore (newNode, referenceNode = null) {
176
+ if (referenceNode !== null && referenceNode.parentNode !== this) {
177
+ throw new Error('The node before which the new node is to be inserted is not a child of this node.')
178
+ }
179
+ if (newNode === referenceNode) {
180
+ // Inserting a node before itself leaves it where it is
181
+ return newNode
182
+ }
183
+ if (typeof newNode.contains === 'function' && newNode.contains(this)) {
184
+ throw new Error('The new node cannot be inserted into itself or one of its own descendants.')
185
+ }
186
+ if (newNode.nodeType === NodeService.DOCUMENT_FRAGMENT_NODE) {
187
+ // The children of a fragment are inserted (moved) in order, and the fragment is left empty
188
+ while (newNode.firstChild) {
189
+ this.insertBefore(newNode.firstChild, referenceNode)
190
+ }
191
+ return newNode
192
+ }
193
+ if (newNode.parentNode) {
194
+ // A node can only be in one place, so it is moved from where it was
195
+ newNode.parentNode.removeChild(newNode)
196
+ }
197
+ const linker = new TreeLinker_1.TreeLinker({
198
+ data: newNode
199
+ })
200
+ this.children.insertBefore(referenceNode ? referenceNode.listLinker : null, linker)
201
+ const inserted = newNode
202
+ inserted.parent = this
203
+ inserted.listLinker = linker
204
+ this.childInserted(inserted)
205
+ return newNode
162
206
  }
163
207
 
164
208
  isDefaultNamespace (namespaceURI) {
@@ -187,31 +231,44 @@ class NodeService extends EventTargetService_1.default {
187
231
 
188
232
  normalize () {}
189
233
  /**
190
- * Remove the given child from this node.
191
- * @param {PseudoNode} childElement The child node, or its TreeLinker from the children list
192
- * @returns {PseudoNode}
234
+ * Remove a child from this node, it no longer has a parent or siblings afterwards.
235
+ * @param {PseudoNode} childElement The child node to remove
236
+ * @returns {PseudoNode} The removed node
193
237
  * @throws {Error} When the node is not a child of this node
194
238
  */
195
239
  removeChild (childElement) {
196
- let found = null
197
- this.children.forEach(linker => {
198
- if (found === null && (linker === childElement || linker.data === childElement)) {
199
- found = linker
200
- }
201
- })
202
- if (found === null) {
240
+ if (!childElement || childElement.parentNode !== this) {
203
241
  throw new Error('The node to be removed is not a child of this node.')
204
242
  }
205
- this.children.remove(found)
206
- return found.data
243
+ const removed = childElement
244
+ this.children.remove(removed.listLinker)
245
+ removed.parent = null
246
+ removed.listLinker = null
247
+ return childElement
207
248
  }
208
249
 
209
250
  /**
210
- * Not implemented yet.
211
- * @throws {Error}
251
+ * Replace a child of this node with another node (which is moved if it is already in a tree).
252
+ * @param {PseudoNode} newChild The node which takes the place
253
+ * @param {PseudoNode} oldChild The child of this node to replace
254
+ * @returns {PseudoNode} The replaced node
255
+ * @throws {Error} When the old node is not a child of this node
212
256
  */
213
257
  replaceChild (newChild, oldChild) {
214
- throw new Error('NodeService.replaceChild() is not implemented yet.')
258
+ if (!oldChild || oldChild.parentNode !== this) {
259
+ throw new Error('The node to be replaced is not a child of this node.')
260
+ }
261
+ if (newChild === oldChild) {
262
+ return oldChild
263
+ }
264
+ // The new node goes where the old one was, which is before the old node's next sibling (unless that is the new node)
265
+ let reference = oldChild.nextSibling
266
+ if (reference === newChild) {
267
+ reference = newChild.nextSibling
268
+ }
269
+ this.removeChild(oldChild)
270
+ this.insertBefore(newChild, reference)
271
+ return oldChild
215
272
  }
216
273
  }
217
274
  exports.NodeService = NodeService
@@ -1 +1 @@
1
- "use strict";require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.for-each.js");var __importDefault=function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.NodeService=void 0;const generateNodeList_1=__importDefault(require("../factories/generateNodeList")),EventTargetService_1=__importDefault(require("./EventTargetService"));class NodeService extends EventTargetService_1.default{constructor(){super(),this.nodeValueStore="",this.textContentStore="",this.nodeNameValue="",this.children=(0,generateNodeList_1.default)(),this.parent=null,this.next=null,this.prev=null}get baseURI(){return window.location||"/"}get childNodes(){return this.children}get firstChild(){return this.children.first?this.children.first.data:null}get isConnected(){return!!this.parent}get lastChild(){return this.children.last?this.children.last.data:null}get nextSibling(){return this.isConnected?this.next:null}get nodeName(){return this.nodeNameValue||""}get nodeType(){return NodeService.DEFAULT_NODE}get nodeValue(){return this.nodeValueStore}set nodeValue(e){this.nodeValueStore=e}get ownerDocument(){return null}get parentNode(){return this.parent}get parentElement(){return this.parent&&this.parent.nodeType===NodeService.ELEMENT_NODE?this.parent:null}get previousSibling(){return this.isConnected?this.prev:null}get textContent(){return this.textContentStore}set textContent(e){this.textContentStore=e}appendChild(e){return this.children.append(e),e}cloneNode(e=!1){throw new Error(`NodeService.cloneNode(${e}) is not implemented yet.`)}compareDocumentPosition(e){throw new Error("NodeService.compareDocumentPosition() is not implemented yet.")}contains(e){throw new Error("NodeService.contains() is not implemented yet.")}getRootNode(e={composed:!1}){return this.parent?this.parent.getRootNode(e):this}hasChildNodes(){return this.children.length>0}insertBefore(e,t){throw new Error("NodeService.insertBefore() is not implemented yet.")}isDefaultNamespace(e){return null===e}isEqualNode(e){throw new Error("NodeService.isEqualNode() is not implemented yet.")}isSameNode(e){return this===e}lookupPrefix(e){return null}lookupNamespaceURI(e){return null}normalize(){}removeChild(e){let t=null;if(this.children.forEach(r=>{null!==t||r!==e&&r.data!==e||(t=r)}),null===t)throw new Error("The node to be removed is not a child of this node.");return this.children.remove(t),t.data}replaceChild(e,t){throw new Error("NodeService.replaceChild() is not implemented yet.")}}exports.NodeService=NodeService,NodeService.DEFAULT_NODE=0,NodeService.ELEMENT_NODE=1,NodeService.ATTRIBUTE_NODE=2,NodeService.TEXT_NODE=3,NodeService.CDATA_SECTION_NODE=4,NodeService.ENTITY_REFERENCE_NODE=5,NodeService.ENTITY_NODE=6,NodeService.PROCESSING_INSTRUCTION_NODE=7,NodeService.COMMENT_NODE=8,NodeService.DOCUMENT_NODE=9,NodeService.DOCUMENT_TYPE_NODE=10,NodeService.DOCUMENT_FRAGMENT_NODE=11,NodeService.NOTATION_NODE=12;
1
+ "use strict";var __importDefault=function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.NodeService=void 0;const generateNodeList_1=__importDefault(require("../factories/generateNodeList")),TreeLinker_1=require("collect-your-stuff/dist/collections/linked-tree-list/TreeLinker"),EventTargetService_1=__importDefault(require("./EventTargetService"));class NodeService extends EventTargetService_1.default{constructor(){super(),this.nodeValueStore="",this.textContentStore="",this.nodeNameValue="",this.children=(0,generateNodeList_1.default)(),this.parent=null,this.listLinker=null}get baseURI(){return window.location||"/"}get childNodes(){return this.children}get firstChild(){return this.children.first?this.children.first.data:null}get isConnected(){return!!this.parent}get lastChild(){return this.children.last?this.children.last.data:null}get nextSibling(){return this.listLinker&&this.listLinker.next?this.listLinker.next.data:null}get nodeName(){return this.nodeNameValue||""}get nodeType(){return NodeService.DEFAULT_NODE}get nodeValue(){return this.nodeValueStore}set nodeValue(e){this.nodeValueStore=e}get ownerDocument(){return null}get parentNode(){return this.parent}get parentElement(){return this.parent&&this.parent.nodeType===NodeService.ELEMENT_NODE?this.parent:null}get previousSibling(){return this.listLinker&&this.listLinker.prev?this.listLinker.prev.data:null}get textContent(){return this.textContentStore}set textContent(e){this.textContentStore=e}appendChild(e){return this.insertBefore(e,null)}childInserted(e){}cloneNode(e=!1){throw new Error(`NodeService.cloneNode(${e}) is not implemented yet.`)}compareDocumentPosition(e){throw new Error("NodeService.compareDocumentPosition() is not implemented yet.")}contains(e){let t=e;for(;t;){if(t===this)return!0;t=t.parentNode}return!1}getRootNode(e={composed:!1}){return this.parent?this.parent.getRootNode(e):this}hasChildNodes(){return this.children.length>0}insertBefore(e,t=null){if(null!==t&&t.parentNode!==this)throw new Error("The node before which the new node is to be inserted is not a child of this node.");if(e===t)return e;if("function"==typeof e.contains&&e.contains(this))throw new Error("The new node cannot be inserted into itself or one of its own descendants.");if(e.nodeType===NodeService.DOCUMENT_FRAGMENT_NODE){for(;e.firstChild;)this.insertBefore(e.firstChild,t);return e}e.parentNode&&e.parentNode.removeChild(e);const r=new TreeLinker_1.TreeLinker({data:e});this.children.insertBefore(t?t.listLinker:null,r);const i=e;return i.parent=this,i.listLinker=r,this.childInserted(i),e}isDefaultNamespace(e){return null===e}isEqualNode(e){throw new Error("NodeService.isEqualNode() is not implemented yet.")}isSameNode(e){return this===e}lookupPrefix(e){return null}lookupNamespaceURI(e){return null}normalize(){}removeChild(e){if(!e||e.parentNode!==this)throw new Error("The node to be removed is not a child of this node.");const t=e;return this.children.remove(t.listLinker),t.parent=null,t.listLinker=null,e}replaceChild(e,t){if(!t||t.parentNode!==this)throw new Error("The node to be replaced is not a child of this node.");if(e===t)return t;let r=t.nextSibling;return r===e&&(r=e.nextSibling),this.removeChild(t),this.insertBefore(e,r),t}}exports.NodeService=NodeService,NodeService.DEFAULT_NODE=0,NodeService.ELEMENT_NODE=1,NodeService.ATTRIBUTE_NODE=2,NodeService.TEXT_NODE=3,NodeService.CDATA_SECTION_NODE=4,NodeService.ENTITY_REFERENCE_NODE=5,NodeService.ENTITY_NODE=6,NodeService.PROCESSING_INSTRUCTION_NODE=7,NodeService.COMMENT_NODE=8,NodeService.DOCUMENT_NODE=9,NodeService.DOCUMENT_TYPE_NODE=10,NodeService.DOCUMENT_FRAGMENT_NODE=11,NodeService.NOTATION_NODE=12;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pseudo-dom",
3
3
  "description": "Mock the DOM on the server side for managing state and testing",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
7
7
  "author": "Joshua Heagle",
@@ -18,7 +18,7 @@
18
18
  "watch:test": "gulp watchTest"
19
19
  },
20
20
  "dependencies": {
21
- "collect-your-stuff": "^1.2.8",
21
+ "collect-your-stuff": "^2.1.0",
22
22
  "core-js": "^3.50.0"
23
23
  },
24
24
  "devDependencies": {