@lightningjs/blits 1.30.2 → 1.31.1

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 (152) hide show
  1. package/CHANGELOG.md +1035 -1019
  2. package/LICENSE +202 -202
  3. package/NOTICE +12 -12
  4. package/README.md +33 -33
  5. package/bin/index.js +8 -8
  6. package/blits.d.ts +6 -6
  7. package/index.d.ts +1099 -1099
  8. package/index.js +28 -28
  9. package/package.json +57 -56
  10. package/public/assets/fps_sprite.base64.js +1 -1
  11. package/scripts/postpublish.js +59 -59
  12. package/scripts/prepublishOnly.js +84 -84
  13. package/scripts/prepush.js +78 -78
  14. package/src/announcer/announcer.js +162 -162
  15. package/src/announcer/speechSynthesis.js +107 -107
  16. package/src/application.js +100 -100
  17. package/src/component/base/announcer.js +27 -27
  18. package/src/component/base/events.js +69 -69
  19. package/src/component/base/index.js +33 -33
  20. package/src/component/base/methods.js +244 -244
  21. package/src/component/base/router.js +41 -41
  22. package/src/component/base/timeouts_intervals.js +99 -99
  23. package/src/component/base/utils.js +72 -72
  24. package/src/component/setup/computed.js +48 -48
  25. package/src/component/setup/index.js +62 -62
  26. package/src/component/setup/input.js +30 -30
  27. package/src/component/setup/methods.js +37 -37
  28. package/src/component/setup/props.js +62 -62
  29. package/src/component/setup/routes.js +42 -42
  30. package/src/component/setup/state.js +59 -59
  31. package/src/component/setup/watch.js +35 -35
  32. package/src/component.js +437 -437
  33. package/src/components/Circle.js +73 -73
  34. package/src/components/FPScounter.js +1510 -1510
  35. package/src/components/RouterView.js +81 -81
  36. package/src/components/Sprite.js +94 -94
  37. package/src/components/index.js +28 -28
  38. package/src/constants.js +21 -21
  39. package/src/engine.js +1 -1
  40. package/src/engines/L3/element.js +744 -744
  41. package/src/engines/L3/fontLoader.js +38 -38
  42. package/src/engines/L3/index.js +7 -7
  43. package/src/engines/L3/launch.js +123 -123
  44. package/src/engines/L3/shaderLoader.js +12 -12
  45. package/src/engines/index.js +1 -1
  46. package/src/focus.js +90 -90
  47. package/src/helpers/deepEqualArray.js +50 -50
  48. package/src/helpers/fetchJson.js +34 -34
  49. package/src/helpers/sequence.js +26 -26
  50. package/src/launch.js +123 -123
  51. package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
  52. package/src/lib/codegenerator/generator.js +788 -788
  53. package/src/lib/colors/colors.js +92 -92
  54. package/src/lib/colors/htmlColors.js +168 -168
  55. package/src/lib/componentId.js +49 -49
  56. package/src/lib/eventListeners.js +123 -123
  57. package/src/lib/hooks.js +59 -59
  58. package/src/lib/lifecycle.js +85 -85
  59. package/src/lib/log.js +117 -117
  60. package/src/lib/precompiler/precompiler.js +73 -73
  61. package/src/lib/reactivity/effect.js +93 -93
  62. package/src/lib/reactivity/reactive.js +208 -208
  63. package/src/lib/reactivityguard/computedprops.js +218 -218
  64. package/src/lib/symbols.js +108 -108
  65. package/src/lib/templateparser/parser.js +386 -386
  66. package/src/lib/utils.js +27 -27
  67. package/src/plugin.js +27 -27
  68. package/src/plugins/appstate.js +26 -26
  69. package/src/plugins/index.js +20 -20
  70. package/src/plugins/language.js +152 -152
  71. package/src/plugins/log.js +25 -25
  72. package/src/plugins/theme.js +94 -94
  73. package/src/router/router.js +516 -514
  74. package/src/router/transitions/fadeIn.js +27 -27
  75. package/src/router/transitions/fadeInOut.js +33 -33
  76. package/src/router/transitions/index.js +21 -21
  77. package/src/router/transitions/slideIn.js +28 -28
  78. package/src/router/transitions/slideInOut.js +33 -33
  79. package/src/settings.js +40 -40
  80. package/vite/blitsFileConverter.js +67 -67
  81. package/vite/index.js +28 -28
  82. package/vite/msdfGenerator.js +269 -269
  83. package/vite/preCompiler.js +47 -42
  84. package/vite/reactivityGuard.js +45 -45
  85. package/vscode/data/template-attributes.json +589 -589
  86. package/.editorconfig +0 -7
  87. package/.eslintignore +0 -1
  88. package/.eslintrc.cjs +0 -65
  89. package/.github/workflows/comment-lint-results.yml +0 -125
  90. package/.github/workflows/comment-test-results.yml +0 -109
  91. package/.github/workflows/deploy-docs.yml +0 -27
  92. package/.github/workflows/run-blits-tests.yml +0 -142
  93. package/.github/workflows/run-lint.yml +0 -128
  94. package/.husky/pre-commit +0 -1
  95. package/.husky/pre-push +0 -1
  96. package/.vscode/extensions.json +0 -7
  97. package/.vscode/settings.json +0 -9
  98. package/COPYING +0 -1
  99. package/banner.png +0 -0
  100. package/docs/.nojekyll +0 -0
  101. package/docs/README.md +0 -19
  102. package/docs/_sidebar.md +0 -37
  103. package/docs/built-in/directives.md +0 -50
  104. package/docs/built-in/for-loop.md +0 -204
  105. package/docs/built-in/layout.md +0 -168
  106. package/docs/components/component_state.md +0 -90
  107. package/docs/components/computed_properties.md +0 -45
  108. package/docs/components/lifecycle_events.md +0 -42
  109. package/docs/components/methods.md +0 -45
  110. package/docs/components/props.md +0 -56
  111. package/docs/components/user_input.md +0 -186
  112. package/docs/components/utility_methods.md +0 -320
  113. package/docs/components/watchers.md +0 -34
  114. package/docs/essentials/application_root.md +0 -19
  115. package/docs/essentials/components.md +0 -68
  116. package/docs/essentials/displaying_images.md +0 -67
  117. package/docs/essentials/displaying_text.md +0 -135
  118. package/docs/essentials/element_attributes.md +0 -195
  119. package/docs/essentials/settings.md +0 -117
  120. package/docs/essentials/template_syntax.md +0 -83
  121. package/docs/getting_started/file_structure.md +0 -58
  122. package/docs/getting_started/getting_started.md +0 -45
  123. package/docs/getting_started/intro.md +0 -20
  124. package/docs/index.html +0 -47
  125. package/docs/performance/lazy-loading.md +0 -161
  126. package/docs/plugins/global_app_state.md +0 -98
  127. package/docs/plugins/language.md +0 -172
  128. package/docs/plugins/text-to-speech-announcer.md +0 -120
  129. package/docs/plugins/theme.md +0 -176
  130. package/docs/router/basics.md +0 -175
  131. package/docs/router/hooks.md +0 -70
  132. package/docs/router/transitions.md +0 -146
  133. package/docs/sidebar.json +0 -171
  134. package/docs/transitions_animations/transitions.md +0 -150
  135. package/jsconfig.json +0 -12
  136. package/src/component/setup/props.test.js +0 -238
  137. package/src/component.test.js +0 -388
  138. package/src/components/Circle.orig.js +0 -31
  139. package/src/components/FPScounter.orig.js +0 -121
  140. package/src/components/RouterView.orig.js +0 -54
  141. package/src/components/Sprite.orig.js +0 -58
  142. package/src/components/index.orig.js +0 -28
  143. package/src/engines/L3/element.test.js +0 -579
  144. package/src/helpers/deepEqualArray.test.js +0 -127
  145. package/src/lib/codegenerator/generator.test.js +0 -3801
  146. package/src/lib/colors/colors.test.js +0 -290
  147. package/src/lib/componentId.test.js +0 -84
  148. package/src/lib/eventListeners.test.js +0 -236
  149. package/src/lib/reactivity/effect.test.js +0 -35
  150. package/src/lib/reactivity/reactive.test.js +0 -27
  151. package/src/lib/templateparser/parser.test.js +0 -1411
  152. package/src/router/router.test.js +0 -473
