@esportsplus/template 0.12.12 → 0.14.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.

Potentially problematic release.


This version of @esportsplus/template might be problematic. Click here for more details.

package/src/attributes.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { effect, root } from '@esportsplus/reactivity';
2
- import { ATTRIBUTES } from './constants';
3
2
  import { Attributes, Element } from './types';
4
3
  import { className, isArray, isObject, raf, removeAttribute, setAttribute } from './utilities';
5
4
  import event from './event';
@@ -9,7 +8,8 @@ let attributes: Record<string, unknown> = {},
9
8
  delimiters: Record<string, string> = {
10
9
  class: ' ',
11
10
  style: ';'
12
- };
11
+ },
12
+ key = Symbol();
13
13
 
14
14
 
15
15
  function attribute(element: Element, name: string, value: unknown) {
@@ -55,12 +55,22 @@ function reactive(element: Element, id: string, name: string, value: unknown, wa
55
55
  }
56
56
 
57
57
  function set(element: Element, value: unknown, name: string) {
58
- if (typeof value === 'function') {
58
+ if (name === 'style' && isObject(value)) {
59
+ for (let key in value) {
60
+ set(element, value[key], name);
61
+ }
62
+ }
63
+ else if (isArray(value)) {
64
+ for (let i = 0, n = value.length; i < n; i++) {
65
+ set(element, value[i], name);
66
+ }
67
+ }
68
+ else if (typeof value === 'function') {
59
69
  if (name.startsWith('on')) {
60
- event(element, name, value);
70
+ event(element, name as `on${string}`, value);
61
71
  }
62
72
  else {
63
- reactive(element, ('e' + store(element)[ATTRIBUTES]++), name, value, true);
73
+ reactive(element, ('e' + store(element)[key]++), name, value, true);
64
74
  }
65
75
  }
66
76
  else {
@@ -70,8 +80,8 @@ function set(element: Element, value: unknown, name: string) {
70
80
 
71
81
  function store(element: Element) {
72
82
  return (
73
- element[ATTRIBUTES] || (element[ATTRIBUTES] = { [ATTRIBUTES]: 0 })
74
- ) as Attributes & { [ATTRIBUTES]: number };
83
+ element[key] || (element[key] = { [key]: 0 })
84
+ ) as Attributes & { [key]: number };
75
85
  }
76
86
 
77
87
  function update(element: Element, id: null | string, name: string, value: unknown, wait = false) {
@@ -155,47 +165,38 @@ function update(element: Element, id: null | string, name: string, value: unknow
155
165
  }
156
166
 
157
167
 
158
- export default {
159
- apply: (element: Element) => {
160
- for (let key in attributes) {
161
- attribute(element, key, attributes[key]);
162
- }
168
+ const apply = (element: Element) => {
169
+ for (let key in attributes) {
170
+ attribute(element, key, attributes[key]);
171
+ }
163
172
 
164
- attributes = {};
165
- },
166
- set: (element: Element, value: unknown, name: string) => {
167
- if (name === 'style' && isObject(value)) {
168
- for (let key in value) {
169
- set(element, value[key], name);
170
- }
171
- }
172
- else if (isArray(value)) {
173
- for (let i = 0, n = value.length; i < n; i++) {
174
- set(element, value[i], name);
175
- }
176
- }
177
- else {
178
- set(element, value, name);
179
- }
180
- },
181
- spread: function (element: Element, attributes: Attributes | Attributes[]) {
182
- if (isObject(attributes)) {
183
- for (let name in attributes) {
184
- this.set(element, attributes[name], name);
185
- }
173
+ attributes = {};
174
+ };
175
+
176
+ const spread = function (element: Element, attributes: Attributes | Attributes[]) {
177
+ if (isObject(attributes)) {
178
+ for (let name in attributes) {
179
+ set(element, attributes[name], name);
186
180
  }
187
- else if (isArray(attributes)) {
188
- for (let i = 0, n = attributes.length; i < n; i++) {
189
- let attrs = attributes[i];
181
+ }
182
+ else if (isArray(attributes)) {
183
+ for (let i = 0, n = attributes.length; i < n; i++) {
184
+ let attrs = attributes[i];
190
185
 
191
- if (!isObject(attrs)) {
192
- continue;
193
- }
186
+ if (!isObject(attrs)) {
187
+ continue;
188
+ }
194
189
 
195
- for (let name in attrs) {
196
- this.set(element, attrs[name], name);
197
- }
190
+ for (let name in attrs) {
191
+ set(element, attrs[name], name);
198
192
  }
199
193
  }
200
194
  }
201
- };
195
+ else {
196
+ throw new Error('@esportsplus/template: attributes must be of type `Attributes` or `Attributes[]`; Received ' + JSON.stringify(attributes));
197
+ }
198
+ };
199
+
200
+
201
+ export default { apply, spread };
202
+ export { apply, spread }
package/src/constants.ts CHANGED
@@ -1,6 +1,3 @@
1
- const ATTRIBUTES = Symbol();
2
-
3
-
4
1
  const NODE_CLOSING = 1;
5
2
 
6
3
  const NODE_COMMENT = 2;
@@ -36,12 +33,6 @@ const NODE_WHITELIST: Record<string, number> = {
36
33
 
37
34
  const REGEX_EMPTY_TEXT_NODES = /(>|})\s+(<|{)/g;
38
35
 
39
- const REGEX_EVENTS = /(?:\s*on[\w-:]+\s*=\s*["'][^"']*["'])/g;
40
-
41
- const REGEX_EXTRA_WHITESPACE = /\s\s+/g;
42
-
43
- const REGEX_SLOT_ATTRIBUTES = /<[\w-]+([^><]*{{\$}}[^><]*)>/g;
44
-
45
36
  const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{{\$}}/g;
46
37
 
47
38
 
@@ -54,27 +45,29 @@ const RENDERABLE_TEMPLATE = Symbol();
54
45
 
55
46
  const SLOT = Symbol();
56
47
 
48
+ const SLOT_CLEANUP = Symbol();
49
+
57
50
  const SLOT_HTML = '<!--$-->';
58
51
 
59
52
  const SLOT_MARKER = '{{$}}';
60
53
 
54
+ const SLOT_MARKER_LENGTH = SLOT_MARKER.length;
55
+
61
56
 
62
57
  export {
63
- ATTRIBUTES,
64
58
  NODE_CLOSING,
65
59
  NODE_ELEMENT,
66
60
  NODE_SLOT,
67
61
  NODE_VOID,
68
62
  NODE_WHITELIST,
69
63
  REGEX_EMPTY_TEXT_NODES,
70
- REGEX_EVENTS,
71
- REGEX_EXTRA_WHITESPACE,
72
- REGEX_SLOT_ATTRIBUTES,
73
64
  REGEX_SLOT_NODES,
74
65
  RENDERABLE,
75
66
  RENDERABLE_REACTIVE,
76
67
  RENDERABLE_TEMPLATE,
77
68
  SLOT,
69
+ SLOT_CLEANUP,
78
70
  SLOT_HTML,
79
- SLOT_MARKER
71
+ SLOT_MARKER,
72
+ SLOT_MARKER_LENGTH
80
73
  };
package/src/event.ts CHANGED
@@ -1,51 +1,31 @@
1
1
  import { root } from '@esportsplus/reactivity';
2
+ import { SLOT_CLEANUP } from './constants';
2
3
  import { Element } from './types';
3
4
  import { addEventListener, defineProperty, parentElement } from './utilities';
4
5
 
5
6
 
6
- let capture = new Set(['blur', 'focus', 'scroll']),
7
+ let capture = new Set<`on${string}`>(['onblur', 'onfocus', 'onscroll']),
8
+ controllers = new Map<
9
+ `on${string}`,
10
+ (AbortController & { listeners: number }) | null
11
+ >(),
7
12
  keys: Record<string, symbol> = {},
8
- passive = new Set([
9
- 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mousewheel',
10
- 'scroll',
11
- 'touchcancel', 'touchend', 'touchleave', 'touchmove', 'touchstart',
12
- 'wheel'
13
+ passive = new Set<`on${string}`>([
14
+ 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel',
15
+ 'onscroll',
16
+ 'ontouchcancel', 'ontouchend', 'ontouchleave', 'ontouchmove', 'ontouchstart',
17
+ 'onwheel'
13
18
  ]);
14
19
 
15
20
 
16
- function register(event: string) {
17
- let key = keys[event] = Symbol(),
18
- type = event.slice(2);
21
+ (['onmousemove', 'onmousewheel', 'onscroll', 'ontouchend', 'ontouchmove', 'ontouchstart', 'onwheel'] as `on${string}`[]).map(event => {
22
+ controllers.set(event, null);
23
+ });
19
24
 
20
- addEventListener.call(window.document, type, (e) => {
21
- let node = e.target as Element | null;
22
25
 
23
- defineProperty(e, 'currentTarget', {
24
- configurable: true,
25
- get() {
26
- return node || window.document;
27
- }
28
- });
29
-
30
- while (node) {
31
- if (key in node) {
32
- return (node[key] as Function).call(node, e);
33
- }
34
-
35
- node = parentElement.call(node);
36
- }
37
- }, {
38
- capture: capture.has(type),
39
- passive: passive.has(type)
40
- });
41
-
42
- return key;
43
- }
44
-
45
-
46
- export default (element: Element, event: string, listener: Function): void => {
26
+ export default (element: Element, event: `on${string}`, listener: Function) => {
47
27
  if (event === 'onmount') {
48
- let interval: ReturnType<typeof setInterval> = setInterval(() => {
28
+ let interval = setInterval(() => {
49
29
  retry--;
50
30
 
51
31
  if (element.isConnected) {
@@ -66,5 +46,64 @@ export default (element: Element, event: string, listener: Function): void => {
66
46
  return root(() => listener(element));
67
47
  }
68
48
 
69
- element[keys[event] || register(event)] = listener;
49
+ let controller = controllers.get(event),
50
+ signal: AbortController['signal'] | undefined;
51
+
52
+ if (controller === null) {
53
+ let { abort, signal } = new AbortController();
54
+
55
+ controllers.set(
56
+ event,
57
+ controller = {
58
+ abort,
59
+ signal,
60
+ listeners: 0,
61
+ }
62
+ );
63
+ }
64
+
65
+ if (controller) {
66
+ controller.listeners++;
67
+
68
+ ( element[SLOT_CLEANUP] ??= [] ).push(() => {
69
+ if (--controller.listeners) {
70
+ return;
71
+ }
72
+
73
+ controller.abort();
74
+ controllers.set(event, null);
75
+ })
76
+ signal = controller.signal;
77
+ }
78
+
79
+ let key = keys[event];
80
+
81
+ if (!key) {
82
+ key = keys[event] = Symbol();
83
+
84
+ addEventListener.call(window.document, event.slice(2), (e) => {
85
+ let node = e.target as Element | null;
86
+
87
+ defineProperty(e, 'currentTarget', {
88
+ configurable: true,
89
+ get() {
90
+ return node || window.document;
91
+ }
92
+ });
93
+
94
+ while (node) {
95
+ if (key in node) {
96
+ return (node[key] as Function).call(node, e);
97
+ }
98
+
99
+ node = parentElement.call(node);
100
+ }
101
+ }, {
102
+ capture: capture.has(event),
103
+ passive: passive.has(event),
104
+ signal
105
+ });
106
+ }
107
+
108
+ element[key] = listener;
70
109
  };
package/src/html/cache.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
- NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_EVENTS, REGEX_EXTRA_WHITESPACE,
3
- REGEX_SLOT_ATTRIBUTES, REGEX_SLOT_NODES, SLOT_HTML, SLOT_MARKER
2
+ NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES,
3
+ REGEX_SLOT_NODES, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH
4
4
  } from '~/constants';
5
5
  import { RenderableTemplate, Template } from '~/types';
6
6
  import { firstChild, firstElementChild, fragment, nextElementSibling, nextSibling } from '~/utilities';
7
- import a from '~/attributes';
7
+ import { spread } from '~/attributes';
8
8
  import s from '~/slot';
9
9
 
10
10
 
@@ -16,9 +16,10 @@ function build(literals: TemplateStringsArray, values: unknown[]) {
16
16
  return set(literals, literals[0]);
17
17
  }
18
18
 
19
- let attributes: Record<string, (null | string)[]> = {},
20
- buffer = '',
21
- html = minify(literals.join(SLOT_MARKER)),
19
+ let buffer = '',
20
+ html = literals.join(SLOT_MARKER)
21
+ .replace(REGEX_EMPTY_TEXT_NODES, '$1$2')
22
+ .trim(),
22
23
  index = 0,
23
24
  level = 0,
24
25
  levels = [{
@@ -26,73 +27,11 @@ function build(literals: TemplateStringsArray, values: unknown[]) {
26
27
  elements: 0,
27
28
  path: [] as NonNullable<Template['slots']>[0]['path']
28
29
  }],
30
+ parsed = html.split(SLOT_MARKER),
29
31
  slot = 0,
30
32
  slots: Template['slots'] = [],
31
33
  total = values.length;
32
34
 
33
- {
34
- let attribute = '',
35
- buffer = '',
36
- char = '',
37
- quote = '';
38
-
39
- for (let match of html.matchAll(REGEX_SLOT_ATTRIBUTES)) {
40
- let found = match[1],
41
- metadata = attributes[found];
42
-
43
- if (metadata) {
44
- continue;
45
- }
46
-
47
- metadata = attributes[found] = [];
48
-
49
- for (let i = 0, n = found.length; i < n; i++) {
50
- char = found[i];
51
-
52
- if (char === ' ') {
53
- buffer = '';
54
- }
55
- else if (char === '=') {
56
- attribute = buffer;
57
- buffer = '';
58
- }
59
- else if (char === '"' || char === "'") {
60
- if (!attribute) {
61
- }
62
- else if (!quote) {
63
- quote = char;
64
- }
65
- else if (quote === char) {
66
- attribute = '';
67
- buffer = '';
68
- quote = '';
69
- }
70
- }
71
- else if (char === '{' && char !== buffer) {
72
- buffer = char;
73
- }
74
- else {
75
- buffer += char;
76
-
77
- if (buffer === SLOT_MARKER) {
78
- buffer = '';
79
-
80
- if (attribute) {
81
- metadata.push(attribute);
82
-
83
- if (!quote) {
84
- attribute = '';
85
- }
86
- }
87
- else {
88
- metadata.push(null);
89
- }
90
- }
91
- }
92
- }
93
- }
94
- }
95
-
96
35
  for (let match of html.matchAll(REGEX_SLOT_NODES)) {
97
36
  let parent = levels[level],
98
37
  type = match[1] === undefined ? NODE_SLOT : (NODE_WHITELIST[match[1].toLowerCase()] || NODE_ELEMENT);
@@ -106,24 +45,18 @@ function build(literals: TemplateStringsArray, values: unknown[]) {
106
45
  let attr = match[2];
107
46
 
108
47
  if (attr) {
109
- let metadata = attributes[attr],
48
+ let i = attr.indexOf(SLOT_MARKER),
110
49
  path = methods(parent.children, parent.path, firstChild, nextSibling);
111
50
 
112
- if (!metadata) {
113
- throw new Error(`Template: attribute metadata could not be found for '${attr}'`);
114
- }
115
-
116
- for (let i = 0, n = metadata.length; i < n; i++) {
117
- let name = metadata[i];
118
-
51
+ while (i !== -1) {
119
52
  slots.push({
120
- fn: name === null ? a.spread : a.set,
121
- name,
53
+ fn: spread,
122
54
  path,
123
55
  slot
124
56
  });
125
57
 
126
- buffer += literals[slot++];
58
+ buffer += parsed[slot++];
59
+ i = attr.indexOf(SLOT_MARKER, i + SLOT_MARKER_LENGTH);
127
60
  }
128
61
  }
129
62
 
@@ -140,17 +73,16 @@ function build(literals: TemplateStringsArray, values: unknown[]) {
140
73
  parent.elements++;
141
74
  }
142
75
  else if (type === NODE_SLOT) {
143
- buffer += literals[slot] + SLOT_HTML;
76
+ buffer += parsed[slot] + SLOT_HTML;
144
77
  slots.push({
145
78
  fn: s,
146
- name: null,
147
79
  path: methods(parent.children, parent.path, firstChild, nextSibling),
148
80
  slot: slot++
149
81
  });
150
82
  }
151
83
 
152
84
  if (slot === total) {
153
- buffer += literals[slot];
85
+ buffer += parsed[slot];
154
86
  break;
155
87
  }
156
88
 
@@ -164,7 +96,7 @@ function build(literals: TemplateStringsArray, values: unknown[]) {
164
96
  index = (match.index || 0) + match[0].length;
165
97
  }
166
98
 
167
- return set(literals, minify(buffer.replace(REGEX_EVENTS, '')), slots);
99
+ return set(literals, buffer, slots);
168
100
  }
169
101
 
170
102
  function methods(children: number, copy: (typeof firstChild)[], first: (typeof firstChild), next: (typeof firstChild)) {
@@ -179,10 +111,6 @@ function methods(children: number, copy: (typeof firstChild)[], first: (typeof f
179
111
  return methods;
180
112
  }
181
113
 
182
- function minify(html: string) {
183
- return html.replace(REGEX_EMPTY_TEXT_NODES, '$1$2').replace(REGEX_EXTRA_WHITESPACE, ' ').trim();
184
- }
185
-
186
114
  function set(literals: TemplateStringsArray, html: string, slots: Template['slots'] = null) {
187
115
  let template = {
188
116
  fragment: fragment(html),
@@ -2,7 +2,7 @@ import { root, ReactiveArray } from '@esportsplus/reactivity';
2
2
  import { Element, Elements, Renderable, RenderableReactive, RenderableTemplate, Template } from '~/types';
3
3
  import { Slot } from '~/slot';
4
4
  import { cloneNode, firstChild, nextSibling } from '~/utilities';
5
- import a from '~/attributes';
5
+ import { apply } from '~/attributes';
6
6
  import cache from './cache';
7
7
 
8
8
 
@@ -41,11 +41,11 @@ function render<T>(renderable: Renderable<T>, template: Template) {
41
41
  values = renderable.values;
42
42
 
43
43
  for (let i = slots.length - 1; i >= 0; i--) {
44
- let { fn, name, path, slot } = slots[i];
44
+ let { fn, path, slot } = slots[i];
45
45
 
46
46
  if (path === previous) {}
47
47
  else {
48
- a.apply(node);
48
+ apply(node);
49
49
 
50
50
  node = fragment;
51
51
  previous = path;
@@ -59,7 +59,9 @@ function render<T>(renderable: Renderable<T>, template: Template) {
59
59
  fn(node, values[slot], name);
60
60
  }
61
61
 
62
- a.apply(node);
62
+ if (node) {
63
+ apply(node);
64
+ }
63
65
  }
64
66
 
65
67
  for (let element = firstChild.call(fragment as Element); element; element = nextSibling.call(element)) {
package/src/slot.ts CHANGED
@@ -1,17 +1,15 @@
1
1
  import { effect, root, DIRTY } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_REACTIVE, SLOT } from './constants';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, SLOT, SLOT_CLEANUP } from './constants';
3
3
  import { hydrate } from './html';
4
4
  import { Element, Elements, RenderableReactive, RenderableTemplate } from './types';
5
5
  import { firstChild, isArray, isObject, nextSibling, nodeValue, raf, text } from './utilities'
6
6
 
7
7
 
8
- // Using a private symbol since 'SLOT' is used as a different flag in 'render.ts'
9
- let cleanup: Slot[] = [],
10
- key = Symbol(),
8
+ let cleanup: VoidFunction[] = [],
11
9
  scheduled = false;
12
10
 
13
11
 
14
- function afterGroups(anchor: Element, groups: Elements[]) {
12
+ function after(anchor: Element, groups: Elements[]) {
15
13
  for (let i = 0, n = groups.length; i < n; i++) {
16
14
  let group = groups[i];
17
15
 
@@ -24,37 +22,15 @@ function afterGroups(anchor: Element, groups: Elements[]) {
24
22
  return groups;
25
23
  }
26
24
 
27
- function removeGroup(group?: Elements) {
28
- if (group === undefined) {
29
- return group;
30
- }
31
-
32
- for (let i = 0, n = group.length; i < n; i++) {
33
- let item = group[i];
34
-
35
- if (key in item) {
36
- cleanup.push(item[key] as Slot);
37
- }
38
-
39
- item.remove();
40
- }
41
-
42
- if (!scheduled && cleanup.length) {
43
- schedule();
44
- }
45
-
46
- return group;
47
- }
48
-
49
- function removeGroups(groups: Elements[]) {
25
+ function remove(...groups: Elements[]) {
50
26
  for (let i = 0, n = groups.length; i < n; i++) {
51
27
  let group = groups[i];
52
28
 
53
29
  for (let j = 0, o = group.length; j < o; j++) {
54
30
  let item = group[j];
55
31
 
56
- if (key in item) {
57
- cleanup.push(item[key] as Slot);
32
+ if (item[SLOT_CLEANUP]) {
33
+ cleanup.push(...item[SLOT_CLEANUP]);
58
34
  }
59
35
 
60
36
  item.remove();
@@ -80,14 +56,14 @@ function render(anchor: Element | null, input: unknown, slot?: Slot): Elements |
80
56
  groups.push( render(null, input[i]) as Elements );
81
57
  }
82
58
 
83
- return anchor ? afterGroups(anchor, groups) : groups;
59
+ return anchor ? after(anchor, groups) : groups;
84
60
  }
85
61
 
86
62
  let nodes: Elements = [];
87
63
 
88
64
  if (RENDERABLE in input) {
89
65
  if (input[RENDERABLE] === RENDERABLE_REACTIVE) {
90
- return afterGroups(anchor!, hydrate.reactive(input as RenderableReactive, slot!));
66
+ return after(anchor!, hydrate.reactive(input as RenderableReactive, slot!));
91
67
  }
92
68
  else {
93
69
  nodes = hydrate.static(input as RenderableTemplate<unknown>);
@@ -135,15 +111,19 @@ function schedule() {
135
111
 
136
112
  raf.add(() => {
137
113
  try {
138
- let slot;
114
+ let fn;
139
115
 
140
- while (slot = cleanup.pop()) {
141
- slot.clear();
116
+ while (fn = cleanup.pop()) {
117
+ fn();
142
118
  }
143
119
  }
144
- catch(e) {}
120
+ catch(e) { }
145
121
 
146
122
  scheduled = false;
123
+
124
+ if (cleanup.length) {
125
+ schedule();
126
+ }
147
127
  });
148
128
  }
149
129
 
@@ -157,7 +137,9 @@ class Slot {
157
137
 
158
138
 
159
139
  constructor(marker: Element) {
160
- marker[key] = this;
140
+ ( marker[SLOT_CLEANUP] ??= [] ).push(() => {
141
+ this.clear();
142
+ });
161
143
 
162
144
  this.marker = marker;
163
145
  this.nodes = [];
@@ -185,16 +167,22 @@ class Slot {
185
167
  }
186
168
 
187
169
  clear() {
188
- removeGroups(this.nodes);
170
+ remove(...this.nodes);
189
171
  this.text = null;
190
172
  }
191
173
 
192
174
  pop() {
193
- return removeGroup(this.nodes.pop());
175
+ let group = this.nodes.pop();
176
+
177
+ if (!group) {
178
+ return undefined;
179
+ }
180
+
181
+ return remove(group);
194
182
  }
195
183
 
196
184
  push(...groups: Elements[]) {
197
- afterGroups(this.anchor(), groups);
185
+ after(this.anchor(), groups);
198
186
 
199
187
  for (let i = 0, n = groups.length; i < n; i++) {
200
188
  this.nodes.push(groups[i]);
@@ -252,17 +240,23 @@ class Slot {
252
240
  }
253
241
 
254
242
  shift() {
255
- return removeGroup(this.nodes.shift());
243
+ let group = this.nodes.shift();
244
+
245
+ if (!group) {
246
+ return undefined;
247
+ }
248
+
249
+ return remove(group);
256
250
  }
257
251
 
258
252
  splice(start: number, stop: number = this.nodes.length, ...groups: Elements[]) {
259
- return removeGroups(
260
- this.nodes.splice(start, stop, ...afterGroups(this.anchor(start), groups))
253
+ return remove(
254
+ ...this.nodes.splice(start, stop, ...after(this.anchor(start), groups))
261
255
  );
262
256
  }
263
257
 
264
258
  unshift(...groups: Elements[]) {
265
- return this.nodes.unshift(...afterGroups(this.marker, groups));
259
+ return this.nodes.unshift(...after(this.marker, groups));
266
260
  }
267
261
  }
268
262