@lightningjs/ui-components
Advanced tools
Comparing version 1.3.0-beta.1 to 1.3.0
@@ -135,3 +135,3 @@ /** | ||
keyboard.selectedIndex = row; | ||
keyboard.Items.children[row].selectedIndex = column; | ||
keyboard._Items.children[row].selectedIndex = column; | ||
} | ||
@@ -149,3 +149,3 @@ } | ||
row = keyboard.selectedIndex; | ||
column = keyboard.Items.children[row].selectedIndex; | ||
column = keyboard._Items.children[row].selectedIndex; | ||
} | ||
@@ -152,0 +152,0 @@ return { row, column }; |
@@ -18,3 +18,2 @@ /** | ||
*/ | ||
export * from './elements'; | ||
@@ -21,0 +20,0 @@ export * from './layout'; |
/** | ||
* Copyright 2021 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -18,3 +18,2 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
*/ | ||
import lng from '@lightningjs/core'; | ||
@@ -224,2 +223,3 @@ import Column from '.'; | ||
type: Column, | ||
h: 500, | ||
itemSpacing: args.itemSpacing, | ||
@@ -249,2 +249,3 @@ items: Array.apply(null, { length: 15 }).map(() => ({ | ||
plinko: true, | ||
h: 500, | ||
items: Array.apply(null, { length: 15 }).map((_, i) => ({ | ||
@@ -603,1 +604,177 @@ type: ExpandingRow, | ||
}; | ||
export const LazyUpCount = args => | ||
class LazyUpCount extends lng.Component { | ||
static _template() { | ||
return { | ||
Column: { | ||
type: Column, | ||
h: 500, | ||
itemSpacing: args.itemSpacing, | ||
scrollIndex: args.scrollIndex, | ||
lazyUpCount: args.lazyUpCount, | ||
items: Array.apply(null, { length: 20 }).map((_, i) => ({ | ||
type: Button, | ||
buttonText: `Button ${i + 1}` | ||
})), | ||
alwaysScroll: args.alwaysScroll | ||
} | ||
}; | ||
} | ||
_getFocused() { | ||
return this.tag('Column'); | ||
} | ||
}; | ||
LazyUpCount.args = { | ||
scrollIndex: 0, | ||
lazyUpCount: 5, | ||
itemTransition: 0.4 | ||
}; | ||
LazyUpCount.argTypes = { | ||
itemTransition: { | ||
control: { type: 'number', min: 0, step: 0.1 } | ||
}, | ||
scroll: { | ||
control: { type: 'select', options: [1, 5, 15, 20] } | ||
}, | ||
scrollIndex: { | ||
control: { type: 'number', min: 0 } | ||
}, | ||
lazyUpCount: { | ||
control: { type: 'number', min: 0 } | ||
}, | ||
alwaysScroll: { | ||
control: { type: 'boolean' } | ||
} | ||
}; | ||
LazyUpCount.parameters = { | ||
argActions: { | ||
scroll: function(index, component) { | ||
component.tag('Column').scrollTo(index - 1); | ||
}, | ||
itemTransition: (duration, component) => { | ||
component.tag('Column').itemTransition = { | ||
duration, | ||
timingFunction: component.tag('Column')._itemTransition.timingFunction | ||
}; | ||
} | ||
} | ||
}; | ||
export const AddingItems = args => | ||
class AddingItems extends lng.Component { | ||
static _template() { | ||
return { | ||
Column: { | ||
type: Column, | ||
h: 500, | ||
itemSpacing: args.itemSpacing, | ||
scrollIndex: args.scrollIndex, | ||
items: Array.apply(null, { length: 20 }).map((_, i) => ({ | ||
type: Button, | ||
buttonText: `Button ${i + 1}` | ||
})) | ||
} | ||
}; | ||
} | ||
_init() { | ||
super._init(); | ||
setTimeout(() => { | ||
this.tag('Column').appendItemsAt( | ||
[ | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 0' | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 1' | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 2' | ||
} | ||
], | ||
3 | ||
); | ||
}, 3000); | ||
setTimeout(() => { | ||
this.tag('Column').prependItems([ | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 3', | ||
w: 150 | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 4', | ||
w: 150 | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 5', | ||
w: 150 | ||
} | ||
]); | ||
}, 3750); | ||
} | ||
_getFocused() { | ||
return this.tag('Column'); | ||
} | ||
}; | ||
AddingItems.args = { | ||
itemSpacing: 20, | ||
scrollIndex: 0 | ||
}; | ||
AddingItems.argTypes = { | ||
itemSpacing: { | ||
control: { type: 'range', min: 0, max: 100, step: 5 } | ||
}, | ||
scrollIndex: { | ||
control: 'number' | ||
} | ||
}; | ||
export const RemovingItems = args => | ||
class RemovingItems extends lng.Component { | ||
static _template() { | ||
return { | ||
Column: { | ||
type: Column, | ||
h: 500, | ||
itemSpacing: args.itemSpacing, | ||
scrollIndex: args.scrollIndex, | ||
items: Array.apply(null, { length: 20 }).map((_, i) => ({ | ||
type: Button, | ||
buttonText: `Button ${i + 1}` | ||
})) | ||
} | ||
}; | ||
} | ||
_init() { | ||
super._init(); | ||
setTimeout(() => { | ||
this.tag('Column').removeItemAt(1); | ||
}, 3000); | ||
} | ||
_getFocused() { | ||
return this.tag('Column'); | ||
} | ||
}; | ||
RemovingItems.args = { | ||
itemSpacing: 20, | ||
scrollIndex: 0 | ||
}; | ||
RemovingItems.argTypes = { | ||
itemSpacing: { | ||
control: { type: 'range', min: 0, max: 100, step: 5 } | ||
}, | ||
scrollIndex: { | ||
control: 'number' | ||
} | ||
}; |
/** | ||
* Copyright 2021 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -18,5 +18,4 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
*/ | ||
import FocusManager from '../FocusManager'; | ||
import { getY, getW } from '../../utils'; | ||
import { getY, getW, delayForAnimation } from '../../utils'; | ||
import { debounce } from 'debounce'; | ||
@@ -31,2 +30,14 @@ export default class Column extends FocusManager { | ||
static get properties() { | ||
return [ | ||
...super.properties, | ||
'itemSpacing', | ||
'scrollIndex', | ||
'alwaysScroll', | ||
'lazyUpCount', | ||
'neverScroll', | ||
'autoResize' | ||
]; | ||
} | ||
_construct() { | ||
@@ -36,15 +47,4 @@ super._construct(); | ||
this._itemSpacing = 0; | ||
this._itemPosX = 0; | ||
this._itemPosY = 0; | ||
this._scrollIndex = 0; | ||
this._whenEnabled = new Promise(resolve => (this._firstEnable = resolve)); | ||
this.debounceDelay = Number.isInteger(this.debounceDelay) | ||
? this.debounceDelay | ||
: 30; | ||
this._update = debounce(this._updateLayout, this.debounceDelay); | ||
this._updateImmediate = debounce( | ||
this._updateLayout, | ||
this.debounceDelay, | ||
true | ||
); | ||
this._performRenderDebounce = debounce(this._performRender.bind(this), 0); | ||
} | ||
@@ -55,12 +55,36 @@ | ||
if (!this.h) { | ||
// if height is undefinend or 0, set the Columns's height | ||
this.h = | ||
this.parent && // if the Column is a child item in a FocusManager (like Row) | ||
// if height is undefined or 0, set the Columns's height | ||
if ( | ||
// if the Column is a child item in a FocusManager (like Row) | ||
this.parent && | ||
this.parent.parent && | ||
this.parent.parent instanceof FocusManager | ||
? this.parent.parent.h | ||
: this.stage.h; | ||
this.parent.parent instanceof FocusManager && | ||
this.parent.parent.h | ||
) { | ||
this.h = this.parent.parent.h; | ||
} else { | ||
let parent = this.p; | ||
while (parent && !parent.h) { | ||
parent = parent.p; | ||
} | ||
this.h = | ||
(parent && parent.h) || | ||
this.stage.h / this.stage.getRenderPrecision(); | ||
} | ||
} | ||
this.Items.transition('y').on( | ||
'finish', | ||
this._transitionListener.bind(this) | ||
); | ||
} | ||
_update() { | ||
this._updateLayout(); | ||
} | ||
_setItemSpacing(itemSpacing) { | ||
return itemSpacing || 0; | ||
} | ||
get _itemTransition() { | ||
@@ -85,2 +109,7 @@ return ( | ||
this._smooth = true; | ||
if (this._lazyItems && this._lazyItems.length) { | ||
delayForAnimation(() => { | ||
this.appendItems(this._lazyItems.splice(0, 1)); | ||
}); | ||
} | ||
return super.selectNext(); | ||
@@ -129,4 +158,3 @@ } | ||
// end of Items container < end of last item | ||
Math.abs(this._itemsY - this.h) < | ||
lastChild.y + this.Items.childList.last.h | ||
Math.abs(this.itemPosY - this.h) < lastChild.y + lastChild.h | ||
); | ||
@@ -150,3 +178,3 @@ } | ||
this._performRender(); | ||
this._performRenderDebounce(); | ||
} | ||
@@ -201,2 +229,3 @@ | ||
this.Items.y = this.itemPosY; | ||
this._updateTransitionTarget(this.Items, 'y', this.itemPosY); | ||
} | ||
@@ -224,5 +253,8 @@ } else if (this._shouldScroll()) { | ||
} else { | ||
const scrollTarget = | ||
-scrollItem.y + (scrollItem === this.selected ? scrollOffset : 0); | ||
this.Items.patch({ | ||
y: -scrollItem.y + (scrollItem === this.selected ? scrollOffset : 0) | ||
y: scrollTarget | ||
}); | ||
this._updateTransitionTarget(this.Items, 'y', scrollTarget); | ||
} | ||
@@ -235,43 +267,2 @@ } | ||
get onScreenItems() { | ||
return this.Items.children.filter(child => this._isOnScreen(child)); | ||
} | ||
_isOnScreen(child) { | ||
if (!child) return false; | ||
const y = getY(child); | ||
if (!Number.isFinite(y)) return false; | ||
// to calculate the target absolute Y position of the item, we need to use | ||
// 1) the entire column's absolute position, | ||
// 2) the target animation value of the items container, and | ||
// 3) the target value of the item itself | ||
const ItemY = | ||
this.core.renderContext.py + this.Items.transition('y').targetValue + y; | ||
const { h } = child; | ||
// check that the child is inside the bounds of the stage | ||
const withinTopStageBounds = ItemY + h > 0; | ||
// stage height needs to be adjusted with precision since all other values assume the original height and width (pre-scaling) | ||
const withinBottomStageBounds = | ||
ItemY < this.stage.h / this.stage.getRenderPrecision(); | ||
// check that the child is inside the bounds of any clipping | ||
let withinTopClippingBounds = true; | ||
let withinBottomClippingBounds = true; | ||
if (this.core._scissor && this.core._scissor.length) { | ||
// _scissor consists of [ left position (x), top position (y), width, height ] | ||
const topBounds = this.core._scissor[1]; | ||
const bottomBounds = topBounds + this.core._scissor[3]; | ||
withinTopClippingBounds = Math.round(ItemY + h) > Math.round(topBounds); | ||
withinBottomClippingBounds = Math.round(ItemY) < Math.round(bottomBounds); | ||
} | ||
return ( | ||
withinTopStageBounds && | ||
withinBottomStageBounds && | ||
withinTopClippingBounds && | ||
withinBottomClippingBounds | ||
); | ||
} | ||
_updateLayout() { | ||
@@ -289,6 +280,8 @@ this._whenEnabled.then(() => { | ||
child.patch({ y: nextY }); | ||
this._updateTransitionTarget(child, 'y', nextY); | ||
} | ||
nextY += child.h; | ||
if (i < this.Items.children.length - 1) { | ||
nextY += this.itemSpacing; | ||
const extraItemSpacing = child.extraItemSpacing || 0; | ||
nextY += this.itemSpacing + extraItemSpacing; | ||
} | ||
@@ -305,4 +298,11 @@ | ||
} | ||
const itemChanged = this.Items.w !== nextW || this.Items.h !== nextY; | ||
this.Items.patch({ w: nextW, h: nextY }); | ||
if (this.autoResize) { | ||
this.h = this.Items.h; | ||
this.w = this.Items.w; | ||
} | ||
const lastChild = this.Items.childList.last; | ||
@@ -330,60 +330,68 @@ const endOfLastChild = lastChild ? getY(lastChild) + lastChild.h : 0; | ||
this._performRender(); | ||
itemChanged && this.fireAncestors('$itemChanged'); | ||
this._performRenderDebounce(); | ||
}); | ||
} | ||
get itemSpacing() { | ||
return this._itemSpacing; | ||
get _itemsY() { | ||
return getY(this.Items); | ||
} | ||
set itemSpacing(itemSpacing) { | ||
if (itemSpacing !== this._itemSpacing) { | ||
this._itemSpacing = itemSpacing; | ||
this._update(); | ||
appendItems(items = []) { | ||
const itemWidth = this.renderWidth; | ||
this._smooth = false; | ||
if (items.length > this.lazyUpCount + 2) { | ||
this._lazyItems = items.splice(this.lazyUpCount + 2); | ||
} | ||
} | ||
get scrollIndex() { | ||
return this._scrollIndex; | ||
items.forEach(item => { | ||
item.parentFocus = this.hasFocus(); | ||
item = this.Items.childList.a(item); | ||
item.w = getW(item) || itemWidth; | ||
}); | ||
this.stage.update(); | ||
this._update(); | ||
this._refocus(); | ||
} | ||
set scrollIndex(scrollIndex) { | ||
if (scrollIndex !== this._scrollIndex) { | ||
this._scrollIndex = scrollIndex; | ||
this._update(); | ||
} | ||
} | ||
appendItemsAt(items = [], idx) { | ||
const addIndex = Number.isInteger(idx) ? idx : this.Items.children.length; | ||
this._smooth = false; | ||
this._lastAppendedIdx = addIndex; | ||
set itemPosX(x) { | ||
this.Items.x = this._itemPosX = x; | ||
} | ||
items.forEach((item, itemIdx) => { | ||
this.Items.childList.addAt( | ||
{ | ||
...item, | ||
parentFocus: this.hasFocus() | ||
}, | ||
addIndex + itemIdx | ||
); | ||
}); | ||
get itemPosX() { | ||
return this._itemPosX; | ||
} | ||
if (this.selectedIndex >= this._lastAppendedIdx) { | ||
this._selectedIndex += items.length; | ||
} | ||
set itemPosY(y) { | ||
this.Items.y = this._itemPosY = y; | ||
this._update(); | ||
this._refocus(); | ||
} | ||
get itemPosY() { | ||
return this._itemPosY; | ||
prependItems(items) { | ||
this.appendItemsAt(items, 0); | ||
} | ||
get _itemsY() { | ||
return getY(this.Items); | ||
} | ||
appendItems(items = []) { | ||
const itemWidth = this.renderWidth; | ||
removeItemAt(index) { | ||
this._smooth = false; | ||
this.Items.childList.removeAt(index); | ||
items.forEach(item => { | ||
item.parentFocus = this.hasFocus(); | ||
item = this.Items.childList.a(item); | ||
item.w = getW(item) || itemWidth; | ||
}); | ||
this.stage.update(); | ||
this._updateLayout(); | ||
this._update.clear(); | ||
if ( | ||
this.selectedIndex > index || | ||
this.selectedIndex === this.Items.children.length | ||
) { | ||
this._selectedIndex--; | ||
} | ||
this._update(); | ||
this._refocus(); | ||
@@ -403,7 +411,11 @@ } | ||
} | ||
this.Items.transition('y').on('finish', () => (this._smooth = false)); | ||
} | ||
_transitionListener() { | ||
this._smooth = false; | ||
this.transitionDone(); | ||
} | ||
$itemChanged() { | ||
this._updateImmediate(); | ||
this._update(); | ||
} | ||
@@ -415,5 +427,6 @@ | ||
this.Items.childList.remove(item); | ||
this._updateImmediate(); | ||
this._update(); | ||
if (wasSelected || this.selectedIndex >= this.items.length) { | ||
// eslint-disable-next-line no-self-assign | ||
this.selectedIndex = this._selectedIndex; | ||
@@ -429,7 +442,16 @@ } | ||
$columnChanged() { | ||
this._updateImmediate(); | ||
this._update(); | ||
} | ||
_isOnScreen(child) { | ||
if (!child) return false; | ||
return this._isComponentVerticallyVisible(child); | ||
} | ||
// can be overridden | ||
onScreenEffect() {} | ||
// can be overridden | ||
transitionDone() {} | ||
} |
/** | ||
* Copyright 2021 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -18,3 +18,2 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
*/ | ||
import lng from '@lightningjs/core'; | ||
@@ -21,0 +20,0 @@ import FocusManager from '.'; |
/** | ||
* Copyright 2021 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -23,21 +23,34 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
*/ | ||
import Base from '../../elements/Base'; | ||
import { getX, getY, isComponentOnScreen } from '../../utils'; | ||
import lng from '@lightningjs/core'; | ||
export default class FocusManager extends Base { | ||
static get tags() { | ||
return ['Items']; | ||
} | ||
export default class FocusManager extends lng.Component { | ||
static _template() { | ||
return { Items: {} }; | ||
static get properties() { | ||
return ['direction']; | ||
} | ||
_construct() { | ||
super._construct(); | ||
this._selectedIndex = 0; | ||
this._itemPosX = 0; | ||
this._itemPosY = 0; | ||
this.direction = this.direction || 'row'; | ||
} | ||
get direction() { | ||
return this._direction; | ||
_init() { | ||
this._checkSkipFocus(); | ||
} | ||
set direction(direction) { | ||
this._direction = direction; | ||
get Items() { | ||
if (!this.tag('Items')) { | ||
this.patch({ Items: {} }); | ||
} | ||
return this._Items; | ||
} | ||
_setDirection(direction) { | ||
const state = { | ||
@@ -52,6 +65,7 @@ none: 'None', | ||
} | ||
return direction; | ||
} | ||
get Items() { | ||
return this.tag('Items'); | ||
_getItems() { | ||
return this._Items.children; | ||
} | ||
@@ -64,5 +78,40 @@ | ||
set items(items) { | ||
this.Items.childList.clear(); | ||
this._resetItems(); | ||
this._selectedIndex = 0; | ||
this.appendItems(items); | ||
this._checkSkipFocus(); | ||
} | ||
set itemPosX(x) { | ||
this.Items.x = this._itemPosX = x; | ||
} | ||
get itemPosX() { | ||
return this._itemPosX; | ||
} | ||
set itemPosY(y) { | ||
this.Items.y = this._itemPosY = y; | ||
} | ||
get itemPosY() { | ||
return this._itemPosY; | ||
} | ||
_resetItems() { | ||
this.Items.childList.clear(); | ||
this.Items.patch({ | ||
w: 0, | ||
h: 0, | ||
x: this.itemPosX, | ||
y: this.itemPosY | ||
}); | ||
} | ||
appendItems(items = []) { | ||
this.Items.childList.a(items); | ||
this._refocus(); | ||
} | ||
_checkSkipFocus() { | ||
// If the first item has skip focus when appended get the next focusable item | ||
@@ -75,7 +124,2 @@ const initialSelection = this.Items.children[this.selectedIndex]; | ||
appendItems(items = []) { | ||
this.Items.childList.a(items); | ||
this._refocus(); | ||
} | ||
get selected() { | ||
@@ -100,4 +144,3 @@ return this.Items.children[this.selectedIndex]; | ||
if (this.selected) { | ||
this.render(this.selected, this.prevSelected); | ||
this.signal('selectedChange', this.selected, this.prevSelected); | ||
this._selectedChange(this.selected, this.prevSelected); | ||
} | ||
@@ -110,2 +153,7 @@ // Don't call refocus until after a new render in case of a situation like Plinko nav | ||
_selectedChange(selected, prevSelected) { | ||
this.render(selected, prevSelected); | ||
this.signal('selectedChange', selected, prevSelected); | ||
} | ||
// Override | ||
@@ -154,6 +202,8 @@ render() {} | ||
this.selectedIndex = previousItemIndex; | ||
return true; | ||
} else if (this.wrapSelected) { | ||
this.selectedIndex = this._lastFocusableIndex(); | ||
return true; | ||
} | ||
return true; | ||
return false; | ||
} | ||
@@ -177,6 +227,8 @@ | ||
this.selectedIndex = nextIndex; | ||
return true; | ||
} else if (this.wrapSelected) { | ||
this.selectedIndex = this._firstFocusableIndex(); | ||
return true; | ||
} | ||
return true; | ||
return false; | ||
} | ||
@@ -209,3 +261,6 @@ | ||
.map((item, index) => { | ||
const [x, y] = item.core.getAbsoluteCoords(0, 0); | ||
let [x, y] = [0, 0]; | ||
if (item.core) { | ||
[x, y] = item.core.getAbsoluteCoords(0, 0); | ||
} | ||
return { | ||
@@ -227,3 +282,3 @@ index, | ||
}) | ||
.sort(function(a, b) { | ||
.sort(function (a, b) { | ||
return a.distance - b.distance; | ||
@@ -235,2 +290,9 @@ }); | ||
/** | ||
* TODO: Update Base to remove the focus/unfocus calls and add a second "BaseComponent" that does have them | ||
* | ||
*/ | ||
_focus() {} | ||
_unfocus() {} | ||
_getFocused() { | ||
@@ -249,2 +311,133 @@ const { selected } = this; | ||
_updateTransitionTarget(element, property, newValue) { | ||
if ( | ||
element && | ||
element.transition(property) && | ||
!element.transition(property).isRunning() && | ||
element.transition(property).targetValue !== newValue | ||
) { | ||
element.transition(property).updateTargetValue(newValue); | ||
} | ||
} | ||
/** | ||
* Return list of items that are currently fully and partially on screen | ||
* @returns {Array} Array of matching lng.Component objects or empty array | ||
*/ | ||
get onScreenItems() { | ||
return this.Items.children.filter(child => this._isOnScreen(child)); | ||
} | ||
_isOnScreenCompletely(child) { | ||
// 'isFullyOnScreen' method has been added to the Base class. | ||
// in case child does _not_ extend Base, 'isComponentOnScreen' | ||
// from the 'util' module will be invoked. The same method is | ||
// invoked by Base class | ||
return child.isFullyOnScreen | ||
? child.isFullyOnScreen() | ||
: isComponentOnScreen(child); | ||
} | ||
get fullyOnScreenItems() { | ||
return this.Items.children.reduce((rv, item) => { | ||
if (item instanceof FocusManager) { | ||
return [ | ||
...rv, | ||
...item.Items.children.filter(this._isOnScreenCompletely) | ||
]; | ||
} else if (this._isOnScreenCompletely(item)) { | ||
return [...rv, item]; | ||
} else { | ||
return rv; | ||
} | ||
}, []); | ||
} | ||
_isOnScreen() { | ||
throw new Error("'_isOnScreen' must be implemented by 'row'/'column'"); | ||
} | ||
_isComponentHorizontallyVisible(child) { | ||
// get child's destination X; If child is moving to a destination, | ||
// get the value of where child will end up | ||
const x = getX(child); | ||
if (!Number.isFinite(x)) return false; | ||
// to calculate the target absolute X position of the item, we need to use | ||
// 1) the entire component's absolute position, | ||
// 2) the target animation value of the items container, and | ||
// 3) the target value of the item itself | ||
const transitionX = this.getTransitionXTargetValue(); | ||
// get absolute position of FocusManager on screen | ||
const px = this.core.renderContext.px; | ||
const itemX = px + transitionX + x; | ||
// _scissor consists of [ left position (x), top position (y), width, height ] | ||
const [leftBounds = null, , clipWidth = null] = this.core._scissor || []; | ||
const stageW = this.stage.w / this.stage.getRenderPrecision(); | ||
const { w } = child; | ||
const withinLeftStageBounds = itemX >= 0; | ||
const withinRightStageBounds = itemX + w <= stageW; | ||
// short circuit | ||
if (!withinLeftStageBounds || !withinRightStageBounds) return false; | ||
let withinLeftClippingBounds = true; | ||
let withinRightClippingBounds = true; | ||
if (Number.isFinite(leftBounds)) { | ||
withinLeftClippingBounds = | ||
Math.round(itemX + w) >= Math.round(leftBounds); | ||
withinRightClippingBounds = | ||
Math.round(itemX) <= Math.round(leftBounds + clipWidth); | ||
} | ||
return withinLeftClippingBounds && withinRightClippingBounds; | ||
} | ||
_isComponentVerticallyVisible(child) { | ||
// get child's destination Y; If child is moving to a destination, | ||
// get the value of where child will end up | ||
const y = getY(child); | ||
if (!Number.isFinite(y)) return false; | ||
// to calculate the target absolute Y position of the item, we need to use | ||
// 1) the entire component's absolute position, | ||
// 2) the target animation value of the items container, and | ||
// 3) the target value of the item itself | ||
const transitionY = this.getTransitionYTargetValue(); | ||
// get absolute position of FocusManager on screen | ||
const py = this.core.renderContext.py; | ||
// _scissor consists of [ left position (x), top position (y), width, height ] | ||
const [, topBounds = null, , clipHeight = null] = this.core._scissor || []; | ||
const { h } = child; | ||
const itemY = py + transitionY + y; | ||
const stageH = this.stage.h / this.stage.getRenderPrecision(); | ||
const withinTopStageBounds = itemY + h >= 0; | ||
const withingBottomStageBounds = itemY <= stageH; | ||
// short circuit | ||
if (!withinTopStageBounds || !withingBottomStageBounds) return false; | ||
let withinTopClippingBounds = true; | ||
let withinBottomClippingBounds = true; | ||
if (Number.isFinite(topBounds)) { | ||
withinTopClippingBounds = Math.round(itemY + h) > Math.round(topBounds); | ||
withinBottomClippingBounds = | ||
Math.round(itemY) < Math.round(topBounds + clipHeight); | ||
} | ||
return withinTopClippingBounds && withinBottomClippingBounds; | ||
} | ||
getTransitionXTargetValue() { | ||
return this.Items.transition('x').targetValue; | ||
} | ||
getTransitionYTargetValue() { | ||
return this.Items.transition('y').targetValue; | ||
} | ||
static _states() { | ||
@@ -251,0 +444,0 @@ return [ |
/** | ||
* Copyright 2021 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -20,4 +20,3 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
import FocusManager from '../FocusManager'; | ||
import { getX, getH } from '../../utils'; | ||
import { debounce } from 'debounce'; | ||
import { getX, getH, delayForAnimation } from '../../utils'; | ||
export default class Row extends FocusManager { | ||
@@ -31,2 +30,17 @@ static _template() { | ||
static get properties() { | ||
return [ | ||
...super.properties, | ||
'itemSpacing', | ||
'scrollIndex', | ||
'alwaysScroll', | ||
'neverScroll', | ||
'lazyScroll', | ||
'lazyUpCount', | ||
'autoResize', | ||
'startLazyScrollIndex', | ||
'stopLazyScrollIndex' | ||
]; | ||
} | ||
_construct() { | ||
@@ -36,10 +50,3 @@ super._construct(); | ||
this._itemSpacing = 0; | ||
this._itemPosX = 0; | ||
this._itemPosY = 0; | ||
this._scrollIndex = 0; | ||
this._whenEnabled = new Promise(resolve => (this._firstEnable = resolve)); | ||
this.debounceDelay = Number.isInteger(this.debounceDelay) | ||
? this.debounceDelay | ||
: 1; | ||
this._update = debounce(this._updateLayout, this.debounceDelay); | ||
} | ||
@@ -50,12 +57,36 @@ | ||
if (!this.w) { | ||
// if width is undefinend or 0, set the Row's width | ||
this.w = | ||
this.parent && // if the Row is a child item in a FocusManager (like Column) | ||
// if width is undefined or 0, set the Row's width | ||
if ( | ||
// if the Row is a child item in a FocusManager (like Column) | ||
this.parent && | ||
this.parent.parent && | ||
this.parent.parent instanceof FocusManager | ||
? this.parent.parent.w | ||
: this.stage.w; | ||
this.parent.parent instanceof FocusManager && | ||
this.parent.parent.w | ||
) { | ||
this.w = this.parent.parent.w; | ||
} else { | ||
let parent = this.p; | ||
while (parent && !parent.w) { | ||
parent = parent.p; | ||
} | ||
this.w = | ||
(parent && parent.w) || | ||
this.stage.h / this.stage.getRenderPrecision(); | ||
} | ||
} | ||
this.Items.transition('x').on( | ||
'finish', | ||
this._transitionListener.bind(this) | ||
); | ||
} | ||
_update() { | ||
this._updateLayout(); | ||
} | ||
_setItemSpacing(itemSpacing) { | ||
return itemSpacing || 0; | ||
} | ||
get _itemTransition() { | ||
@@ -80,2 +111,7 @@ return ( | ||
this._smooth = true; | ||
if (this._lazyItems && this._lazyItems.length) { | ||
delayForAnimation(() => { | ||
this.appendItems(this._lazyItems.splice(0, 1)); | ||
}); | ||
} | ||
return super.selectNext(); | ||
@@ -119,45 +155,10 @@ } | ||
get onScreenItems() { | ||
return this.Items.children.filter(child => this._isOnScreen(child)); | ||
} | ||
_isOnScreen(child) { | ||
// Since this is a Row, scrolling should be done only when focused item (this.selected) is fully visible horizontally | ||
// as scrolling is happening along X axis. So, if we have a row that has height greater than screen's height, it still | ||
// can scroll. Method below the '_isComponentHorizontallyVisible' does not take "full" visibility into an account. | ||
// At the same time, 'isFullyOnScreen' method on the Base class or utils method checks that element is fully visible | ||
// both vertically and horizontally. | ||
// At a later time, we should investigate this further. | ||
_isOnScreenForScrolling(child) { | ||
if (!child) return false; | ||
const x = getX(child); | ||
if (!Number.isFinite(x)) return false; | ||
// to calculate the target absolute X position of the item, we need to use | ||
// 1) the entire row's absolute position, | ||
// 2) the target animation value of the items container, and | ||
// 3) the target value of the item itself | ||
const ItemX = | ||
this.core.renderContext.px + this.Items.transition('x').targetValue + x; | ||
const { w } = child; | ||
// check that the child is inside the bounds of the stage | ||
const withinLeftStageBounds = ItemX > 0; | ||
// stage width needs to be adjusted with precision since all other values assume the original height and width (pre-scaling) | ||
const withinRightStageBounds = | ||
ItemX + w < this.stage.w / this.stage.getRenderPrecision(); | ||
// check that the child is inside the bounds of any clipping | ||
let withinLeftClippingBounds = true; | ||
let withinRightClippingBounds = true; | ||
if (this.core._scissor && this.core._scissor.length) { | ||
// _scissor consists of [ left position (x), top position (y), width, height ] | ||
const leftBounds = this.core._scissor[0]; | ||
const rightBounds = leftBounds + this.core._scissor[2]; | ||
withinLeftClippingBounds = Math.round(ItemX + w) > Math.round(leftBounds); | ||
withinRightClippingBounds = Math.round(ItemX) < Math.round(rightBounds); | ||
} | ||
return ( | ||
withinLeftStageBounds && | ||
withinRightStageBounds && | ||
withinLeftClippingBounds && | ||
withinRightClippingBounds | ||
); | ||
} | ||
_isOnScreenCompletely(child) { | ||
if (!child) return false; | ||
const itemX = child.core.renderContext.px; | ||
@@ -169,6 +170,16 @@ const rowX = this.core.renderContext.px; | ||
_shouldScroll() { | ||
let shouldScroll = this.alwaysScroll; | ||
if ( | ||
this.lazyScroll && | ||
(this.selectedIndex <= this.startLazyScrollIndex || | ||
this.selectedIndex >= this.stopLazyScrollIndex) | ||
) { | ||
return true; | ||
} | ||
let shouldScroll = this.alwaysScroll || this._selectedPastAdded; | ||
if (!shouldScroll && !this.neverScroll) { | ||
const isCompletelyOnScreen = this._isOnScreenForScrolling(this.selected); | ||
if (this.lazyScroll) { | ||
shouldScroll = !this._isOnScreenCompletely(this.selected); | ||
shouldScroll = !isCompletelyOnScreen; | ||
} else { | ||
@@ -180,3 +191,3 @@ const lastChild = this.Items.childList.last; | ||
this.shouldScrollRight() || | ||
!this._isOnScreenCompletely(this.selected)); | ||
!isCompletelyOnScreen); | ||
} | ||
@@ -187,16 +198,56 @@ } | ||
_getPrependedOffset() { | ||
this._selectedPastAdded = false; | ||
return this.Items.x - this._totalAddedWidth; | ||
} | ||
_getLazyScrollX(prev) { | ||
let itemsContainerX; | ||
const prevIndex = this.Items.childList.getIndex(prev); | ||
if (prevIndex === this._lastFocusableIndex()) { | ||
itemsContainerX = -this.Items.children[0].x; | ||
} else if (prevIndex > this.selectedIndex) { | ||
itemsContainerX = -this.selected.x; | ||
} else if (prevIndex < this.selectedIndex) { | ||
itemsContainerX = this.w - this.selected.x - this.selected.w; | ||
const prevIndex = this.Items.childList.getIndex(this.prevSelected); | ||
if (this._selectedPastAdded) { | ||
return this._getPrependedOffset(); | ||
} | ||
return itemsContainerX; | ||
if (this.selectedIndex <= this.startLazyScrollIndex) { | ||
// if navigating on items before start lazy scroll index, use normal scroll logic | ||
return this._getScrollX(); | ||
} else if ( | ||
this.selectedIndex >= this.stopLazyScrollIndex && | ||
this.selectedIndex < prevIndex | ||
) { | ||
// if navigating left on items after stop lazy scroll index, only shift by size of prev item | ||
const currItemsX = this.Items.transition('x') | ||
? this.Items.transition('x').targetValue | ||
: this.Items.x; | ||
return ( | ||
currItemsX + | ||
(this.prevSelected.w + | ||
this.itemSpacing + | ||
(this.selected.extraItemSpacing || 0)) | ||
); | ||
} else if (prev) { | ||
// otherwise, no start/stop indexes, perform normal lazy scroll | ||
let itemsContainerX; | ||
const prevIndex = this.Items.childList.getIndex(prev); | ||
if (prevIndex === -1) { | ||
// No matches found in childList, start set x to 0 | ||
return; | ||
} | ||
if (prevIndex > this.selectedIndex) { | ||
itemsContainerX = -this.selected.x; | ||
} else if (prevIndex < this.selectedIndex) { | ||
itemsContainerX = this.w - this.selected.x - this.selected.w; | ||
} | ||
return itemsContainerX; | ||
} | ||
// if no prev item or start/stop index, default to normal scroll logic | ||
return this._getScrollX(); | ||
} | ||
_getScrollX() { | ||
if (this._selectedPastAdded) { | ||
return this._getPrependedOffset(); | ||
} | ||
let itemsContainerX; | ||
@@ -208,2 +259,3 @@ let itemIndex = this.selectedIndex - this.scrollIndex; | ||
} | ||
if (this.Items.children[itemIndex]) { | ||
@@ -214,2 +266,3 @@ itemsContainerX = this.Items.children[itemIndex].transition('x') | ||
} | ||
return itemsContainerX; | ||
@@ -230,6 +283,5 @@ } | ||
} else if (this._shouldScroll()) { | ||
itemsContainerX = | ||
this.lazyScroll && prev | ||
? this._getLazyScrollX(prev) | ||
: this._getScrollX(); | ||
itemsContainerX = this.lazyScroll | ||
? this._getLazyScrollX(prev) | ||
: this._getScrollX(); | ||
} | ||
@@ -243,2 +295,3 @@ if (itemsContainerX !== undefined) { | ||
this.Items.x = itemsContainerX; | ||
this._updateTransitionTarget(this.Items, 'x', itemsContainerX); | ||
} | ||
@@ -262,6 +315,8 @@ } | ||
child.patch({ x: nextX }); | ||
this._updateTransitionTarget(child, 'x', nextX); | ||
} | ||
nextX += child.w; | ||
if (i < this.Items.children.length - 1) { | ||
nextX += this.itemSpacing; | ||
const extraItemSpacing = child.extraItemSpacing || 0; | ||
nextX += this.itemSpacing + extraItemSpacing; | ||
} | ||
@@ -278,4 +333,11 @@ | ||
} | ||
this.Items.patch({ h: nextH, w: nextX }); | ||
const itemChanged = this.Items.h !== nextH || this.Items.w !== nextX; | ||
this.Items.patch({ h: nextH, w: nextX + (this._totalAddedWidth || 0) }); | ||
if (this.autoResize) { | ||
this.h = this.Items.h; | ||
this.w = this.Items.w; | ||
} | ||
const lastChild = this.Items.childList.last; | ||
@@ -299,60 +361,78 @@ const endOfLastChild = lastChild ? getX(lastChild) + lastChild.w : 0; | ||
} | ||
this.fireAncestors('$itemChanged'); | ||
itemChanged && this.fireAncestors('$itemChanged'); | ||
this.render(this.selected, this.prevSelected); | ||
} | ||
get itemSpacing() { | ||
return this._itemSpacing; | ||
get _itemsX() { | ||
return getX(this.Items); | ||
} | ||
set itemSpacing(itemSpacing) { | ||
if (itemSpacing !== this._itemSpacing) { | ||
this._itemSpacing = itemSpacing; | ||
this._update(); | ||
} | ||
} | ||
_isOnScreen(child) { | ||
if (!child) return false; | ||
get scrollIndex() { | ||
return this._scrollIndex; | ||
return this._isComponentHorizontallyVisible(child); | ||
} | ||
set scrollIndex(scrollIndex) { | ||
if (scrollIndex !== this._scrollIndex) { | ||
this._scrollIndex = scrollIndex; | ||
this._update(); | ||
appendItems(items = []) { | ||
const itemHeight = this.renderHeight; | ||
this._smooth = false; | ||
if (items.length > this.lazyUpCount + 2) { | ||
this._lazyItems = items.splice(this.lazyUpCount + 2); | ||
} | ||
} | ||
set itemPosX(x) { | ||
this.Items.x = this._itemPosX = x; | ||
items.forEach(item => { | ||
item.parentFocus = this.hasFocus(); | ||
item = this.Items.childList.a(item); | ||
item.h = item.h || itemHeight; | ||
}); | ||
this.stage.update(); | ||
this._update(); | ||
this._refocus(); | ||
} | ||
get itemPosX() { | ||
return this._itemPosX; | ||
} | ||
appendItemsAt(items = [], idx) { | ||
const addIndex = Number.isInteger(idx) ? idx : this.Items.children.length; | ||
this._smooth = false; | ||
this._lastAppendedIdx = addIndex; | ||
this._totalAddedWidth = 0; | ||
set itemPosY(y) { | ||
this.Items.y = this._itemPosY = y; | ||
} | ||
items.forEach((item, itemIdx) => { | ||
this.Items.childList.addAt( | ||
{ | ||
...item, | ||
parentFocus: this.hasFocus(), | ||
h: item.h || this.Items.h | ||
}, | ||
addIndex + itemIdx | ||
); | ||
const extraItemSpacing = item.extraItemSpacing || 0; | ||
this._totalAddedWidth += item.w + this.itemSpacing + extraItemSpacing; | ||
}); | ||
get itemPosY() { | ||
return this._itemPosY; | ||
if (this.selectedIndex >= this._lastAppendedIdx) { | ||
this._selectedPastAdded = true; | ||
this._selectedIndex += items.length; | ||
} | ||
this._update(); | ||
this._refocus(); | ||
} | ||
get _itemsX() { | ||
return getX(this.Items); | ||
prependItems(items) { | ||
this.appendItemsAt(items, 0); | ||
} | ||
appendItems(items = []) { | ||
const itemHeight = this.renderHeight; | ||
removeItemAt(index) { | ||
this._smooth = false; | ||
this.Items.childList.removeAt(index); | ||
items.forEach(item => { | ||
item.parentFocus = this.hasFocus(); | ||
item = this.Items.childList.a(item); | ||
item.h = item.h || itemHeight; | ||
}); | ||
this.stage.update(); | ||
this._updateLayout(); | ||
this._update.clear(); | ||
if ( | ||
this.selectedIndex > index || | ||
this.selectedIndex === this.Items.children.length | ||
) { | ||
this._selectedIndex--; | ||
} | ||
this._update(); | ||
this._refocus(); | ||
@@ -365,4 +445,12 @@ } | ||
_transitionListener() { | ||
this._smooth = false; | ||
this.transitionDone(); | ||
} | ||
// can be overridden | ||
onScreenEffect() {} | ||
// can be overridden | ||
transitionDone() {} | ||
} |
/** | ||
* Copyright 2021 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -366,1 +366,253 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
}; | ||
export const LazyScrollIndexes = ({ | ||
startLazyScrollIndex, | ||
stopLazyScrollIndex | ||
}) => | ||
class LazyScrollIndexes extends lng.Component { | ||
static _template() { | ||
return { | ||
Row: { | ||
type: Row, | ||
w: 1920 - 160, // x offset from preview.js * 2 | ||
itemSpacing: 20, | ||
alwaysScroll: false, | ||
neverScroll: false, | ||
lazyScroll: true, | ||
scrollIndex: 0, | ||
items: Array.apply(null, { length: 12 }).map((_, i) => ({ | ||
type: Button, | ||
buttonText: `Button ${i + 1} ${ | ||
i === startLazyScrollIndex ? '(start lazy scroll)' : '' | ||
} ${i === stopLazyScrollIndex ? '(stop lazy scroll)' : ''}`, | ||
w: 250 | ||
})), | ||
startLazyScrollIndex, | ||
stopLazyScrollIndex | ||
} | ||
}; | ||
} | ||
_getFocused() { | ||
return this.tag('Row'); | ||
} | ||
}; | ||
LazyScrollIndexes.args = { | ||
startLazyScrollIndex: 1, | ||
stopLazyScrollIndex: 10 | ||
}; | ||
LazyScrollIndexes.argTypes = { | ||
startLazyScrollIndex: { | ||
control: 'number' | ||
}, | ||
stopLazyScrollIndex: { | ||
control: 'number' | ||
} | ||
}; | ||
export const AddingItems = args => | ||
class AddingItems extends lng.Component { | ||
static _template() { | ||
return { | ||
Row: { | ||
type: Row, | ||
w: 1920 - 160, // x offset from preview.js * 2 | ||
itemSpacing: args.itemSpacing, | ||
alwaysScroll: args.alwaysScroll, | ||
neverScroll: args.neverScroll, | ||
lazyScroll: args.lazyScroll, | ||
scrollIndex: args.scrollIndex, | ||
items: Array.apply(null, { length: 12 }).map((_, i) => ({ | ||
type: Button, | ||
buttonText: `Button ${i}`, | ||
w: 150 | ||
})) | ||
} | ||
}; | ||
} | ||
_init() { | ||
super._init(); | ||
setTimeout(() => { | ||
this.tag('Row').appendItemsAt( | ||
[ | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 0', | ||
w: 150 | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 1', | ||
w: 150 | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 2', | ||
w: 150 | ||
} | ||
], | ||
3 | ||
); | ||
}, 3000); | ||
setTimeout(() => { | ||
this.tag('Row').prependItems([ | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 3', | ||
w: 150 | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 4', | ||
w: 150 | ||
}, | ||
{ | ||
type: Button, | ||
buttonText: 'New Button 5', | ||
w: 150 | ||
} | ||
]); | ||
}, 3750); | ||
} | ||
_getFocused() { | ||
return this.tag('Row'); | ||
} | ||
}; | ||
AddingItems.args = { | ||
itemSpacing: 20, | ||
scrollIndex: 0, | ||
alwaysScroll: false, | ||
neverScroll: false, | ||
lazyScroll: false | ||
}; | ||
AddingItems.argTypes = { | ||
itemSpacing: { | ||
control: { type: 'range', min: 0, max: 100, step: 5 } | ||
}, | ||
scrollIndex: { | ||
control: 'number' | ||
}, | ||
alwaysScroll: { | ||
control: 'boolean' | ||
}, | ||
neverScroll: { | ||
control: 'boolean' | ||
}, | ||
lazyScroll: { | ||
control: 'boolean' | ||
} | ||
}; | ||
export const LazyUpCount = args => | ||
class LazyUpCount extends lng.Component { | ||
static _template() { | ||
return { | ||
Row: { | ||
type: Row, | ||
w: 1920 - 160, // x offset from preview.js * 2 | ||
itemSpacing: args.itemSpacing, | ||
alwaysScroll: args.alwaysScroll, | ||
neverScroll: args.neverScroll, | ||
lazyScroll: args.lazyScroll, | ||
lazyUpCount: args.lazyUpCount, | ||
scrollIndex: args.scrollIndex, | ||
items: Array.apply(null, { length: 12 }).map((_, i) => ({ | ||
type: Button, | ||
buttonText: `Button ${i + 1}`, | ||
w: 150 | ||
})) | ||
} | ||
}; | ||
} | ||
_getFocused() { | ||
return this.tag('Row'); | ||
} | ||
}; | ||
LazyUpCount.args = { | ||
itemSpacing: 20, | ||
scrollIndex: 0, | ||
alwaysScroll: false, | ||
neverScroll: false, | ||
lazyScroll: false, | ||
lazyUpCount: 4 | ||
}; | ||
LazyUpCount.argTypes = { | ||
itemSpacing: { | ||
control: { type: 'range', min: 0, max: 100, step: 5 } | ||
}, | ||
scrollIndex: { | ||
control: 'number' | ||
}, | ||
lazyUpCount: { | ||
control: 'number' | ||
}, | ||
alwaysScroll: { | ||
control: 'boolean' | ||
}, | ||
neverScroll: { | ||
control: 'boolean' | ||
}, | ||
lazyScroll: { | ||
control: 'boolean' | ||
} | ||
}; | ||
export const RemovingItems = args => | ||
class RemovingItems extends lng.Component { | ||
static _template() { | ||
return { | ||
Row: { | ||
type: Row, | ||
w: 1920 - 160, // x offset from preview.js * 2 | ||
itemSpacing: args.itemSpacing, | ||
alwaysScroll: args.alwaysScroll, | ||
neverScroll: args.neverScroll, | ||
lazyScroll: args.lazyScroll, | ||
scrollIndex: args.scrollIndex, | ||
items: ['A', 'B', 'C', 'D', 'E'].map(letter => ({ | ||
type: Button, | ||
buttonText: letter, | ||
w: 150 | ||
})) | ||
} | ||
}; | ||
} | ||
_init() { | ||
super._init(); | ||
setTimeout(() => { | ||
this.tag('Row').removeItemAt(1); | ||
}, 3000); | ||
} | ||
_getFocused() { | ||
return this.tag('Row'); | ||
} | ||
}; | ||
RemovingItems.args = { | ||
itemSpacing: 20, | ||
scrollIndex: 0, | ||
alwaysScroll: false, | ||
neverScroll: false, | ||
lazyScroll: false | ||
}; | ||
RemovingItems.argTypes = { | ||
itemSpacing: { | ||
control: { type: 'range', min: 0, max: 100, step: 5 } | ||
}, | ||
scrollIndex: { | ||
control: 'number' | ||
}, | ||
alwaysScroll: { | ||
control: 'boolean' | ||
}, | ||
neverScroll: { | ||
control: 'boolean' | ||
}, | ||
lazyScroll: { | ||
control: 'boolean' | ||
} | ||
}; |
{ | ||
"name": "@lightningjs/ui-components", | ||
"version": "1.3.0-beta.1", | ||
"version": "1.3.0", | ||
"dependencies": { | ||
@@ -9,3 +9,3 @@ "debounce": "^1.2.1" | ||
"peerDependencies": { | ||
"@lightningjs/core": "^2.x" | ||
"@lightningjs/core": "^2.1.1" | ||
}, | ||
@@ -18,28 +18,28 @@ "browser": "index.js", | ||
"Styles.js", | ||
"{bin,elements,layout,mixins,Styles,utils}/**/*", | ||
"!elements|layout|Styles/**/*.stories.js", | ||
"!{elements,layout,utils}/**/*.test.js", | ||
"!elements|layout/lightning-test-renderer.js", | ||
"!elements|layout/lightning-test-utils.js", | ||
"!elements|layout/**/__snapshots__", | ||
"{bin,elements,layout,mixins,Styles,textures,utils}/**/*", | ||
"!{elements,layout,mixins,Styles,textures,utils}/**/*.stories.js", | ||
"!{elements,layout,mixins,Styles,textures,utils}/**/*.test.js", | ||
"test/lightning-test-renderer.js", | ||
"test/lightning-test-utils.js", | ||
"!{elements,layout,mixins,Styles,textures,utils}/**/__snapshots__", | ||
"!public/" | ||
], | ||
"devDependencies": { | ||
"@babel/core": "^7.10.5", | ||
"@babel/plugin-proposal-class-properties": "^7.10.4", | ||
"@babel/plugin-transform-modules-commonjs": "^7.10.4", | ||
"@babel/plugin-transform-runtime": "^7.12.10", | ||
"@babel/preset-env": "^7.10.4", | ||
"@babel/preset-react": "^7.10.4", | ||
"@commitlint/cli": "^9.1.2", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@lightningjs/core": "^2.3.0", | ||
"@semantic-release/changelog": "^5.0.1", | ||
"@semantic-release/git": "^9.0.0", | ||
"@storybook/addon-docs": "^6.3.10", | ||
"@storybook/addon-essentials": "^6.3.10", | ||
"@storybook/addon-storysource": "^6.3.10", | ||
"@storybook/html": "^6.3.10", | ||
"@babel/core": "^7.13.10", | ||
"@babel/plugin-proposal-class-properties": "^7.12.1", | ||
"@babel/plugin-transform-modules-commonjs": "^7.12.1", | ||
"@babel/plugin-transform-runtime": "^7.13.10", | ||
"@babel/preset-env": "^7.13.10", | ||
"@babel/preset-react": "^7.12.10", | ||
"@commitlint/cli": "^17.0.0", | ||
"@commitlint/config-conventional": "^17.0.0", | ||
"@lightningjs/core": "2.5.0", | ||
"@semantic-release/changelog": "^6.0.1", | ||
"@semantic-release/git": "^10.0.1", | ||
"@storybook/addon-docs": "^6.5.3", | ||
"@storybook/addon-essentials": "^6.5.3", | ||
"@storybook/addon-storysource": "^6.5.3", | ||
"@storybook/html": "^6.5.3", | ||
"babel-eslint": "^10.1.0", | ||
"babel-loader": "^8.1.0", | ||
"babel-loader": "^8.2.2", | ||
"canvas": "^2.7.0", | ||
@@ -50,10 +50,9 @@ "eslint": "^7.22.0", | ||
"eslint-plugin-prettier": "^3.3.1", | ||
"gh-pages": "^3.1.0", | ||
"husky": "^3.1.0", | ||
"jest": "^24.9.0", | ||
"jest-environment-jsdom-fifteen": "^1.0.2", | ||
"gh-pages": "^4.0.0", | ||
"husky": "^8.0.1", | ||
"jest": "^26.6.1", | ||
"jest-webgl-canvas-mock": "^0.2.3", | ||
"lint-staged": "^9.5.0", | ||
"prettier": "^1.19.1", | ||
"semantic-release": "^17.1.1" | ||
"lint-staged": "^12.1.2", | ||
"prettier": "^2.4.1", | ||
"semantic-release": "^19.0.2" | ||
}, | ||
@@ -60,0 +59,0 @@ "husky": { |
/** | ||
* Copyright 2020 Comcast Cable Communications Management, LLC | ||
* Copyright 2022 Comcast Cable Communications Management, LLC | ||
* | ||
@@ -115,3 +115,3 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
const value = object[key]; | ||
if (Object.prototype.hasOwnProperty.call(target, key)) { | ||
if (target.hasOwnProperty(key)) { | ||
_clone[key] = getMergeValue(key, target, object); | ||
@@ -191,8 +191,8 @@ } else { | ||
/** | ||
* Returns an array of strings and icon or badge objects from a string using the syntax: | ||
* 'This is a {ICON:<title>|<url>} and {BADGE:<title>} badge test.' | ||
* Returns an array of strings and icon, badge, newline, and text objects from a string using the syntax: | ||
* 'This is an {ICON:<title>|<url>} and {BADGE:<title>} badge test with a {NEWLINE} newline and {TEXT:<text>|<style>}.' | ||
* | ||
* i.e. 'This is an {ICON:settings|./assets/icons/settings.png} icon and {BADGE:<HD>} badge.' | ||
* would create the object: | ||
* { | ||
* i.e. 'This is an {ICON:settings|./assets/icons/settings.png} icon and {BADGE:HD} badge with a{NEWLINE} and {TEXT:red text|red}.' | ||
* would create the array: | ||
* [ | ||
* 'This is an ', | ||
@@ -202,6 +202,10 @@ * { icon: './assets/icons/settings.png', title: 'settings' }, | ||
* { badge: 'HD' }, | ||
* ' badge.' | ||
* } | ||
* ' badge with a', | ||
* { newline: true }, | ||
* ' and ', | ||
* { text: 'red text', style: 'red' }, | ||
* '.' | ||
* ] | ||
* | ||
* @param {*} str | ||
* @param {(string|object)} str | ||
* | ||
@@ -212,8 +216,12 @@ * @return {array} | ||
const content = []; | ||
if (str && typeof str === 'string') { | ||
const regex = /({ICON.*?}|{BADGE:.*?})/g; | ||
if ((str && typeof str === 'string') || str.text) { | ||
const string = typeof str === 'string' ? str : str.text; | ||
const regex = /({ICON.*?}|{BADGE:.*?}|{NEWLINE}|{TEXT:.*?})/g; | ||
const iconRegEx = /^{ICON:(.*?)?\|(.*?)?}$/g; | ||
const badgeRegEx = /^{BADGE:(.*?)}$/g; | ||
const iconRegEx = /^{ICON:(.*?)?\|(.*?)?}$/g; | ||
const splitStr = str.split(regex); | ||
const newlineRegEx = /^{NEWLINE}$/g; | ||
const textRegEx = /^{TEXT:(.*?)?\|(.*?)?}$/g; | ||
const splitStr = string.split(regex); | ||
if (splitStr && splitStr.length) { | ||
@@ -224,8 +232,13 @@ splitStr.forEach(item => { | ||
const icon = iconRegEx.exec(item); | ||
const newline = newlineRegEx.exec(item); | ||
const text = textRegEx.exec(item); | ||
if (badge && badge[1]) { | ||
formattedItem = { badge: badge[1] }; | ||
} | ||
if (icon && icon[1]) { | ||
} else if (icon && icon[1]) { | ||
formattedItem = { title: icon[1], icon: icon[2] || icon[1] }; | ||
} else if (newline) { | ||
formattedItem = { newline: true }; | ||
} else if (text && text[1]) { | ||
formattedItem = { text: text[1], style: text[2] }; | ||
} | ||
@@ -274,12 +287,58 @@ content.push(formattedItem); | ||
/** | ||
* Deep equality check two values | ||
* | ||
* @param {any} valA - value to be compared against valB | ||
* @param {any} valB - value to be compared against valA | ||
* | ||
* @return {boolean} - returns true if values are equal | ||
*/ | ||
export function stringifyCompare(valA, valB) { | ||
return JSON.stringify(valA) === JSON.stringify(valB); | ||
export function objectPropertyOf(object, path) { | ||
return path.reduce( | ||
(obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined), | ||
object | ||
); | ||
} | ||
export function stringifyCompare(objA, objB) { | ||
return JSON.stringify(objA) === JSON.stringify(objB); | ||
} | ||
export function isComponentOnScreen(component) { | ||
if (!component) return false; | ||
const { | ||
w, | ||
h, | ||
core: { renderContext: { px, py }, _scissor: scissor = [] } = {} | ||
} = component; | ||
const stageH = component.stage.h / component.stage.getRenderPrecision(); | ||
const stageW = component.stage.w / component.stage.getRenderPrecision(); | ||
const wVis = px >= 0 && px + w <= stageW; | ||
const hVis = py >= 0 && py + h <= stageH; | ||
if (!wVis || !hVis) return false; | ||
if (scissor && scissor.length) { | ||
const [ | ||
leftBounds = null, | ||
topBounds = null, | ||
clipWidth = null, | ||
clipHeight = null | ||
] = scissor; | ||
const withinLeftClippingBounds = | ||
Math.round(px + w) >= Math.round(leftBounds); | ||
const withinRightClippingBounds = | ||
Math.round(px) <= Math.round(leftBounds + clipWidth); | ||
const withinTopClippingBounds = Math.round(py + h) >= Math.round(topBounds); | ||
const withinBottomClippingBounds = | ||
Math.round(py + h) <= Math.round(topBounds + clipHeight); | ||
return ( | ||
withinLeftClippingBounds && | ||
withinRightClippingBounds && | ||
withinTopClippingBounds && | ||
withinBottomClippingBounds | ||
); | ||
} | ||
return true; | ||
} | ||
export function delayForAnimation(callback, delay = 16) { | ||
setTimeout(callback, delay); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
1315021
29
133
0
100
12303
1
2