@lightningjs/blits 2.0.0-beta.1 → 2.0.0-beta.4

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 (45) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +2 -0
  3. package/element-test-full.txt +0 -0
  4. package/index.d.ts +106 -13
  5. package/package.json +25 -4
  6. package/scripts/prepublishOnly.js +13 -4
  7. package/scripts/prepush.js +21 -1
  8. package/src/announcer/announcer.js +50 -4
  9. package/src/announcer/speechSynthesis.js +156 -37
  10. package/src/application.js +95 -3
  11. package/src/component/base/methods.js +5 -2
  12. package/src/component/base/router.js +6 -0
  13. package/src/component/base/timeouts_intervals.js +56 -0
  14. package/src/component/setup/routes.js +5 -0
  15. package/src/component/setup/state.js +2 -0
  16. package/src/component.js +64 -24
  17. package/src/components/Circle.js +5 -3
  18. package/src/components/FPScounter.js +1591 -1577
  19. package/src/components/RouterView.js +141 -124
  20. package/src/components/Sprite.js +29 -8
  21. package/src/components/index.js +0 -2
  22. package/src/engines/L3/element.js +101 -7
  23. package/src/engines/L3/launch.js +4 -1
  24. package/src/{focus.js → focus/focus.js} +6 -18
  25. package/src/focus/helpers.js +31 -0
  26. package/src/focus/hover.js +94 -0
  27. package/src/launch.js +1 -0
  28. package/src/lib/codegenerator/generator.js +44 -8
  29. package/src/lib/lifecycle.js +34 -3
  30. package/src/lib/log.js +4 -4
  31. package/src/lib/precompiler/precompiler.js +23 -14
  32. package/src/lib/reactivity/effect.js +3 -2
  33. package/src/lib/reactivityguard/computedprops.js +86 -15
  34. package/src/lib/shaders/shaders.js +2 -1
  35. package/src/lib/symbols.js +2 -0
  36. package/src/plugins/appstate.d.ts +26 -0
  37. package/src/plugins/index.d.ts +28 -0
  38. package/src/plugins/language.d.ts +37 -0
  39. package/src/plugins/storage/storage.d.ts +31 -0
  40. package/src/plugins/theme.d.ts +43 -0
  41. package/src/plugins/theme.js +30 -3
  42. package/src/router/router.js +78 -14
  43. package/test-output.txt +0 -0
  44. package/vite/preCompiler.js +15 -5
  45. package/vscode/data/template-attributes.json +335 -86
@@ -16,141 +16,158 @@
16
16
  */
17
17
 
18
18
  import Component from '../component.js'
19
- import Router from '../router/router.js'
19
+ import Router, { state as routerState } from '../router/router.js'
20
20
  import symbols from '../lib/symbols.js'
21
- import Focus from '../focus.js'
21
+ import Focus from '../focus/focus.js'
22
22
 
23
23
  let hashchangeHandler = null
24
24
 
25
+ /** @typedef {{ $input?: (event: any) => boolean, $focus?: (event: any) => void }} RouterViewParent */
26
+
25
27
  export default () =>
