rework cache and lifecycle

This commit is contained in:
Rene
2020-12-30 00:25:57 +01:00
parent fca2dff869
commit 67c412bc55
23 changed files with 781 additions and 1264 deletions
+120 -147
View File
@@ -12,9 +12,6 @@ function isNumber(obj) {
function isString(obj) { function isString(obj) {
return typeof obj === 'string'; return typeof obj === 'string';
} }
function isBoolean(obj) {
return typeof obj === 'boolean';
}
function isFunction(obj) { function isFunction(obj) {
return typeof obj === 'function'; return typeof obj === 'function';
} }
@@ -434,49 +431,29 @@ const absoluteCoordinates = (elm) => {
: zeroObj$1; : zeroObj$1;
}; };
function createCache(cacheUpdateInfo, isReference) { const createCache = (update, options) => {
const cache = {}; const { _equal, _initialValue } = options || {};
const allProps = keys(cacheUpdateInfo); let _value = _initialValue;
each(allProps, (prop) => {
cache[prop] = { let _previous;
_changed: false,
_value: isReference ? cacheUpdateInfo[prop] : undefined, return (force, context) => {
const prev = _value;
const newVal = update(context, _value, _previous);
const changed = force || (_equal ? !_equal(prev, newVal) : prev !== newVal);
if (changed) {
_value = newVal;
_previous = prev;
}
return {
_value,
_previous,
_changed: changed,
}; };
});
const updateCacheProp = (prop, value, equal) => {
const curr = cache[prop]._value;
cache[prop]._value = value;
cache[prop]._previous = curr;
cache[prop]._changed = equal ? !equal(curr, value) : curr !== value;
}; };
};
const flush = (props, force) => {
const result = assignDeep({}, cache, {
_anythingChanged: false,
});
each(props, (prop) => {
const changed = force || cache[prop]._changed;
result._anythingChanged = result._anythingChanged || changed;
result[prop]._changed = changed;
cache[prop]._changed = false;
});
return result;
};
return (propsToUpdate, force) => {
const finalPropsToUpdate = (isString(propsToUpdate) ? [propsToUpdate] : propsToUpdate) || allProps;
each(finalPropsToUpdate, (prop) => {
const cacheVal = cache[prop];
const curr = cacheUpdateInfo[prop];
const arr = isReference ? false : isArray(curr);
const value = arr ? curr[0] : curr;
const equal = arr ? curr[1] : null;
updateCacheProp(prop, isReference ? value : value(cacheVal._value, cacheVal._previous), equal);
});
return flush(finalPropsToUpdate, force);
};
}
const firstLetterToUpper = (str) => str.charAt(0).toUpperCase() + str.slice(1); const firstLetterToUpper = (str) => str.charAt(0).toUpperCase() + str.slice(1);
@@ -822,21 +799,21 @@ const getEnvironment = () => {
return environmentInstance; return environmentInstance;
}; };
const createLifecycleBase = (defaultOptionsWithTemplate, cacheUpdateInfo, initialOptions, updateFunction) => { const getPropByPath = (obj, path) => obj && path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj);
const createLifecycleBase = (defaultOptionsWithTemplate, initialOptions, updateFunction) => {
const { _template: optionsTemplate, _options: defaultOptions } = transformOptions(defaultOptionsWithTemplate); const { _template: optionsTemplate, _options: defaultOptions } = transformOptions(defaultOptionsWithTemplate);
const options = assignDeep({}, defaultOptions, validateOptions(initialOptions || {}, optionsTemplate, null, true)._validated); const options = assignDeep({}, defaultOptions, validateOptions(initialOptions || {}, optionsTemplate, null, true)._validated);
const cacheChange = createCache(cacheUpdateInfo);
const cacheOptions = createCache(options, true);
const update = (hints) => { const update = (hints) => {
const hasForce = isBoolean(hints._force); const { _force, _changedOptions } = hints;
const force = hints._force === true;
const changedCache = cacheChange(force ? null : hints._changedCache || (hasForce ? null : []), force);
const changedOptions = cacheOptions(force ? null : hints._changedOptions, !!hints._changedOptions || force);
if (changedOptions._anythingChanged || changedCache._anythingChanged) { const checkOption = (path) => ({
updateFunction(changedOptions, changedCache); _value: getPropByPath(options, path),
} _changed: _force || getPropByPath(_changedOptions, path) !== undefined,
});
updateFunction(!!_force, checkOption);
}; };
update({ update({
@@ -845,101 +822,89 @@ const createLifecycleBase = (defaultOptionsWithTemplate, cacheUpdateInfo, initia
return { return {
_options(newOptions) { _options(newOptions) {
if (newOptions) { if (newOptions) {
const { _validated: changedOptions } = validateOptions(newOptions, optionsTemplate, options, true); const { _validated: _changedOptions } = validateOptions(newOptions, optionsTemplate, options, true);
assignDeep(options, changedOptions); assignDeep(options, _changedOptions);
update({ update({
_changedOptions: keys(changedOptions), _changedOptions,
}); });
} }
return options; return options;
}, },
_update: (force) => { _update: (_force) => {
update({ update({
_force: !!force, _force,
});
},
_updateCache: (cachePropsToUpdate) => {
update({
_changedCache: cachePropsToUpdate,
}); });
}, },
}; };
}; };
const overflowBehaviorAllowedValues = 'visible-hidden visible-scroll scroll hidden'; const overflowBehaviorAllowedValues = 'visible-hidden visible-scroll scroll hidden';
const defaultOptionsWithTemplate = {
paddingAbsolute: [false, optionsTemplateTypes.boolean],
overflowBehavior: {
x: ['scroll', overflowBehaviorAllowedValues],
y: ['scroll', overflowBehaviorAllowedValues],
},
};
const cssMarginEnd = cssProperty('margin-inline-end'); const cssMarginEnd = cssProperty('margin-inline-end');
const cssBorderEnd = cssProperty('border-inline-end'); const cssBorderEnd = cssProperty('border-inline-end');
const createStructureLifecycle = (target, initialOptions) => { const createStructureLifecycle = (target, initialOptions) => {
const { host, viewport, content } = target; const { host, padding: paddingElm, viewport, content } = target;
const destructFns = []; const destructFns = [];
const env = getEnvironment(); const env = getEnvironment();
const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid; const scrollbarsOverlaid = env._nativeScrollbarIsOverlaid;
const supportsScrollbarStyling = env._nativeScrollbarStyling; const supportsScrollbarStyling = env._nativeScrollbarStyling;
const supportFlexboxGlue = env._flexboxGlue; const supportFlexboxGlue = env._flexboxGlue;
const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y; const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y;
const { _options, _update, _updateCache } = createLifecycleBase( const updatePaddingCache = createCache(() => topRightBottomLeft(host, 'padding'), {
{ _equal: equalTRBL,
paddingAbsolute: [false, optionsTemplateTypes.boolean], });
overflowBehavior: { const { _options, _update } = createLifecycleBase(defaultOptionsWithTemplate, initialOptions, (force, checkOption) => {
x: ['scroll', overflowBehaviorAllowedValues], const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute');
y: ['scroll', overflowBehaviorAllowedValues], const { _value: padding, _changed: paddingChanged } = updatePaddingCache(force);
},
},
{
padding: [() => topRightBottomLeft(host, 'padding'), equalTRBL],
},
initialOptions,
(options, cache) => {
const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = options.paddingAbsolute;
const { _value: padding, _changed: paddingChanged } = cache.padding;
if (paddingAbsoluteChanged || paddingChanged) { if (paddingAbsoluteChanged || paddingChanged) {
const paddingStyle = { const paddingStyle = {
t: 0, t: 0,
r: 0, r: 0,
b: 0, b: 0,
l: 0, l: 0,
}; };
if (!paddingAbsolute) { if (!paddingAbsolute) {
paddingStyle.t = -padding.t; paddingStyle.t = -padding.t;
paddingStyle.r = -(padding.r + padding.l); paddingStyle.r = -(padding.r + padding.l);
paddingStyle.b = -(padding.b + padding.t); paddingStyle.b = -(padding.b + padding.t);
paddingStyle.l = -padding.l; paddingStyle.l = -padding.l;
}
if (!supportsScrollbarStyling) {
paddingStyle.r -= env._nativeScrollbarSize.y;
paddingStyle.b -= env._nativeScrollbarSize.x;
}
style(viewport, {
top: paddingStyle.t,
left: paddingStyle.l,
'margin-right': paddingStyle.r,
'margin-bottom': paddingStyle.b,
});
} }
console.log(options); if (!supportsScrollbarStyling) {
console.log(cache); paddingStyle.r -= env._nativeScrollbarSize.y;
paddingStyle.b -= env._nativeScrollbarSize.x;
}
style(paddingElm, {
top: paddingStyle.t,
left: paddingStyle.l,
'margin-right': paddingStyle.r,
'margin-bottom': paddingStyle.b,
'max-width': `calc(100% + ${paddingStyle.r * -1}px)`,
});
} }
); });
const onSizeChanged = () => { const onSizeChanged = () => {
_updateCache('padding'); _update();
}; };
const onTrinsicChanged = (widthIntrinsic, heightIntrinsic) => { const onTrinsicChanged = (widthIntrinsic, heightIntrinsicCache) => {
if (heightIntrinsic) { const { _changed, _value } = heightIntrinsicCache;
if (_changed) {
style(content, { style(content, {
height: 'auto', height: _value ? 'auto' : '100%',
});
} else {
style(content, {
height: '100%',
}); });
} }
}; };
@@ -963,6 +928,7 @@ const ResizeObserverConstructor = jsAPI('ResizeObserver');
const classNameSizeObserver = 'os-size-observer'; const classNameSizeObserver = 'os-size-observer';
const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`; const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`;
const classNameSizeObserverListener = `${classNameSizeObserver}-listener`; const classNameSizeObserverListener = `${classNameSizeObserver}-listener`;
const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`;
const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`; const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`;
const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`; const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
const cAF = cancelAnimationFrame; const cAF = cancelAnimationFrame;
@@ -979,14 +945,14 @@ const createSizeObserver = (target, onSizeChangedCallback, options) => {
const sizeObserver = baseElements[0]; const sizeObserver = baseElements[0];
const listenerElement = sizeObserver.firstChild; const listenerElement = sizeObserver.firstChild;
const onSizeChangedCallbackProxy = (dir) => { const onSizeChangedCallbackProxy = (directionCache) => {
if (direction) { if (direction) {
const rtl = getDirection(sizeObserver) === 'rtl'; const rtl = getDirection(sizeObserver) === 'rtl';
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount); scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
scrollTop(sizeObserver, scrollAmount); scrollTop(sizeObserver, scrollAmount);
} }
onSizeChangedCallback(isString(dir) ? dir : undefined); onSizeChangedCallback(isString((directionCache || {})._value) ? directionCache : undefined);
}; };
const offListeners = []; const offListeners = [];
@@ -1001,6 +967,7 @@ const createSizeObserver = (target, onSizeChangedCallback, options) => {
`<div class="${classNameSizeObserverListenerItem}" dir="ltr"><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}"></div></div><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}" style="width: 200%; height: 200%"></div></div></div>` `<div class="${classNameSizeObserverListenerItem}" dir="ltr"><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}"></div></div><div class="${classNameSizeObserverListenerItem}"><div class="${classNameSizeObserverListenerItemFinal}" style="width: 200%; height: 200%"></div></div></div>`
); );
appendChildren(listenerElement, observerElementChildren); appendChildren(listenerElement, observerElementChildren);
addClass(listenerElement, classNameSizeObserverListenerScroll);
const observerElementChildrenRoot = observerElementChildren[0]; const observerElementChildrenRoot = observerElementChildren[0];
const shrinkElement = observerElementChildrenRoot.lastChild; const shrinkElement = observerElementChildrenRoot.lastChild;
const expandElement = observerElementChildrenRoot.firstChild; const expandElement = observerElementChildrenRoot.firstChild;
@@ -1017,15 +984,13 @@ const createSizeObserver = (target, onSizeChangedCallback, options) => {
scrollTop(shrinkElement, scrollAmount); scrollTop(shrinkElement, scrollAmount);
}; };
const onResized = function onResized() { const onResized = () => {
rAFId = 0; rAFId = 0;
if (!isDirty) { if (isDirty) {
return; cacheSize = currSize;
onSizeChangedCallbackProxy();
} }
cacheSize = currSize;
onSizeChangedCallbackProxy();
}; };
const onScroll = (scrollEvent) => { const onScroll = (scrollEvent) => {
@@ -1060,14 +1025,14 @@ const createSizeObserver = (target, onSizeChangedCallback, options) => {
} }
if (direction) { if (direction) {
let dirCache; const updateDirectionCache = createCache(() => getDirection(sizeObserver));
offListeners.push( offListeners.push(
on(sizeObserver, scrollEventName, (event) => { on(sizeObserver, scrollEventName, (event) => {
const dir = getDirection(sizeObserver); const directionCache = updateDirectionCache();
const changed = dir !== dirCache; const { _value, _changed } = directionCache;
if (changed) { if (_changed) {
if (dir === 'rtl') { if (_value === 'rtl') {
style(listenerElement, { style(listenerElement, {
left: 'auto', left: 'auto',
right: 0, right: 0,
@@ -1079,8 +1044,7 @@ const createSizeObserver = (target, onSizeChangedCallback, options) => {
}); });
} }
dirCache = dir; onSizeChangedCallbackProxy(directionCache);
onSizeChangedCallbackProxy(dir);
} }
preventDefault(event); preventDefault(event);
@@ -1107,7 +1071,12 @@ const IntersectionObserverConstructor = jsAPI('IntersectionObserver');
const createTrinsicObserver = (target, onTrinsicChangedCallback) => { const createTrinsicObserver = (target, onTrinsicChangedCallback) => {
const trinsicObserver = createDOM(`<div class="${classNameTrinsicObserver}"></div>`)[0]; const trinsicObserver = createDOM(`<div class="${classNameTrinsicObserver}"></div>`)[0];
const offListeners = []; const offListeners = [];
let heightIntrinsic = false; const updateHeightIntrinsicCache = createCache(
(ioEntryOrSize) => ioEntryOrSize.h === 0 || ioEntryOrSize.isIntersecting || ioEntryOrSize.intersectionRatio > 0,
{
_initialValue: false,
}
);
if (IntersectionObserverConstructor) { if (IntersectionObserverConstructor) {
const intersectionObserverInstance = new IntersectionObserverConstructor( const intersectionObserverInstance = new IntersectionObserverConstructor(
@@ -1116,11 +1085,10 @@ const createTrinsicObserver = (target, onTrinsicChangedCallback) => {
const last = entries.pop(); const last = entries.pop();
if (last) { if (last) {
const newHeightIntrinsic = last.isIntersecting || last.intersectionRatio > 0; const heightIntrinsicCache = updateHeightIntrinsicCache(0, last);
if (newHeightIntrinsic !== heightIntrinsic) { if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(false, newHeightIntrinsic); onTrinsicChangedCallback(false, heightIntrinsicCache);
heightIntrinsic = newHeightIntrinsic;
} }
} }
} }
@@ -1135,11 +1103,10 @@ const createTrinsicObserver = (target, onTrinsicChangedCallback) => {
offListeners.push( offListeners.push(
createSizeObserver(trinsicObserver, () => { createSizeObserver(trinsicObserver, () => {
const newSize = offsetSize(trinsicObserver); const newSize = offsetSize(trinsicObserver);
const newHeightIntrinsic = newSize.h === 0; const heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
if (newHeightIntrinsic !== heightIntrinsic) { if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(false, newHeightIntrinsic); onTrinsicChangedCallback(false, heightIntrinsicCache);
heightIntrinsic = newHeightIntrinsic;
} }
}) })
); );
@@ -1153,6 +1120,7 @@ const createTrinsicObserver = (target, onTrinsicChangedCallback) => {
}; };
const classNameHost = 'os-host'; const classNameHost = 'os-host';
const classNamePadding = 'os-padding';
const classNameViewport = 'os-viewport'; const classNameViewport = 'os-viewport';
const classNameContent = 'os-content'; const classNameContent = 'os-content';
@@ -1162,24 +1130,29 @@ const normalizeTarget = (target) => {
const _host = isTextarea ? createDiv() : target; const _host = isTextarea ? createDiv() : target;
const _padding = createDiv(classNamePadding);
const _viewport = createDiv(classNameViewport); const _viewport = createDiv(classNameViewport);
const _content = createDiv(classNameContent); const _content = createDiv(classNameContent);
appendChildren(_padding, _viewport);
appendChildren(_viewport, _content); appendChildren(_viewport, _content);
appendChildren(_content, contents(target)); appendChildren(_content, contents(target));
appendChildren(target, _viewport); appendChildren(target, _padding);
addClass(_host, classNameHost); addClass(_host, classNameHost);
return { return {
target, target,
host: _host, host: _host,
padding: _padding,
viewport: _viewport, viewport: _viewport,
content: _content, content: _content,
}; };
} }
const { host, viewport, content } = target; const { host, padding, viewport, content } = target;
addClass(host, classNameHost); addClass(host, classNameHost);
addClass(padding, classNamePadding);
addClass(viewport, classNameViewport); addClass(viewport, classNameViewport);
addClass(content, classNameContent); addClass(content, classNameContent);
return target; return target;
@@ -1191,10 +1164,10 @@ const OverlayScrollbars = (target, options, extensions) => {
const { host } = osTarget; const { host } = osTarget;
lifecycles.push(createStructureLifecycle(osTarget)); lifecycles.push(createStructureLifecycle(osTarget));
const onSizeChanged = (direction) => { const onSizeChanged = (directionCache) => {
if (direction) { if (directionCache) {
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(direction); lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(directionCache);
}); });
} else { } else {
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
@@ -1203,9 +1176,9 @@ const OverlayScrollbars = (target, options, extensions) => {
} }
}; };
const onTrinsicChanged = (widthIntrinsic, heightIntrinsic) => { const onTrinsicChanged = (widthIntrinsic, heightIntrinsicCache) => {
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsic); lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsicCache);
}); });
}; };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+148 -154
View File
@@ -21,9 +21,6 @@
function isString(obj) { function isString(obj) {
return typeof obj === 'string'; return typeof obj === 'string';
} }
function isBoolean(obj) {
return typeof obj === 'boolean';
}
function isFunction(obj) { function isFunction(obj) {
return typeof obj === 'function'; return typeof obj === 'function';
} }
@@ -487,49 +484,32 @@
: zeroObj$1; : zeroObj$1;
}; };
function createCache(cacheUpdateInfo, isReference) { var createCache = function createCache(update, options) {
var cache = {}; var _ref = options || {},
var allProps = keys(cacheUpdateInfo); _equal = _ref._equal,
each(allProps, function (prop) { _initialValue = _ref._initialValue;
cache[prop] = {
_changed: false, var _value = _initialValue;
_value: isReference ? cacheUpdateInfo[prop] : undefined,
var _previous;
return function (force, context) {
var prev = _value;
var newVal = update(context, _value, _previous);
var changed = force || (_equal ? !_equal(prev, newVal) : prev !== newVal);
if (changed) {
_value = newVal;
_previous = prev;
}
return {
_value: _value,
_previous: _previous,
_changed: changed,
}; };
});
var updateCacheProp = function updateCacheProp(prop, value, equal) {
var curr = cache[prop]._value;
cache[prop]._value = value;
cache[prop]._previous = curr;
cache[prop]._changed = equal ? !equal(curr, value) : curr !== value;
}; };
};
var flush = function flush(props, force) {
var result = assignDeep({}, cache, {
_anythingChanged: false,
});
each(props, function (prop) {
var changed = force || cache[prop]._changed;
result._anythingChanged = result._anythingChanged || changed;
result[prop]._changed = changed;
cache[prop]._changed = false;
});
return result;
};
return function (propsToUpdate, force) {
var finalPropsToUpdate = (isString(propsToUpdate) ? [propsToUpdate] : propsToUpdate) || allProps;
each(finalPropsToUpdate, function (prop) {
var cacheVal = cache[prop];
var curr = cacheUpdateInfo[prop];
var arr = isReference ? false : isArray(curr);
var value = arr ? curr[0] : curr;
var equal = arr ? curr[1] : null;
updateCacheProp(prop, isReference ? value : value(cacheVal._value, cacheVal._previous), equal);
});
return flush(finalPropsToUpdate, force);
};
}
var firstLetterToUpper = function firstLetterToUpper(str) { var firstLetterToUpper = function firstLetterToUpper(str) {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
@@ -893,24 +873,34 @@
return environmentInstance; return environmentInstance;
}; };
var createLifecycleBase = function createLifecycleBase(defaultOptionsWithTemplate, cacheUpdateInfo, initialOptions, updateFunction) { var getPropByPath = function getPropByPath(obj, path) {
return (
obj &&
path.split('.').reduce(function (o, prop) {
return o && hasOwnProperty(o, prop) ? o[prop] : undefined;
}, obj)
);
};
var createLifecycleBase = function createLifecycleBase(defaultOptionsWithTemplate, initialOptions, updateFunction) {
var _transformOptions = transformOptions(defaultOptionsWithTemplate), var _transformOptions = transformOptions(defaultOptionsWithTemplate),
optionsTemplate = _transformOptions._template, optionsTemplate = _transformOptions._template,
defaultOptions = _transformOptions._options; defaultOptions = _transformOptions._options;
var options = assignDeep({}, defaultOptions, validateOptions(initialOptions || {}, optionsTemplate, null, true)._validated); var options = assignDeep({}, defaultOptions, validateOptions(initialOptions || {}, optionsTemplate, null, true)._validated);
var cacheChange = createCache(cacheUpdateInfo);
var cacheOptions = createCache(options, true);
var update = function update(hints) { var update = function update(hints) {
var hasForce = isBoolean(hints._force); var _force = hints._force,
var force = hints._force === true; _changedOptions = hints._changedOptions;
var changedCache = cacheChange(force ? null : hints._changedCache || (hasForce ? null : []), force);
var changedOptions = cacheOptions(force ? null : hints._changedOptions, !!hints._changedOptions || force);
if (changedOptions._anythingChanged || changedCache._anythingChanged) { var checkOption = function checkOption(path) {
updateFunction(changedOptions, changedCache); return {
} _value: getPropByPath(options, path),
_changed: _force || getPropByPath(_changedOptions, path) !== undefined,
};
};
updateFunction(!!_force, checkOption);
}; };
update({ update({
@@ -920,34 +910,37 @@
_options: function _options(newOptions) { _options: function _options(newOptions) {
if (newOptions) { if (newOptions) {
var _validateOptions = validateOptions(newOptions, optionsTemplate, options, true), var _validateOptions = validateOptions(newOptions, optionsTemplate, options, true),
changedOptions = _validateOptions._validated; _changedOptions = _validateOptions._validated;
assignDeep(options, changedOptions); assignDeep(options, _changedOptions);
update({ update({
_changedOptions: keys(changedOptions), _changedOptions: _changedOptions,
}); });
} }
return options; return options;
}, },
_update: function _update(force) { _update: function _update(_force) {
update({ update({
_force: !!force, _force: _force,
});
},
_updateCache: function _updateCache(cachePropsToUpdate) {
update({
_changedCache: cachePropsToUpdate,
}); });
}, },
}; };
}; };
var overflowBehaviorAllowedValues = 'visible-hidden visible-scroll scroll hidden'; var overflowBehaviorAllowedValues = 'visible-hidden visible-scroll scroll hidden';
var defaultOptionsWithTemplate = {
paddingAbsolute: [false, optionsTemplateTypes.boolean],
overflowBehavior: {
x: ['scroll', overflowBehaviorAllowedValues],
y: ['scroll', overflowBehaviorAllowedValues],
},
};
var cssMarginEnd = cssProperty('margin-inline-end'); var cssMarginEnd = cssProperty('margin-inline-end');
var cssBorderEnd = cssProperty('border-inline-end'); var cssBorderEnd = cssProperty('border-inline-end');
var createStructureLifecycle = function createStructureLifecycle(target, initialOptions) { var createStructureLifecycle = function createStructureLifecycle(target, initialOptions) {
var host = target.host, var host = target.host,
paddingElm = target.padding,
viewport = target.viewport, viewport = target.viewport,
content = target.content; content = target.content;
var destructFns = []; var destructFns = [];
@@ -956,80 +949,67 @@
var supportsScrollbarStyling = env._nativeScrollbarStyling; var supportsScrollbarStyling = env._nativeScrollbarStyling;
var supportFlexboxGlue = env._flexboxGlue; var supportFlexboxGlue = env._flexboxGlue;
var directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y; var directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y;
var updatePaddingCache = createCache(
function () {
return topRightBottomLeft(host, 'padding');
},
{
_equal: equalTRBL,
}
);
var _createLifecycleBase = createLifecycleBase( var _createLifecycleBase = createLifecycleBase(defaultOptionsWithTemplate, initialOptions, function (force, checkOption) {
{ var _checkOption = checkOption('paddingAbsolute'),
paddingAbsolute: [false, optionsTemplateTypes.boolean], paddingAbsolute = _checkOption._value,
overflowBehavior: { paddingAbsoluteChanged = _checkOption._changed;
x: ['scroll', overflowBehaviorAllowedValues],
y: ['scroll', overflowBehaviorAllowedValues],
},
},
{
padding: [
function () {
return topRightBottomLeft(host, 'padding');
},
equalTRBL,
],
},
initialOptions,
function (options, cache) {
var _options$paddingAbsol = options.paddingAbsolute,
paddingAbsolute = _options$paddingAbsol._value,
paddingAbsoluteChanged = _options$paddingAbsol._changed;
var _cache$padding = cache.padding,
padding = _cache$padding._value,
paddingChanged = _cache$padding._changed;
if (paddingAbsoluteChanged || paddingChanged) { var _updatePaddingCache = updatePaddingCache(force),
var paddingStyle = { padding = _updatePaddingCache._value,
t: 0, paddingChanged = _updatePaddingCache._changed;
r: 0,
b: 0,
l: 0,
};
if (!paddingAbsolute) { if (paddingAbsoluteChanged || paddingChanged) {
paddingStyle.t = -padding.t; var paddingStyle = {
paddingStyle.r = -(padding.r + padding.l); t: 0,
paddingStyle.b = -(padding.b + padding.t); r: 0,
paddingStyle.l = -padding.l; b: 0,
} l: 0,
};
if (!supportsScrollbarStyling) { if (!paddingAbsolute) {
paddingStyle.r -= env._nativeScrollbarSize.y; paddingStyle.t = -padding.t;
paddingStyle.b -= env._nativeScrollbarSize.x; paddingStyle.r = -(padding.r + padding.l);
} paddingStyle.b = -(padding.b + padding.t);
paddingStyle.l = -padding.l;
style(viewport, {
top: paddingStyle.t,
left: paddingStyle.l,
'margin-right': paddingStyle.r,
'margin-bottom': paddingStyle.b,
});
} }
console.log(options); if (!supportsScrollbarStyling) {
console.log(cache); paddingStyle.r -= env._nativeScrollbarSize.y;
paddingStyle.b -= env._nativeScrollbarSize.x;
}
style(paddingElm, {
top: paddingStyle.t,
left: paddingStyle.l,
'margin-right': paddingStyle.r,
'margin-bottom': paddingStyle.b,
'max-width': 'calc(100% + ' + paddingStyle.r * -1 + 'px)',
});
} }
), }),
_options = _createLifecycleBase._options, _options = _createLifecycleBase._options,
_update = _createLifecycleBase._update, _update = _createLifecycleBase._update;
_updateCache = _createLifecycleBase._updateCache;
var onSizeChanged = function onSizeChanged() { var onSizeChanged = function onSizeChanged() {
_updateCache('padding'); _update();
}; };
var onTrinsicChanged = function onTrinsicChanged(widthIntrinsic, heightIntrinsic) { var onTrinsicChanged = function onTrinsicChanged(widthIntrinsic, heightIntrinsicCache) {
if (heightIntrinsic) { var _changed = heightIntrinsicCache._changed,
_value = heightIntrinsicCache._value;
if (_changed) {
style(content, { style(content, {
height: 'auto', height: _value ? 'auto' : '100%',
});
} else {
style(content, {
height: '100%',
}); });
} }
}; };
@@ -1052,6 +1032,7 @@
var classNameSizeObserver = 'os-size-observer'; var classNameSizeObserver = 'os-size-observer';
var classNameSizeObserverAppear = classNameSizeObserver + '-appear'; var classNameSizeObserverAppear = classNameSizeObserver + '-appear';
var classNameSizeObserverListener = classNameSizeObserver + '-listener'; var classNameSizeObserverListener = classNameSizeObserver + '-listener';
var classNameSizeObserverListenerScroll = classNameSizeObserverListener + '-scroll';
var classNameSizeObserverListenerItem = classNameSizeObserverListener + '-item'; var classNameSizeObserverListenerItem = classNameSizeObserverListener + '-item';
var classNameSizeObserverListenerItemFinal = classNameSizeObserverListenerItem + '-final'; var classNameSizeObserverListenerItemFinal = classNameSizeObserverListenerItem + '-final';
var cAF = cancelAnimationFrame; var cAF = cancelAnimationFrame;
@@ -1074,14 +1055,14 @@
var sizeObserver = baseElements[0]; var sizeObserver = baseElements[0];
var listenerElement = sizeObserver.firstChild; var listenerElement = sizeObserver.firstChild;
var onSizeChangedCallbackProxy = function onSizeChangedCallbackProxy(dir) { var onSizeChangedCallbackProxy = function onSizeChangedCallbackProxy(directionCache) {
if (direction) { if (direction) {
var rtl = getDirection(sizeObserver) === 'rtl'; var rtl = getDirection(sizeObserver) === 'rtl';
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount); scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
scrollTop(sizeObserver, scrollAmount); scrollTop(sizeObserver, scrollAmount);
} }
onSizeChangedCallback(isString(dir) ? dir : undefined); onSizeChangedCallback(isString((directionCache || {})._value) ? directionCache : undefined);
}; };
var offListeners = []; var offListeners = [];
@@ -1108,6 +1089,7 @@
'" style="width: 200%; height: 200%"></div></div></div>' '" style="width: 200%; height: 200%"></div></div></div>'
); );
appendChildren(listenerElement, observerElementChildren); appendChildren(listenerElement, observerElementChildren);
addClass(listenerElement, classNameSizeObserverListenerScroll);
var observerElementChildrenRoot = observerElementChildren[0]; var observerElementChildrenRoot = observerElementChildren[0];
var shrinkElement = observerElementChildrenRoot.lastChild; var shrinkElement = observerElementChildrenRoot.lastChild;
var expandElement = observerElementChildrenRoot.firstChild; var expandElement = observerElementChildrenRoot.firstChild;
@@ -1127,12 +1109,10 @@
var onResized = function onResized() { var onResized = function onResized() {
rAFId = 0; rAFId = 0;
if (!isDirty) { if (isDirty) {
return; cacheSize = currSize;
onSizeChangedCallbackProxy();
} }
cacheSize = currSize;
onSizeChangedCallbackProxy();
}; };
var onScroll = function onScroll(scrollEvent) { var onScroll = function onScroll(scrollEvent) {
@@ -1171,14 +1151,17 @@
} }
if (direction) { if (direction) {
var dirCache; var updateDirectionCache = createCache(function () {
return getDirection(sizeObserver);
});
offListeners.push( offListeners.push(
on(sizeObserver, scrollEventName, function (event) { on(sizeObserver, scrollEventName, function (event) {
var dir = getDirection(sizeObserver); var directionCache = updateDirectionCache();
var changed = dir !== dirCache; var _value = directionCache._value,
_changed = directionCache._changed;
if (changed) { if (_changed) {
if (dir === 'rtl') { if (_value === 'rtl') {
style(listenerElement, { style(listenerElement, {
left: 'auto', left: 'auto',
right: 0, right: 0,
@@ -1190,8 +1173,7 @@
}); });
} }
dirCache = dir; onSizeChangedCallbackProxy(directionCache);
onSizeChangedCallbackProxy(dir);
} }
preventDefault(event); preventDefault(event);
@@ -1218,7 +1200,14 @@
var createTrinsicObserver = function createTrinsicObserver(target, onTrinsicChangedCallback) { var createTrinsicObserver = function createTrinsicObserver(target, onTrinsicChangedCallback) {
var trinsicObserver = createDOM('<div class="' + classNameTrinsicObserver + '"></div>')[0]; var trinsicObserver = createDOM('<div class="' + classNameTrinsicObserver + '"></div>')[0];
var offListeners = []; var offListeners = [];
var heightIntrinsic = false; var updateHeightIntrinsicCache = createCache(
function (ioEntryOrSize) {
return ioEntryOrSize.h === 0 || ioEntryOrSize.isIntersecting || ioEntryOrSize.intersectionRatio > 0;
},
{
_initialValue: false,
}
);
if (IntersectionObserverConstructor) { if (IntersectionObserverConstructor) {
var intersectionObserverInstance = new IntersectionObserverConstructor( var intersectionObserverInstance = new IntersectionObserverConstructor(
@@ -1227,11 +1216,10 @@
var last = entries.pop(); var last = entries.pop();
if (last) { if (last) {
var newHeightIntrinsic = last.isIntersecting || last.intersectionRatio > 0; var heightIntrinsicCache = updateHeightIntrinsicCache(0, last);
if (newHeightIntrinsic !== heightIntrinsic) { if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(false, newHeightIntrinsic); onTrinsicChangedCallback(false, heightIntrinsicCache);
heightIntrinsic = newHeightIntrinsic;
} }
} }
} }
@@ -1248,11 +1236,10 @@
offListeners.push( offListeners.push(
createSizeObserver(trinsicObserver, function () { createSizeObserver(trinsicObserver, function () {
var newSize = offsetSize(trinsicObserver); var newSize = offsetSize(trinsicObserver);
var newHeightIntrinsic = newSize.h === 0; var heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
if (newHeightIntrinsic !== heightIntrinsic) { if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(false, newHeightIntrinsic); onTrinsicChangedCallback(false, heightIntrinsicCache);
heightIntrinsic = newHeightIntrinsic;
} }
}) })
); );
@@ -1266,6 +1253,7 @@
}; };
var classNameHost = 'os-host'; var classNameHost = 'os-host';
var classNamePadding = 'os-padding';
var classNameViewport = 'os-viewport'; var classNameViewport = 'os-viewport';
var classNameContent = 'os-content'; var classNameContent = 'os-content';
@@ -1275,26 +1263,32 @@
var _host = isTextarea ? createDiv() : target; var _host = isTextarea ? createDiv() : target;
var _padding = createDiv(classNamePadding);
var _viewport = createDiv(classNameViewport); var _viewport = createDiv(classNameViewport);
var _content = createDiv(classNameContent); var _content = createDiv(classNameContent);
appendChildren(_padding, _viewport);
appendChildren(_viewport, _content); appendChildren(_viewport, _content);
appendChildren(_content, contents(target)); appendChildren(_content, contents(target));
appendChildren(target, _viewport); appendChildren(target, _padding);
addClass(_host, classNameHost); addClass(_host, classNameHost);
return { return {
target: target, target: target,
host: _host, host: _host,
padding: _padding,
viewport: _viewport, viewport: _viewport,
content: _content, content: _content,
}; };
} }
var host = target.host, var host = target.host,
padding = target.padding,
viewport = target.viewport, viewport = target.viewport,
content = target.content; content = target.content;
addClass(host, classNameHost); addClass(host, classNameHost);
addClass(padding, classNamePadding);
addClass(viewport, classNameViewport); addClass(viewport, classNameViewport);
addClass(content, classNameContent); addClass(content, classNameContent);
return target; return target;
@@ -1306,10 +1300,10 @@
var host = osTarget.host; var host = osTarget.host;
lifecycles.push(createStructureLifecycle(osTarget)); lifecycles.push(createStructureLifecycle(osTarget));
var onSizeChanged = function onSizeChanged(direction) { var onSizeChanged = function onSizeChanged(directionCache) {
if (direction) { if (directionCache) {
each(lifecycles, function (lifecycle) { each(lifecycles, function (lifecycle) {
lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(direction); lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(directionCache);
}); });
} else { } else {
each(lifecycles, function (lifecycle) { each(lifecycles, function (lifecycle) {
@@ -1318,9 +1312,9 @@
} }
}; };
var onTrinsicChanged = function onTrinsicChanged(widthIntrinsic, heightIntrinsic) { var onTrinsicChanged = function onTrinsicChanged(widthIntrinsic, heightIntrinsicCache) {
each(lifecycles, function (lifecycle) { each(lifecycles, function (lifecycle) {
lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsic); lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsicCache);
}); });
}; };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,71 +1,67 @@
import { import {
CacheUpdateInfo,
CachePropsToUpdate,
Cache, Cache,
OptionsValidated,
OptionsWithOptionsTemplate, OptionsWithOptionsTemplate,
transformOptions, transformOptions,
validateOptions, validateOptions,
assignDeep, assignDeep,
createCache, hasOwnProperty,
isBoolean, isEmptyObject,
keys,
} from 'support'; } from 'support';
import { PlainObject } from 'typings'; import { CSSDirection, PlainObject } from 'typings';
interface LifecycleUpdateHints<O, C> { interface LifecycleBaseUpdateHints<O> {
_force?: boolean; _force?: boolean;
_changedOptions?: CachePropsToUpdate<O>; _changedOptions?: OptionsValidated<O>;
_changedCache?: CachePropsToUpdate<C>;
} }
interface AbstractLifecycle<O extends PlainObject> { export interface LifecycleBase<O extends PlainObject> {
_options(newOptions?: O): O; _options(newOptions?: O): O;
_update(force?: boolean): void; _update(force?: boolean): void;
} }
export interface Lifecycle<T extends PlainObject> extends AbstractLifecycle<T> { export interface Lifecycle<T extends PlainObject> extends LifecycleBase<T> {
_destruct(): void; _destruct(): void;
_onSizeChanged?(): void; _onSizeChanged?(): void;
_onDirectionChanged?(direction: 'ltr' | 'rtl'): void; _onDirectionChanged?(directionCache: Cache<CSSDirection>): void;
_onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsic: boolean): void; _onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>): void;
} }
export interface LifecycleBase<O extends PlainObject, C extends PlainObject> extends AbstractLifecycle<O> { export interface LifecycleOptionInfo<T> {
_updateCache(cachePropsToUpdate?: CachePropsToUpdate<C>): void; _value: T;
_changed: boolean;
} }
export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
const getPropByPath = <T>(obj: any, path: string): T =>
obj && path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj);
/** /**
* Creates a object which can be seen as the base of a lifecycle because it provides all the tools to manage a lifecycle and its options, cache and base functions. * Creates a object which can be seen as the base of a lifecycle because it provides all the tools to manage a lifecycle and its options, cache and base functions.
* @param defaultOptionsWithTemplate A object which describes the options and the default options of the lifecycle. * @param defaultOptionsWithTemplate A object which describes the options and the default options of the lifecycle.
* @param cacheUpdateInfo A object which describes how cache updates shall behave.
* @param initialOptions The initialOptions for the lifecylce. (Can be undefined) * @param initialOptions The initialOptions for the lifecylce. (Can be undefined)
* @param updateFunction The update function where cache and options updates are handled. Has two arguments which are the changedOptions and the changedCache objects. * @param updateFunction The update function where cache and options updates are handled. Has two arguments which are the changedOptions and the changedCache objects.
*/ */
export const createLifecycleBase = <O, C>( export const createLifecycleBase = <O>(
defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>, defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>,
cacheUpdateInfo: CacheUpdateInfo<C>,
initialOptions: O | undefined, initialOptions: O | undefined,
updateFunction: (options: Cache<O>, cache: Cache<C>) => any updateFunction: (force: boolean, checkOption: LifecycleCheckOption) => any
): LifecycleBase<O, C> => { ): LifecycleBase<O> => {
const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<O>>(defaultOptionsWithTemplate); const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<O>>(defaultOptionsWithTemplate);
const options: Required<O> = assignDeep( const options: Required<O> = assignDeep(
{}, {},
defaultOptions, defaultOptions,
validateOptions<O>(initialOptions || ({} as O), optionsTemplate, null, true)._validated validateOptions<O>(initialOptions || ({} as O), optionsTemplate, null, true)._validated
); );
const cacheChange = createCache<C>(cacheUpdateInfo);
const cacheOptions = createCache<O>(options, true);
const update = (hints: LifecycleUpdateHints<O, C>) => { const update = (hints: LifecycleBaseUpdateHints<O>) => {
const hasForce = isBoolean(hints._force); // indication that it was called from outside const { _force, _changedOptions } = hints;
const force = hints._force === true; const checkOption: LifecycleCheckOption = (path) => ({
_value: getPropByPath(options, path),
const changedCache = cacheChange(force ? null : hints._changedCache || (hasForce ? null : []), force); _changed: _force || getPropByPath(_changedOptions, path) !== undefined,
const changedOptions = cacheOptions(force ? null : hints._changedOptions, !!hints._changedOptions || force); });
updateFunction(!!_force, checkOption);
if (changedOptions._anythingChanged || changedCache._anythingChanged) {
updateFunction(changedOptions, changedCache);
}
}; };
update({ _force: true }); update({ _force: true });
@@ -73,18 +69,17 @@ export const createLifecycleBase = <O, C>(
return { return {
_options(newOptions?: O) { _options(newOptions?: O) {
if (newOptions) { if (newOptions) {
const { _validated: changedOptions } = validateOptions(newOptions, optionsTemplate, options, true); const { _validated: _changedOptions } = validateOptions(newOptions, optionsTemplate, options, true);
assignDeep(options, changedOptions);
update({ _changedOptions: keys(changedOptions) as CachePropsToUpdate<O> }); if (!isEmptyObject(_changedOptions)) {
assignDeep(options, _changedOptions);
update({ _changedOptions });
}
} }
return options; return options;
}, },
_update: (force?: boolean) => { _update: (_force?: boolean) => {
update({ _force: !!force }); update({ _force });
},
_updateCache: (cachePropsToUpdate?: CachePropsToUpdate<C>) => {
update({ _changedCache: cachePropsToUpdate });
}, },
}; };
}; };
@@ -1,4 +1,16 @@
import { cssProperty, runEach, topRightBottomLeft, TRBL, equalTRBL, optionsTemplateTypes as oTypes, OptionsTemplateValue, style } from 'support'; import {
Cache,
cssProperty,
runEach,
createCache,
topRightBottomLeft,
TRBL,
equalTRBL,
optionsTemplateTypes as oTypes,
OptionsTemplateValue,
style,
OptionsWithOptionsTemplate,
} from 'support';
import { OSTargetObject } from 'typings'; import { OSTargetObject } from 'typings';
import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase'; import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase';
import { getEnvironment, Environment } from 'environment'; import { getEnvironment, Environment } from 'environment';
@@ -11,11 +23,15 @@ export interface StructureLifecycleOptions {
y?: OverflowBehavior; y?: OverflowBehavior;
}; };
} }
interface StructureLifecycleCache {
padding: TRBL;
}
const overflowBehaviorAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden'; const overflowBehaviorAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'visible-hidden visible-scroll scroll hidden';
const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<StructureLifecycleOptions>> = {
paddingAbsolute: [false, oTypes.boolean],
overflowBehavior: {
x: ['scroll', overflowBehaviorAllowedValues],
y: ['scroll', overflowBehaviorAllowedValues],
},
};
const classNameHost = 'os-host'; const classNameHost = 'os-host';
const classNameViewport = 'os-viewport'; const classNameViewport = 'os-viewport';
@@ -38,58 +54,49 @@ export const createStructureLifecycle = (
// direction change is only needed to update scrollbar hiding, therefore its not needed if css can do it, scrollbars are invisible or overlaid on y axis // direction change is only needed to update scrollbar hiding, therefore its not needed if css can do it, scrollbars are invisible or overlaid on y axis
const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y; const directionObserverObsolete = (cssMarginEnd && cssBorderEnd) || supportsScrollbarStyling || scrollbarsOverlaid.y;
const { _options, _update, _updateCache } = createLifecycleBase<StructureLifecycleOptions, StructureLifecycleCache>( const updatePaddingCache = createCache(() => topRightBottomLeft(host, 'padding'), { _equal: equalTRBL });
{
paddingAbsolute: [false, oTypes.boolean],
overflowBehavior: {
x: ['scroll', overflowBehaviorAllowedValues],
y: ['scroll', overflowBehaviorAllowedValues],
},
},
{
padding: [() => topRightBottomLeft(host, 'padding'), equalTRBL],
},
initialOptions,
(options, cache) => {
const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = options.paddingAbsolute;
const { _value: padding, _changed: paddingChanged } = cache.padding;
if (paddingAbsoluteChanged || paddingChanged) { const { _options, _update } = createLifecycleBase<StructureLifecycleOptions>(defaultOptionsWithTemplate, initialOptions, (force, checkOption) => {
const paddingStyle: TRBL = { const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute');
t: 0, const { _value: padding, _changed: paddingChanged } = updatePaddingCache(force);
r: 0,
b: 0,
l: 0,
};
if (!paddingAbsolute) { if (paddingAbsoluteChanged || paddingChanged) {
paddingStyle.t = -padding!.t; const paddingStyle: TRBL = {
paddingStyle.r = -(padding!.r + padding!.l); t: 0,
paddingStyle.b = -(padding!.b + padding!.t); r: 0,
paddingStyle.l = -padding!.l; b: 0,
} l: 0,
};
if (!supportsScrollbarStyling) { if (!paddingAbsolute) {
paddingStyle.r -= env._nativeScrollbarSize.y; paddingStyle.t = -padding!.t;
paddingStyle.b -= env._nativeScrollbarSize.x; paddingStyle.r = -(padding!.r + padding!.l);
} paddingStyle.b = -(padding!.b + padding!.t);
paddingStyle.l = -padding!.l;
style(paddingElm, { top: paddingStyle.t, left: paddingStyle.l, 'margin-right': paddingStyle.r, 'margin-bottom': paddingStyle.b });
} }
console.log(options); // eslint-disable-line if (!supportsScrollbarStyling) {
console.log(cache); // eslint-disable-line paddingStyle.r -= env._nativeScrollbarSize.y;
paddingStyle.b -= env._nativeScrollbarSize.x;
}
style(paddingElm, {
top: paddingStyle.t,
left: paddingStyle.l,
'margin-right': paddingStyle.r,
'margin-bottom': paddingStyle.b,
'max-width': `calc(100% + ${paddingStyle.r * -1}px)`,
});
} }
); });
const onSizeChanged = () => { const onSizeChanged = () => {
_updateCache('padding'); _update();
}; };
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsic: boolean) => { const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => {
if (heightIntrinsic) { const { _changed, _value } = heightIntrinsicCache;
style(content, { height: 'auto' }); if (_changed) {
} else { style(content, { height: _value ? 'auto' : '100%' });
style(content, { height: '100%' });
} }
}; };
@@ -1,4 +1,6 @@
import { import {
Cache,
createCache,
createDOM, createDOM,
style, style,
appendChildren, appendChildren,
@@ -16,6 +18,7 @@ import {
isString, isString,
equalWH, equalWH,
} from 'support'; } from 'support';
import { CSSDirection } from 'typings';
import { getEnvironment } from 'environment'; import { getEnvironment } from 'environment';
const animationStartEventName = 'animationstart'; const animationStartEventName = 'animationstart';
@@ -30,15 +33,12 @@ const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item
const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`; const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
const cAF = cancelAnimationFrame; const cAF = cancelAnimationFrame;
const rAF = requestAnimationFrame; const rAF = requestAnimationFrame;
const getDirection = (elm: HTMLElement) => style(elm, 'direction'); const getDirection = (elm: HTMLElement): CSSDirection => style(elm, 'direction') as CSSDirection;
// TODO:
// 1. MAYBE add comparison function to offsetSize etc.
type Direction = 'ltr' | 'rtl';
export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean }; export type SizeObserverOptions = { _direction?: boolean; _appear?: boolean };
export const createSizeObserver = ( export const createSizeObserver = (
target: HTMLElement, target: HTMLElement,
onSizeChangedCallback: (direction?: Direction) => any, onSizeChangedCallback: (directionCache?: Cache<CSSDirection>) => any,
options?: SizeObserverOptions options?: SizeObserverOptions
): (() => void) => { ): (() => void) => {
const { _direction: direction = false, _appear: appear = false } = options || {}; const { _direction: direction = false, _appear: appear = false } = options || {};
@@ -46,13 +46,13 @@ export const createSizeObserver = (
const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`); const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`);
const sizeObserver = baseElements[0] as HTMLElement; const sizeObserver = baseElements[0] as HTMLElement;
const listenerElement = sizeObserver.firstChild as HTMLElement; const listenerElement = sizeObserver.firstChild as HTMLElement;
const onSizeChangedCallbackProxy = (dir?: Direction) => { const onSizeChangedCallbackProxy = (directionCache?: Cache<CSSDirection>) => {
if (direction) { if (direction) {
const rtl = getDirection(sizeObserver) === 'rtl'; const rtl = getDirection(sizeObserver) === 'rtl';
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount); scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
scrollTop(sizeObserver, scrollAmount); scrollTop(sizeObserver, scrollAmount);
} }
onSizeChangedCallback(isString(dir) ? dir : undefined); onSizeChangedCallback(isString((directionCache || {})._value) ? directionCache : undefined);
}; };
const offListeners: (() => void)[] = []; const offListeners: (() => void)[] = [];
let appearCallback: ((...args: any) => any) | null = appear ? onSizeChangedCallbackProxy : null; let appearCallback: ((...args: any) => any) | null = appear ? onSizeChangedCallbackProxy : null;
@@ -83,14 +83,12 @@ export const createSizeObserver = (
scrollLeft(shrinkElement, scrollAmount); scrollLeft(shrinkElement, scrollAmount);
scrollTop(shrinkElement, scrollAmount); scrollTop(shrinkElement, scrollAmount);
}; };
const onResized = function () { const onResized = () => {
rAFId = 0; rAFId = 0;
if (!isDirty) { if (isDirty) {
return; cacheSize = currSize;
onSizeChangedCallbackProxy();
} }
cacheSize = currSize;
onSizeChangedCallbackProxy();
}; };
const onScroll = (scrollEvent?: Event) => { const onScroll = (scrollEvent?: Event) => {
currSize = offsetSize(listenerElement); currSize = offsetSize(listenerElement);
@@ -104,6 +102,7 @@ export const createSizeObserver = (
} }
reset(); reset();
if (scrollEvent) { if (scrollEvent) {
preventDefault(scrollEvent); preventDefault(scrollEvent);
stopPropagation(scrollEvent); stopPropagation(scrollEvent);
@@ -124,19 +123,18 @@ export const createSizeObserver = (
} }
if (direction) { if (direction) {
let dirCache: string | undefined; const updateDirectionCache = createCache(() => getDirection(sizeObserver));
offListeners.push( offListeners.push(
on(sizeObserver, scrollEventName, (event: Event) => { on(sizeObserver, scrollEventName, (event: Event) => {
const dir = getDirection(sizeObserver); const directionCache = updateDirectionCache();
const changed = dir !== dirCache; const { _value, _changed } = directionCache;
if (changed) { if (_changed) {
if (dir === 'rtl') { if (_value === 'rtl') {
style(listenerElement, { left: 'auto', right: 0 }); style(listenerElement, { left: 'auto', right: 0 });
} else { } else {
style(listenerElement, { left: 0, right: 'auto' }); style(listenerElement, { left: 0, right: 'auto' });
} }
dirCache = dir; onSizeChangedCallbackProxy(directionCache);
onSizeChangedCallbackProxy(dir as Direction);
} }
preventDefault(event); preventDefault(event);
@@ -1,4 +1,4 @@
import { createDOM, offsetSize, jsAPI, runEach, prependChildren, removeElements } from 'support'; import { WH, Cache, createDOM, offsetSize, jsAPI, runEach, prependChildren, removeElements, createCache } from 'support';
import { createSizeObserver } from 'observers/sizeObserver'; import { createSizeObserver } from 'observers/sizeObserver';
const classNameTrinsicObserver = 'os-trinsic-observer'; const classNameTrinsicObserver = 'os-trinsic-observer';
@@ -6,11 +6,19 @@ const IntersectionObserverConstructor = jsAPI('IntersectionObserver');
export const createTrinsicObserver = ( export const createTrinsicObserver = (
target: HTMLElement, target: HTMLElement,
onTrinsicChangedCallback: (widthIntrinsic: boolean, heightIntrinsic: boolean) => any onTrinsicChangedCallback: (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => any
): (() => void) => { ): (() => void) => {
const trinsicObserver = createDOM(`<div class="${classNameTrinsicObserver}"></div>`)[0] as HTMLElement; const trinsicObserver = createDOM(`<div class="${classNameTrinsicObserver}"></div>`)[0] as HTMLElement;
const offListeners: (() => void)[] = []; const offListeners: (() => void)[] = [];
let heightIntrinsic = false; const updateHeightIntrinsicCache = createCache<boolean, IntersectionObserverEntry | WH<number>>(
(ioEntryOrSize) =>
(ioEntryOrSize! as WH<number>).h === 0 ||
(ioEntryOrSize! as IntersectionObserverEntry).isIntersecting ||
(ioEntryOrSize! as IntersectionObserverEntry).intersectionRatio > 0,
{
_initialValue: false,
}
);
if (IntersectionObserverConstructor) { if (IntersectionObserverConstructor) {
const intersectionObserverInstance: IntersectionObserver = new IntersectionObserverConstructor( const intersectionObserverInstance: IntersectionObserver = new IntersectionObserverConstructor(
@@ -18,11 +26,10 @@ export const createTrinsicObserver = (
if (entries && entries.length > 0) { if (entries && entries.length > 0) {
const last = entries.pop(); const last = entries.pop();
if (last) { if (last) {
const newHeightIntrinsic = last.isIntersecting || last.intersectionRatio > 0; const heightIntrinsicCache = updateHeightIntrinsicCache(0, last);
if (newHeightIntrinsic !== heightIntrinsic) { if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(false, newHeightIntrinsic); onTrinsicChangedCallback(false, heightIntrinsicCache);
heightIntrinsic = newHeightIntrinsic;
} }
} }
} }
@@ -35,11 +42,10 @@ export const createTrinsicObserver = (
offListeners.push( offListeners.push(
createSizeObserver(trinsicObserver, () => { createSizeObserver(trinsicObserver, () => {
const newSize = offsetSize(trinsicObserver); const newSize = offsetSize(trinsicObserver);
const newHeightIntrinsic = newSize.h === 0; const heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
if (newHeightIntrinsic !== heightIntrinsic) { if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(false, newHeightIntrinsic); onTrinsicChangedCallback(false, heightIntrinsicCache);
heightIntrinsic = newHeightIntrinsic;
} }
}) })
); );
@@ -1,6 +1,6 @@
import { OSTarget, OSTargetObject } from 'typings'; import { OSTarget, OSTargetObject, CSSDirection } from 'typings';
import { createStructureLifecycle } from 'lifecycles/structureLifecycle'; import { createStructureLifecycle } from 'lifecycles/structureLifecycle';
import { appendChildren, addClass, contents, is, isHTMLElement, createDiv, each } from 'support'; import { Cache, appendChildren, addClass, contents, is, isHTMLElement, createDiv, each } from 'support';
import { createSizeObserver } from 'observers/sizeObserver'; import { createSizeObserver } from 'observers/sizeObserver';
import { createTrinsicObserver } from 'observers/trinsicObserver'; import { createTrinsicObserver } from 'observers/trinsicObserver';
import { Lifecycle } from 'lifecycles/lifecycleBase'; import { Lifecycle } from 'lifecycles/lifecycleBase';
@@ -51,10 +51,10 @@ const OverlayScrollbars = (target: OSTarget, options?: any, extensions?: any): v
lifecycles.push(createStructureLifecycle(osTarget)); lifecycles.push(createStructureLifecycle(osTarget));
// eslint-disable-next-line // eslint-disable-next-line
const onSizeChanged = (direction?: 'ltr' | 'rtl') => { const onSizeChanged = (directionCache?: Cache<CSSDirection>) => {
if (direction) { if (directionCache) {
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(direction); lifecycle._onDirectionChanged && lifecycle._onDirectionChanged(directionCache);
}); });
} else { } else {
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
@@ -62,9 +62,9 @@ const OverlayScrollbars = (target: OSTarget, options?: any, extensions?: any): v
}); });
} }
}; };
const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsic: boolean) => { const onTrinsicChanged = (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => {
each(lifecycles, (lifecycle) => { each(lifecycles, (lifecycle) => {
lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsic); lifecycle._onTrinsicChanged && lifecycle._onTrinsicChanged(widthIntrinsic, heightIntrinsicCache);
}); });
}; };
+35 -92
View File
@@ -1,95 +1,38 @@
import { isArray, isString } from 'support/utils/types'; export interface Cache<T> {
import { assignDeep, keys } from 'support/utils/object'; readonly _value?: T;
import { each } from 'support/utils/array'; readonly _previous?: T;
readonly _changed: boolean;
type UpdateCacheProp<T> = <P extends keyof T>(prop: P, value: T[P], compare: EqualCachePropFunction<T, P> | null) => void;
type UpdateCachePropFunction<T, P extends keyof T> = (current?: T[P], previous?: T[P]) => T[P];
type EqualCachePropFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => boolean;
export interface CacheEntry<T> {
_value?: T;
_previous?: T;
_changed: boolean;
} }
export type Cache<T> = { export interface CacheOptions<T> {
[P in keyof T]: CacheEntry<T[P]>; _equal?: EqualCachePropFunction<T>;
}; _initialValue?: T;
export type CacheUpdated<T> = Cache<T> & { _anythingChanged: boolean };
export type CachePropsToUpdate<T> = Array<keyof T> | keyof T;
export type CacheUpdate<T> = (propsToUpdate?: CachePropsToUpdate<T> | null, force?: boolean) => CacheUpdated<T>;
export type CacheUpdateInfo<T> = {
[P in keyof T]: UpdateCachePropFunction<T, P> | [UpdateCachePropFunction<T, P>, EqualCachePropFunction<T, P>];
};
/**
* Creates a internally managed generic cache which can be updated by the returned function.
* @param cacheUpdateInfo A object which accepts a function or a tuple of functions as values for its properties.
* {
* name: updateFn,
* // or
* name: [updateFn, equalFn]
* }
* The first function is the update function (updateFn) which is executed when this cache prop shall be updated.
* Two params are passed, the first one is the current cache value and the second one is the previous cache value.
*
* The second function is the equal function (equalFn) which is also executed when this cache prop shall be updated,
* but returns a boolean which indicates whether the current value and the new updated value are equal.
* If no equal function is passed a shallow comparison is carried out between the values.
*
* @returns A function which can be called with wither one ar an array of properties which shall be updated. Optionally it can be called with the force param.
* This function returns a object which represents the cache and its state at the time of updating (changed to previous value, current value and previous value).
*/
export function createCache<T>(cacheUpdateInfo: CacheUpdateInfo<T>): CacheUpdate<T>;
export function createCache<T>(referenceObj: T, isReference: true): CacheUpdate<T>;
export function createCache<T>(cacheUpdateInfo: CacheUpdateInfo<T> | T, isReference?: true): CacheUpdate<T> {
const cache: Cache<T> = {} as any;
const allProps: Array<keyof T> = keys(cacheUpdateInfo) as Array<keyof T>;
each(allProps, (prop) => {
cache[prop] = { _changed: false, _value: isReference ? cacheUpdateInfo[prop] : undefined } as any;
});
const updateCacheProp: UpdateCacheProp<T> = (prop, value, equal): void => {
const curr = cache[prop]._value;
cache[prop]._value = value;
cache[prop]._previous = curr;
cache[prop]._changed = equal ? !equal(curr, value) : curr !== value;
};
const flush = (props: Array<keyof T>, force?: boolean): CacheUpdated<T> => {
const result: CacheUpdated<T> = assignDeep({}, cache, { _anythingChanged: false });
each(props, (prop: keyof T) => {
const changed = force || cache[prop]._changed;
result._anythingChanged = result._anythingChanged || changed;
result[prop]._changed = changed;
cache[prop]._changed = false;
});
return result;
};
return (propsToUpdate, force) => {
const finalPropsToUpdate: Array<keyof T> =
(isString(propsToUpdate) ? ([propsToUpdate] as Array<keyof T>) : (propsToUpdate as Array<keyof T>)) || allProps;
each(finalPropsToUpdate, (prop) => {
const cacheVal = cache[prop];
const curr = cacheUpdateInfo[prop];
const arr = isReference ? false : isArray(curr);
const value = arr ? curr[0] : curr;
const equal = arr ? curr[1] : null;
updateCacheProp(prop, isReference ? value : value(cacheVal._value, cacheVal._previous), equal);
});
return flush(finalPropsToUpdate, force);
};
} }
export type CacheUpdate<T, C> = (force?: boolean | 0, context?: C) => Cache<T>;
export type UpdateCachePropFunction<T, C> = (context?: C, current?: T, previous?: T) => T;
export type EqualCachePropFunction<T> = (a?: T, b?: T) => boolean;
export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T, C>, options?: CacheOptions<T>): CacheUpdate<T, C> => {
const { _equal, _initialValue } = options || {};
let _value: T | undefined = _initialValue;
let _previous: T | undefined;
return (force, context) => {
const prev = _value;
const newVal = update(context, _value, _previous);
const changed = force || (_equal ? !_equal(prev, newVal) : prev !== newVal);
if (changed) {
_value = newVal;
_previous = prev;
}
return {
_value,
_previous,
_changed: changed,
};
};
};
@@ -2,6 +2,7 @@ import { each, from } from 'support/utils/array';
const matches = (elm: Element | null, selector: string): boolean => { const matches = (elm: Element | null, selector: string): boolean => {
if (elm) { if (elm) {
/* istanbul ignore next */
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
const fn = Element.prototype.matches || Element.prototype.msMatchesSelector; const fn = Element.prototype.matches || Element.prototype.msMatchesSelector;
@@ -12,6 +12,8 @@ export interface OSTargetObject {
export type OSTarget = OSTargetElement | OSTargetObject; export type OSTarget = OSTargetElement | OSTargetObject;
export type CSSDirection = 'ltr' | 'rtl';
/* /*
export namespace OverlayScrollbars { export namespace OverlayScrollbars {
export type ResizeBehavior = 'none' | 'both' | 'horizontal' | 'vertical'; export type ResizeBehavior = 'none' | 'both' | 'horizontal' | 'vertical';
@@ -1,4 +1,4 @@
import { optionsTemplateTypes as oTypes, Cache } from 'support'; import { optionsTemplateTypes as oTypes } from 'support';
import { createLifecycleBase } from 'lifecycles/lifecycleBase'; import { createLifecycleBase } from 'lifecycles/lifecycleBase';
interface TestLifecycleOptions { interface TestLifecycleOptions {
@@ -9,17 +9,9 @@ interface TestLifecycleOptions {
number?: number; number?: number;
}; };
} }
interface TestLifecycleCache {
number?: number;
constant?: boolean;
object?: {
string?: string;
boolean?: boolean;
};
}
const createLifecycle = (initalOptions?: TestLifecycleOptions, updateFn?: () => any) => const createLifecycle = (initalOptions?: TestLifecycleOptions, updateFn?: (...args: any) => any) =>
createLifecycleBase<TestLifecycleOptions, TestLifecycleCache>( createLifecycleBase<TestLifecycleOptions>(
{ {
number: [0, oTypes.number], number: [0, oTypes.number],
string: ['hi', oTypes.string], string: ['hi', oTypes.string],
@@ -28,28 +20,10 @@ const createLifecycle = (initalOptions?: TestLifecycleOptions, updateFn?: () =>
number: [0, oTypes.number], number: [0, oTypes.number],
}, },
}, },
{
number: (current) => (current || 0) + 1,
constant: () => false,
object: (current) => ({ string: `${current?.string || ''}hi`, boolean: !current?.boolean }),
},
initalOptions, initalOptions,
updateFn || (() => {}) updateFn || (() => {})
); );
const createOptionsUnchangedObj = (exc?: Cache<TestLifecycleOptions>) =>
expect.objectContaining({
number: exc?.number || expect.objectContaining({ _changed: false }),
string: exc?.string || expect.objectContaining({ _changed: false }),
nested: exc?.nested || expect.objectContaining({ _changed: false }),
});
const createCacheUnchangedObj = (exc?: Cache<TestLifecycleCache>) =>
expect.objectContaining({
number: exc?.number || expect.objectContaining({ _changed: false }),
constant: exc?.constant || expect.objectContaining({ _changed: false }),
object: exc?.object || expect.objectContaining({ _changed: false }),
});
describe('lifecycleBase', () => { describe('lifecycleBase', () => {
describe('options', () => { describe('options', () => {
test('correct default options', () => { test('correct default options', () => {
@@ -112,395 +86,160 @@ describe('lifecycleBase', () => {
}); });
}); });
describe('cache', () => {
test('single value cache change', () => {
const updateFn = jest.fn();
const { _updateCache } = createLifecycle({}, updateFn);
_updateCache('number');
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toHaveBeenLastCalledWith(
expect.objectContaining({}),
expect.objectContaining({
number: {
_value: 2,
_changed: true,
_previous: 1,
},
constant: expect.objectContaining({
_changed: false,
}),
})
);
_updateCache('constant');
expect(updateFn).toBeCalledTimes(2);
});
test('multiple value cache change', () => {
const updateFn = jest.fn();
const { _updateCache } = createLifecycle({}, updateFn);
_updateCache(['number', 'object']);
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toHaveBeenLastCalledWith(
expect.objectContaining({}),
expect.objectContaining({
number: {
_value: 2,
_previous: 1,
_changed: true,
},
object: {
_value: { string: 'hihi', boolean: false },
_previous: { string: 'hi', boolean: true },
_changed: true,
},
})
);
_updateCache(['number', 'constant']);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toHaveBeenLastCalledWith(
expect.objectContaining({}),
expect.objectContaining({
number: {
_value: 3,
_previous: 2,
_changed: true,
},
constant: expect.objectContaining({
_changed: false,
}),
})
);
_updateCache(['constant']);
expect(updateFn).toBeCalledTimes(3);
});
});
describe('update', () => { describe('update', () => {
test('initial call', () => { test('initial call', () => {
const updateFn = jest.fn(); const updateFn = jest.fn();
createLifecycle({}, updateFn); createLifecycle({}, updateFn);
expect(updateFn).toBeCalledTimes(1); expect(updateFn).toBeCalledTimes(1);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(true, expect.objectContaining({}));
expect.objectContaining({
number: expect.objectContaining({
_value: 0,
_changed: true,
}),
string: expect.objectContaining({
_value: 'hi',
_changed: true,
}),
nested: expect.objectContaining({
_value: {
boolean: false,
number: 0,
},
_changed: true,
}),
}),
expect.objectContaining({
number: expect.objectContaining({
_value: 1,
_changed: true,
}),
constant: expect.objectContaining({
_value: false,
_changed: true,
}),
object: expect.objectContaining({
_value: {
string: 'hi',
boolean: true,
},
_changed: true,
}),
})
);
}); });
test('updates correctly on options change', () => { test('updates correctly on options change', () => {
let checkOption = (...args: any): any => {}; // eslint-disable-line
const updateFn = jest.fn(); const updateFn = jest.fn();
const { _options } = createLifecycle({}, updateFn); const update = (force: any, check: any): void => {
updateFn(force, check);
checkOption = check;
};
const { _options } = createLifecycle({}, update);
_options({ number: 5 }); _options({ number: 5 });
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(false, expect.objectContaining({}));
createOptionsUnchangedObj({ let { _value, _changed } = checkOption('number');
number: { expect(_value).toBe(5);
_value: 5, expect(_changed).toBe(true);
_previous: 0, ({ _value, _changed } = checkOption('string'));
_changed: true, expect(_value).toBe('hi');
}, expect(_changed).toBe(false);
}), ({ _value, _changed } = checkOption('nested.boolean'));
createCacheUnchangedObj() expect(_value).toBe(false);
); expect(_changed).toBe(false);
({ _value, _changed } = checkOption('nested.number'));
expect(_value).toBe(0);
expect(_changed).toBe(false);
_options({ number: 5, string: 'test', nested: { number: 3 } }); _options({ number: 5, string: 'test', nested: { number: 3 } });
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(false, expect.objectContaining({}));
createOptionsUnchangedObj({ ({ _value, _changed } = checkOption('number'));
string: { expect(_value).toBe(5);
_value: 'test', expect(_changed).toBe(false);
_previous: 'hi', ({ _value, _changed } = checkOption('string'));
_changed: true, expect(_value).toBe('test');
}, expect(_changed).toBe(true);
nested: { ({ _value, _changed } = checkOption('nested.boolean'));
_value: expect.objectContaining({ number: 3 }), expect(_value).toBe(false);
_previous: expect.objectContaining({ number: 3 }), // because reference, number is 3 instead of expected 0 expect(_changed).toBe(false);
_changed: true, ({ _value, _changed } = checkOption('nested.number'));
}, expect(_value).toBe(3);
}), expect(_changed).toBe(true);
createCacheUnchangedObj()
);
_options({ string: 'test', nested: { number: 3 } }); _options({ string: 'test', nested: { number: 3 } });
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
}); });
test('updates correctly on cache change', () => {
const updateFn = jest.fn();
const { _updateCache } = createLifecycle({}, updateFn);
_updateCache('number');
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj(),
createCacheUnchangedObj({
number: {
_value: 2,
_previous: 1,
_changed: true,
},
})
);
_updateCache(['number', 'object', 'constant']);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj(),
createCacheUnchangedObj({
number: {
_value: 3,
_previous: 2,
_changed: true,
},
object: {
_value: { string: 'hihi', boolean: false },
_previous: { string: 'hi', boolean: true },
_changed: true,
},
})
);
_updateCache('constant');
expect(updateFn).toBeCalledTimes(3);
});
test('updates correctly on update call', () => { test('updates correctly on update call', () => {
let checkOption = (...args: any): any => {}; // eslint-disable-line
const updateFn = jest.fn(); const updateFn = jest.fn();
const { _update, _options } = createLifecycle({}, updateFn); const update = (force: any, check: any): void => {
updateFn(force, check);
checkOption = check;
};
const { _update, _options } = createLifecycle({}, update);
_update(); _update();
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(false, expect.objectContaining({}));
createOptionsUnchangedObj(), let { _value, _changed } = checkOption('number');
createCacheUnchangedObj({ expect(_value).toBe(0);
number: { expect(_changed).toBe(false);
_value: 2, ({ _value, _changed } = checkOption('string'));
_previous: 1, expect(_value).toBe('hi');
_changed: true, expect(_changed).toBe(false);
}, ({ _value, _changed } = checkOption('nested.boolean'));
object: { expect(_value).toBe(false);
_value: { string: 'hihi', boolean: false }, expect(_changed).toBe(false);
_previous: { string: 'hi', boolean: true }, ({ _value, _changed } = checkOption('nested.number'));
_changed: true, expect(_value).toBe(0);
}, expect(_changed).toBe(false);
})
);
_update(true); _update(true);
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(true, expect.objectContaining({}));
expect.objectContaining({ ({ _value, _changed } = checkOption('number'));
number: expect.objectContaining({ expect(_value).toBe(0);
_value: 0, expect(_changed).toBe(true);
_changed: true, ({ _value, _changed } = checkOption('string'));
}), expect(_value).toBe('hi');
string: expect.objectContaining({ expect(_changed).toBe(true);
_value: 'hi', ({ _value, _changed } = checkOption('nested.boolean'));
_changed: true, expect(_value).toBe(false);
}), expect(_changed).toBe(true);
nested: expect.objectContaining({ ({ _value, _changed } = checkOption('nested.number'));
_value: { expect(_value).toBe(0);
boolean: false, expect(_changed).toBe(true);
number: 0,
},
_changed: true,
}),
}),
expect.objectContaining({
number: {
_value: 3,
_previous: 2,
_changed: true,
},
constant: {
_value: false,
_previous: false,
_changed: true,
},
object: {
_value: {
string: 'hihihi',
boolean: true,
},
_previous: {
string: 'hihi',
boolean: false,
},
_changed: true,
},
})
);
_options({ number: 3, nested: { boolean: true } }); _options({ number: 3, nested: { boolean: true } });
_update(true); _update(true);
expect(updateFn).toBeCalledTimes(5); expect(updateFn).toBeCalledTimes(5);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(true, expect.objectContaining({}));
expect.objectContaining({ ({ _value, _changed } = checkOption('number'));
number: expect.objectContaining({ expect(_value).toBe(3);
_value: 3, expect(_changed).toBe(true);
_changed: true, ({ _value, _changed } = checkOption('string'));
}), expect(_value).toBe('hi');
string: expect.objectContaining({ expect(_changed).toBe(true);
_value: 'hi', ({ _value, _changed } = checkOption('nested.boolean'));
_changed: true, expect(_value).toBe(true);
}), expect(_changed).toBe(true);
nested: expect.objectContaining({ ({ _value, _changed } = checkOption('nested.number'));
_value: { expect(_value).toBe(0);
boolean: true, expect(_changed).toBe(true);
number: 0,
},
_changed: true,
}),
}),
expect.objectContaining({
number: {
_value: 4,
_previous: 3,
_changed: true,
},
constant: {
_value: false,
_previous: false,
_changed: true,
},
object: {
_value: {
string: 'hihihihi',
boolean: false,
},
_previous: {
string: 'hihihi',
boolean: true,
},
_changed: true,
},
})
);
_options({ number: 3, nested: { boolean: true } }); _options({ number: 3, nested: { boolean: true } });
_update(); _update();
expect(updateFn).toBeCalledTimes(6); expect(updateFn).toBeCalledTimes(6);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(false, expect.objectContaining({}));
createOptionsUnchangedObj({ ({ _value, _changed } = checkOption('number'));
number: expect.objectContaining({ expect(_value).toBe(3);
_value: 3, expect(_changed).toBe(false);
_changed: false, ({ _value, _changed } = checkOption('string'));
}), expect(_value).toBe('hi');
string: expect.objectContaining({ expect(_changed).toBe(false);
_value: 'hi', ({ _value, _changed } = checkOption('nested.boolean'));
_changed: false, expect(_value).toBe(true);
}), expect(_changed).toBe(false);
nested: expect.objectContaining({ ({ _value, _changed } = checkOption('nested.number'));
_value: { expect(_value).toBe(0);
boolean: true, expect(_changed).toBe(false);
number: 0,
},
_changed: false,
}),
}),
createCacheUnchangedObj({
number: {
_value: 5,
_previous: 4,
_changed: true,
},
object: {
_value: { string: 'hihihihihi', boolean: true },
_previous: { string: 'hihihihi', boolean: false },
_changed: true,
},
})
);
_options({ number: 4, nested: { boolean: false }, string: 'hi' }); _options({ number: 4, nested: { boolean: false }, string: 'hi' });
expect(updateFn).toBeCalledTimes(7); expect(updateFn).toBeCalledTimes(7);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(false, expect.objectContaining({}));
createOptionsUnchangedObj({ ({ _value, _changed } = checkOption('number'));
number: expect.objectContaining({ expect(_value).toBe(4);
_value: 4, expect(_changed).toBe(true);
_changed: true, ({ _value, _changed } = checkOption('string'));
}), expect(_value).toBe('hi');
string: expect.objectContaining({ expect(_changed).toBe(false);
_value: 'hi', ({ _value, _changed } = checkOption('nested.boolean'));
_changed: false, expect(_value).toBe(false);
}), expect(_changed).toBe(true);
nested: expect.objectContaining({ ({ _value, _changed } = checkOption('nested.number'));
_value: { expect(_value).toBe(0);
boolean: false, expect(_changed).toBe(false);
number: 0,
},
_changed: true,
}),
}),
createCacheUnchangedObj()
);
_update(); _update();
expect(updateFn).toBeCalledTimes(8); expect(updateFn).toBeCalledTimes(8);
expect(updateFn).toHaveBeenLastCalledWith( expect(updateFn).toHaveBeenLastCalledWith(false, expect.objectContaining({}));
createOptionsUnchangedObj({
number: expect.objectContaining({ _options();
_value: 4, expect(updateFn).toBeCalledTimes(8);
_changed: false,
}), _options({ number: 4, nested: { boolean: false }, string: 'hi' });
string: expect.objectContaining({ expect(updateFn).toBeCalledTimes(8);
_value: 'hi',
_changed: false,
}),
nested: expect.objectContaining({
_value: {
boolean: false,
number: 0,
},
_changed: false,
}),
}),
createCacheUnchangedObj({
number: expect.objectContaining({
_changed: true,
}),
object: expect.objectContaining({
_changed: true,
}),
})
);
}); });
}); });
}); });
@@ -1,10 +1,10 @@
import { createCache } from 'support/cache'; import { createCache } from 'support/cache';
const createUpdater = <T>(updaterReturn: (i: number) => T) => { const createUpdater = <T, C = unknown>(updaterReturn: (i: number) => T) => {
const fn = jest.fn(); const fn = jest.fn();
let index = 0; let index = 0;
const update = (curr?: T, prev?: T): T => { const update = (context?: C, curr?: T, prev?: T): T => {
fn(curr, prev); fn(context, curr, prev);
index += 1; index += 1;
return updaterReturn(index); return updaterReturn(index);
}; };
@@ -13,360 +13,223 @@ const createUpdater = <T>(updaterReturn: (i: number) => T) => {
}; };
describe('cache', () => { describe('cache', () => {
describe('cache with cacheUpdateInfo object', () => { test('creates and updates cache', () => {
test('creates and updates simple cache', () => { const [fn, updater] = createUpdater((i) => `${i}`);
interface Test { const update = createCache<string>(updater);
number: number;
boolean: boolean;
string: string;
object: {};
}
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i);
const [updateBooleanFn, updateBoolean] = createUpdater<boolean>((i) => !!(i % 2));
const [updateStringFn, updateString] = createUpdater<string>((i) => `${i}`);
const [updateObjFn, updateObj] = createUpdater<object>((i) => ({ [i]: i }));
const updateCache = createCache<Test>({ let { _value, _previous, _changed } = update();
number: updateNumber, expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
boolean: updateBoolean, expect(_value).toBe('1');
string: updateString, expect(_previous).toBe(undefined);
object: updateObj, expect(_changed).toBe(true);
});
expect(updateCache('number').number._value).toBe(1); ({ _value, _previous, _changed } = update());
expect(updateNumberFn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenLastCalledWith(undefined, '1', undefined);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined); expect(_value).toBe('2');
expect(_previous).toBe('1');
expect(_changed).toBe(true);
});
expect(updateCache('number').number._value).toBe(2); test('creates and updates cache with context', () => {
expect(updateNumberFn).toHaveBeenCalledTimes(2); interface ContextObj {
expect(updateNumberFn).toHaveBeenLastCalledWith(1, undefined); test: string;
even: number;
}
const updateFn = jest.fn();
const updater = (context?: ContextObj, current?: boolean, previous?: boolean) => {
updateFn(context, current, previous);
return context!.test === 'test' || context!.even % 2 === 0;
};
const update = createCache(updater);
const firstCtx = { test: 'test', even: 2 };
expect(updateCache('number').number._value).toBe(3); let { _value, _previous, _changed } = update(0, firstCtx);
expect(updateNumberFn).toHaveBeenCalledTimes(3); expect(updateFn).toHaveBeenLastCalledWith(firstCtx, undefined, undefined);
expect(updateNumberFn).toHaveBeenLastCalledWith(2, 1); expect(_value).toBe(true);
expect(_previous).toBe(undefined);
expect(_changed).toBe(true);
let { string, boolean, object, number } = updateCache('number'); ({ _value, _previous, _changed } = update(0, firstCtx));
expect(string._value).toBe(undefined); expect(updateFn).toHaveBeenLastCalledWith(firstCtx, true, undefined);
expect(string._changed).toBe(false); expect(_value).toBe(true);
expect(boolean._value).toBe(undefined); expect(_previous).toBe(undefined);
expect(boolean._changed).toBe(false); expect(_changed).toBe(false);
expect(object._value).toBe(undefined);
expect(object._changed).toBe(false);
expect(number._value).toBe(4);
expect(number._changed).toBe(true);
expect(updateBooleanFn).not.toHaveBeenCalled(); const scndCtx = { test: 'nah', even: 1 };
expect(updateStringFn).not.toHaveBeenCalled();
expect(updateObjFn).not.toHaveBeenCalled();
({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object'])); ({ _value, _previous, _changed } = update(0, scndCtx));
expect(string._value).toBe('1'); expect(updateFn).toHaveBeenLastCalledWith(scndCtx, true, undefined);
expect(string._changed).toBe(true); expect(_value).toBe(false);
expect(boolean._value).toBe(!!(1 % 2)); expect(_previous).toBe(true);
expect(boolean._changed).toBe(true); expect(_changed).toBe(true);
expect(object._value).toEqual({ 1: 1 });
expect(object._changed).toEqual(true);
expect(number._value).toBe(4);
expect(number._changed).toBe(false);
expect(updateBooleanFn).toHaveBeenCalledTimes(1); ({ _value, _previous, _changed } = update(0, scndCtx));
expect(updateBooleanFn).toHaveBeenLastCalledWith(undefined, undefined); expect(updateFn).toHaveBeenLastCalledWith(scndCtx, false, true);
expect(_value).toBe(false);
expect(_previous).toBe(true);
expect(_changed).toBe(false);
expect(updateStringFn).toHaveBeenCalledTimes(1); ({ _value, _previous, _changed } = update(true, scndCtx));
expect(updateStringFn).toHaveBeenLastCalledWith(undefined, undefined); expect(updateFn).toHaveBeenLastCalledWith(scndCtx, false, true);
expect(_value).toBe(false);
expect(_previous).toBe(false);
expect(_changed).toBe(true);
});
expect(updateObjFn).toHaveBeenCalledTimes(1); describe('equal', () => {
expect(updateObjFn).toHaveBeenLastCalledWith(undefined, undefined); test('with equal always true', () => {
const [fn, updater] = createUpdater((i) => i);
const update = createCache<number>(updater, { _equal: () => true });
updateCache(['string', 'boolean', 'object']); let { _value, _previous, _changed } = update();
expect(updateBooleanFn).toHaveBeenCalledTimes(2); expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
expect(updateBooleanFn).toHaveBeenLastCalledWith(!!(1 % 2), undefined); expect(_value).toBe(undefined);
expect(_previous).toBe(undefined);
expect(_changed).toBe(false);
expect(updateStringFn).toHaveBeenCalledTimes(2); ({ _value, _previous, _changed } = update());
expect(updateStringFn).toHaveBeenLastCalledWith('1', undefined); expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
expect(_value).toBe(undefined);
expect(updateObjFn).toHaveBeenCalledTimes(2); expect(_previous).toBe(undefined);
expect(updateObjFn).toHaveBeenLastCalledWith({ 1: 1 }, undefined); expect(_changed).toBe(false);
updateCache(['string', 'boolean', 'object']);
expect(updateBooleanFn).toHaveBeenCalledTimes(3);
expect(updateBooleanFn).toHaveBeenLastCalledWith(!!(2 % 2), !!(1 % 2));
expect(updateStringFn).toHaveBeenCalledTimes(3);
expect(updateStringFn).toHaveBeenLastCalledWith('2', '1');
expect(updateObjFn).toHaveBeenCalledTimes(3);
expect(updateObjFn).toHaveBeenLastCalledWith({ 2: 2 }, { 1: 1 });
updateCache(['string', 'boolean', 'object']);
({ string, boolean, object, number } = updateCache());
expect(string._value).toBe('5');
expect(string._changed).toBe(true);
expect(boolean._value).toBe(!!(5 % 2));
expect(boolean._changed).toBe(true);
expect(object._value).toEqual({ 5: 5 });
expect(object._changed).toEqual(true);
expect(number._value).toBe(5);
expect(number._changed).toBe(true);
expect(updateBooleanFn).toHaveBeenCalledTimes(5);
expect(updateStringFn).toHaveBeenCalledTimes(5);
expect(updateObjFn).toHaveBeenCalledTimes(5);
expect(updateNumberFn).toHaveBeenCalledTimes(5);
}); });
test('doesnt update if nothing changes with primitives', () => { test('with equal always false', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>(() => 0); const [fn, updater] = createUpdater(() => 1);
const updateCache = createCache({ const update = createCache<number>(updater, { _equal: () => false });
number: updateNumber,
});
let { _value, _changed } = updateCache('number').number; let { _value, _previous, _changed } = update();
expect(_value).toBe(0); expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(0);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, undefined);
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(0);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, 0);
const changed = updateCache('number');
expect(Object.prototype.hasOwnProperty.call(changed, 'changed')).toBe(false);
expect(Object.prototype.hasOwnProperty.call(changed, 'number')).toBe(true);
});
test('doesnt update if nothing changes with non primitives', () => {
const constObj = { a: 0, b: 0 };
const [updateConstObjFn, updateConstObj] = createUpdater<{ a: number; b: number }>(() => constObj);
const [updateSimilarObjFn, updateSimilarObj] = createUpdater<{ a: number; b: number }>(() => ({ ...constObj }));
const [updateComparisonObjFn, updateComparisonObj] = createUpdater<{ a: number; b: number }>(() => ({ ...constObj }));
const updateCache = createCache({
constObj: updateConstObj,
similarObj: updateSimilarObj,
comparisonObj: [
updateComparisonObj,
(a?: { a: number; b: number }, b?: { a: number; b: number }): boolean => !!(a && b && a.a === b.a && a.b === b.b),
],
});
let { _value, _changed } = updateCache('constObj').constObj;
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateConstObjFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('constObj').constObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(false);
expect(updateConstObjFn).toHaveBeenLastCalledWith(constObj, undefined);
({ _value, _changed } = updateCache('constObj').constObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(false);
expect(updateConstObjFn).toHaveBeenLastCalledWith(constObj, constObj);
({ _value, _changed } = updateCache('similarObj').similarObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateSimilarObjFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('similarObj').similarObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateSimilarObjFn).toHaveBeenLastCalledWith(constObj, undefined);
({ _value, _changed } = updateCache('similarObj').similarObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateSimilarObjFn).toHaveBeenLastCalledWith(constObj, constObj);
({ _value, _changed } = updateCache('comparisonObj').comparisonObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateComparisonObjFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('comparisonObj').comparisonObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(false);
expect(updateComparisonObjFn).toHaveBeenLastCalledWith(constObj, undefined);
({ _value, _changed } = updateCache('comparisonObj').comparisonObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(false);
expect(updateComparisonObjFn).toHaveBeenLastCalledWith(constObj, constObj);
const result = updateCache();
expect(Object.prototype.hasOwnProperty.call(result, 'constObj')).toBe(true);
expect(Object.prototype.hasOwnProperty.call(result, 'similarObj')).toBe(true);
expect(Object.prototype.hasOwnProperty.call(result, 'comparisonObj')).toBe(true);
});
test('updates definitely with force', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>(() => 0);
const [, updateString] = createUpdater<number>(() => 0);
const updateCache = createCache({
number: updateNumber,
string: updateString,
});
let { _value, _changed } = updateCache('number', true).number;
expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('number', true).number);
expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, undefined);
({ _value, _changed } = updateCache('number', true).number);
expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, 0);
let { number, string } = updateCache('number', true);
expect(number._changed).toBe(true);
expect(string._changed).toBe(false);
({ number, string } = updateCache(['number', 'string'], true));
expect(number._changed).toBe(true);
expect(string._changed).toBe(true);
({ number, string } = updateCache('string', true));
expect(number._changed).toBe(false);
expect(string._changed).toBe(true);
});
test('custom comparison on primitves', () => {
const [updateStringFn, updateString] = createUpdater<string>(() => 'hi');
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i);
const updateCache = createCache({
string: [updateString, () => false],
number: [updateNumber, () => true],
});
let { _value, _changed } = updateCache('string').string;
expect(_value).toBe('hi');
expect(_changed).toBe(true);
expect(updateStringFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('string').string);
expect(_value).toBe('hi');
expect(_changed).toBe(true);
expect(updateStringFn).toHaveBeenLastCalledWith('hi', undefined);
({ _value, _changed } = updateCache('string').string);
expect(_value).toBe('hi');
expect(_changed).toBe(true);
expect(updateStringFn).toHaveBeenLastCalledWith('hi', 'hi');
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(1); expect(_value).toBe(1);
expect(_changed).toBe(false); expect(_previous).toBe(undefined);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined); expect(_changed).toBe(true);
({ _value, _changed } = updateCache('number').number); ({ _value, _previous, _changed } = update());
expect(_value).toBe(2); expect(fn).toHaveBeenLastCalledWith(undefined, 1, undefined);
expect(_changed).toBe(false); expect(_value).toBe(1);
expect(updateNumberFn).toHaveBeenLastCalledWith(1, undefined); expect(_previous).toBe(1);
expect(_changed).toBe(true);
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(3);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(2, 1);
}); });
test('updates all entries with null or undefined as argument', () => { test('with object equal', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i); const obj = { a: -1, b: -1 };
const [updateNumberFn2, updateNumber2] = createUpdater<number>((i) => i); const [fn, updater] = createUpdater((i) => ({ a: i, b: i + 1 }));
const updateCache = createCache({ const update = createCache<typeof obj>(updater, { _equal: (a, b) => a?.a === b?.a && a?.b === b?.b });
number: updateNumber,
number2: updateNumber2,
});
updateCache(); let { _value, _previous, _changed } = update();
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined); expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
expect(updateNumberFn2).toHaveBeenLastCalledWith(undefined, undefined); expect(_value).toEqual({ a: 1, b: 2 });
expect(_previous).toBe(undefined);
expect(_changed).toBe(true);
updateCache(null); ({ _value, _previous, _changed } = update());
expect(updateNumberFn).toHaveBeenLastCalledWith(1, undefined); expect(fn).toHaveBeenLastCalledWith(undefined, { a: 1, b: 2 }, undefined);
expect(updateNumberFn2).toHaveBeenLastCalledWith(1, undefined); expect(_value).toEqual({ a: 2, b: 3 });
expect(_previous).toEqual({ a: 1, b: 2 });
expect(_changed).toBe(true);
}); });
}); });
describe('cache with reference object', () => { describe('inital value', () => {
test('creates and updates simple cache', () => { test('creates and updates cache with inital value', () => {
interface Test { const [fn, updater] = createUpdater((i) => i);
number: number; const update = createCache<number>(updater, { _initialValue: 0 });
boolean: boolean;
string: string;
object: {};
}
const refObj: Test = {
number: 0,
boolean: false,
string: 'hi',
object: {},
};
const updateCache = createCache<Test>(refObj, true); let { _value, _previous, _changed } = update();
expect(fn).toHaveBeenLastCalledWith(undefined, 0, undefined);
let { _value, _changed, _previous } = updateCache('number').number;
expect(_value).toBe(0);
expect(_changed).toBe(false);
refObj.number = 1;
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(1); expect(_value).toBe(1);
expect(_previous).toBe(0);
expect(_changed).toBe(true); expect(_changed).toBe(true);
refObj.number = 2; ({ _value, _previous, _changed } = update());
({ _value, _changed } = updateCache('string').number); expect(fn).toHaveBeenLastCalledWith(undefined, 1, 0);
expect(_value).toBe(1); expect(_value).toBe(2);
expect(_changed).toBe(false);
refObj.number = 3;
({ _value, _changed, _previous } = updateCache('number').number);
expect(_value).toBe(3);
expect(_previous).toBe(1); expect(_previous).toBe(1);
expect(_changed).toBe(true); expect(_changed).toBe(true);
});
let { number, boolean, string, object } = updateCache(); test('creates and updates cache with inital value and equal', () => {
expect(number._value).toBe(3); const obj = { a: -1, b: -1 };
expect(number._changed).toBe(false); const [fn, updater] = createUpdater((i) => ({ a: i, b: i + 1 }));
expect(boolean._value).toBe(false); const update = createCache<typeof obj>(updater, { _initialValue: obj, _equal: (a, b) => a?.a === b?.a && a?.b === b?.b });
expect(boolean._changed).toBe(false);
expect(string._value).toBe('hi');
expect(string._changed).toBe(false);
expect(object._value).toEqual({});
expect(object._changed).toBe(false);
refObj.string = 'hi2'; let { _value, _previous, _changed } = update();
refObj.boolean = true; expect(fn).toHaveBeenLastCalledWith(undefined, obj, undefined);
({ number, boolean, string, object } = updateCache()); expect(_value).toEqual({ a: 1, b: 2 });
expect(number._value).toBe(3); expect(_previous).toBe(obj);
expect(number._changed).toBe(false); expect(_changed).toBe(true);
expect(boolean._value).toBe(true);
expect(boolean._changed).toBe(true);
expect(string._value).toBe('hi2');
expect(string._changed).toBe(true);
expect(object._value).toEqual({});
expect(object._changed).toBe(false);
({ number, boolean, string, object } = updateCache(null, true)); ({ _value, _previous, _changed } = update());
expect(number._value).toBe(3); expect(fn).toHaveBeenLastCalledWith(undefined, { a: 1, b: 2 }, obj);
expect(number._changed).toBe(true); expect(_value).toEqual({ a: 2, b: 3 });
expect(boolean._value).toBe(true); expect(_previous).toEqual({ a: 1, b: 2 });
expect(boolean._changed).toBe(true); expect(_changed).toBe(true);
expect(string._value).toBe('hi2'); });
expect(string._changed).toBe(true); });
expect(object._value).toEqual({});
expect(object._changed).toBe(true); describe('constant', () => {
test('updates constant initially without intial value', () => {
const [fn, updater] = createUpdater(() => true);
const update = createCache<boolean>(updater);
let { _value, _previous, _changed } = update();
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
expect(_value).toBe(true);
expect(_previous).toBe(undefined);
expect(_changed).toBe(true);
({ _value, _previous, _changed } = update());
expect(fn).toHaveBeenLastCalledWith(undefined, true, undefined);
expect(_value).toBe(true);
expect(_previous).toBe(undefined);
expect(_changed).toBe(false);
});
test('doesnt update constant with initial value', () => {
const obj = { constant: true };
const [fn, updater] = createUpdater(() => obj);
const update = createCache<typeof obj>(updater, { _initialValue: obj });
let { _value, _previous, _changed } = update();
expect(fn).toHaveBeenLastCalledWith(undefined, obj, undefined);
expect(_value).toBe(obj);
expect(_previous).toBe(undefined);
expect(_changed).toBe(false);
({ _value, _previous, _changed } = update());
expect(fn).toHaveBeenLastCalledWith(undefined, obj, undefined);
expect(_value).toBe(obj);
expect(_previous).toBe(undefined);
expect(_changed).toBe(false);
});
test('updates constant with force', () => {
const [fn, updater] = createUpdater(() => 'constant');
const update = createCache<string>(updater);
let { _value, _previous, _changed } = update();
expect(fn).toHaveBeenLastCalledWith(undefined, undefined, undefined);
expect(_value).toBe('constant');
expect(_previous).toBe(undefined);
expect(_changed).toBe(true);
({ _value, _previous, _changed } = update(true));
expect(fn).toHaveBeenLastCalledWith(undefined, 'constant', undefined);
expect(_value).toBe('constant');
expect(_previous).toBe('constant');
expect(_changed).toBe(true);
({ _value, _previous, _changed } = update(false));
expect(fn).toHaveBeenLastCalledWith(undefined, 'constant', 'constant');
expect(_value).toBe('constant');
expect(_previous).toBe('constant');
expect(_changed).toBe(false);
({ _value, _previous, _changed } = update());
expect(fn).toHaveBeenLastCalledWith(undefined, 'constant', 'constant');
expect(_value).toBe('constant');
expect(_previous).toBe('constant');
expect(_changed).toBe(false);
}); });
}); });
}); });
@@ -167,8 +167,8 @@ startBtn?.addEventListener('click', start);
createSizeObserver( createSizeObserver(
targetElm as HTMLElement, targetElm as HTMLElement,
(direction?: 'ltr' | 'rtl') => { (directionCache?: any) => {
if (direction) { if (directionCache) {
directionIterations += 1; directionIterations += 1;
} else { } else {
sizeIterations += 1; sizeIterations += 1;
@@ -16,8 +16,8 @@ const waitForOptions = {
}, },
}; };
let heightIntrinsic: boolean | undefined;
let heightIterations = 0; let heightIterations = 0;
let heightIntrinsicCache: boolean;
const envElm = document.querySelector('#env'); const envElm = document.querySelector('#env');
const targetElm = document.querySelector('#target'); const targetElm = document.querySelector('#target');
const checkElm = document.querySelector('#check'); const checkElm = document.querySelector('#check');
@@ -86,7 +86,7 @@ const changeWhileHidden = async () => {
selectOption(displaySelect as HTMLSelectElement, 'displayBlock'); selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
await waitFor(() => { await waitFor(() => {
should.equal(heightIntrinsicCache, false); should.equal(heightIntrinsic, false);
}, waitForOptions); }, waitForOptions);
}; };
@@ -100,7 +100,7 @@ const changeWhileHidden = async () => {
selectOption(displaySelect as HTMLSelectElement, 'displayBlock'); selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
await waitFor(() => { await waitFor(() => {
should.equal(heightIntrinsicCache, true); should.equal(heightIntrinsic, true);
}, waitForOptions); }, waitForOptions);
}; };
@@ -126,10 +126,10 @@ const start = async () => {
startBtn?.addEventListener('click', start); startBtn?.addEventListener('click', start);
createTrinsicObserver(targetElm as HTMLElement, (widthIntrinsic: boolean, heightIntrinsic: boolean) => { createTrinsicObserver(targetElm as HTMLElement, (widthIntrinsic, heightIntrinsicCache) => {
if (heightIntrinsic !== heightIntrinsicCache) { if (heightIntrinsicCache._changed) {
heightIterations += 1; heightIterations += 1;
heightIntrinsicCache = heightIntrinsic; heightIntrinsic = heightIntrinsicCache._value;
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (changesSlot) { if (changesSlot) {
@@ -1,17 +1,18 @@
import { CacheUpdateInfo, CachePropsToUpdate, Cache, OptionsWithOptionsTemplate } from 'support'; import { Cache, OptionsWithOptionsTemplate } from 'support';
import { PlainObject } from 'typings'; import { CSSDirection, PlainObject } from 'typings';
interface AbstractLifecycle<O extends PlainObject> { export interface LifecycleBase<O extends PlainObject> {
_options(newOptions?: O): O; _options(newOptions?: O): O;
_update(force?: boolean): void; _update(force?: boolean): void;
} }
export interface Lifecycle<T extends PlainObject> extends AbstractLifecycle<T> { export interface Lifecycle<T extends PlainObject> extends LifecycleBase<T> {
_destruct(): void; _destruct(): void;
_onSizeChanged?(): void; _onSizeChanged?(): void;
_onDirectionChanged?(direction: 'ltr' | 'rtl'): void; _onDirectionChanged?(directionCache: Cache<CSSDirection>): void;
_onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsic: boolean): void; _onTrinsicChanged?(widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>): void;
} }
export interface LifecycleBase<O extends PlainObject, C extends PlainObject> extends AbstractLifecycle<O> { export interface LifecycleOptionInfo<T> {
_updateCache(cachePropsToUpdate?: CachePropsToUpdate<C>): void; _value: T;
_changed: boolean;
} }
export declare const createLifecycleBase: <O, C>(defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>, cacheUpdateInfo: CacheUpdateInfo<C>, initialOptions: O | undefined, updateFunction: (options: Cache<O>, cache: Cache<C>) => any) => LifecycleBase<O, C>; export declare type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
export {}; export declare const createLifecycleBase: <O>(defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>, initialOptions: O | undefined, updateFunction: (force: boolean, checkOption: LifecycleCheckOption) => any) => LifecycleBase<O>;
@@ -1,7 +1,7 @@
declare type Direction = 'ltr' | 'rtl'; import { Cache } from 'support';
import { CSSDirection } from 'typings';
export declare type SizeObserverOptions = { export declare type SizeObserverOptions = {
_direction?: boolean; _direction?: boolean;
_appear?: boolean; _appear?: boolean;
}; };
export declare const createSizeObserver: (target: HTMLElement, onSizeChangedCallback: (direction?: "ltr" | "rtl" | undefined) => any, options?: SizeObserverOptions | undefined) => (() => void); export declare const createSizeObserver: (target: HTMLElement, onSizeChangedCallback: (directionCache?: Cache<CSSDirection> | undefined) => any, options?: SizeObserverOptions | undefined) => (() => void);
export {};
@@ -1 +1,2 @@
export declare const createTrinsicObserver: (target: HTMLElement, onTrinsicChangedCallback: (widthIntrinsic: boolean, heightIntrinsic: boolean) => any) => (() => void); import { Cache } from 'support';
export declare const createTrinsicObserver: (target: HTMLElement, onTrinsicChangedCallback: (widthIntrinsic: boolean, heightIntrinsicCache: Cache<boolean>) => any) => (() => void);
+12 -20
View File
@@ -1,21 +1,13 @@
declare type UpdateCachePropFunction<T, P extends keyof T> = (current?: T[P], previous?: T[P]) => T[P]; export interface Cache<T> {
declare type EqualCachePropFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => boolean; readonly _value?: T;
export interface CacheEntry<T> { readonly _previous?: T;
_value?: T; readonly _changed: boolean;
_previous?: T;
_changed: boolean;
} }
export declare type Cache<T> = { export interface CacheOptions<T> {
[P in keyof T]: CacheEntry<T[P]>; _equal?: EqualCachePropFunction<T>;
}; _initialValue?: T;
export declare type CacheUpdated<T> = Cache<T> & { }
_anythingChanged: boolean; export declare type CacheUpdate<T, C> = (force?: boolean | 0, context?: C) => Cache<T>;
}; export declare type UpdateCachePropFunction<T, C> = (context?: C, current?: T, previous?: T) => T;
export declare type CachePropsToUpdate<T> = Array<keyof T> | keyof T; export declare type EqualCachePropFunction<T> = (a?: T, b?: T) => boolean;
export declare type CacheUpdate<T> = (propsToUpdate?: CachePropsToUpdate<T> | null, force?: boolean) => CacheUpdated<T>; export declare const createCache: <T, C = undefined>(update: UpdateCachePropFunction<T, C>, options?: CacheOptions<T> | undefined) => CacheUpdate<T, C>;
export declare type CacheUpdateInfo<T> = {
[P in keyof T]: UpdateCachePropFunction<T, P> | [UpdateCachePropFunction<T, P>, EqualCachePropFunction<T, P>];
};
export declare function createCache<T>(cacheUpdateInfo: CacheUpdateInfo<T>): CacheUpdate<T>;
export declare function createCache<T>(referenceObj: T, isReference: true): CacheUpdate<T>;
export {};
+2
View File
@@ -5,7 +5,9 @@ export declare type OSTargetElement = HTMLElement | HTMLTextAreaElement;
export interface OSTargetObject { export interface OSTargetObject {
target: OSTargetElement; target: OSTargetElement;
host: HTMLElement; host: HTMLElement;
padding: HTMLElement;
viewport: HTMLElement; viewport: HTMLElement;
content: HTMLElement; content: HTMLElement;
} }
export declare type OSTarget = OSTargetElement | OSTargetObject; export declare type OSTarget = OSTargetElement | OSTargetObject;
export declare type CSSDirection = 'ltr' | 'rtl';