@@ -1,744 +1,744 @@
1
- /*
2
- * Copyright 2023 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 { renderer } from './launch.js'
19
- import colors from '../../lib/colors/colors.js'
20
-
21
- import { Log } from '../../lib/log.js'
22
- import symbols from '../../lib/symbols.js'
23
- import Settings from '../../settings.js'
24
-
25
- // temporary counter to work around shader caching issues
26
- let counter = 0
27
-
28
- /**
29
- * Creates a padding object from a value and direction.
30
- * @param {number|object|string|undefined} padding - The padding value.
31
- * @param {string} direction - The layout direction ('vertical' or 'horizontal').
32
- * @returns {{start: number, end: number, oppositeStart: number, oppositeEnd: number}} The padding object.
33
- */
34
- const createPaddingObject = (padding, direction) => {
35
- if (padding === undefined) {
36
- return { start: 0, end: 0, oppositeStart: 0, oppositeEnd: 0 }
37
- }
38
-
39
- if (typeof padding === 'number') {
40
- return { start: padding, end: padding, oppositeStart: padding, oppositeEnd: padding }
41
- }
42
-
43
- // todo: do we need to do this runtime every time? or can we optimize this?
44
- if (isObjectString(padding) === true) {
45
- padding = parseToObject(padding)
46
- }
47
-
48
- if (typeof padding === 'object') {
49
- const {
50
- top = undefined,
51
- right = undefined,
52
- bottom = undefined,
53
- left = undefined,
54
- x = 0,
55
- y = 0,
56
- } = padding
57
-
58
- // use specific values if provided, otherwise fall back to x or y
59
- return direction === 'vertical'
60
- ? {
61
- start: top !== undefined ? top : y,
62
- end: bottom !== undefined ? bottom : y,
63
- oppositeStart: left !== undefined ? left : x,
64
- oppositeEnd: right !== undefined ? right : x,
65
- }
66
- : {
67
- start: left !== undefined ? left : x,
68
- end: right !== undefined ? right : x,
69
- oppositeStart: top !== undefined ? top : y,
70
- oppositeEnd: bottom !== undefined ? bottom : y,
71
- }
72
- }
73
- return { start: 0, end: 0, oppositeStart: 0, oppositeEnd: 0 }
74
- }
75
-
76
- /**
77
- * Layout function for arranging children in a layout container.
78
- * @param {object} config - The layout configuration object.
79
- * @this {import('../../component.js').BlitsElement}
80
- */
81
- const layoutFn = function (config) {
82
- const position = config.direction === 'vertical' ? 'y' : 'x'
83
- const oppositePosition = config.direction === 'vertical' ? 'x' : 'y'
84
- const oppositeMount = config.direction === 'vertical' ? 'mountX' : 'mountY'
85
- const dimension = config.direction === 'vertical' ? 'height' : 'width'
86
- const oppositeDimension = config.direction === 'vertical' ? 'width' : 'height'
87
- const padding = createPaddingObject(config.padding, config.direction)
88
-
89
- let offset = padding.start
90
-
91
- const children = this.node.children
92
- const childrenLength = children.length
93
- let otherDimension = 0
94
- const gap = config.gap || 0
95
- for (let i = 0; i < childrenLength; i++) {
96
- if (this.children[i] !== undefined && this.children[i].props.raw.show === false) {
97
- continue
98
- }
99
- const node = children[i]
100
- node[position] = offset
101
- node[oppositePosition] = padding.oppositeStart
102
- // todo: temporary text check, due to 1px width of empty text node
103
- if (dimension === 'width') {
104
- offset += node.width + (node.width !== ('text' in node ? 1 : 0) ? gap : 0)
105
- } else {
106
- offset +=
107
- 'text' in node
108
- ? node.width > 1
109
- ? node.height + gap
110
- : 0
111
- : node.height !== 0
112
- ? node.height + gap
113
- : 0
114
- }
115
- otherDimension = Math.max(
116
- otherDimension,
117
- node[oppositeDimension] + padding.oppositeStart + padding.oppositeEnd
118
- )
119
- }
120
- // adjust the size of the layout container
121
- this.node[dimension] = offset - gap + padding.end
122
- this.node[oppositeDimension] = otherDimension
123
-
124
- const align = {
125
- start: 0,
126
- end: 1,
127
- center: 0.5,
128
- }[config['align-items'] || 'start']
129
-
130
- if (align !== 0) {
131
- for (let i = 0; i < childrenLength; i++) {
132
- const node = children[i]
133
- node[oppositePosition] = otherDimension * align
134
- node[oppositeMount] = align
135
- }
136
- }
137
-
138
- // emit an updated event
139
- if (config['@updated'] !== undefined) {
140
- config['@updated']({ w: this.node.width, h: this.node.height }, this)
141
- }
142
-
143
- // trigger layout on parent if parent is a layout
144
- if (this.config.parent && this.config.parent.props.__layout === true) {
145
- this.config.parent.triggerLayout(this.config.parent.props)
146
- }
147
- }
148
-
149
- /**
150
- * Checks if a value is a transition object.
151
- * @param {any} value - The value to check.
152
- * @returns {boolean} True if the value is a transition object, false otherwise.
153
- */
154
- const isTransition = (value) => {
155
- return value !== null && typeof value === 'object' && 'transition' in value === true
156
- }
157
-
158
- /**
159
- * Checks if a string is an object string (starts and ends with curly braces).
160
- * @param {string} str - The string to check.
161
- * @returns {boolean} True if the string is an object string, false otherwise.
162
- */
163
- const isObjectString = (str) => {
164
- return typeof str === 'string' && str.startsWith('{') && str.endsWith('}')
165
- }
166
-
167
- /**
168
- * Parses a string into an object, converting single quotes to double and adding quotes to keys.
169
- * @param {string} str - The string to parse.
170
- * @returns {object} The parsed object.
171
- */
172
- const parseToObject = (str) => {
173
- return JSON.parse(str.replace(/'/g, '"').replace(/([\w-_]+)\s*:/g, '"$1":'))
174
- }
175
-
176
- /**
177
- * Parses a percentage string or returns the value as-is.
178
- * @param {string|number} v - The value to parse (may be a percentage string).
179
- * @param {string} base - The base property name ('width' or 'height').
180
- * @returns {number|string} The parsed value as a number or the original value.
181
- * @this {{ element: { config: { parent?: { node?: Record<string, number> } } } }}
182
- */
183
- const parsePercentage = function (v, base) {
184
- if (typeof v !== 'string') {
185
- return v
186
- } else if (v.indexOf('%') === v.length - 1) {
187
- return (
188
- (this.element.config.parent &&
189
- (this.element.config.parent.node[base] || 0) * (parseFloat(v) / 100)) ||
190
- 0
191
- )
192
- }
193
- return v
194
- }
195
-
196
- /**
197
- * Unpacks a transition value from an object or returns the value as-is.
198
- * @param {any} v - The value to unpack.
199
- * @returns {any} The unpacked value.
200
- */
201
- const unpackTransition = (v) => {
202
- if (typeof v !== 'object' || v === null) return v
203
- if (v.constructor === Object) {
204
- if ('value' in v === true) {
205
- return v.value
206
- }
207
- if ('transition' in v === true) {
208
- return unpackTransition(v.transition)
209
- }
210
- }
211
- return v
212
- }
213
-
214
- const colorMap = {
215
- top: 'colorTop',
216
- bottom: 'colorBottom',
217
- left: 'colorLeft',
218
- right: 'colorRight',
219
- }
220
-
221
- /**
222
- * Default text settings for text nodes (initialized on first use).
223
- * @type {object|null}
224
- */
225
- let textDefaults = null
226
-
227
- /**
228
- * @typedef {import('../../component.js').BlitsElement} BlitsElement
229
- *
230
- * This is always a BlitsElement
231
- * @this {BlitsElement} this
232
- */
233
- const propsTransformer = {
234
- set parent(v) {
235
- this.props['parent'] = v === 'root' ? renderer.root : v.node
236
- },
237
- set rotation(v) {
238
- this.props['rotation'] = v * (Math.PI / 180)
239
- },
240
- set w(v) {
241
- this.props['width'] = parsePercentage.call(this, v, 'width')
242
- },
243
- set width(v) {
244
- this.props['width'] = parsePercentage.call(this, v, 'width')
245
- },
246
- set h(v) {
247
- this.props['height'] = parsePercentage.call(this, v, 'height')
248
- },
249
- set height(v) {
250
- this.props['height'] = parsePercentage.call(this, v, 'height')
251
- },
252
- set x(v) {
253
- this.props['x'] = parsePercentage.call(this, v, 'width')
254
- },
255
- set y(v) {
256
- this.props['y'] = parsePercentage.call(this, v, 'height')
257
- },
258
- set z(v) {
259
- this.props['zIndex'] = v
260
- },
261
- set zIndex(v) {
262
- this.props['zIndex'] = v
263
- },
264
- set color(v) {
265
- if (typeof v === 'string' && v.startsWith('{') === false) {
266
- this.props['color'] = colors.normalize(v)
267
- } else if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
268
- this.props['color'] = 0
269
- Object.entries(v).forEach((color) => {
270
- this.props[colorMap[color[0]]] = colors.normalize(color[1])
271
- })
272
- }
273
- },
274
- set src(v) {
275
- this.props['src'] = v
276
- if (this.raw['color'] === undefined) {
277
- this.props['color'] = this.props['src'] ? 0xffffffff : 0x00000000
278
- }
279
- // apply auto sizing when no width or height specified
280
- if (!('w' in this.raw) && !('w' in this.raw) && !('h' in this.raw) && !('height' in this.raw)) {
281
- this.props['autosize'] = true
282
- }
283
- },
284
- set texture(v) {
285
- this.props['texture'] = v
286
-
287
- if (this.raw['color'] === undefined && (v === null || v === undefined)) {
288
- this.props['color'] = 0x00000000
289
- } else if (this.raw['color'] === undefined) {
290
- this.props['color'] = 0xffffffff
291
- }
292
- },
293
- set fit(v) {
294
- const resizeMode = {}
295
-
296
- if (v === 'cover' || v === 'contain') {
297
- this.props['textureOptions'] = { resizeMode: { type: v } }
298
- return
299
- }
300
-
301
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
302
- resizeMode['type'] = v.type || 'cover'
303
-
304
- if (typeof v.position === 'number') {
305
- resizeMode['clipY'] = resizeMode['clipX'] = v.position
306
- }
307
-
308
- if (typeof v.position === 'object') {
309
- resizeMode['clipX'] = 'x' in v.position === true ? v.position.x : null
310
- resizeMode['clipY'] = 'y' in v.position === true ? v.position.y : null
311
- }
312
- this.props['textureOptions'] = { resizeMode }
313
- }
314
- },
315
- set rtt(v) {
316
- this.props['rtt'] = v
317
- if (this.raw['color'] === undefined) {
318
- this.props['color'] = v === true ? 0xffffffff : 0x00000000
319
- }
320
- },
321
- set mount(v) {
322
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
323
- if ('x' in v === true) {
324
- this.props['mountX'] = v.x
325
- }
326
- if ('y' in v === true) {
327
- this.props['mountY'] = v.y
328
- }
329
- } else {
330
- this.props['mountX'] = v
331
- this.props['mountY'] = v
332
- }
333
- },
334
- set pivot(v) {
335
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
336
- if ('x' in v === true) {
337
- this.props['pivotX'] = v.x
338
- }
339
- if ('y' in v === true) {
340
- this.props['pivotY'] = v.y
341
- }
342
- } else {
343
- this.props['pivotX'] = v
344
- this.props['pivotY'] = v
345
- }
346
- },
347
- set scale(v) {
348
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
349
- if ('x' in v === true) {
350
- this.props['scaleX'] = v.x
351
- }
352
- if ('y' in v === true) {
353
- this.props['scaleY'] = v.y
354
- }
355
- } else {
356
- this.props['scale'] = v
357
- }
358
- },
359
- set show(v) {
360
- if (v) {
361
- this.props['alpha'] = this.raw['alpha'] !== undefined ? this.raw['alpha'] : 1
362
- } else {
363
- this.props['alpha'] = 0
364
- }
365
- },
366
- set alpha(v) {
367
- if (this.raw['show'] === undefined || this.raw['show'] == true) {
368
- this.props['alpha'] = v
369
- }
370
- },
371
- set shader(v) {
372
- if (v !== null) {
373
- this.props['shader'] = renderer.createShader(v.type, v.props)
374
- } else {
375
- this.props['shader'] = renderer.createShader('DefaultShader')
376
- }
377
- },
378
- set effects(v) {
379
- for (let i = 0; i < v.length; i++) {
380
- if (v[i].props && v[i].props.color) {
381
- v[i].props.color = colors.normalize(v[i].props.color)
382
- }
383
- }
384
- if (this.element.node === undefined) {
385
- this.props['shader'] = renderer.createShader('DynamicShader', {
386
- effects: v.map((effect) => {
387
- return renderer.createEffect(effect.type, effect.props)
388
- }),
389
- })
390
- }
391
- },
392
- set clipping(v) {
393
- this.props['clipping'] = v
394
- },
395
- set overflow(v) {
396
- this.props['clipping'] = !!!v
397
- },
398
- set font(v) {
399
- this.props['fontFamily'] = v
400
- },
401
- set size(v) {
402
- this.props['fontSize'] = v
403
- },
404
- set wordwrap(v) {
405
- Log.warn('The wordwrap attribute is deprecated, use maxwidth instead')
406
- this.props['width'] = v
407
- this.props['contain'] = 'width'
408
- },
409
- set maxwidth(v) {
410
- this.props['width'] = v
411
- this.props['contain'] = 'width'
412
- },
413
- set maxheight(v) {
414
- this.props['height'] = v
415
- this.props['contain'] = 'both'
416
- },
417
- set contain(v) {
418
- this.props['contain'] = v
419
- },
420
- set maxlines(v) {
421
- this.props['maxLines'] = v
422
- },
423
- set textoverflow(v) {
424
- this.props['overflowSuffix'] = v === false ? ' ' : v === true ? undefined : v
425
- },
426
- set letterspacing(v) {
427
- this.props['letterSpacing'] = v || 1
428
- },
429
- set lineheight(v) {
430
- this.props['lineHeight'] = v
431
- },
432
- set align(v) {
433
- this.props['textAlign'] = v
434
- },
435
- set content(v) {
436
- this.props['text'] = '' + v
437
- },
438
- set placement(v) {
439
- let x, y
440
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
441
- if ('x' in v === true) {
442
- x = v.x
443
- }
444
- if ('y' in v === true) {
445
- y = v.y
446
- }
447
- } else {
448
- v === 'center' || v === 'right' ? (x = v) : (y = v)
449
- }
450
-
451
- // Set X position
452
- if (x === 'center') {
453
- this.x = '50%'
454
- this.props['mountX'] = 0.5
455
- } else if (x === 'right') {
456
- this.x = '100%'
457
- this.props['mountX'] = 1
458
- }
459
-
460
- // Set Y position
461
- if (y === 'middle') {
462
- this.y = '50%'
463
- this.props['mountY'] = 0.5
464
- } else if (y === 'bottom') {
465
- this.y = '100%'
466
- this.props['mountY'] = 1
467
- }
468
- },
469
- set 'inspector-data'(v) {
470
- if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
471
- this.props['data'] = v
472
- }
473
- },
474
- }
475
-
476
- const Element = {
477
- /**
478
- * Populates the element with data
479
- * @param {import('../../component.js').BlitsElementProps} data
480
- */
481
- populate(data) {
482
- const props = data
483
- props['node'] = this.config.node
484
-
485
- if (props[symbols.isSlot] === true) {
486
- this[symbols.isSlot] = true
487
- }
488
-
489
- this.props.element = this
490
-
491
- this.props['parent'] = props['parent'] || this.config.parent
492
- //@ts-ignore This might be a left over from the old code?
493
- delete props.parent
494
-
495
- this.props.raw = data
496
-
497
- const propKeys = Object.keys(props)
498
- const length = propKeys.length
499
-
500
- for (let i = 0; i < length; i++) {
501
- const key = propKeys[i]
502
- const value = props[key]
503
- if (value !== undefined) {
504
- this.props[key] = unpackTransition(value)
505
- }
506
- }
507
-
508
- // correct for default white nodes (but not for text nodes)
509
- if (this.props.props['color'] === undefined && '__textnode' in props === false) {
510
- this.props.props['color'] = 0
511
- }
512
-
513
- this.node = props.__textnode
514
- ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
515
- : renderer.createNode(this.props.props)
516
-
517
- if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
518
- this.node.on('loaded', (el, { type, dimensions }) => {
519
- props['@loaded']({ w: dimensions.width, h: dimensions.height, type }, this)
520
- })
521
- }
522
-
523
- if (props['@error'] !== undefined && typeof props['@error'] === 'function') {
524
- this.node.on('failed', (el, error) => {
525
- props['@error'](error, this)
526
- })
527
- }
528
-
529
- if (props.__layout === true) {
530
- this.triggerLayout = layoutFn.bind(this)
531
- }
532
-
533
- if (this.config.parent.props !== undefined && this.config.parent.props.__layout === true) {
534
- this.config.parent.triggerLayout(this.config.parent.props)
535
- this.node.on('loaded', () => {
536
- this.config.parent.triggerLayout(this.config.parent.props)
537
- })
538
- }
539
- },
540
- /**
541
- * Set an individual property on the node
542
- *
543
- * @this {import('../../component').BlitsElement} this
544
- *
545
- * @param {import('../..//component.js').BlitsElementProps} prop
546
- * @param {any} value
547
- * @returns {void}
548
- */
549
- set(prop, value) {
550
- if (value === undefined) return
551
- // @ts-ignore
552
- if (this.props.raw[prop] === value) return
553
- // @ts-ignore
554
- this.props.raw[prop] = value
555
-
556
- this.props.props = {}
557
- // @ts-ignore
558
- this.props[prop] = unpackTransition(value)
559
-
560
- const propsKeys = Object.keys(this.props.props)
561
-
562
- if (propsKeys.length === 1) {
563
- if (isTransition(value) === true) {
564
- return this.animate(propsKeys[0], this.props.props[propsKeys[0]], value.transition)
565
- }
566
- // set the prop to the value on the node
567
- this.node[propsKeys[0]] = this.props.props[propsKeys[0]]
568
- } else {
569
- for (let i = 0; i < propsKeys.length; i++) {
570
- // todo: fix code duplication
571
- if (isTransition(value) === true) {
572
- return this.animate(propsKeys[i], this.props.props[propsKeys[i]], value.transition)
573
- }
574
- // set the prop to the value on the node
575
- this.node[propsKeys[i]] = this.props.props[propsKeys[i]]
576
- }
577
- }
578
-
579
- if (this.config.parent.props && this.config.parent.props.__layout === true) {
580
- this.config.parent.triggerLayout(this.config.parent.props)
581
- }
582
- },
583
- animate(prop, value, transition) {
584
- // check if a transition is already scheduled to run on the same prop
585
- // and cancels it if it does
586
- if (
587
- this.scheduledTransitions[prop] !== undefined &&
588
- this.scheduledTransitions[prop].f.state === 'scheduled'
589
- ) {
590
- this.scheduledTransitions[prop].f.stop()
591
- }
592
-
593
- // if current value is the same as the value to animate to, instantly resolve
594
- if (this.node[prop] === value) return
595
-
596
- const props = {}
597
- props[prop] = value
598
-
599
- // construct animate function
600
- const f = this.node.animate(props, {
601
- duration:
602
- typeof transition === 'object'
603
- ? 'duration' in transition
604
- ? transition.duration
605
- : 300
606
- : 300,
607
- easing:
608
- typeof transition === 'object'
609
- ? 'easing' in transition
610
- ? transition.easing
611
- : 'ease'
612
- : 'ease',
613
- delay: typeof transition === 'object' ? ('delay' in transition ? transition.delay : 0) : 0,
614
- })
615
-
616
- // capture the current value to be used in the transition start
617
- const startValue = this.node[prop]
618
-
619
- // schedule the transition for this prop, so it can be canceled /
620
- // removed if another transition for the same prop starts in the mean time
621
- this.scheduledTransitions[prop] = {
622
- v: props[prop],
623
- f,
624
- }
625
-
626
- if (transition.start !== undefined && typeof transition.start === 'function') {
627
- // fire transition start callback when animation really starts (depending on specified delay)
628
- f.once('animating', () => {
629
- transition.start.call(this.component, this, prop, startValue)
630
- })
631
- }
632
-
633
- if (this.config.parent.props && this.config.parent.props.__layout === true) {
634
- f.on('tick', () => {
635
- this.config.parent.triggerLayout(this.config.parent.props)
636
- })
637
- }
638
-
639
- if (transition.progress !== undefined && typeof transition.progress === 'function') {
640
- let prevProgress = 0
641
- f.on('tick', (_node, { progress }) => {
642
- transition.progress.call(this.component, this, prop, progress, prevProgress)
643
- prevProgress = progress
644
- })
645
- }
646
-
647
- f.once('stopped', () => {
648
- if (
649
- this.scheduledTransitions[prop] !== undefined &&
650
- this.scheduledTransitions[prop].canceled === true
651
- ) {
652
- return
653
- }
654
- // fire transition end callback when animation ends (if specified)
655
- if (this.node !== undefined && transition.end && typeof transition.end === 'function') {
656
- transition.end.call(this.component, this, prop, this.node[prop])
657
- }
658
- // remove the prop from scheduled transitions
659
- delete this.scheduledTransitions[prop]
660
- })
661
-
662
- // start animation
663
- f.start()
664
- },
665
- destroy() {
666
- if (this.node === null) return
667
-
668
- Log.debug('Deleting Node', this.nodeId)
669
- // Clearing transition end callback functions
670
- const transitionProps = Object.keys(this.scheduledTransitions)
671
- for (let i = 0; i < transitionProps.length; i++) {
672
- const transition = this.scheduledTransitions[transitionProps[i]]
673
- if (transition !== undefined) {
674
- transition.canceled = true
675
- if (transition.f !== undefined) transition.f.stop()
676
- }
677
- }
678
-
679
- // not setting to null and deleting,
680
- // because transition stopped might still be fired (maybe a renderer fix resolves that)
681
- this.scheduledTransitions = {}
682
-
683
- this.component = null
684
- delete this.component
685
-
686
- this.config = null
687
- delete this.config
688
-
689
- this.counter = null
690
- delete this.counter
691
-
692
- this.props.raw = {}
693
- this.props.element = null
694
- this.props.props = null
695
- this.props = {}
696
- delete this.props
697
-
698
- this.triggerLayout = null
699
- delete this.triggerLayout
700
-
701
- this.forComponent = null
702
- delete this.forComponent
703
-
704
- this.node.destroy()
705
- this.node = null
706
- },
707
- get nodeId() {
708
- return this.node && this.node.id
709
- },
710
- get ref() {
711
- return this.props.ref || null
712
- },
713
- get parent() {
714
- return this.node && this.node.parent
715
- },
716
- get children() {
717
- return this.component[symbols.getChildren]().filter((child) => {
718
- return child.parent === (this[symbols.isSlot] ? this.node.children[0] : this.node)
719
- })
720
- },
721
- }
722
-
723
- /**
724
- * Returns a new Blits Element
725
- *
726
- * @param {Object} config - The configuration object for the element
727
- * @param {import('../../component.js').BlitsComponent} component - The component to which the element belongs
728
- * @returns {import('../../component.js').BlitsElement} - The new Blits Element
729
- */
730
- export default (config, component) => {
731
- if (textDefaults === null) {
732
- textDefaults = {
733
- fontSize: 32,
734
- fontFamily: Settings.get('defaultFont', 'sans-serif'),
735
- }
736
- }
737
- return Object.assign(Object.create(Element), {
738
- props: Object.assign(Object.create(propsTransformer), { props: {} }),
739
- scheduledTransitions: {},
740
- config,
741
- component,
742
- counter: counter++,
743
- })
744
- }
1
+ /*
2
+ * Copyright 2023 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 { renderer } from './launch.js'
19
+ import colors from '../../lib/colors/colors.js'
20
+
21
+ import { Log } from '../../lib/log.js'
22
+ import symbols from '../../lib/symbols.js'
23
+ import Settings from '../../settings.js'
24
+
25
+ // temporary counter to work around shader caching issues
26
+ let counter = 0
27
+
28
+ /**
29
+ * Creates a padding object from a value and direction.
30
+ * @param {number|object|string|undefined} padding - The padding value.
31
+ * @param {string} direction - The layout direction ('vertical' or 'horizontal').
32
+ * @returns {{start: number, end: number, oppositeStart: number, oppositeEnd: number}} The padding object.
33
+ */
34
+ const createPaddingObject = (padding, direction) => {
35
+ if (padding === undefined) {
36
+ return { start: 0, end: 0, oppositeStart: 0, oppositeEnd: 0 }
37
+ }
38
+
39
+ if (typeof padding === 'number') {
40
+ return { start: padding, end: padding, oppositeStart: padding, oppositeEnd: padding }
41
+ }
42
+
43
+ // todo: do we need to do this runtime every time? or can we optimize this?
44
+ if (isObjectString(padding) === true) {
45
+ padding = parseToObject(padding)
46
+ }
47
+
48
+ if (typeof padding === 'object') {
49
+ const {
50
+ top = undefined,
51
+ right = undefined,
52
+ bottom = undefined,
53
+ left = undefined,
54
+ x = 0,
55
+ y = 0,
56
+ } = padding
57
+
58
+ // use specific values if provided, otherwise fall back to x or y
59
+ return direction === 'vertical'
60
+ ? {
61
+ start: top !== undefined ? top : y,
62
+ end: bottom !== undefined ? bottom : y,
63
+ oppositeStart: left !== undefined ? left : x,
64
+ oppositeEnd: right !== undefined ? right : x,
65
+ }
66
+ : {
67
+ start: left !== undefined ? left : x,
68
+ end: right !== undefined ? right : x,
69
+ oppositeStart: top !== undefined ? top : y,
70
+ oppositeEnd: bottom !== undefined ? bottom : y,
71
+ }
72
+ }
73
+ return { start: 0, end: 0, oppositeStart: 0, oppositeEnd: 0 }
74
+ }
75
+
76
+ /**
77
+ * Layout function for arranging children in a layout container.
78
+ * @param {object} config - The layout configuration object.
79
+ * @this {import('../../component.js').BlitsElement}
80
+ */
81
+ const layoutFn = function (config) {
82
+ const position = config.direction === 'vertical' ? 'y' : 'x'
83
+ const oppositePosition = config.direction === 'vertical' ? 'x' : 'y'
84
+ const oppositeMount = config.direction === 'vertical' ? 'mountX' : 'mountY'
85
+ const dimension = config.direction === 'vertical' ? 'height' : 'width'
86
+ const oppositeDimension = config.direction === 'vertical' ? 'width' : 'height'
87
+ const padding = createPaddingObject(config.padding, config.direction)
88
+
89
+ let offset = padding.start
90
+
91
+ const children = this.node.children
92
+ const childrenLength = children.length
93
+ let otherDimension = 0
94
+ const gap = config.gap || 0
95
+ for (let i = 0; i < childrenLength; i++) {
96
+ if (this.children[i] !== undefined && this.children[i].props.raw.show === false) {
97
+ continue
98
+ }
99
+ const node = children[i]
100
+ node[position] = offset
101
+ node[oppositePosition] = padding.oppositeStart
102
+ // todo: temporary text check, due to 1px width of empty text node
103
+ if (dimension === 'width') {
104
+ offset += node.width + (node.width !== ('text' in node ? 1 : 0) ? gap : 0)
105
+ } else {
106
+ offset +=
107
+ 'text' in node
108
+ ? node.width > 1
109
+ ? node.height + gap
110
+ : 0
111
+ : node.height !== 0
112
+ ? node.height + gap
113
+ : 0
114
+ }
115
+ otherDimension = Math.max(
116
+ otherDimension,
117
+ node[oppositeDimension] + padding.oppositeStart + padding.oppositeEnd
118
+ )
119
+ }
120
+ // adjust the size of the layout container
121
+ this.node[dimension] = offset - gap + padding.end
122
+ this.node[oppositeDimension] = otherDimension
123
+
124
+ const align = {
125
+ start: 0,
126
+ end: 1,
127
+ center: 0.5,
128
+ }[config['align-items'] || 'start']
129
+
130
+ if (align !== 0) {
131
+ for (let i = 0; i < childrenLength; i++) {
132
+ const node = children[i]
133
+ node[oppositePosition] = otherDimension * align
134
+ node[oppositeMount] = align
135
+ }
136
+ }
137
+
138
+ // emit an updated event
139
+ if (config['@updated'] !== undefined) {
140
+ config['@updated']({ w: this.node.width, h: this.node.height }, this)
141
+ }
142
+
143
+ // trigger layout on parent if parent is a layout
144
+ if (this.config.parent && this.config.parent.props.__layout === true) {
145
+ this.config.parent.triggerLayout(this.config.parent.props)
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Checks if a value is a transition object.
151
+ * @param {any} value - The value to check.
152
+ * @returns {boolean} True if the value is a transition object, false otherwise.
153
+ */
154
+ const isTransition = (value) => {
155
+ return value !== null && typeof value === 'object' && 'transition' in value === true
156
+ }
157
+
158
+ /**
159
+ * Checks if a string is an object string (starts and ends with curly braces).
160
+ * @param {string} str - The string to check.
161
+ * @returns {boolean} True if the string is an object string, false otherwise.
162
+ */
163
+ const isObjectString = (str) => {
164
+ return typeof str === 'string' && str.startsWith('{') && str.endsWith('}')
165
+ }
166
+
167
+ /**
168
+ * Parses a string into an object, converting single quotes to double and adding quotes to keys.
169
+ * @param {string} str - The string to parse.
170
+ * @returns {object} The parsed object.
171
+ */
172
+ const parseToObject = (str) => {
173
+ return JSON.parse(str.replace(/'/g, '"').replace(/([\w-_]+)\s*:/g, '"$1":'))
174
+ }
175
+
176
+ /**
177
+ * Parses a percentage string or returns the value as-is.
178
+ * @param {string|number} v - The value to parse (may be a percentage string).
179
+ * @param {string} base - The base property name ('width' or 'height').
180
+ * @returns {number|string} The parsed value as a number or the original value.
181
+ * @this {{ element: { config: { parent?: { node?: Record<string, number> } } } }}
182
+ */
183
+ const parsePercentage = function (v, base) {
184
+ if (typeof v !== 'string') {
185
+ return v
186
+ } else if (v.indexOf('%') === v.length - 1) {
187
+ return (
188
+ (this.element.config.parent &&
189
+ (this.element.config.parent.node[base] || 0) * (parseFloat(v) / 100)) ||
190
+ 0
191
+ )
192
+ }
193
+ return v
194
+ }
195
+
196
+ /**
197
+ * Unpacks a transition value from an object or returns the value as-is.
198
+ * @param {any} v - The value to unpack.
199
+ * @returns {any} The unpacked value.
200
+ */
201
+ const unpackTransition = (v) => {
202
+ if (typeof v !== 'object' || v === null) return v
203
+ if (v.constructor === Object) {
204
+ if ('value' in v === true) {
205
+ return v.value
206
+ }
207
+ if ('transition' in v === true) {
208
+ return unpackTransition(v.transition)
209
+ }
210
+ }
211
+ return v
212
+ }
213
+
214
+ const colorMap = {
215
+ top: 'colorTop',
216
+ bottom: 'colorBottom',
217
+ left: 'colorLeft',
218
+ right: 'colorRight',
219
+ }
220
+
221
+ /**
222
+ * Default text settings for text nodes (initialized on first use).
223
+ * @type {object|null}
224
+ */
225
+ let textDefaults = null
226
+
227
+ /**
228
+ * @typedef {import('../../component.js').BlitsElement} BlitsElement
229
+ *
230
+ * This is always a BlitsElement
231
+ * @this {BlitsElement} this
232
+ */
233
+ const propsTransformer = {
234
+ set parent(v) {
235
+ this.props['parent'] = v === 'root' ? renderer.root : v.node
236
+ },
237
+ set rotation(v) {
238
+ this.props['rotation'] = v * (Math.PI / 180)
239
+ },
240
+ set w(v) {
241
+ this.props['width'] = parsePercentage.call(this, v, 'width')
242
+ },
243
+ set width(v) {
244
+ this.props['width'] = parsePercentage.call(this, v, 'width')
245
+ },
246
+ set h(v) {
247
+ this.props['height'] = parsePercentage.call(this, v, 'height')
248
+ },
249
+ set height(v) {
250
+ this.props['height'] = parsePercentage.call(this, v, 'height')
251
+ },
252
+ set x(v) {
253
+ this.props['x'] = parsePercentage.call(this, v, 'width')
254
+ },
255
+ set y(v) {
256
+ this.props['y'] = parsePercentage.call(this, v, 'height')
257
+ },
258
+ set z(v) {
259
+ this.props['zIndex'] = v
260
+ },
261
+ set zIndex(v) {
262
+ this.props['zIndex'] = v
263
+ },
264
+ set color(v) {
265
+ if (typeof v === 'string' && v.startsWith('{') === false) {
266
+ this.props['color'] = colors.normalize(v)
267
+ } else if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
268
+ this.props['color'] = 0
269
+ Object.entries(v).forEach((color) => {
270
+ this.props[colorMap[color[0]]] = colors.normalize(color[1])
271
+ })
272
+ }
273
+ },
274
+ set src(v) {
275
+ this.props['src'] = v
276
+ if (this.raw['color'] === undefined) {
277
+ this.props['color'] = this.props['src'] ? 0xffffffff : 0x00000000
278
+ }
279
+ // apply auto sizing when no width or height specified
280
+ if (!('w' in this.raw) && !('w' in this.raw) && !('h' in this.raw) && !('height' in this.raw)) {
281
+ this.props['autosize'] = true
282
+ }
283
+ },
284
+ set texture(v) {
285
+ this.props['texture'] = v
286
+
287
+ if (this.raw['color'] === undefined && (v === null || v === undefined)) {
288
+ this.props['color'] = 0x00000000
289
+ } else if (this.raw['color'] === undefined) {
290
+ this.props['color'] = 0xffffffff
291
+ }
292
+ },
293
+ set fit(v) {
294
+ const resizeMode = {}
295
+
296
+ if (v === 'cover' || v === 'contain') {
297
+ this.props['textureOptions'] = { resizeMode: { type: v } }
298
+ return
299
+ }
300
+
301
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
302
+ resizeMode['type'] = v.type || 'cover'
303
+
304
+ if (typeof v.position === 'number') {
305
+ resizeMode['clipY'] = resizeMode['clipX'] = v.position
306
+ }
307
+
308
+ if (typeof v.position === 'object') {
309
+ resizeMode['clipX'] = 'x' in v.position === true ? v.position.x : null
310
+ resizeMode['clipY'] = 'y' in v.position === true ? v.position.y : null
311
+ }
312
+ this.props['textureOptions'] = { resizeMode }
313
+ }
314
+ },
315
+ set rtt(v) {
316
+ this.props['rtt'] = v
317
+ if (this.raw['color'] === undefined) {
318
+ this.props['color'] = v === true ? 0xffffffff : 0x00000000
319
+ }
320
+ },
321
+ set mount(v) {
322
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
323
+ if ('x' in v === true) {
324
+ this.props['mountX'] = v.x
325
+ }
326
+ if ('y' in v === true) {
327
+ this.props['mountY'] = v.y
328
+ }
329
+ } else {
330
+ this.props['mountX'] = v
331
+ this.props['mountY'] = v
332
+ }
333
+ },
334
+ set pivot(v) {
335
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
336
+ if ('x' in v === true) {
337
+ this.props['pivotX'] = v.x
338
+ }
339
+ if ('y' in v === true) {
340
+ this.props['pivotY'] = v.y
341
+ }
342
+ } else {
343
+ this.props['pivotX'] = v
344
+ this.props['pivotY'] = v
345
+ }
346
+ },
347
+ set scale(v) {
348
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
349
+ if ('x' in v === true) {
350
+ this.props['scaleX'] = v.x
351
+ }
352
+ if ('y' in v === true) {
353
+ this.props['scaleY'] = v.y
354
+ }
355
+ } else {
356
+ this.props['scale'] = v
357
+ }
358
+ },
359
+ set show(v) {
360
+ if (v) {
361
+ this.props['alpha'] = this.raw['alpha'] !== undefined ? this.raw['alpha'] : 1
362
+ } else {
363
+ this.props['alpha'] = 0
364
+ }
365
+ },
366
+ set alpha(v) {
367
+ if (this.raw['show'] === undefined || this.raw['show'] == true) {
368
+ this.props['alpha'] = v
369
+ }
370
+ },
371
+ set shader(v) {
372
+ if (v !== null) {
373
+ this.props['shader'] = renderer.createShader(v.type, v.props)
374
+ } else {
375
+ this.props['shader'] = renderer.createShader('DefaultShader')
376
+ }
377
+ },
378
+ set effects(v) {
379
+ for (let i = 0; i < v.length; i++) {
380
+ if (v[i].props && v[i].props.color) {
381
+ v[i].props.color = colors.normalize(v[i].props.color)
382
+ }
383
+ }
384
+ if (this.element.node === undefined) {
385
+ this.props['shader'] = renderer.createShader('DynamicShader', {
386
+ effects: v.map((effect) => {
387
+ return renderer.createEffect(effect.type, effect.props)
388
+ }),
389
+ })
390
+ }
391
+ },
392
+ set clipping(v) {
393
+ this.props['clipping'] = v
394
+ },
395
+ set overflow(v) {
396
+ this.props['clipping'] = !!!v
397
+ },
398
+ set font(v) {
399
+ this.props['fontFamily'] = v
400
+ },
401
+ set size(v) {
402
+ this.props['fontSize'] = v
403
+ },
404
+ set wordwrap(v) {
405
+ Log.warn('The wordwrap attribute is deprecated, use maxwidth instead')
406
+ this.props['width'] = v
407
+ this.props['contain'] = 'width'
408
+ },
409
+ set maxwidth(v) {
410
+ this.props['width'] = v
411
+ this.props['contain'] = 'width'
412
+ },
413
+ set maxheight(v) {
414
+ this.props['height'] = v
415
+ this.props['contain'] = 'both'
416
+ },
417
+ set contain(v) {
418
+ this.props['contain'] = v
419
+ },
420
+ set maxlines(v) {
421
+ this.props['maxLines'] = v
422
+ },
423
+ set textoverflow(v) {
424
+ this.props['overflowSuffix'] = v === false ? ' ' : v === true ? undefined : v
425
+ },
426
+ set letterspacing(v) {
427
+ this.props['letterSpacing'] = v || 1
428
+ },
429
+ set lineheight(v) {
430
+ this.props['lineHeight'] = v
431
+ },
432
+ set align(v) {
433
+ this.props['textAlign'] = v
434
+ },
435
+ set content(v) {
436
+ this.props['text'] = '' + v
437
+ },
438
+ set placement(v) {
439
+ let x, y
440
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
441
+ if ('x' in v === true) {
442
+ x = v.x
443
+ }
444
+ if ('y' in v === true) {
445
+ y = v.y
446
+ }
447
+ } else {
448
+ v === 'center' || v === 'right' ? (x = v) : (y = v)
449
+ }
450
+
451
+ // Set X position
452
+ if (x === 'center') {
453
+ this.x = '50%'
454
+ this.props['mountX'] = 0.5
455
+ } else if (x === 'right') {
456
+ this.x = '100%'
457
+ this.props['mountX'] = 1
458
+ }
459
+
460
+ // Set Y position
461
+ if (y === 'middle') {
462
+ this.y = '50%'
463
+ this.props['mountY'] = 0.5
464
+ } else if (y === 'bottom') {
465
+ this.y = '100%'
466
+ this.props['mountY'] = 1
467
+ }
468
+ },
469
+ set 'inspector-data'(v) {
470
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
471
+ this.props['data'] = v
472
+ }
473
+ },
474
+ }
475
+
476
+ const Element = {
477
+ /**
478
+ * Populates the element with data
479
+ * @param {import('../../component.js').BlitsElementProps} data
480
+ */
481
+ populate(data) {
482
+ const props = data
483
+ props['node'] = this.config.node
484
+
485
+ if (props[symbols.isSlot] === true) {
486
+ this[symbols.isSlot] = true
487
+ }
488
+
489
+ this.props.element = this
490
+
491
+ this.props['parent'] = props['parent'] || this.config.parent
492
+ //@ts-ignore This might be a left over from the old code?
493
+ delete props.parent
494
+
495
+ this.props.raw = data
496
+
497
+ const propKeys = Object.keys(props)
498
+ const length = propKeys.length
499
+
500
+ for (let i = 0; i < length; i++) {
501
+ const key = propKeys[i]
502
+ const value = props[key]
503
+ if (value !== undefined) {
504
+ this.props[key] = unpackTransition(value)
505
+ }
506
+ }
507
+
508
+ // correct for default white nodes (but not for text nodes)
509
+ if (this.props.props['color'] === undefined && '__textnode' in props === false) {
510
+ this.props.props['color'] = 0
511
+ }
512
+
513
+ this.node = props.__textnode
514
+ ? renderer.createTextNode({ ...textDefaults, ...this.props.props })
515
+ : renderer.createNode(this.props.props)
516
+
517
+ if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
518
+ this.node.on('loaded', (el, { type, dimensions }) => {
519
+ props['@loaded']({ w: dimensions.width, h: dimensions.height, type }, this)
520
+ })
521
+ }
522
+
523
+ if (props['@error'] !== undefined && typeof props['@error'] === 'function') {
524
+ this.node.on('failed', (el, error) => {
525
+ props['@error'](error, this)
526
+ })
527
+ }
528
+
529
+ if (props.__layout === true) {
530
+ this.triggerLayout = layoutFn.bind(this)
531
+ }
532
+
533
+ if (this.config.parent.props !== undefined && this.config.parent.props.__layout === true) {
534
+ this.config.parent.triggerLayout(this.config.parent.props)
535
+ this.node.on('loaded', () => {
536
+ this.config.parent.triggerLayout(this.config.parent.props)
537
+ })
538
+ }
539
+ },
540
+ /**
541
+ * Set an individual property on the node
542
+ *
543
+ * @this {import('../../component').BlitsElement} this
544
+ *
545
+ * @param {import('../..//component.js').BlitsElementProps} prop
546
+ * @param {any} value
547
+ * @returns {void}
548
+ */
549
+ set(prop, value) {
550
+ if (value === undefined) return
551
+ // @ts-ignore
552
+ if (this.props.raw[prop] === value) return
553
+ // @ts-ignore
554
+ this.props.raw[prop] = value
555
+
556
+ this.props.props = {}
557
+ // @ts-ignore
558
+ this.props[prop] = unpackTransition(value)
559
+
560
+ const propsKeys = Object.keys(this.props.props)
561
+
562
+ if (propsKeys.length === 1) {
563
+ if (isTransition(value) === true) {
564
+ return this.animate(propsKeys[0], this.props.props[propsKeys[0]], value.transition)
565
+ }
566
+ // set the prop to the value on the node
567
+ this.node[propsKeys[0]] = this.props.props[propsKeys[0]]
568
+ } else {
569
+ for (let i = 0; i < propsKeys.length; i++) {
570
+ // todo: fix code duplication
571
+ if (isTransition(value) === true) {
572
+ return this.animate(propsKeys[i], this.props.props[propsKeys[i]], value.transition)
573
+ }
574
+ // set the prop to the value on the node
575
+ this.node[propsKeys[i]] = this.props.props[propsKeys[i]]
576
+ }
577
+ }
578
+
579
+ if (this.config.parent.props && this.config.parent.props.__layout === true) {
580
+ this.config.parent.triggerLayout(this.config.parent.props)
581
+ }
582
+ },
583
+ animate(prop, value, transition) {
584
+ // check if a transition is already scheduled to run on the same prop
585
+ // and cancels it if it does
586
+ if (
587
+ this.scheduledTransitions[prop] !== undefined &&
588
+ this.scheduledTransitions[prop].f.state === 'scheduled'
589
+ ) {
590
+ this.scheduledTransitions[prop].f.stop()
591
+ }
592
+
593
+ // if current value is the same as the value to animate to, instantly resolve
594
+ if (this.node[prop] === value) return
595
+
596
+ const props = {}
597
+ props[prop] = value
598
+
599
+ // construct animate function
600
+ const f = this.node.animate(props, {
601
+ duration:
602
+ typeof transition === 'object'
603
+ ? 'duration' in transition
604
+ ? transition.duration
605
+ : 300
606
+ : 300,
607
+ easing:
608
+ typeof transition === 'object'
609
+ ? 'easing' in transition
610
+ ? transition.easing
611
+ : 'ease'
612
+ : 'ease',
613
+ delay: typeof transition === 'object' ? ('delay' in transition ? transition.delay : 0) : 0,
614
+ })
615
+
616
+ // capture the current value to be used in the transition start
617
+ const startValue = this.node[prop]
618
+
619
+ // schedule the transition for this prop, so it can be canceled /
620
+ // removed if another transition for the same prop starts in the mean time
621
+ this.scheduledTransitions[prop] = {
622
+ v: props[prop],
623
+ f,
624
+ }
625
+
626
+ if (transition.start !== undefined && typeof transition.start === 'function') {
627
+ // fire transition start callback when animation really starts (depending on specified delay)
628
+ f.once('animating', () => {
629
+ transition.start.call(this.component, this, prop, startValue)
630
+ })
631
+ }
632
+
633
+ if (this.config.parent.props && this.config.parent.props.__layout === true) {
634
+ f.on('tick', () => {
635
+ this.config.parent.triggerLayout(this.config.parent.props)
636
+ })
637
+ }
638
+
639
+ if (transition.progress !== undefined && typeof transition.progress === 'function') {
640
+ let prevProgress = 0
641
+ f.on('tick', (_node, { progress }) => {
642
+ transition.progress.call(this.component, this, prop, progress, prevProgress)
643
+ prevProgress = progress
644
+ })
645
+ }
646
+
647
+ f.once('stopped', () => {
648
+ if (
649
+ this.scheduledTransitions[prop] !== undefined &&
650
+ this.scheduledTransitions[prop].canceled === true
651
+ ) {
652
+ return
653
+ }
654
+ // fire transition end callback when animation ends (if specified)
655
+ if (this.node !== undefined && transition.end && typeof transition.end === 'function') {
656
+ transition.end.call(this.component, this, prop, this.node[prop])
657
+ }
658
+ // remove the prop from scheduled transitions
659
+ delete this.scheduledTransitions[prop]
660
+ })
661
+
662
+ // start animation
663
+ f.start()
664
+ },
665
+ destroy() {
666
+ if (this.node === null) return
667
+
668
+ Log.debug('Deleting Node', this.nodeId)
669
+ // Clearing transition end callback functions
670
+ const transitionProps = Object.keys(this.scheduledTransitions)
671
+ for (let i = 0; i < transitionProps.length; i++) {
672
+ const transition = this.scheduledTransitions[transitionProps[i]]
673
+ if (transition !== undefined) {
674
+ transition.canceled = true
675
+ if (transition.f !== undefined) transition.f.stop()
676
+ }
677
+ }
678
+
679
+ // not setting to null and deleting,
680
+ // because transition stopped might still be fired (maybe a renderer fix resolves that)
681
+ this.scheduledTransitions = {}
682
+
683
+ this.component = null
684
+ delete this.component
685
+
686
+ this.config = null
687
+ delete this.config
688
+
689
+ this.counter = null
690
+ delete this.counter
691
+
692
+ this.props.raw = {}
693
+ this.props.element = null
694
+ this.props.props = null
695
+ this.props = {}
696
+ delete this.props
697
+
698
+ this.triggerLayout = null
699
+ delete this.triggerLayout
700
+
701
+ this.forComponent = null
702
+ delete this.forComponent
703
+
704
+ this.node.destroy()
705
+ this.node = null
706
+ },
707
+ get nodeId() {
708
+ return this.node && this.node.id
709
+ },
710
+ get ref() {
711
+ return this.props.ref || null
712
+ },
713
+ get parent() {
714
+ return this.node && this.node.parent
715
+ },
716
+ get children() {
717
+ return this.component[symbols.getChildren]().filter((child) => {
718
+ return child.parent === (this[symbols.isSlot] ? this.node.children[0] : this.node)
719
+ })
720
+ },
721
+ }
722
+
723
+ /**
724
+ * Returns a new Blits Element
725
+ *
726
+ * @param {Object} config - The configuration object for the element
727
+ * @param {import('../../component.js').BlitsComponent} component - The component to which the element belongs
728
+ * @returns {import('../../component.js').BlitsElement} - The new Blits Element
729
+ */
730
+ export default (config, component) => {
731
+ if (textDefaults === null) {
732
+ textDefaults = {
733
+ fontSize: 32,
734
+ fontFamily: Settings.get('defaultFont', 'sans-serif'),
735
+ }
736
+ }
737
+ return Object.assign(Object.create(Element), {
738
+ props: Object.assign(Object.create(propsTransformer), { props: {} }),
739
+ scheduledTransitions: {},
740
+ config,
741
+ component,
742
+ counter: counter++,
743
+ })
744
+ }