26
- Component('RouterView', {
27
- /* eslint-disable no-unused-vars */
28
- code: {
29
- render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
30
- const elms = []
31
- const validAttributes = [
32
- 'parent',
33
- 'rotation',
34
- 'w',
35
- 'width',
36
- 'h',
37
- 'height',
38
- 'x',
39
- 'y',
40
- 'z',
41
- 'zIndex',
42
- 'color',
43
- 'src',
44
- 'texture',
45
- 'fit',
46
- 'rtt',
47
- 'mount',
48
- 'pivot',
49
- 'scale',
50
- 'show',
51
- 'alpha',
52
- 'rounded',
53
- 'border',
54
- 'shadow',
55
- 'shader',
56
- 'clipping',
57
- 'overflow',
58
- 'font',
59
- 'size',
60
- 'maxwidth',
61
- 'maxheight',
62
- 'maxlines',
63
- 'textoverflow',
64
- 'letterspacing',
65
- 'lineheight',
66
- 'contain',
67
- 'align',
68
- 'content',
69
- 'placement',
70
- 'inspector-data',
71
- ]
72
- const elementConfigs = []
73
- const forloops = []
74
- const props = []
75
- const created = []
76
- const effects = {}
77
- const skips = []
78
- let componentType
79
- let rootComponent = component
80
- let propData
81
- let slotComponent
82
- let inSlot = false
83
- let slotChildCounter = 0
84
- let cmps = []
85
- elementConfigs[0] = {}
28
+ Component(
29
+ 'RouterView',
30
+ /** @type {any} */ ({
31
+ /* eslint-disable no-unused-vars */
32
+ code: {
33
+ render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
34
+ const elms = []
35
+ const validAttributes = [
36
+ 'parent',
37
+ 'rotation',
38
+ 'w',
39
+ 'width',
40
+ 'h',
41
+ 'height',
42
+ 'x',
43
+ 'y',
44
+ 'z',
45
+ 'zIndex',
46
+ 'color',
47
+ 'src',
48
+ 'texture',
49
+ 'fit',
50
+ 'rtt',
51
+ 'mount',
52
+ 'pivot',
53
+ 'scale',
54
+ 'show',
55
+ 'alpha',
56
+ 'rounded',
57
+ 'border',
58
+ 'shadow',
59
+ 'shader',
60
+ 'clipping',
61
+ 'overflow',
62
+ 'font',
63
+ 'size',
64
+ 'maxwidth',
65
+ 'maxheight',
66
+ 'maxlines',
67
+ 'textoverflow',
68
+ 'letterspacing',
69
+ 'lineheight',
70
+ 'contain',
71
+ 'align',
72
+ 'content',
73
+ 'placement',
74
+ 'inspector-data',
75
+ 'holder',
76
+ ]
77
+ const elementConfigs = []
78
+ const forloops = []
79
+ const props = []
80
+ const created = []
81
+ const effects = {}
82
+ const skips = []
83
+ let componentType
84
+ let rootComponent = component
85
+ let propData
86
+ let slotComponent
87
+ let inSlot = false
88
+ let slotChildCounter = 0
89
+ let cmps = []
90
+ elementConfigs[0] = {}
91
+ elementConfigs[0]['___wrapper'] = true
86
92
 
87
- elms[0] = this.element(
88
- { parent: parent || 'root' },
89
- inSlot === true ? slotComponent : component
90
- )
93
+ elms[0] = this.element(
94
+ { parent: parent || 'root' },
95
+ inSlot === true ? slotComponent : component
96
+ )
91
97
 
92
- elementConfigs[0]['w'] = parent.node.w * (100 / 100)
93
- elementConfigs[0]['h'] = parent.node.h * (100 / 100)
94
- elms[0].populate(elementConfigs[0])
98
+ elementConfigs[0]['w'] = parent.node.w * (100 / 100)
99
+ elementConfigs[0]['height'] = parent.node.h * (100 / 100)
100
+ elms[0].populate(elementConfigs[0])
95
101
 
96
- if (inSlot === true) {
97
- slotChildCounter -= 1
98
- }
102
+ if (inSlot === true) {
103
+ slotChildCounter -= 1
104
+ }
99
105
 
100
- return {
101
- elms,
102
- cleanup: () => {
103
- rootComponent = null
104
- propData = null
105
- slotComponent = null
106
- parent = null
107
- component = null
108
- cmps.length = 0
109
- elms.length = 0
110
- components.length = 0
111
- elementConfigs.length = 0
112
- forloops.length = 0
113
- props.length = 0
114
- skips.length = 0
115
- },
116
- skips,
117
- }
106
+ return {
107
+ elms,
108
+ cleanup: () => {
109
+ rootComponent = null
110
+ propData = null
111
+ slotComponent = null
112
+ parent = null
113
+ component = null
114
+ cmps.length = 0
115
+ elms.length = 0
116
+ components.length = 0
117
+ elementConfigs.length = 0
118
+ forloops.length = 0
119
+ props.length = 0
120
+ skips.length = 0
121
+ },
122
+ skips,
123
+ }
124
+ },
125
+ effects: [],
126
+ context: {},
118
127
  },
119
- effects: [],
120
- context: {},
121
- },
122
- state() {
123
- return {
124
- activeView: null,
125
- }
126
- },
127
- hooks: {
128
- async ready() {
129
- if (
130
- this[symbols.parent][symbols.routerHooks] &&
131
- this[symbols.parent][symbols.routerHooks].init
132
- ) {
133
- await this[symbols.parent][symbols.routerHooks].init.apply(this[symbols.parent])
128
+ state() {
129
+ return {
130
+ activeView: null,
134
131
  }
135
- hashchangeHandler = () => Router.navigate.apply(this)
136
- Router.navigate.apply(this)
137
- window.addEventListener('hashchange', hashchangeHandler)
138
- },
139
- destroy() {
140
- window.removeEventListener('hashchange', hashchangeHandler, false)
141
132
  },
142
- focus() {
143
- if (this.activeView && Focus.get() === this) {
144
- this.activeView.$focus()
145
- }
133
+ hooks: {
134
+ async ready() {
135
+ const parent = this[symbols.parent]
136
+ if (parent && parent[symbols.routerHooks] && parent[symbols.routerHooks].init) {
137
+ await parent[symbols.routerHooks].init.apply(parent)
138
+ }
139
+
140
+ hashchangeHandler = () => Router.navigate.apply(this)
141
+ Router.navigate.apply(this)
142
+ window.addEventListener('hashchange', hashchangeHandler)
143
+ },
144
+ destroy() {
145
+ window.removeEventListener('hashchange', hashchangeHandler, false)
146
+ },
147
+ focus() {
148
+ if (this.activeView && Focus.get() === this) {
149
+ this.activeView.$focus()
150
+ }
151
+ },
146
152
  },
147
- },
148
- input: {
149
- back(e) {
150
- const navigating = Router.back.call(this)
151
- if (navigating === false) {
152
- this[symbols.parent].$focus(e)
153
- }
153
+ input: {
154
+ back(e) {
155
+ const parent = this[symbols.parent]
156
+
157
+ if (routerState.backNavigation === false) {
158
+ if (parent && parent.$input) {
159
+ parent.$input(e)
160
+ }
161
+ return
162
+ }
163
+
164
+ const navigating = Router.back.call(this)
165
+ if (navigating === false) {
166
+ if (parent && parent.$focus) {
167
+ parent.$focus(e)
168
+ }
169
+ }
170
+ },
154
171
  },
155
- },
156
- })
172
+ })
173
+ )
@@ -65,6 +65,7 @@ export default () =>
65
65
  'content',
66
66
  'placement',
67
67
  'inspector-data',
68
+ 'holder',
68
69
  ]
69
70
  const elementConfigs = []
70
71
  const forloops = []
@@ -80,6 +81,7 @@ export default () =>
80
81
  let slotChildCounter = 0
81
82
  let cmps = []
82
83
  elementConfigs[0] = {}
84
+ elementConfigs[0]['___wrapper'] = true
83
85
 
84
86
  elms[0] = this.element(
85
87
  { parent: parent || 'root' },
@@ -90,7 +92,9 @@ export default () =>
90
92
  elementConfigs[0]['h'] = parent.node.h * (100 / 100)
91
93
  elementConfigs[0]['texture'] = component.texture
92
94
  elementConfigs[0]['color'] = component.color
93
- elementConfigs[0]['effects'] = component.effects
95
+ elementConfigs[0]['rounded'] = component.rounded
96
+ elementConfigs[0]['border'] = component.border
97
+ elementConfigs[0]['shadow'] = component.shadow
94
98
  elms[0].populate(elementConfigs[0])
95
99
 
96
100
  if (inSlot === true) {
@@ -123,6 +127,15 @@ export default () =>
123
127
  function anonymous(component, elms, context, components, rootComponent, skips, effect) {
124
128
  elms[0].set('color', component.color)
125
129
  },
130
+ function anonymous(component, elms, context, components, rootComponent, skips, effect) {
131
+ elms[0].set('rounded', component.rounded)
132
+ },
133
+ function anonymous(component, elms, context, components, rootComponent, skips, effect) {
134
+ elms[0].set('border', component.border)
135
+ },
136
+ function anonymous(component, elms, context, components, rootComponent, skips, effect) {
137
+ elms[0].set('shadow', component.shadow)
138
+ },
126
139
  ],
127
140
  context: {},
128
141
  },
@@ -131,6 +144,9 @@ export default () =>
131
144
  map: undefined,
132
145
  frame: undefined,
133
146
  color: undefined,
147
+ rounded: undefined,
148
+ border: undefined,
149
+ shadow: undefined,
134
150
  },
135
151
  state() {
136
152
  return {
@@ -167,15 +183,20 @@ export default () =>
167
183
  // Resolve frame data from sprite map
168
184
  let options = null
169
185
  if (
170
- this.map !== undefined &&
171
186
  this.map !== null &&
172
- this.frame !== undefined &&
173
- this.frame !== null
187
+ this.map !== undefined &&
188
+ this.frame !== null &&
189
+ this.frame !== undefined
174
190
  ) {
175
- options =
176
- 'frames' in this.map
177
- ? Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
178
- : this.map[this.frame]
191
+ if (
192
+ this.map.frames !== null &&
193
+ this.map.frames !== undefined &&
194
+ this.frame in this.map.frames
195
+ ) {
196
+ options = Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
197
+ } else if (this.frame in this.map) {
198
+ options = this.map[this.frame]
199
+ }
179
200
  }
180
201
 
181
202
  // If no map but frame is object (manual subtexture)
@@ -18,11 +18,9 @@
18
18
  import Circle from './Circle.js'
19
19
  import RouterView from './RouterView.js'
20
20
  import Sprite from './Sprite.js'
21
- import FPScounter from './FPScounter.js'
22
21
 
23
22
  export default () => ({
24
23
  Circle: Circle(),
25
24
  RouterView: RouterView(),
26
25
  Sprite: Sprite(),
27
- FPScounter: FPScounter(),
28
26
  })
@@ -24,6 +24,8 @@ import symbols from '../../lib/symbols.js'
24
24
  import Settings from '../../settings.js'
25
25
  import shaders from '../../lib/shaders/shaders.js'
26
26
 
27
+ const holderComponentMap = new WeakMap()
28
+
27
29
  /**
28
30
  * Creates a padding object from a value and direction.
29
31
  * @param {number|object|string|undefined} padding - The padding value.
@@ -210,15 +212,29 @@ const propsTransformer = {
210
212
  this.props['rotation'] = v * (Math.PI / 180)
211
213
  },
212
214
  set w(v) {
213
- if (v < 0) v = 0
214
- this.props['w'] = parsePercentage.call(this, v, 'w')
215
+ const parsed = parsePercentage.call(this, v, 'w')
216
+ this.props['w'] = parsed
217
+ if (
218
+ this.___wrapper === true &&
219
+ this.element.component.eol !== true &&
220
+ this.element.component[symbols.holder] !== undefined
221
+ ) {
222
+ this.element.component[symbols.holder].set('w', parsed)
223
+ }
215
224
  },
216
225
  set width(v) {
217
226
  this.w = v
218
227
  },
219
228
  set h(v) {
220
- if (v < 0) v = 0
221
- this.props['h'] = parsePercentage.call(this, v, 'h')
229
+ const parsed = parsePercentage.call(this, v, 'h')
230
+ this.props['h'] = parsed
231
+ if (
232
+ this.___wrapper === true &&
233
+ this.element.component.eol !== true &&
234
+ this.element.component[symbols.holder] !== undefined
235
+ ) {
236
+ this.element.component[symbols.holder].set('h', parsed)
237
+ }
222
238
  },
223
239
  set height(v) {
224
240
  this.h = v
@@ -509,6 +525,9 @@ const propsTransformer = {
509
525
  this.props['data'] = v
510
526
  }
511
527
  },
528
+ set holder(v) {
529
+ this.props['interactive'] = v
530
+ },
512
531
  }
513
532
 
514
533
  export const elementAttributes = Object.keys(propsTransformer)
@@ -564,6 +583,10 @@ const Element = {
564
583
  ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
565
584
  : renderer.createNode(this.props.props)
566
585
 
586
+ if (this.props['holder'] === true) {
587
+ holderComponentMap.set(this.node, this.component)
588
+ }
589
+
567
590
  if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
568
591
  this.node.on('loaded', (el, { type, dimensions }) => {
569
592
  props['@loaded']({ w: dimensions.w, h: dimensions.h, type }, this)
@@ -587,6 +610,39 @@ const Element = {
587
610
  })
588
611
  }
589
612
  },
613
+ /**
614
+ * Sets framework-provided inspector metadata
615
+ * Only sets if inspector is enabled and in dev mode
616
+ * @param {Object} data - Framework inspector metadata to merge
617
+ */
618
+ setInspectorMetadata(data) {
619
+ // Early return if inspector not enabled (performance optimization)
620
+ if (inspectorEnabled !== true) {
621
+ return
622
+ }
623
+
624
+ // Early return if element is destroyed (props.props is null)
625
+ if (this.props.props === undefined || this.props.props === null) {
626
+ return
627
+ }
628
+
629
+ // Initialize data object if it doesn't exist
630
+ if (this.props['data'] === undefined) {
631
+ this.props['data'] = {}
632
+ }
633
+ if (this.props.props['data'] === undefined) {
634
+ this.props.props['data'] = {}
635
+ }
636
+
637
+ // Merge framework data (blits-* keys data attributes)
638
+ Object.assign(this.props['data'], data)
639
+ Object.assign(this.props.props['data'], data)
640
+
641
+ // Sync to renderer node so inspector can see it
642
+ if (this.node !== undefined && this.node !== null) {
643
+ this.node.data = { ...this.props.props['data'] }
644
+ }
645
+ },
590
646
  /**
591
647
  * Set an individual property on the node
592
648
  *
@@ -632,13 +688,31 @@ const Element = {
632
688
  }
633
689
  },
634
690
  animate(prop, value, transition) {
691
+ // Clear any existing debounce timeout for this property
692
+ if (this.debounceTimeouts[prop] !== undefined) {
693
+ clearTimeout(this.debounceTimeouts[prop])
694
+ Log.debug(`Cleared debounce timeout for property "${prop}"`)
695
+ }
696
+
697
+ // Debounce the animation execution
698
+ this.debounceTimeouts[prop] = setTimeout(() => {
699
+ delete this.debounceTimeouts[prop]
700
+ this._executeAnimation(prop, value, transition)
701
+ }, 0)
702
+ },
703
+ _executeAnimation(prop, value, transition) {
635
704
  // check if a transition is already scheduled to run on the same prop
636
705
  // and cancels it if it does
706
+ const stateOfAnimation =
707
+ this.scheduledTransitions[prop] !== undefined
708
+ ? this.scheduledTransitions[prop].f.state
709
+ : undefined
710
+
637
711
  if (
638
- this.scheduledTransitions[prop] !== undefined &&
639
- this.scheduledTransitions[prop].f.state === 'scheduled'
712
+ stateOfAnimation !== undefined &&
713
+ (stateOfAnimation === 'scheduled' || stateOfAnimation === 'running')
640
714
  ) {
641
- this.scheduledTransitions[prop].f.stop()
715
+ this.scheduledTransitions[prop].f.stop(stateOfAnimation === 'running' ? false : true)
642
716
  }
643
717
 
644
718
  // if current value is the same as the value to animate to, instantly resolve
@@ -674,6 +748,11 @@ const Element = {
674
748
  f,
675
749
  }
676
750
 
751
+ // Update inspector metadata when transition starts
752
+ if (inspectorEnabled === true) {
753
+ this.setInspectorMetadata({ 'blits-isTransitioning': true })
754
+ }
755
+
677
756
  if (transition.start !== undefined && typeof transition.start === 'function') {
678
757
  // fire transition start callback when animation really starts (depending on specified delay)
679
758
  f.once('animating', () => {
@@ -708,6 +787,12 @@ const Element = {
708
787
  }
709
788
  // remove the prop from scheduled transitions
710
789
  delete this.scheduledTransitions[prop]
790
+ // Update inspector metadata when transition ends
791
+ if (inspectorEnabled === true) {
792
+ this.setInspectorMetadata({
793
+ 'blits-isTransitioning': Object.keys(this.scheduledTransitions).length > 0,
794
+ })
795
+ }
711
796
  })
712
797
 
713
798
  // start animation
@@ -717,6 +802,14 @@ const Element = {
717
802
  if (this.node === null) return
718
803
 
719
804
  Log.debug('Deleting Node', this.nodeId)
805
+
806
+ // Clear all pending debounce timeouts
807
+ const debounceProps = Object.keys(this.debounceTimeouts)
808
+ for (let i = 0; i < debounceProps.length; i++) {
809
+ clearTimeout(this.debounceTimeouts[debounceProps[i]])
810
+ }
811
+ this.debounceTimeouts = null
812
+
720
813
  // Clearing transition end callback functions
721
814
  const transitionProps = Object.keys(this.scheduledTransitions)
722
815
  for (let i = 0; i < transitionProps.length; i++) {
@@ -799,6 +892,7 @@ export default (config, component) => {
799
892
  return Object.assign(Object.create(Element), {
800
893
  props: Object.assign(Object.create(propsTransformer), { props: {} }),
801
894
  scheduledTransitions: {},
895
+ debounceTimeouts: {},
802
896
  config,
803
897
  component,
804
898
  })
@@ -100,6 +100,7 @@ export default (App, target, settings = {}) => {
100
100
  textureMemory: textureMemorySettings(settings),
101
101
  createImageBitmapSupport: 'auto',
102
102
  targetFPS: 'maxFPS' in settings ? settings.maxFPS : 0,
103
+ platform: 'platform' in settings ? settings.platform : null,
103
104
  },
104
105
  ...(settings.advanced || {}),
105
106
  },
@@ -118,7 +119,9 @@ export default (App, target, settings = {}) => {
118
119
 
119
120
  shaderLoader()
120
121
  fontLoader()
121
- initApp()
122
+ // Defer app initialization until after the current call stack is cleared,
123
+ // allowing the renderer to finish setting up before the app starts creating components
124
+ Promise.resolve().then(initApp)
122
125
 
123
126
  return renderer
124
127
  }
@@ -15,11 +15,12 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import symbols from './lib/symbols.js'
19
- import { state } from './router/router.js'
20
- import Settings from './settings.js'
21
- import { DEFAULT_HOLD_TIMEOUT_MS } from './constants.js'
22
- import { Log } from './lib/log.js'
18
+ import symbols from '../lib/symbols.js'
19
+ import { state } from '../router/router.js'
20
+ import Settings from '../settings.js'
21
+ import { DEFAULT_HOLD_TIMEOUT_MS } from '../constants.js'
22
+ import { Log } from '../lib/log.js'
23
+ import { getAncestors } from './helpers.js'
23
24
 
24
25
  let focusedComponent = null
25
26
  let focusChain = []
@@ -99,19 +100,6 @@ export default {
99
100
  },
100
101
  }
101
102
 
102
- /**
103
- * Recursive function that retrieves the ancestors of a component
104
- * @param {Array} components
105
- * @returns array components
106
- */
107
- const getAncestors = (components) => {
108
- if (components[0][symbols.parent] !== undefined) {
109
- components.unshift(components[0][symbols.parent])
110
- return getAncestors(components)
111
- }
112
- return components
113
- }
114
-
115
103
  /**
116
104
  * Get the component in the ancestors chain that has a handler for a certain key code
117
105
  * @param {Object} component
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright 2025 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ import symbols from '../lib/symbols.js'
19
+
20
+ /**
21
+ * Recursive function that retrieves the ancestors of a component
22
+ * @param {Array} components
23
+ * @returns array components
24
+ */
25
+ export const getAncestors = (components) => {
26
+ if (components[0][symbols.parent] !== undefined) {
27
+ components.unshift(components[0][symbols.parent])
28
+ return getAncestors(components)
29
+ }
30
+ return components
31
+ }