Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ declare module '@lightningjs/blits' {
* Watchers for changes to state variables, props or computed properties
*/
watch?: W & ComponentContext<P, S, M, C>

}

export interface RouterHooks {
Expand Down Expand Up @@ -904,7 +905,9 @@ declare module '@lightningjs/blits' {
}
}

type ComponentFactory = () => void
type ComponentFactory = (() => void) & {
needsInteractive?: boolean
}

// Launch Related

Expand Down Expand Up @@ -1353,7 +1356,7 @@ declare module '@lightningjs/blits' {
M extends Methods,
C extends Computed,
W extends Watch>(name: string, config: ComponentConfig<P, S, M, C, W>) : ComponentFactory
/**
/**
* Blits Launch
*/
Launch(App: ComponentFactory, target: HTMLElement | String, settings?: Settings) : void
Expand Down
1 change: 1 addition & 0 deletions src/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const Application = (config) => {

if (node === null) {
currentComponent = undefined
currentNode = null
Hover.clear()
return
}
Expand Down
10 changes: 10 additions & 0 deletions src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,16 @@ const Component = (name = required('name'), config = required('config')) => {
// To determine whether dynamic component is actual Blits component or not
factory[symbols.isComponent] = true

// Getter to determine if the component needs interactive hover support
Object.defineProperty(factory, 'needsInteractive', {
get() {
return (
(config.hooks && !!(config.hooks.hover || config.hooks.unhover)) ||
!!(config.code && config.code.usesHoverState)
)
},
})

return factory
}

Expand Down
26 changes: 26 additions & 0 deletions src/engines/L3/element.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1255,3 +1255,29 @@ test('Element - Set maxheight property', (assert) => {
assert.equal(el.props.props['contain'], 'height', 'maxheight should set contain to height')
assert.end()
})

test('Element - holder sets interactive to the passed value (true)', (assert) => {
assert.capture(renderer, 'createNode', () => new EventEmitter())
const component = {}
const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component)
el.populate({ parent: { node: new EventEmitter() }, holder: true })
assert.equal(
el.props.props['interactive'],
true,
'interactive should be true when holder value is true'
)
assert.end()
})

test('Element - holder sets interactive to the passed value (false)', (assert) => {
assert.capture(renderer, 'createNode', () => new EventEmitter())
const component = {}
const el = element({ parent: { node: { w: 1920, h: 1080 } } }, component)
el.populate({ parent: { node: new EventEmitter() }, holder: false })
assert.equal(
el.props.props['interactive'],
false,
'interactive should be false when holder value is false'
)
assert.end()
})
7 changes: 5 additions & 2 deletions src/lib/codegenerator/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export default function (templateObject = { children: [] }, devMode = false) {
}, skips}
`)

const renderCodeString = ctx.renderCode.join('\n')

return {
render: new Function(
'parent',
Expand All @@ -96,7 +98,7 @@ export default function (templateObject = { children: [] }, devMode = false) {
'effect',
'getRaw',
'Log',
ctx.renderCode.join('\n')
renderCodeString
),
effects: ctx.effectsCode.map(
(code) =>
Expand All @@ -112,6 +114,7 @@ export default function (templateObject = { children: [] }, devMode = false) {
)
),
context: ctx.context,
usesHoverState: renderCodeString.includes('.$isHovered'),
}
}

Expand Down Expand Up @@ -187,7 +190,7 @@ const generateElementCode = function (
}

if (options.holder) {
renderCode.push(`elementConfigs[${counter}]['holder'] = true`)
renderCode.push(`elementConfigs[${counter}]['holder'] = cmps[${counter}].needsInteractive`)
}

Object.keys(templateObject).forEach((key) => {
Expand Down
24 changes: 12 additions & 12 deletions src/lib/codegenerator/generator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ test('Generate code for a template with custom components', (assert) => {

elementConfigs[1] = {}
elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
skips[1] = []
if (typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) {
let props = cmps[1][Symbol.for('config')].props
Expand Down Expand Up @@ -1591,7 +1591,7 @@ test('Generate code for a template with custom components', (assert) => {

elementConfigs[3] = {}
elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[3]['holder'] = true
elementConfigs[3]['holder'] = cmps[3].needsInteractive
skips[3] = []
if (typeof cmps[3] !== 'undefined' && cmps[3][Symbol.for('config')].props !== undefined) {
let props = cmps[3][Symbol.for('config')].props
Expand Down Expand Up @@ -1730,7 +1730,7 @@ test('Generate code for a template with an unregistered custom component', (asse
parent = elms[0]
elementConfigs[1] = {}
elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
skips[1] = []
if (typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) {
let props = cmps[1][Symbol.for('config')].props
Expand Down Expand Up @@ -1775,7 +1775,7 @@ test('Generate code for a template with an unregistered custom component', (asse
parent = elms[0]
elementConfigs[3] = {}
elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[3]['holder'] = true
elementConfigs[3]['holder'] = cmps[3].needsInteractive
skips[3] = []
if (typeof cmps[3] !== 'undefined' && cmps[3][Symbol.for('config')].props !== undefined) {
let props = cmps[3][Symbol.for('config')].props
Expand Down Expand Up @@ -1913,7 +1913,7 @@ test('Generate code for a template with custom components with arguments', (asse
parent = elms[0]
elementConfigs[1] = {}
elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
elementConfigs[1]['x'] = 10
skips[1] = []
if(typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) {
Expand Down Expand Up @@ -1960,7 +1960,7 @@ test('Generate code for a template with custom components with arguments', (asse
parent = elms[0]
elementConfigs[3] = {}
elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[3]['holder'] = true
elementConfigs[3]['holder'] = cmps[3].needsInteractive
elementConfigs[3]['x'] = 100
elementConfigs[3]['img'] = component.img
skips[3] = []
Expand Down Expand Up @@ -2107,7 +2107,7 @@ test('Generate code for a template with custom components with argument value as

elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)

elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
elementConfigs[1]['x'] = 1000
elementConfigs[1]['y'] = 100
elementConfigs[1]['h'] = parent.node.h * (45 / 100)
Expand Down Expand Up @@ -2262,7 +2262,7 @@ test('Generate code for a template with custom components with reactive props',
parent = elms[0]
elementConfigs[1] = {}
elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
elementConfigs[1]['x'] = 10
elementConfigs[1]['img'] = component.image
skips[1] = []
Expand Down Expand Up @@ -2316,7 +2316,7 @@ test('Generate code for a template with custom components with reactive props',
parent = elms[0]
elementConfigs[3] = {}
elms[3] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[3]['holder'] = true
elementConfigs[3]['holder'] = cmps[3].needsInteractive
elementConfigs[3]['x'] = 100
elementConfigs[3]['img'] = component.image
skips[3] = []
Expand Down Expand Up @@ -2592,7 +2592,7 @@ test('Generate code for a template with slot content', (assert) => {

elementConfigs[1] = {}
elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
elementConfigs[1]['w'] = 1920
elementConfigs[1]['h'] = 1080
skips[1] = []
Expand Down Expand Up @@ -2781,7 +2781,7 @@ test('Generate code for a template with slot content, using a named slot', (asse

elementConfigs[1] = {}
elms[1] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
elementConfigs[1]['w'] = 1920
elementConfigs[1]['h'] = 1080
skips[1] = []
Expand Down Expand Up @@ -4411,7 +4411,7 @@ test('Generate code for a template with a simple for-loop on a Component with a
if(elms[1][scope.key] === undefined) {
elms[1][scope.key] = this.element({parent: parent || 'root'}, inSlot === true ? slotComponent : component)
}
elementConfigs[1]['holder'] = true
elementConfigs[1]['holder'] = cmps[1].needsInteractive
skips[1] = []
if(typeof cmps[1] !== 'undefined' && cmps[1][Symbol.for('config')].props !== undefined) {
// attributes that are a prop should be removed from element config (even if it's a know element prop)
Expand Down
4 changes: 4 additions & 0 deletions src/lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ export const registerHooks = (hooks = {}, identifier) => {
if (typeof hooks[hook] === 'function') cbs[identifier][hook] = hooks[hook]
})
}

export const hasHook = (hook, identifier) => {
return cbs[identifier] !== undefined && hook in cbs[identifier]
}
25 changes: 24 additions & 1 deletion src/lib/precompiler/precompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export default (source, filePath, mode) => {
// Use MagicString to track changes
const s = new MagicString(source)

// Find component definition start positions for scoping JS-layer checks
const componentStarts = [...source.matchAll(/Blits\.(Component|Application)\s*\(/g)]
.map((m) => m.index)
.sort((a, b) => a - b)

for (const template of templates) {
if (template[2]) {
const templateContent = template[2]
Expand All @@ -52,10 +57,28 @@ export default (source, filePath, mode) => {
// Generate the code
const code = generator.call({ components: {} }, parsed, mode === 'development')

// Determine the component scope containing this template
let scopeStart = 0
let scopeEnd = source.length
for (let i = 0; i < componentStarts.length; i++) {
if (componentStarts[i] <= templateStartIndex) {
scopeStart = componentStarts[i]
scopeEnd = i + 1 < componentStarts.length ? componentStarts[i + 1] : source.length
}
}
const componentScope = source.slice(scopeStart, scopeEnd)

// Derive usesHoverState from both template ($$isHovered) and JS layer (this.$isHovered)
// Strip comments before checking to avoid false positives from commented-out code
const strippedScope = componentScope
.replace(/\/\/[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '')
const usesHoverState = code.usesHoverState || strippedScope.includes('this.$isHovered')

// Insert the code in the component using the 'code' key, replacing the template key
const replacement = `/* eslint-disable no-unused-vars */ \ncode: { render: ${code.render.toString()}, effects: [${code.effects.map(
(fn) => fn.toString()
)}], context: {}}`
)}], context: {}, usesHoverState: ${usesHoverState}}`

// MagicString tracks all changes automatically, no offset needed
s.overwrite(templateStartIndex, templateEndIndex, replacement)
Expand Down
7 changes: 5 additions & 2 deletions src/router/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import Settings from '../settings.js'
* @property {Object} hooks - The hooks of the route
* @property {Object} transition - The transition of the route
* @property {Object} announce - The announce of the route
* @property {(options: Object, parentEl: BlitsElement, parentComponent: BlitsComponent, rootComponent?: BlitsComponent) => BlitsComponent} component - The component factory of the route
* @property {BlitsComponentFactory} component - The component factory of the route (or a lazy import function)
*
* @typedef {Object} Hash
* @property {string} path - The path of the hash
Expand Down Expand Up @@ -427,8 +427,11 @@ export const navigate = async function () {

if (!view) {
// create a holder element for the new view
// for lazy imports needsInteractive is undefined, default to true
const needsInteractive =
route.component.needsInteractive !== undefined ? route.component.needsInteractive : true
holder = stage.element({ parent: this[symbols.children][0] })
holder.populate({})
holder.populate({ holder: needsInteractive })
holder.set('w', '100%')
holder.set('h', '100%')

Expand Down
Loading