introduce plugin system and split option validation into a plugin

This commit is contained in:
Rene
2022-06-22 15:10:48 +02:00
parent a799c470ed
commit 0a3eac283f
29 changed files with 1317 additions and 1055 deletions
+148 -162
View File
@@ -102,7 +102,7 @@ function each(source, callback) {
return source;
}
const indexOf = (arr, item, fromIndex) => isArray(arr) ? arr.indexOf(item, fromIndex) : -1;
const indexOf = (arr, item, fromIndex) => arr.indexOf(item, fromIndex);
const push = (array, items, arrayIsSingleItem) => {
!arrayIsSingleItem && !isString(items) && isArrayLike(items) ? Array.prototype.push.apply(array, items) : array.push(items);
return array;
@@ -606,6 +606,28 @@ const absoluteCoordinates = elm => {
} : zeroObj;
};
const classNameEnvironment = 'os-environment';
const classNameEnvironmentFlexboxGlue = `${classNameEnvironment}-flexbox-glue`;
const classNameEnvironmentFlexboxGlueMax = `${classNameEnvironmentFlexboxGlue}-max`;
const classNameHost = 'os-host';
const classNamePadding = 'os-padding';
const classNameViewport = 'os-viewport';
const classNameViewportArrange = `${classNameViewport}-arrange`;
const classNameContent = 'os-content';
const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`;
const classNameSizeObserver = 'os-size-observer';
const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`;
const classNameSizeObserverListener = `${classNameSizeObserver}-listener`;
const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`;
const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`;
const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
const classNameTrinsicObserver = 'os-trinsic-observer';
const classNameScrollbar = 'os-scrollbar';
const classNameScrollbarHorizontal = `${classNameScrollbar}-horizontal`;
const classNameScrollbarVertical = `${classNameScrollbar}-vertical`;
const classNameScrollbarTrack = 'os-scrollbar-track';
const classNameScrollbarHandle = 'os-scrollbar-handle';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
@@ -635,174 +657,74 @@ var _extends$1 = {exports: {}};
const _extends = getDefaultExportFromCjs(_extends$1.exports);
const {
stringify
} = JSON;
const templateTypePrefixSuffix = ['__TPL_', '_TYPE__'];
const optionsTemplateTypes = ['boolean', 'number', 'string', 'array', 'object', 'function', 'null'].reduce((result, item) => {
result[item] = templateTypePrefixSuffix[0] + item + templateTypePrefixSuffix[1];
return result;
}, {});
const stringify = value => JSON.stringify(value, (_, val) => {
if (isFunction(val)) {
throw new Error();
}
const validateRecursive = (options, template, optionsDiff, doWriteErrors, propPath) => {
const validatedOptions = {};
return val;
});
const optionsCopy = _extends({}, options);
const props = keys(template).filter(prop => hasOwnProperty(options, prop));
each(props, prop => {
const optionsDiffValue = isUndefined(optionsDiff[prop]) ? {} : optionsDiff[prop];
const optionsValue = options[prop];
const templateValue = template[prop];
const templateIsComplex = isPlainObject(templateValue);
const propPrefix = propPath ? `${propPath}.` : '';
if (templateIsComplex && isPlainObject(optionsValue)) {
const validatedResult = validateRecursive(optionsValue, templateValue, optionsDiffValue, doWriteErrors, propPrefix + prop);
validatedOptions[prop] = validatedResult._validated;
optionsCopy[prop] = validatedResult._foreign;
each([optionsCopy, validatedOptions], value => {
if (isEmptyObject(value[prop])) {
delete value[prop];
}
});
} else if (!templateIsComplex) {
let isValid = false;
const errorEnumStrings = [];
const errorPossibleTypes = [];
const optionsValueType = type(optionsValue);
const templateValueArr = !isArray(templateValue) ? [templateValue] : templateValue;
each(templateValueArr, currTemplateType => {
let typeString;
each(optionsTemplateTypes, (value, key) => {
if (value === currTemplateType) {
typeString = key;
}
});
const isEnumString = isUndefined(typeString);
if (isEnumString && isString(optionsValue)) {
const enumStringSplit = currTemplateType.split(' ');
isValid = !!enumStringSplit.find(possibility => possibility === optionsValue);
push(errorEnumStrings, enumStringSplit);
} else {
isValid = optionsTemplateTypes[optionsValueType] === currTemplateType;
}
push(errorPossibleTypes, isEnumString ? optionsTemplateTypes.string : typeString);
return !isValid;
});
if (isValid) {
const isPrimitiveArr = isArray(optionsValue) && !optionsValue.some(val => !isNumber(val) && !isString(val) && !isBoolean(val));
const doStringifyComparison = isPrimitiveArr || isPlainObject(optionsValue);
if (doStringifyComparison ? stringify(optionsValue) !== stringify(optionsDiffValue) : optionsValue !== optionsDiffValue) {
validatedOptions[prop] = optionsValue;
}
} else if (doWriteErrors) {
console.warn(`${`The option "${propPrefix}${prop}" wasn't set, because it doesn't accept the type [ ${optionsValueType.toUpperCase()} ] with the value of "${optionsValue}".\r\n` + `Accepted types are: [ ${errorPossibleTypes.join(', ').toUpperCase()} ].\r\n`}${errorEnumStrings.length > 0 ? `\r\nValid strings are: [ ${errorEnumStrings.join(', ')} ].` : ''}`);
}
delete optionsCopy[prop];
}
});
return {
_foreign: optionsCopy,
_validated: validatedOptions
};
};
const validateOptions = (options, template, optionsDiff, doWriteErrors) => validateRecursive(options, template, optionsDiff || {}, doWriteErrors || false);
const transformOptions = optionsWithOptionsTemplate => {
const result = {
_template: {},
_options: {}
};
each(keys(optionsWithOptionsTemplate), key => {
const val = optionsWithOptionsTemplate[key];
if (isArray(val)) {
result._template[key] = val[1];
result._options[key] = val[0];
} else {
const tmpResult = transformOptions(val);
result._template[key] = tmpResult._template;
result._options[key] = tmpResult._options;
}
});
return result;
};
const classNameEnvironment = 'os-environment';
const classNameEnvironmentFlexboxGlue = `${classNameEnvironment}-flexbox-glue`;
const classNameEnvironmentFlexboxGlueMax = `${classNameEnvironmentFlexboxGlue}-max`;
const classNameHost = 'os-host';
const classNamePadding = 'os-padding';
const classNameViewport = 'os-viewport';
const classNameViewportArrange = `${classNameViewport}-arrange`;
const classNameContent = 'os-content';
const classNameViewportScrollbarStyling = `${classNameViewport}-scrollbar-styled`;
const classNameSizeObserver = 'os-size-observer';
const classNameSizeObserverAppear = `${classNameSizeObserver}-appear`;
const classNameSizeObserverListener = `${classNameSizeObserver}-listener`;
const classNameSizeObserverListenerScroll = `${classNameSizeObserverListener}-scroll`;
const classNameSizeObserverListenerItem = `${classNameSizeObserverListener}-item`;
const classNameSizeObserverListenerItemFinal = `${classNameSizeObserverListenerItem}-final`;
const classNameTrinsicObserver = 'os-trinsic-observer';
const classNameScrollbar = 'os-scrollbar';
const classNameScrollbarHorizontal = `${classNameScrollbar}-horizontal`;
const classNameScrollbarVertical = `${classNameScrollbar}-vertical`;
const classNameScrollbarTrack = 'os-scrollbar-track';
const classNameScrollbarHandle = 'os-scrollbar-handle';
const numberAllowedValues = optionsTemplateTypes.number;
const arrayNullValues = [optionsTemplateTypes.array, optionsTemplateTypes.null];
const stringArrayNullAllowedValues = [optionsTemplateTypes.string, optionsTemplateTypes.array, optionsTemplateTypes.null];
const booleanTrueTemplate = [true, optionsTemplateTypes.boolean];
const booleanFalseTemplate = [false, optionsTemplateTypes.boolean];
const resizeAllowedValues = 'none both horizontal vertical';
const overflowAllowedValues = 'hidden scroll visible visible-hidden';
const scrollbarsVisibilityAllowedValues = 'visible hidden auto';
const scrollbarsAutoHideAllowedValues = 'never scroll leavemove';
const defaultOptionsWithTemplate = {
resize: ['none', resizeAllowedValues],
paddingAbsolute: booleanFalseTemplate,
const defaultOptions = {
resize: 'none',
paddingAbsolute: false,
updating: {
elementEvents: [[['img', 'load']], arrayNullValues],
attributes: [null, arrayNullValues],
debounce: [[0, 33], [optionsTemplateTypes.number, optionsTemplateTypes.array, optionsTemplateTypes.null]]
elementEvents: [['img', 'load']],
attributes: null,
debounce: [0, 33]
},
overflow: {
x: ['scroll', overflowAllowedValues],
y: ['scroll', overflowAllowedValues]
x: 'scroll',
y: 'scroll'
},
scrollbars: {
visibility: ['auto', scrollbarsVisibilityAllowedValues],
autoHide: ['never', scrollbarsAutoHideAllowedValues],
autoHideDelay: [800, numberAllowedValues],
dragScroll: booleanTrueTemplate,
clickScroll: booleanFalseTemplate,
touch: booleanTrueTemplate
visibility: 'auto',
autoHide: 'never',
autoHideDelay: 800,
dragScroll: true,
clickScroll: false,
touch: true
},
textarea: {
dynWidth: booleanFalseTemplate,
dynHeight: booleanFalseTemplate,
inheritedAttrs: [['style', 'class'], stringArrayNullAllowedValues]
dynWidth: false,
dynHeight: false,
inheritedAttrs: ['style', 'class']
},
nativeScrollbarsOverlaid: {
show: booleanFalseTemplate,
initialize: booleanFalseTemplate
show: false,
initialize: false
},
callbacks: {
onUpdated: [null, [optionsTemplateTypes.function, optionsTemplateTypes.null]]
onUpdated: null
}
};
const {
_template: optionsTemplate,
_options: defaultOptions
} = transformOptions(defaultOptionsWithTemplate);
const getOptionsDiff = (currOptions, newOptions) => {
const diff = {};
const optionsKeys = keys(newOptions).concat(keys(currOptions));
each(optionsKeys, optionKey => {
const currOptionValue = currOptions[optionKey];
const newOptionValue = newOptions[optionKey];
if (isObject(currOptionValue) && isObject(newOptionValue)) {
assignDeep(diff[optionKey] = {}, getOptionsDiff(currOptionValue, newOptionValue));
} else if (hasOwnProperty(newOptions, optionKey) && newOptionValue !== currOptionValue) {
let isDiff = true;
if (isArray(currOptionValue) || isArray(newOptionValue)) {
try {
if (stringify(currOptionValue) === stringify(newOptionValue)) {
isDiff = false;
}
} catch (_unused) {}
}
if (isDiff) {
diff[optionKey] = newOptionValue;
}
}
});
return diff;
};
let environmentInstance;
const {
@@ -2408,25 +2330,81 @@ const createLifecycleHub = (options, structureSetup, scrollbarsSetup) => {
};
};
const OverlayScrollbars = (target, options, extensions) => {
const pluginRegistry = {};
const getPlugins = () => assignDeep({}, pluginRegistry);
const addPlugin = addedPlugin => each(isArray(addedPlugin) ? addedPlugin : [addedPlugin], plugin => {
pluginRegistry[plugin[0]] = plugin[1];
});
const templateTypePrefixSuffix = ['__TPL_', '_TYPE__'];
const optionsTemplateTypes = ['boolean', 'number', 'string', 'array', 'object', 'function', 'null'].reduce((result, item) => {
result[item] = templateTypePrefixSuffix[0] + item + templateTypePrefixSuffix[1];
return result;
}, {});
const numberAllowedValues = optionsTemplateTypes.number;
const booleanAllowedValues = optionsTemplateTypes.boolean;
const arrayNullValues = [optionsTemplateTypes.array, optionsTemplateTypes.null];
const stringArrayNullAllowedValues = [optionsTemplateTypes.string, optionsTemplateTypes.array, optionsTemplateTypes.null];
const resizeAllowedValues = 'none both horizontal vertical';
const overflowAllowedValues = 'hidden scroll visible visible-hidden';
const scrollbarsVisibilityAllowedValues = 'visible hidden auto';
const scrollbarsAutoHideAllowedValues = 'never scroll leavemove';
({
resize: resizeAllowedValues,
paddingAbsolute: booleanAllowedValues,
updating: {
elementEvents: arrayNullValues,
attributes: arrayNullValues,
debounce: [optionsTemplateTypes.number, optionsTemplateTypes.array, optionsTemplateTypes.null]
},
overflow: {
x: overflowAllowedValues,
y: overflowAllowedValues
},
scrollbars: {
visibility: scrollbarsVisibilityAllowedValues,
autoHide: scrollbarsAutoHideAllowedValues,
autoHideDelay: numberAllowedValues,
dragScroll: booleanAllowedValues,
clickScroll: booleanAllowedValues,
touch: booleanAllowedValues
},
textarea: {
dynWidth: booleanAllowedValues,
dynHeight: booleanAllowedValues,
inheritedAttrs: stringArrayNullAllowedValues
},
nativeScrollbarsOverlaid: {
show: booleanAllowedValues,
initialize: booleanAllowedValues
},
callbacks: {
onUpdated: [optionsTemplateTypes.function, optionsTemplateTypes.null]
}
});
const optionsValidationPluginName = '__osOptionsValidationPlugin';
const OverlayScrollbars = (target, options) => {
const {
_getDefaultOptions
} = getEnvironment();
const currentOptions = assignDeep({}, _getDefaultOptions(), validateOptions(options || {}, optionsTemplate, null, true)._validated);
const plugins = getPlugins();
const optionsValidationPlugin = plugins[optionsValidationPluginName];
const validateOptions = optionsValidationPlugin && optionsValidationPlugin._;
const currentOptions = assignDeep({}, _getDefaultOptions(), validateOptions ? validateOptions(options || {}, true) : options);
const structureSetup = createStructureSetup(target);
const scrollbarsSetup = createScrollbarsSetup(target, structureSetup);
const lifecycleHub = createLifecycleHub(currentOptions, structureSetup, scrollbarsSetup);
const instance = {
options(newOptions) {
if (newOptions) {
const {
_validated: _changedOptions
} = validateOptions(newOptions, optionsTemplate, currentOptions, true);
const changedOptions = getOptionsDiff(currentOptions, validateOptions ? validateOptions(newOptions, true) : newOptions);
if (!isEmptyObject(_changedOptions)) {
assignDeep(currentOptions, _changedOptions);
if (!isEmptyObject(changedOptions)) {
assignDeep(currentOptions, changedOptions);
lifecycleHub._update(_changedOptions);
lifecycleHub._update(changedOptions);
}
}
@@ -2441,9 +2419,17 @@ const OverlayScrollbars = (target, options, extensions) => {
destroy: () => lifecycleHub._destroy()
};
each(keys(plugins), pluginName => {
const pluginInstance = plugins[pluginName];
if (isFunction(pluginInstance)) {
pluginInstance(OverlayScrollbars, instance);
}
});
instance.update(true);
return instance;
};
OverlayScrollbars.extend = addPlugin;
export { OverlayScrollbars as default };
//# sourceMappingURL=overlayscrollbars.esm.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+151 -164
View File
@@ -114,7 +114,7 @@
return source;
}
var indexOf = function indexOf(arr, item, fromIndex) {
return isArray(arr) ? arr.indexOf(item, fromIndex) : -1;
return arr.indexOf(item, fromIndex);
};
var push = function push(array, items, arrayIsSingleItem) {
!arrayIsSingleItem && !isString(items) && isArrayLike(items) ? Array.prototype.push.apply(array, items) : array.push(items);
@@ -680,6 +680,28 @@
} : zeroObj;
};
var classNameEnvironment = 'os-environment';
var classNameEnvironmentFlexboxGlue = classNameEnvironment + "-flexbox-glue";
var classNameEnvironmentFlexboxGlueMax = classNameEnvironmentFlexboxGlue + "-max";
var classNameHost = 'os-host';
var classNamePadding = 'os-padding';
var classNameViewport = 'os-viewport';
var classNameViewportArrange = classNameViewport + "-arrange";
var classNameContent = 'os-content';
var classNameViewportScrollbarStyling = classNameViewport + "-scrollbar-styled";
var classNameSizeObserver = 'os-size-observer';
var classNameSizeObserverAppear = classNameSizeObserver + "-appear";
var classNameSizeObserverListener = classNameSizeObserver + "-listener";
var classNameSizeObserverListenerScroll = classNameSizeObserverListener + "-scroll";
var classNameSizeObserverListenerItem = classNameSizeObserverListener + "-item";
var classNameSizeObserverListenerItemFinal = classNameSizeObserverListenerItem + "-final";
var classNameTrinsicObserver = 'os-trinsic-observer';
var classNameScrollbar = 'os-scrollbar';
var classNameScrollbarHorizontal = classNameScrollbar + "-horizontal";
var classNameScrollbarVertical = classNameScrollbar + "-vertical";
var classNameScrollbarTrack = 'os-scrollbar-track';
var classNameScrollbarHandle = 'os-scrollbar-handle';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
@@ -709,180 +731,76 @@
var _extends = getDefaultExportFromCjs(_extends$1.exports);
var stringify = JSON.stringify;
var templateTypePrefixSuffix = ['__TPL_', '_TYPE__'];
var optionsTemplateTypes = ['boolean', 'number', 'string', 'array', 'object', 'function', 'null'].reduce(function (result, item) {
result[item] = templateTypePrefixSuffix[0] + item + templateTypePrefixSuffix[1];
return result;
}, {});
var validateRecursive = function validateRecursive(options, template, optionsDiff, doWriteErrors, propPath) {
var validatedOptions = {};
var optionsCopy = _extends({}, options);
var props = keys(template).filter(function (prop) {
return hasOwnProperty(options, prop);
});
each(props, function (prop) {
var optionsDiffValue = isUndefined(optionsDiff[prop]) ? {} : optionsDiff[prop];
var optionsValue = options[prop];
var templateValue = template[prop];
var templateIsComplex = isPlainObject(templateValue);
var propPrefix = propPath ? propPath + "." : '';
if (templateIsComplex && isPlainObject(optionsValue)) {
var validatedResult = validateRecursive(optionsValue, templateValue, optionsDiffValue, doWriteErrors, propPrefix + prop);
validatedOptions[prop] = validatedResult._validated;
optionsCopy[prop] = validatedResult._foreign;
each([optionsCopy, validatedOptions], function (value) {
if (isEmptyObject(value[prop])) {
delete value[prop];
}
});
} else if (!templateIsComplex) {
var isValid = false;
var errorEnumStrings = [];
var errorPossibleTypes = [];
var optionsValueType = type(optionsValue);
var templateValueArr = !isArray(templateValue) ? [templateValue] : templateValue;
each(templateValueArr, function (currTemplateType) {
var typeString;
each(optionsTemplateTypes, function (value, key) {
if (value === currTemplateType) {
typeString = key;
}
});
var isEnumString = isUndefined(typeString);
if (isEnumString && isString(optionsValue)) {
var enumStringSplit = currTemplateType.split(' ');
isValid = !!enumStringSplit.find(function (possibility) {
return possibility === optionsValue;
});
push(errorEnumStrings, enumStringSplit);
} else {
isValid = optionsTemplateTypes[optionsValueType] === currTemplateType;
}
push(errorPossibleTypes, isEnumString ? optionsTemplateTypes.string : typeString);
return !isValid;
});
if (isValid) {
var isPrimitiveArr = isArray(optionsValue) && !optionsValue.some(function (val) {
return !isNumber(val) && !isString(val) && !isBoolean(val);
});
var doStringifyComparison = isPrimitiveArr || isPlainObject(optionsValue);
if (doStringifyComparison ? stringify(optionsValue) !== stringify(optionsDiffValue) : optionsValue !== optionsDiffValue) {
validatedOptions[prop] = optionsValue;
}
} else if (doWriteErrors) {
console.warn("" + ("The option \"" + propPrefix + prop + "\" wasn't set, because it doesn't accept the type [ " + optionsValueType.toUpperCase() + " ] with the value of \"" + optionsValue + "\".\r\n" + ("Accepted types are: [ " + errorPossibleTypes.join(', ').toUpperCase() + " ].\r\n")) + (errorEnumStrings.length > 0 ? "\r\nValid strings are: [ " + errorEnumStrings.join(', ') + " ]." : ''));
}
delete optionsCopy[prop];
var stringify = function stringify(value) {
return JSON.stringify(value, function (_, val) {
if (isFunction(val)) {
throw new Error();
}
return val;
});
return {
_foreign: optionsCopy,
_validated: validatedOptions
};
};
var validateOptions = function validateOptions(options, template, optionsDiff, doWriteErrors) {
return validateRecursive(options, template, optionsDiff || {}, doWriteErrors || false);
};
var transformOptions = function transformOptions(optionsWithOptionsTemplate) {
var result = {
_template: {},
_options: {}
};
each(keys(optionsWithOptionsTemplate), function (key) {
var val = optionsWithOptionsTemplate[key];
if (isArray(val)) {
result._template[key] = val[1];
result._options[key] = val[0];
} else {
var tmpResult = transformOptions(val);
result._template[key] = tmpResult._template;
result._options[key] = tmpResult._options;
}
});
return result;
};
var classNameEnvironment = 'os-environment';
var classNameEnvironmentFlexboxGlue = classNameEnvironment + "-flexbox-glue";
var classNameEnvironmentFlexboxGlueMax = classNameEnvironmentFlexboxGlue + "-max";
var classNameHost = 'os-host';
var classNamePadding = 'os-padding';
var classNameViewport = 'os-viewport';
var classNameViewportArrange = classNameViewport + "-arrange";
var classNameContent = 'os-content';
var classNameViewportScrollbarStyling = classNameViewport + "-scrollbar-styled";
var classNameSizeObserver = 'os-size-observer';
var classNameSizeObserverAppear = classNameSizeObserver + "-appear";
var classNameSizeObserverListener = classNameSizeObserver + "-listener";
var classNameSizeObserverListenerScroll = classNameSizeObserverListener + "-scroll";
var classNameSizeObserverListenerItem = classNameSizeObserverListener + "-item";
var classNameSizeObserverListenerItemFinal = classNameSizeObserverListenerItem + "-final";
var classNameTrinsicObserver = 'os-trinsic-observer';
var classNameScrollbar = 'os-scrollbar';
var classNameScrollbarHorizontal = classNameScrollbar + "-horizontal";
var classNameScrollbarVertical = classNameScrollbar + "-vertical";
var classNameScrollbarTrack = 'os-scrollbar-track';
var classNameScrollbarHandle = 'os-scrollbar-handle';
var numberAllowedValues = optionsTemplateTypes.number;
var arrayNullValues = [optionsTemplateTypes.array, optionsTemplateTypes.null];
var stringArrayNullAllowedValues = [optionsTemplateTypes.string, optionsTemplateTypes.array, optionsTemplateTypes.null];
var booleanTrueTemplate = [true, optionsTemplateTypes.boolean];
var booleanFalseTemplate = [false, optionsTemplateTypes.boolean];
var resizeAllowedValues = 'none both horizontal vertical';
var overflowAllowedValues = 'hidden scroll visible visible-hidden';
var scrollbarsVisibilityAllowedValues = 'visible hidden auto';
var scrollbarsAutoHideAllowedValues = 'never scroll leavemove';
var defaultOptionsWithTemplate = {
resize: ['none', resizeAllowedValues],
paddingAbsolute: booleanFalseTemplate,
var defaultOptions = {
resize: 'none',
paddingAbsolute: false,
updating: {
elementEvents: [[['img', 'load']], arrayNullValues],
attributes: [null, arrayNullValues],
debounce: [[0, 33], [optionsTemplateTypes.number, optionsTemplateTypes.array, optionsTemplateTypes.null]]
elementEvents: [['img', 'load']],
attributes: null,
debounce: [0, 33]
},
overflow: {
x: ['scroll', overflowAllowedValues],
y: ['scroll', overflowAllowedValues]
x: 'scroll',
y: 'scroll'
},
scrollbars: {
visibility: ['auto', scrollbarsVisibilityAllowedValues],
autoHide: ['never', scrollbarsAutoHideAllowedValues],
autoHideDelay: [800, numberAllowedValues],
dragScroll: booleanTrueTemplate,
clickScroll: booleanFalseTemplate,
touch: booleanTrueTemplate
visibility: 'auto',
autoHide: 'never',
autoHideDelay: 800,
dragScroll: true,
clickScroll: false,
touch: true
},
textarea: {
dynWidth: booleanFalseTemplate,
dynHeight: booleanFalseTemplate,
inheritedAttrs: [['style', 'class'], stringArrayNullAllowedValues]
dynWidth: false,
dynHeight: false,
inheritedAttrs: ['style', 'class']
},
nativeScrollbarsOverlaid: {
show: booleanFalseTemplate,
initialize: booleanFalseTemplate
show: false,
initialize: false
},
callbacks: {
onUpdated: [null, [optionsTemplateTypes.function, optionsTemplateTypes.null]]
onUpdated: null
}
};
var getOptionsDiff = function getOptionsDiff(currOptions, newOptions) {
var diff = {};
var optionsKeys = keys(newOptions).concat(keys(currOptions));
each(optionsKeys, function (optionKey) {
var currOptionValue = currOptions[optionKey];
var newOptionValue = newOptions[optionKey];
var _transformOptions = transformOptions(defaultOptionsWithTemplate),
optionsTemplate = _transformOptions._template,
defaultOptions = _transformOptions._options;
if (isObject(currOptionValue) && isObject(newOptionValue)) {
assignDeep(diff[optionKey] = {}, getOptionsDiff(currOptionValue, newOptionValue));
} else if (hasOwnProperty(newOptions, optionKey) && newOptionValue !== currOptionValue) {
var isDiff = true;
if (isArray(currOptionValue) || isArray(newOptionValue)) {
try {
if (stringify(currOptionValue) === stringify(newOptionValue)) {
isDiff = false;
}
} catch (_unused) {}
}
if (isDiff) {
diff[optionKey] = newOptionValue;
}
}
});
return diff;
};
var environmentInstance;
var abs$1 = Math.abs,
@@ -2510,24 +2428,85 @@
};
};
var OverlayScrollbars = function OverlayScrollbars(target, options, extensions) {
var pluginRegistry = {};
var getPlugins = function getPlugins() {
return assignDeep({}, pluginRegistry);
};
var addPlugin = function addPlugin(addedPlugin) {
return each(isArray(addedPlugin) ? addedPlugin : [addedPlugin], function (plugin) {
pluginRegistry[plugin[0]] = plugin[1];
});
};
var templateTypePrefixSuffix = ['__TPL_', '_TYPE__'];
var optionsTemplateTypes = ['boolean', 'number', 'string', 'array', 'object', 'function', 'null'].reduce(function (result, item) {
result[item] = templateTypePrefixSuffix[0] + item + templateTypePrefixSuffix[1];
return result;
}, {});
var numberAllowedValues = optionsTemplateTypes.number;
var booleanAllowedValues = optionsTemplateTypes.boolean;
var arrayNullValues = [optionsTemplateTypes.array, optionsTemplateTypes.null];
var stringArrayNullAllowedValues = [optionsTemplateTypes.string, optionsTemplateTypes.array, optionsTemplateTypes.null];
var resizeAllowedValues = 'none both horizontal vertical';
var overflowAllowedValues = 'hidden scroll visible visible-hidden';
var scrollbarsVisibilityAllowedValues = 'visible hidden auto';
var scrollbarsAutoHideAllowedValues = 'never scroll leavemove';
({
resize: resizeAllowedValues,
paddingAbsolute: booleanAllowedValues,
updating: {
elementEvents: arrayNullValues,
attributes: arrayNullValues,
debounce: [optionsTemplateTypes.number, optionsTemplateTypes.array, optionsTemplateTypes.null]
},
overflow: {
x: overflowAllowedValues,
y: overflowAllowedValues
},
scrollbars: {
visibility: scrollbarsVisibilityAllowedValues,
autoHide: scrollbarsAutoHideAllowedValues,
autoHideDelay: numberAllowedValues,
dragScroll: booleanAllowedValues,
clickScroll: booleanAllowedValues,
touch: booleanAllowedValues
},
textarea: {
dynWidth: booleanAllowedValues,
dynHeight: booleanAllowedValues,
inheritedAttrs: stringArrayNullAllowedValues
},
nativeScrollbarsOverlaid: {
show: booleanAllowedValues,
initialize: booleanAllowedValues
},
callbacks: {
onUpdated: [optionsTemplateTypes.function, optionsTemplateTypes.null]
}
});
var optionsValidationPluginName = '__osOptionsValidationPlugin';
var OverlayScrollbars = function OverlayScrollbars(target, options) {
var _getEnvironment = getEnvironment(),
_getDefaultOptions = _getEnvironment._getDefaultOptions;
var currentOptions = assignDeep({}, _getDefaultOptions(), validateOptions(options || {}, optionsTemplate, null, true)._validated);
var plugins = getPlugins();
var optionsValidationPlugin = plugins[optionsValidationPluginName];
var validateOptions = optionsValidationPlugin && optionsValidationPlugin._;
var currentOptions = assignDeep({}, _getDefaultOptions(), validateOptions ? validateOptions(options || {}, true) : options);
var structureSetup = createStructureSetup(target);
var scrollbarsSetup = createScrollbarsSetup(target, structureSetup);
var lifecycleHub = createLifecycleHub(currentOptions, structureSetup, scrollbarsSetup);
var instance = {
options: function options(newOptions) {
if (newOptions) {
var _validateOptions = validateOptions(newOptions, optionsTemplate, currentOptions, true),
_changedOptions = _validateOptions._validated;
var changedOptions = getOptionsDiff(currentOptions, validateOptions ? validateOptions(newOptions, true) : newOptions);
if (!isEmptyObject(_changedOptions)) {
assignDeep(currentOptions, _changedOptions);
if (!isEmptyObject(changedOptions)) {
assignDeep(currentOptions, changedOptions);
lifecycleHub._update(_changedOptions);
lifecycleHub._update(changedOptions);
}
}
@@ -2543,9 +2522,17 @@
return lifecycleHub._destroy();
}
};
each(keys(plugins), function (pluginName) {
var pluginInstance = plugins[pluginName];
if (isFunction(pluginInstance)) {
pluginInstance(OverlayScrollbars, instance);
}
});
instance.update(true);
return instance;
};
OverlayScrollbars.extend = addPlugin;
return OverlayScrollbars;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,7 +16,6 @@ import {
getBoundingClientRect,
assignDeep,
cssProperty,
PartialOptions,
} from 'support';
import {
classNameEnvironment,
@@ -25,7 +24,7 @@ import {
classNameViewportScrollbarStyling,
} from 'classnames';
import { OSOptions, defaultOptions } from 'options';
import { OSTargetElement } from 'typings';
import { OSTargetElement, PartialOptions } from 'typings';
type StructureInitializationElementFn<T> = ((target: OSTargetElement) => HTMLElement | T) | T;
@@ -255,10 +254,8 @@ const createEnvironment = (): Environment => {
const isZoom = deltaIsBigger && difference && dprChanged;
if (isZoom) {
const newScrollbarSize = (environmentInstance._nativeScrollbarSize = getNativeScrollbarSize(
body,
envElm
));
const newScrollbarSize = (environmentInstance._nativeScrollbarSize =
getNativeScrollbarSize(body, envElm));
removeElements(envElm);
if (scrollbarSize.x !== newScrollbarSize.x || scrollbarSize.y !== newScrollbarSize.y) {
@@ -3,7 +3,6 @@ import {
WH,
TRBL,
CacheValues,
PartialOptions,
each,
hasOwnProperty,
isNumber,
@@ -18,7 +17,7 @@ import { lifecycleHubOservers } from 'lifecycles/lifecycleHubObservers';
import { createTrinsicLifecycle } from 'lifecycles/trinsicLifecycle';
import { createPaddingLifecycle } from 'lifecycles/paddingLifecycle';
import { createOverflowLifecycle } from 'lifecycles/overflowLifecycle';
import { StyleObject } from 'typings';
import { StyleObject, PartialOptions } from 'typings';
import { ScrollbarsSetup } from 'setups/scrollbarsSetup';
export type LifecycleCheckOption = <T>(path: string) => LifecycleOptionInfo<T>;
+62 -73
View File
@@ -1,10 +1,13 @@
import {
optionsTemplateTypes as oTypes,
transformOptions,
OptionsTemplateValue,
OptionsWithOptionsTemplateValue,
OptionsWithOptionsTemplate,
} from 'support/options';
import { assignDeep, each, isObject, keys, isArray, hasOwnProperty, isFunction } from 'support';
import { PartialOptions } from 'typings';
const stringify = (value: any) =>
JSON.stringify(value, (_, val) => {
if (isFunction(val)) {
throw new Error();
}
return val;
});
export type ResizeBehavior = 'none' | 'both' | 'horizontal' | 'vertical';
@@ -107,83 +110,69 @@ export interface UpdatedArgs {
forced: boolean;
}
const numberAllowedValues: OptionsTemplateValue<number> = oTypes.number;
const arrayNullValues: OptionsTemplateValue<Array<unknown> | null> = [oTypes.array, oTypes.null];
const stringArrayNullAllowedValues: OptionsTemplateValue<string | ReadonlyArray<string> | null> = [oTypes.string, oTypes.array, oTypes.null];
const booleanTrueTemplate: OptionsWithOptionsTemplateValue<boolean> = [true, oTypes.boolean];
const booleanFalseTemplate: OptionsWithOptionsTemplateValue<boolean> = [false, oTypes.boolean];
// const callbackTemplate: OptionsWithOptionsTemplateValue<Func | null> = [null, [oTypes.function, oTypes.null]];
const resizeAllowedValues: OptionsTemplateValue<ResizeBehavior> = 'none both horizontal vertical';
const overflowAllowedValues: OptionsTemplateValue<OverflowBehavior> = 'hidden scroll visible visible-hidden';
const scrollbarsVisibilityAllowedValues: OptionsTemplateValue<VisibilityBehavior> = 'visible hidden auto';
const scrollbarsAutoHideAllowedValues: OptionsTemplateValue<AutoHideBehavior> = 'never scroll leavemove';
/**
* A object which serves as "default options object" and "options template object".
* I combined these two into one object so that I don't have to define two separate big objects, instead I define one big object.
*
* The property value is a tuple:
* the first value is the default value
* the second value is the template value
* Example:
* {
* a: ['default', [Type.string, Type.null]],
* b: [250, Type.number]
* }
* Property "a" has a default value of 'default' and it can be a string or null
* Property "b" has a default value of 250 and it can be number
*/
const defaultOptionsWithTemplate: OptionsWithOptionsTemplate<OSOptions> = {
resize: ['none', resizeAllowedValues], // none || both || horizontal || vertical || n || b || h || v
paddingAbsolute: booleanFalseTemplate, // true || false
export const defaultOptions: OSOptions = {
resize: 'none', // none || both || horizontal || vertical || n || b || h || v
paddingAbsolute: false, // true || false
updating: {
elementEvents: [[['img', 'load']], arrayNullValues], // array of tuples || null
attributes: [null, arrayNullValues],
debounce: [
[0, 33],
[oTypes.number, oTypes.array, oTypes.null],
], // number || number array || null
elementEvents: [['img', 'load']], // array of tuples || null
attributes: null,
debounce: [0, 33], // number || number array || null
},
overflow: {
x: ['scroll', overflowAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
y: ['scroll', overflowAllowedValues], // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
x: 'scroll', // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
y: 'scroll', // visible-hidden || visible-scroll || hidden || scroll || v-h || v-s || h || s
},
scrollbars: {
visibility: ['auto', scrollbarsVisibilityAllowedValues], // visible || hidden || auto || v || h || a
autoHide: ['never', scrollbarsAutoHideAllowedValues], // never || scroll || leave || move || n || s || l || m
autoHideDelay: [800, numberAllowedValues], // number
dragScroll: booleanTrueTemplate, // true || false
clickScroll: booleanFalseTemplate, // true || false
touch: booleanTrueTemplate, // true || false
visibility: 'auto', // visible || hidden || auto || v || h || a
autoHide: 'never', // never || scroll || leave || move || n || s || l || m
autoHideDelay: 800, // number
dragScroll: true, // true || false
clickScroll: false, // true || false
touch: true, // true || false
},
textarea: {
dynWidth: booleanFalseTemplate, // true || false
dynHeight: booleanFalseTemplate, // true || false
inheritedAttrs: [['style', 'class'], stringArrayNullAllowedValues], // string || array || null
dynWidth: false, // true || false
dynHeight: false, // true || false
inheritedAttrs: ['style', 'class'], // string || array || null
},
nativeScrollbarsOverlaid: {
show: booleanFalseTemplate, // true || false
initialize: booleanFalseTemplate, // true || false
show: false, // true || false
initialize: false, // true || false
},
callbacks: {
onUpdated: [null, [oTypes.function, oTypes.null]],
onUpdated: null,
},
/*
callbacks: {
onInitialized: callbackTemplate, // null || function
onInitializationWithdrawn: callbackTemplate, // null || function
onDestroyed: callbackTemplate, // null || function
onScrollStart: callbackTemplate, // null || function
onScroll: callbackTemplate, // null || function
onScrollStop: callbackTemplate, // null || function
onOverflowChanged: callbackTemplate, // null || function
onOverflowAmountChanged: callbackTemplate, // null || function
onDirectionChanged: callbackTemplate, // null || function
onContentSizeChanged: callbackTemplate, // null || function
onHostSizeChanged: callbackTemplate, // null || function
onUpdated: callbackTemplate, // null || function
},
*/
};
export const { _template: optionsTemplate, _options: defaultOptions } = transformOptions(defaultOptionsWithTemplate);
export const getOptionsDiff = <T>(
currOptions: T,
newOptions: PartialOptions<T>
): PartialOptions<T> => {
const diff: PartialOptions<T> = {};
const optionsKeys = keys(newOptions).concat(keys(currOptions));
each(optionsKeys, (optionKey) => {
const currOptionValue = currOptions[optionKey];
const newOptionValue = newOptions[optionKey];
if (isObject(currOptionValue) && isObject(newOptionValue)) {
assignDeep((diff[optionKey] = {}), getOptionsDiff(currOptionValue, newOptionValue));
} else if (hasOwnProperty(newOptions, optionKey) && newOptionValue !== currOptionValue) {
let isDiff = true;
if (isArray(currOptionValue) || isArray(newOptionValue)) {
try {
if (stringify(currOptionValue) === stringify(newOptionValue)) {
isDiff = false;
}
} catch {}
}
if (isDiff) {
diff[optionKey] = newOptionValue;
}
}
});
return diff;
};
@@ -1,10 +1,18 @@
import { OSTarget, OSInitializationObject } from 'typings';
import { PartialOptions, validateOptions, assignDeep, isEmptyObject } from 'support';
import { OSTarget, OSInitializationObject, PartialOptions } from 'typings';
import { assignDeep, isEmptyObject, each, isFunction, keys, isHTMLElement } from 'support';
import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
import { createScrollbarsSetup, ScrollbarsSetup } from 'setups/scrollbarsSetup';
import { createLifecycleHub } from 'lifecycles/lifecycleHub';
import { OSOptions, optionsTemplate } from 'options';
import { getOptionsDiff, OSOptions } from 'options';
import { getEnvironment } from 'environment';
import {
getPlugins,
addPlugin,
optionsValidationPluginName,
OSPlugin,
OptionsValidationPluginInstance,
} from 'plugins';
import { addInstance, getInstance } from 'instances';
export interface OverlayScrollbarsStatic {
(
@@ -12,6 +20,8 @@ export interface OverlayScrollbarsStatic {
options?: PartialOptions<OSOptions>,
extensions?: any
): OverlayScrollbars;
extend(osPlugin: OSPlugin | OSPlugin[]): void;
}
export interface OverlayScrollbars {
@@ -26,16 +36,24 @@ export interface OverlayScrollbars {
export const OverlayScrollbars: OverlayScrollbarsStatic = (
target: OSTarget | OSInitializationObject,
options?: PartialOptions<OSOptions>,
extensions?: any
options?: PartialOptions<OSOptions>
): OverlayScrollbars => {
const potentialInstance = getInstance(isHTMLElement(target) ? target : target.target);
if (potentialInstance) {
return potentialInstance;
}
const { _getDefaultOptions } = getEnvironment();
const currentOptions: OSOptions = assignDeep(
{},
_getDefaultOptions(),
validateOptions(options || ({} as PartialOptions<OSOptions>), optionsTemplate, null, true)
._validated
);
const plugins = getPlugins();
const optionsValidationPlugin = plugins[
optionsValidationPluginName
] as OptionsValidationPluginInstance;
const validateOptions = (newOptions?: PartialOptions<OSOptions>) => {
const opts = newOptions || {};
const validate = optionsValidationPlugin && optionsValidationPlugin._;
return validate ? validate(opts, true) : opts;
};
const currentOptions: OSOptions = assignDeep({}, _getDefaultOptions(), validateOptions(options));
const structureSetup: StructureSetup = createStructureSetup(target);
const scrollbarsSetup: ScrollbarsSetup = createScrollbarsSetup(target, structureSetup);
const lifecycleHub = createLifecycleHub(currentOptions, structureSetup, scrollbarsSetup);
@@ -43,16 +61,11 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
const instance: OverlayScrollbars = {
options(newOptions?: PartialOptions<OSOptions>) {
if (newOptions) {
const { _validated: _changedOptions } = validateOptions(
newOptions,
optionsTemplate,
currentOptions,
true
);
const changedOptions = getOptionsDiff(currentOptions, validateOptions(newOptions));
if (!isEmptyObject(_changedOptions)) {
assignDeep(currentOptions, _changedOptions);
lifecycleHub._update(_changedOptions);
if (!isEmptyObject(changedOptions)) {
assignDeep(currentOptions, changedOptions);
lifecycleHub._update(changedOptions);
}
}
return currentOptions;
@@ -64,7 +77,18 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
destroy: () => lifecycleHub._destroy(),
};
each(keys(plugins), (pluginName) => {
const pluginInstance = plugins[pluginName];
if (isFunction(pluginInstance)) {
pluginInstance(OverlayScrollbars, instance);
}
});
instance.update(true);
addInstance(structureSetup._targetObj._target, instance);
return instance;
};
OverlayScrollbars.extend = addPlugin;
@@ -0,0 +1 @@
export { optionsValidationPlugin } from 'plugins/optionsValidation/optionsValidation';
@@ -0,0 +1,2 @@
export * from './plugins';
export * from './optionsValidation';
@@ -0,0 +1 @@
export * from 'plugins/optionsValidation/optionsValidation';
@@ -0,0 +1,81 @@
import { OSPlugin } from 'plugins';
import {
OSOptions,
ResizeBehavior,
OverflowBehavior,
VisibilityBehavior,
AutoHideBehavior,
} from 'options';
import {
validateOptions,
OptionsTemplate,
OptionsTemplateValue,
optionsTemplateTypes as oTypes,
} from 'plugins/optionsValidation/validation';
import { PartialOptions } from 'typings';
const numberAllowedValues: OptionsTemplateValue<number> = oTypes.number;
const booleanAllowedValues: OptionsTemplateValue<boolean> = oTypes.boolean;
const arrayNullValues: OptionsTemplateValue<Array<unknown> | null> = [oTypes.array, oTypes.null];
const stringArrayNullAllowedValues: OptionsTemplateValue<string | ReadonlyArray<string> | null> = [
oTypes.string,
oTypes.array,
oTypes.null,
];
const resizeAllowedValues: OptionsTemplateValue<ResizeBehavior> = 'none both horizontal vertical';
const overflowAllowedValues: OptionsTemplateValue<OverflowBehavior> =
'hidden scroll visible visible-hidden';
const scrollbarsVisibilityAllowedValues: OptionsTemplateValue<VisibilityBehavior> =
'visible hidden auto';
const scrollbarsAutoHideAllowedValues: OptionsTemplateValue<AutoHideBehavior> =
'never scroll leavemove';
const optionsTemplate: OptionsTemplate<OSOptions> = {
resize: resizeAllowedValues, // none || both || horizontal || vertical || n || b ||
paddingAbsolute: booleanAllowedValues, // true || false
updating: {
elementEvents: arrayNullValues, // array of tuples || null
attributes: arrayNullValues,
debounce: [oTypes.number, oTypes.array, oTypes.null], // number || number array || null
},
overflow: {
x: overflowAllowedValues, // visible-hidden || visible-scroll || hidden || scrol
y: overflowAllowedValues, // visible-hidden || visible-scroll || hidden || scrol
},
scrollbars: {
visibility: scrollbarsVisibilityAllowedValues, // visible || hidden || auto || v ||
autoHide: scrollbarsAutoHideAllowedValues, // never || scroll || leave || move ||
autoHideDelay: numberAllowedValues, // number
dragScroll: booleanAllowedValues, // true || false
clickScroll: booleanAllowedValues, // true || false
touch: booleanAllowedValues, // true || false
},
textarea: {
dynWidth: booleanAllowedValues, // true || false
dynHeight: booleanAllowedValues, // true || false
inheritedAttrs: stringArrayNullAllowedValues, // string || array || nul
},
nativeScrollbarsOverlaid: {
show: booleanAllowedValues, // true || false
initialize: booleanAllowedValues, // true || false
},
callbacks: {
onUpdated: [oTypes.function, oTypes.null],
},
};
export type OptionsValidationPluginInstance = {
_: (options: PartialOptions<OSOptions>, doWriteErrors?: boolean) => PartialOptions<OSOptions>;
};
export const optionsValidationPluginName = '__osOptionsValidationPlugin';
export const optionsValidationPlugin: OSPlugin<OptionsValidationPluginInstance> = [
optionsValidationPluginName,
{
_: (options: PartialOptions<OSOptions>, doWriteErrors?: boolean) => {
const [validated, foreign] = validateOptions(optionsTemplate, options, doWriteErrors);
return { ...foreign, ...validated };
},
},
];
@@ -4,7 +4,7 @@ import {
OptionsTemplateNativeTypes,
OptionsTemplateTypes,
OptionsTemplateValue,
} from 'support/options/validation';
} from 'plugins/optionsValidation/validation';
import { PlainObject } from 'typings';
import { isArray } from 'support/utils/types';
import { each, keys } from 'support/utils';
@@ -45,8 +45,8 @@ export const transformOptions = <T>(
optionsWithOptionsTemplate[key];
if (isArray(val)) {
result._template[key] = val[1];
result._options[key] = val[0];
result._template[key] = val[1];
} else {
// if (isObject(val))
const tmpResult = transformOptions(val as OptionsWithOptionsTemplate<typeof val>);
@@ -1,14 +1,6 @@
import { each, hasOwnProperty, keys, push, isEmptyObject } from 'support/utils';
import {
type,
isArray,
isUndefined,
isPlainObject,
isString,
isNumber,
isBoolean,
} from 'support/utils/types';
import { PlainObject } from 'typings';
import { type, isArray, isUndefined, isPlainObject, isString } from 'support/utils/types';
import { PlainObject, PartialOptions } from 'typings';
export type OptionsObjectType = Record<string, unknown>;
export type OptionsFunctionType = (this: unknown, ...args: unknown[]) => unknown;
@@ -33,14 +25,10 @@ export type OptionsTemplate<T> = {
: never;
};
export type OptionsValidationResult<T> = {
readonly _foreign: Record<string, unknown>;
readonly _validated: PartialOptions<T>;
};
export type PartialOptions<T> = {
[P in keyof T]?: T[P] extends OptionsObjectType ? PartialOptions<T[P]> : T[P];
};
export type OptionsValidationResult<T> = [
PartialOptions<T>, // validated
Record<string, unknown> // foreign
];
type OptionsTemplateTypeMap = {
__TPL_boolean_TYPE__: boolean;
@@ -70,8 +58,6 @@ type OptionsTemplateTypesDictionary = {
readonly null: OptionsTemplateType<null>;
};
const { stringify } = JSON;
/**
* A prefix and suffix tuple which serves as recognition pattern for template types.
*/
@@ -101,8 +87,8 @@ const optionsTemplateTypes: OptionsTemplateTypesDictionary = [
* foreign : a object which consists of properties which aren't defined inside the template. (foreign properties)
* validated : a object which consists only of valid properties. (property name is inside the template and value has a correct type)
* }
* @param options The options object which shall be validated.
* @param template The template according to which the options object shall be validated.
* @param options The options object which shall be validated.
* @param optionsDiff When provided the returned validated object will only have properties which are different to this objects properties.
* Example (assume all properties are valid to the template):
* Options object : { a: 'a', b: 'b', c: 'c' }
@@ -114,9 +100,8 @@ const optionsTemplateTypes: OptionsTemplateTypesDictionary = [
* @param propPath The propertyPath which lead to this object. (used for error logging)
*/
const validateRecursive = <T extends PlainObject>(
options: PartialOptions<T>,
template: OptionsTemplate<T>,
optionsDiff: T,
options: PartialOptions<T>,
doWriteErrors?: boolean,
propPath?: string
): OptionsValidationResult<T> => {
@@ -125,7 +110,6 @@ const validateRecursive = <T extends PlainObject>(
const props = keys(template).filter((prop) => hasOwnProperty(options, prop));
each(props, (prop: Extract<keyof T, string>) => {
const optionsDiffValue: any = isUndefined(optionsDiff[prop]) ? {} : optionsDiff[prop];
const optionsValue: any = options[prop];
const templateValue: PlainObject | string | OptionsTemplateTypes | Array<OptionsTemplateTypes> =
template[prop];
@@ -134,15 +118,14 @@ const validateRecursive = <T extends PlainObject>(
// if the template has a object as value, it means that the options are complex (verschachtelt)
if (templateIsComplex && isPlainObject(optionsValue)) {
const validatedResult = validateRecursive(
optionsValue,
const [validated, foreign] = validateRecursive(
templateValue as T,
optionsDiffValue,
optionsValue,
doWriteErrors,
propPrefix + prop
);
validatedOptions[prop] = validatedResult._validated as any;
optionsCopy[prop] = validatedResult._foreign as any;
validatedOptions[prop] = validated as any;
optionsCopy[prop] = foreign as any;
each([optionsCopy, validatedOptions], (value) => {
if (isEmptyObject(value[prop])) {
@@ -186,17 +169,7 @@ const validateRecursive = <T extends PlainObject>(
});
if (isValid) {
const isPrimitiveArr =
isArray(optionsValue) &&
!optionsValue.some((val) => !isNumber(val) && !isString(val) && !isBoolean(val));
const doStringifyComparison = isPrimitiveArr || isPlainObject(optionsValue);
if (
doStringifyComparison
? stringify(optionsValue) !== stringify(optionsDiffValue)
: optionsValue !== optionsDiffValue
) {
validatedOptions[prop] = optionsValue;
}
validatedOptions[prop] = optionsValue;
} else if (doWriteErrors) {
console.warn(
`${
@@ -214,44 +187,23 @@ const validateRecursive = <T extends PlainObject>(
}
});
return {
_foreign: optionsCopy,
_validated: validatedOptions,
};
return [validatedOptions, optionsCopy]; // optionsCopy equals now to foreign options
};
/**
* Validates the given options object according to the given template object and returns a object which looks like:
* {
* foreign : a object which consists of properties which aren't defined inside the template. (foreign properties)
* Validates the given options object according to the given template object and returns a tuple which looks like:
* [
* validated : a object which consists only of valid properties. (property name is inside the template and value has a correct type)
* }
* @param options The options object which shall be validated.
* foreign : a object which consists of properties which aren't defined inside the template. (foreign properties)
* ]
* @param template The template according to which the options object shall be validated.
* @param optionsDiff When provided the returned validated object will only have properties which are different to this objects properties.
* Example (assume all properties are valid to the template):
* Options object : { a: 'a', b: 'b', c: 'c' }
* optionsDiff object : { a: 'a', b: 'b', c: undefined }
* Returned validated object : { c: 'c' }
* Because the value of the properties a and b didn't change, they aren't included in the returned object.
* Without the optionsDiff object the returned validated object would be: { a: 'a', b: 'b', c: 'c' }
* @param options The options object which shall be validated.
* @param doWriteErrors True if errors shall be logged into the console, false otherwise.
*/
const validateOptions = <T extends PlainObject>(
options: PartialOptions<T>,
template: OptionsTemplate<T>,
optionsDiff?: T | null,
options: PartialOptions<T>,
doWriteErrors?: boolean
): OptionsValidationResult<T> =>
/*
if (!isEmptyObject(foreign) && doWriteErrors)
console.warn(`The following options are discarded due to invalidity:\r\n ${window.JSON.stringify(foreign, null, 2)}`);
//add values, which aren't specified in the template, to the finished validated object to prevent them from being discarded
if (keepForeignProps) {
Object.assign(result.validated, foreign);
}
*/
validateRecursive<T>(options, template, optionsDiff || ({} as T), doWriteErrors || false);
): OptionsValidationResult<T> => validateRecursive<T>(template, options, doWriteErrors);
export { validateOptions, optionsTemplateTypes };
@@ -0,0 +1,16 @@
import { assignDeep, each, isArray } from 'support';
import { OverlayScrollbars, OverlayScrollbarsStatic } from 'overlayscrollbars';
export type OSPluginInstance =
| Record<string, unknown>
| ((staticObj: OverlayScrollbarsStatic, instanceObj: OverlayScrollbars) => void);
export type OSPlugin<T extends OSPluginInstance = OSPluginInstance> = [string, T];
const pluginRegistry: Record<string, OSPluginInstance> = {};
export const getPlugins = () => assignDeep({}, pluginRegistry);
export const addPlugin = (addedPlugin: OSPlugin | OSPlugin[]) =>
each((isArray(addedPlugin) ? addedPlugin : [addedPlugin]) as OSPlugin[], (plugin) => {
pluginRegistry[plugin[0]] = plugin[1];
});
@@ -67,9 +67,13 @@ const unwrap = (elm: HTMLElement | false | null | undefined) => {
};
const createUniqueViewportArrangeElement = (): HTMLStyleElement | false => {
const { _nativeScrollbarStyling, _nativeScrollbarIsOverlaid, _cssCustomProperties } = getEnvironment();
const { _nativeScrollbarStyling, _nativeScrollbarIsOverlaid, _cssCustomProperties } =
getEnvironment();
/* istanbul ignore next */
const create = !_cssCustomProperties && !_nativeScrollbarStyling && (_nativeScrollbarIsOverlaid.x || _nativeScrollbarIsOverlaid.y);
const create =
!_cssCustomProperties &&
!_nativeScrollbarStyling &&
(_nativeScrollbarIsOverlaid.x || _nativeScrollbarIsOverlaid.y);
const result = create ? document.createElement('style') : false;
if (result) {
@@ -86,8 +90,9 @@ const staticCreationFromStrategy = (
strategy: StructureInitializationStaticElement,
elementClass: string
): HTMLElement => {
const result = initializationValue ? initializationValue : isFunction(strategy) ? strategy(target) : (strategy as null);
return result ? result : createDiv(elementClass);
const result =
initializationValue || (isFunction(strategy) ? strategy(target) : (strategy as null));
return result || createDiv(elementClass);
};
const dynamicCreationFromStrategy = (
@@ -98,7 +103,11 @@ const dynamicCreationFromStrategy = (
defaultValue: boolean
): HTMLElement | false => {
const takeInitializationValue = isBoolean(initializationValue) || initializationValue;
const result = takeInitializationValue ? (initializationValue as boolean | HTMLElement) : isFunction(strategy) ? strategy(target) : strategy;
const result = takeInitializationValue
? (initializationValue as boolean | HTMLElement)
: isFunction(strategy)
? strategy(target)
: strategy;
if (result === null) {
return defaultValue ? createDiv(elementClass) : false;
@@ -107,7 +116,9 @@ const dynamicCreationFromStrategy = (
return result === true ? createDiv(elementClass) : result;
};
export const createStructureSetup = (target: OSTarget | StructureInitialization): StructureSetup => {
export const createStructureSetup = (
target: OSTarget | StructureInitialization
): StructureSetup => {
const { _getInitializationStrategy, _nativeScrollbarStyling } = getEnvironment();
const {
_host: hostInitializationStrategy,
@@ -117,7 +128,9 @@ export const createStructureSetup = (target: OSTarget | StructureInitialization)
} = _getInitializationStrategy() as StructureInitializationStrategy;
const targetIsElm = isHTMLElement(target);
const targetStructureInitialization = target as StructureInitialization;
const targetElement = targetIsElm ? (target as OSTargetElement) : targetStructureInitialization.target;
const targetElement = targetIsElm
? (target as OSTargetElement)
: targetStructureInitialization.target;
const isTextarea = is(targetElement, 'textarea');
const isBody = !isTextarea && is(targetElement, 'body');
const ownerDocument: HTMLDocument = targetElement!.ownerDocument;
@@ -126,9 +139,19 @@ export const createStructureSetup = (target: OSTarget | StructureInitialization)
const evaluatedTargetObj: PreparedOSTargetObject = {
_target: targetElement,
_host: isTextarea
? staticCreationFromStrategy(targetElement, targetStructureInitialization.host, hostInitializationStrategy, classNameHost)
? staticCreationFromStrategy(
targetElement,
targetStructureInitialization.host,
hostInitializationStrategy,
classNameHost
)
: (targetElement as HTMLElement),
_viewport: staticCreationFromStrategy(targetElement, targetStructureInitialization.viewport, viewportInitializationStrategy, classNameViewport),
_viewport: staticCreationFromStrategy(
targetElement,
targetStructureInitialization.viewport,
viewportInitializationStrategy,
classNameViewport
),
_padding: dynamicCreationFromStrategy(
targetElement,
targetStructureInitialization.padding,
@@ -158,13 +181,18 @@ export const createStructureSetup = (target: OSTarget | StructureInitialization)
const value = evaluatedTargetObj[key];
return push(arr, value && !parent(value) ? value : false);
}, [] as HTMLElement[]);
const elementIsGenerated = (elm: HTMLElement | false) => (elm ? indexOf(generatedElements, elm) > -1 : null);
const elementIsGenerated = (elm: HTMLElement | false) =>
elm ? indexOf(generatedElements, elm) > -1 : null;
const { _target, _host, _padding, _viewport, _content, _viewportArrange } = evaluatedTargetObj;
const destroyFns: (() => any)[] = [];
const isTextareaHostGenerated = isTextarea && elementIsGenerated(_host);
const targetContents = isTextarea
? _target
: contents([_content, _viewport, _padding, _host, _target].find((elm) => elementIsGenerated(elm) === false));
: contents(
[_content, _viewport, _padding, _host, _target].find(
(elm) => elementIsGenerated(elm) === false
)
);
const contentSlot = _content || _viewport;
// only insert host for textarea after target if it was generated
@@ -1,5 +1,4 @@
export * from 'support/cache';
export * from 'support/compatibility';
export * from 'support/dom';
export * from 'support/options';
export * from 'support/utils';
@@ -1,2 +0,0 @@
export * from 'support/options/validation';
export * from 'support/options/transformation';
@@ -1,4 +1,4 @@
import { isArrayLike, isString, isArray } from 'support/utils/types';
import { isArrayLike, isString } from 'support/utils/types';
import { PlainObject } from 'typings';
type RunEachItem = ((...args: any) => any | any[]) | null | undefined;
@@ -57,7 +57,7 @@ export function each<T>(
* @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at index 0.
*/
export const indexOf = <T = any>(arr: T[], item: T, fromIndex?: number): number =>
isArray(arr) ? arr.indexOf(item, fromIndex) : -1;
arr.indexOf(item, fromIndex);
/**
* Pushesh all given items into the given array and returns it.
@@ -75,7 +75,7 @@ export const push = <T>(array: T[], items: T | ArrayLike<T>, arrayIsSingleItem?:
* Creates a shallow-copied Array instance from an array-like or iterable object.
* @param arr The object from which the array instance shall be created.
*/
export const from = <T = any>(arr: ArrayLike<T>) => {
export const from = <T = any>(arr: ArrayLike<T> | Set<T>) => {
if (Array.from) {
return Array.from(arr);
}
+18 -12
View File
@@ -1,10 +1,14 @@
export type PartialOptions<T> = {
[P in keyof T]?: T[P] extends Record<string, unknown> ? PartialOptions<T[P]> : T[P];
};
export type PlainObject<T = any> = { [name: string]: T };
export type StyleObject<CustomCssProps = ''> = {
[Key in (keyof CSSStyleDeclaration | (CustomCssProps extends string ? CustomCssProps : ''))]?: string | number;
}
[Key in keyof CSSStyleDeclaration | (CustomCssProps extends string ? CustomCssProps : '')]?:
| string
| number;
};
export type InternalVersionOf<T> = {
[K in keyof T as `_${Uncapitalize<string & K>}`]: T[K];
@@ -27,15 +31,15 @@ type StructureInitializationDynamicElement = HTMLElement | boolean;
/**
* Object for special initialization.
*
*
* Target is always required, if element is not provided or undefined it will be generated.
*
* If element is provided, the provided element takes all its responsibilities.
*
* If element is provided, the provided element takes all its responsibilities.
* DOM hierarchy isn't checked in this case, its assumed that hieararchy is correct in such a case.
*
*
* Undefined means that the environment initialization strategy for the respective element is used.
*/
export interface StructureInitialization {
export interface StructureInitialization {
target: OSTargetElement;
host?: StructureInitializationStaticElement; // only relevant for textarea
viewport?: StructureInitializationStaticElement;
@@ -45,15 +49,17 @@ type StructureInitializationDynamicElement = HTMLElement | boolean;
/**
* Object for special initialization.
*
*
* scrollbarsSlot is the element to which the scrollbars are applied to. If null or undefined the plugin decides by itself whats the scrollbars slot.
*/
export interface ScrollbarsInitialization {
scrollbarsSlot?: null | HTMLElement | ((target: OSTargetElement, host: HTMLElement, viewport: HTMLElement) => null | HTMLElement);
scrollbarsSlot?:
| null
| HTMLElement
| ((target: OSTargetElement, host: HTMLElement, viewport: HTMLElement) => null | HTMLElement);
}
export interface OSInitializationObject extends StructureInitialization, ScrollbarsInitialization {
}
export interface OSInitializationObject extends StructureInitialization, ScrollbarsInitialization {}
export type OSTarget = OSTargetElement | OSInitializationObject;
@@ -1,9 +1,252 @@
import { validateOptions } from 'support/options';
import { defaultOptions, optionsTemplate } from 'options';
import { defaultOptions, getOptionsDiff } from 'options';
describe('options', () => {
test('default options matching the options template', () => {
const { _validated } = validateOptions(defaultOptions, optionsTemplate);
expect(_validated).toEqual(defaultOptions);
test('defaultOptions', () => {
expect(defaultOptions).toEqual({
resize: 'none',
paddingAbsolute: false,
updating: {
elementEvents: [['img', 'load']],
attributes: null,
debounce: [0, 33],
},
overflow: {
x: 'scroll',
y: 'scroll',
},
scrollbars: {
visibility: 'auto',
autoHide: 'never',
autoHideDelay: 800,
dragScroll: true,
clickScroll: false,
touch: true,
},
textarea: {
dynWidth: false,
dynHeight: false,
inheritedAttrs: ['style', 'class'],
},
nativeScrollbarsOverlaid: {
show: false,
initialize: false,
},
callbacks: {
onUpdated: null,
},
});
});
describe('getOptionsDiff', () => {
test('diff simple options', () => {
const options = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
};
const changed = {
a: 0,
b: 0,
};
expect(getOptionsDiff(options, changed)).toEqual(changed);
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual(changed);
});
test('diff nested options', () => {
const options = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
deep: {
a: 1,
b: 2,
c: 3,
d: 4,
},
};
const changed = {
a: 0,
b: 0,
deep: {
a: 0,
b: 0,
},
};
expect(getOptionsDiff(options, changed)).toEqual(changed);
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual(changed);
expect(
getOptionsDiff(options, {
...options,
...changed,
deep: {
...options.deep,
...changed.deep,
},
})
).toEqual(changed);
});
test('diff foreign options', () => {
const options = {
a: 1,
b: 2,
c: 3,
};
const changed = {
a: 0,
d: 4,
e: 5,
};
expect(getOptionsDiff(options, changed)).toEqual(changed);
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual(changed);
});
describe('diff arrays', () => {
test('dont diff same looking arrays with primitve values', () => {
const options = {
a: [],
b: [1, 2, 3],
c: ['1', '2', '3'],
d: [true, false, false],
};
const changed = {
a: [],
b: [1, 2, 3],
c: ['1', '2', '3'],
d: [true, false, false],
};
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual({});
expect(getOptionsDiff(options, changed)).toEqual({});
});
test('dont diff same looking arrays with non-primitve values', () => {
const options = {
a: [],
b: [{ a: 1 }, { b: 2 }, { c: 3 }, ['tuple1', 'tuple2'], []],
c: [
['tuple1', 'tuple2'],
['tuple1', 'tuple2'],
['tuple1', 'tuple2'],
],
};
const changed = {
a: [],
b: [{ a: 1 }, { b: 2 }, { c: 3 }, ['tuple1', 'tuple2'], []],
c: [
['tuple1', 'tuple2'],
['tuple1', 'tuple2'],
['tuple1', 'tuple2'],
],
};
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual({});
expect(getOptionsDiff(options, changed)).toEqual({});
});
test('diff arrays with primitve values', () => {
const options = {
a: [] as string[],
b: [1, 2, 3],
c: ['1', '2', '3'],
d: [true, false, false],
};
const changed = {
a: ['hello'],
b: [1, 2, 3, 4],
c: ['1', '2', '3', '4'],
d: [true, false, false, true],
};
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual(changed);
expect(getOptionsDiff(options, changed)).toEqual(changed);
});
test('diff arrays with mixed values', () => {
const options = {
a: [] as Record<string, unknown>[],
b: [1, 2, 3] as Array<number | (() => void)>,
c: ['1', '2', '3'] as Array<string | Record<string, unknown>>,
d: [true, false, false],
};
const changed = {
a: [{}],
b: [1, 2, 3, () => {}],
c: ['1', '2', '3', {}],
};
expect(getOptionsDiff(options, changed)).toEqual(changed);
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual(changed);
});
test('diff same looking arrays with not serializable values', () => {
const fn = () => {};
const options = {
c: [fn, fn, () => {}],
};
const changed = {
c: [fn, fn, () => {}],
};
expect(
getOptionsDiff(options, {
...options,
...changed,
})
).toEqual(changed);
expect(getOptionsDiff(options, changed)).toEqual(changed);
});
});
});
});
@@ -0,0 +1,32 @@
import { defaultOptions } from 'options';
import { optionsValidationPlugin, optionsValidationPluginName } from 'plugins/optionsValidation';
const getValidationFn = () => {
const [name, instance] = optionsValidationPlugin;
const validationFn = instance._;
expect(name).toBe(optionsValidationPluginName);
expect(typeof validationFn).toBe('function');
return validationFn;
};
describe('optionsValidationPlugin', () => {
test('default options matching the options template', () => {
const validationFn = getValidationFn();
expect(validationFn(defaultOptions)).toEqual(defaultOptions);
});
test('foreign options are in the result', () => {
const validationFn = getValidationFn();
const foreignOps = {
someOption: true,
someDeepOption: {
someDeepOption: false,
},
};
// @ts-ignore
expect(validationFn(foreignOps)).toEqual(foreignOps);
});
});
@@ -1,5 +1,12 @@
import { PlainObject } from 'typings';
import { optionsTemplateTypes as oTypes, transformOptions, OptionsTemplate, OptionsWithOptionsTemplate } from 'support/options';
import {
optionsTemplateTypes as oTypes,
OptionsTemplate,
} from 'plugins/optionsValidation/validation';
import {
transformOptions,
OptionsWithOptionsTemplate,
} from 'plugins/optionsValidation/transformation';
type TestOptionsObj = { propA: 'propA'; null: null };
type TestOptionsEnum = 'A' | 'B' | 'C';
@@ -36,7 +43,7 @@ const options: DeepRequired<TestOptions> = {
func: () => {},
};
const optionsTemplate: OptionsTemplate<Required<TestOptions>> = {
const optionsTemplate: OptionsTemplate<DeepRequired<TestOptions>> = {
str: oTypes.string,
strArrNull: [oTypes.string, oTypes.array, oTypes.null],
nullbool: [oTypes.boolean, oTypes.null],
@@ -51,7 +58,7 @@ const optionsTemplate: OptionsTemplate<Required<TestOptions>> = {
func: oTypes.function,
};
const TestOptionsWithOptionsTemplate: OptionsWithOptionsTemplate<Required<TestOptions>> = {
const TestOptionsWithOptionsTemplate: OptionsWithOptionsTemplate<DeepRequired<TestOptions>> = {
str: [options.str, optionsTemplate.str],
strArrNull: [options.strArrNull, optionsTemplate.strArrNull],
nullbool: [options.nullbool, optionsTemplate.nullbool],
@@ -0,0 +1,303 @@
import {
validateOptions,
optionsTemplateTypes as oTypes,
OptionsTemplate,
} from 'plugins/optionsValidation/validation';
import { assignDeep } from 'support/utils';
type TestOptionsObj = { propA: 'propA'; null: null };
type TestOptionsEnum = 'A' | 'B' | 'C';
type TestOptions = {
str: string;
strArrNull: string | Array<string> | null;
nullbool: boolean | null;
nested: {
num: number;
switch: boolean;
abc: TestOptionsEnum;
};
obj: TestOptionsObj | null;
abc: TestOptionsEnum;
arr: Array<any>;
func: () => void;
};
const options: TestOptions = {
str: 'hi',
strArrNull: null,
nullbool: true,
nested: {
num: 1,
switch: false,
abc: 'B',
},
obj: { propA: 'propA', null: null },
abc: 'A',
arr: [1, 2, 3],
func: () => {},
};
const template: OptionsTemplate<TestOptions> = {
str: oTypes.string,
strArrNull: [oTypes.string, oTypes.array, oTypes.null],
nullbool: [oTypes.boolean, oTypes.null],
nested: {
num: oTypes.number,
switch: oTypes.boolean,
abc: 'A B C',
},
obj: [oTypes.object, oTypes.null],
abc: 'A B C',
arr: oTypes.array,
func: oTypes.function,
};
describe('options validation', () => {
describe('object return & mutation', () => {
test('foreign properties wont affect validated object', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, { nested: foreignObj }, foreignObj);
const [validated, foreign] = validateOptions(template, modifiedOptions);
expect(validated).toEqual(options);
expect(foreign).toEqual({ ...foreignObj, nested: foreignObj });
});
test('passed object isnt returned object', () => {
const clonedOptions = assignDeep({}, options);
const [validated, foreign] = validateOptions(template, clonedOptions);
expect(validated).not.toBe(clonedOptions);
expect(foreign).toEqual({});
});
});
describe('foreign property return', () => {
test('return no foreign property', () => {
const [, foreign] = validateOptions(template, options);
expect(foreign).toEqual({});
});
test('return signle non-object foreign property', () => {
const foreignObj = { foreignProp: 'foreign' };
const modifiedOptions = assignDeep({}, options, foreignObj);
const [, foreign] = validateOptions(template, modifiedOptions);
expect(foreign).toEqual(foreignObj);
});
test('return complex foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, foreignObj);
const [, foreign] = validateOptions(template, modifiedOptions);
expect(foreign).toEqual(foreignObj);
});
test('return nested complex foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, { nested: foreignObj }, foreignObj);
const [, foreign] = validateOptions(template, modifiedOptions);
expect(foreign.nested).toEqual(foreignObj);
delete foreign.nested;
expect(foreign).toEqual(foreignObj);
});
});
describe('value validity', () => {
test('single value doesnt match template', () => {
const modifiedOptions = assignDeep({}, options, { str: 1 });
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('str');
});
test('single enum value doesnt match template', () => {
const modifiedOptions = assignDeep({}, options, { abc: 'testval' });
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('abc');
});
test('multiple values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
str: 1,
abc: 'testval',
nullbool: 'string',
});
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('str');
expect(validated).not.toHaveProperty('abc');
expect(validated).not.toHaveProperty('nullbool');
});
test('single nested value dont match template', () => {
const modifiedOptions = assignDeep({}, options, { nested: { num: 'hi' } });
const [validated] = validateOptions(template, modifiedOptions);
expect(validated.nested).not.toHaveProperty('num');
});
test('single nested enum value dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { abc: 'testabc' },
});
const [validated] = validateOptions(template, modifiedOptions);
expect(validated.nested).not.toHaveProperty('abc');
});
test('multiple nested values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: 'hi', abc: 'testabc' },
});
const [validated] = validateOptions(template, modifiedOptions);
expect(validated.nested).not.toHaveProperty('num');
expect(validated.nested).not.toHaveProperty('abc');
});
test('all nested values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: 'hi', abc: 'testabc', switch: 1 },
});
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('nested');
});
test('all nested values dont match template with foreign property', () => {
const modifiedOptions = assignDeep({}, options, {
nested: {
foreign: 'foreign',
num: 'hi',
abc: 'testabc',
switch: 1,
},
});
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('nested');
});
test('various values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { switch: null },
obj: 1,
abc: 'testest',
func: {},
});
const [validated] = validateOptions(template, modifiedOptions);
expect(validated.nested).not.toHaveProperty('switch');
expect(validated).not.toHaveProperty('obj');
expect(validated).not.toHaveProperty('abc');
expect(validated).not.toHaveProperty('func');
});
test('various values dont match template with foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep(
{},
options,
{
nested: { switch: null },
obj: 1,
abc: 'testest',
func: {},
},
foreignObj,
{ nested: foreignObj }
);
const [validated, foreign] = validateOptions(template, modifiedOptions);
expect(foreign.nested).toEqual(foreignObj);
delete foreign.nested;
expect(foreign).toEqual(foreignObj);
expect(validated.nested).not.toHaveProperty('switch');
expect(validated).not.toHaveProperty('obj');
expect(validated).not.toHaveProperty('abc');
expect(validated).not.toHaveProperty('func');
});
test('nested object is string', () => {
const modifiedOptions = assignDeep({}, options, { nested: 'string' });
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('nested');
});
test('nested object is null', () => {
const modifiedOptions = assignDeep({}, options, { nested: null });
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('nested');
});
test('nested object is undefined', () => {
const modifiedOptions: Partial<TestOptions> = assignDeep({}, options);
modifiedOptions.nested = undefined;
const [validated] = validateOptions(template, modifiedOptions);
expect(validated).not.toHaveProperty('nested');
});
});
describe('error logging', () => {
test('dont log error if nothing is wrong', () => {
const { warn } = console;
console.warn = jest.fn();
validateOptions(template, options, true);
expect(console.warn).not.toBeCalled();
console.warn = warn;
});
test('dont log error if something is wrong and flag is false', () => {
const { warn } = console;
console.warn = jest.fn();
const modifiedOptions = assignDeep({}, options, { str: 1 });
validateOptions(template, modifiedOptions, false);
expect(console.warn).not.toBeCalled();
console.warn = warn;
});
test('log error if something is wrong and flag is true', () => {
const { warn } = console;
console.warn = jest.fn();
// str must be string
validateOptions(template, assignDeep({}, options, { str: 1 }), true);
expect(console.warn).toBeCalledTimes(1);
// abc must be A | B | C
validateOptions(template, assignDeep({}, options, { abc: 'some string' }), true);
expect(console.warn).toBeCalledTimes(2);
// everthing OK
validateOptions(template, assignDeep({}, options, { abc: 'C' }), true);
expect(console.warn).toBeCalledTimes(2);
console.warn = warn;
});
});
});
@@ -8,11 +8,9 @@ import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
import { isHTMLElement } from 'support';
const mockGetEnvironment = jest.fn();
jest.mock('environment', () => {
return {
getEnvironment: jest.fn().mockImplementation(() => mockGetEnvironment()),
};
});
jest.mock('environment', () => ({
getEnvironment: jest.fn().mockImplementation(() => mockGetEnvironment()),
}));
interface StructureSetupProxy {
input: OSTarget | StructureInitialization;
@@ -377,9 +375,11 @@ describe('structureSetup', () => {
describe('complex', () => {
describe('single assigned', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -394,9 +394,11 @@ describe('structureSetup', () => {
});
test('viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -411,9 +413,11 @@ describe('structureSetup', () => {
});
test('content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="content">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -430,9 +434,11 @@ describe('structureSetup', () => {
describe('multiple assigned', () => {
test('padding viewport content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport"><div id="content">${content}</div></div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding"><div id="viewport"><div id="content">${content}</div></div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -449,9 +455,11 @@ describe('structureSetup', () => {
});
test('padding viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -467,9 +475,11 @@ describe('structureSetup', () => {
});
test('padding content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="content">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding"><div id="content">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -485,9 +495,11 @@ describe('structureSetup', () => {
});
test('viewport content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -599,9 +611,11 @@ describe('structureSetup', () => {
describe('mixed', () => {
test('false: padding & content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -618,9 +632,11 @@ describe('structureSetup', () => {
});
test('true: padding & content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -637,9 +653,11 @@ describe('structureSetup', () => {
});
test('true: content | false: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -656,9 +674,11 @@ describe('structureSetup', () => {
});
test('true: padding | false: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -675,9 +695,11 @@ describe('structureSetup', () => {
});
test('false: padding | assigned: content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="content">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -693,9 +715,11 @@ describe('structureSetup', () => {
});
test('true: padding | assigned: content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="content">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -711,9 +735,11 @@ describe('structureSetup', () => {
});
test('false: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -729,9 +755,11 @@ describe('structureSetup', () => {
});
test('true: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -747,9 +775,11 @@ describe('structureSetup', () => {
});
test('false: padding | assigned: viewport & content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -766,9 +796,11 @@ describe('structureSetup', () => {
});
test('true: padding | assigned: viewport & content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -785,9 +817,11 @@ describe('structureSetup', () => {
});
test('false: content | assigned: padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -803,9 +837,11 @@ describe('structureSetup', () => {
});
test('true: content | assigned: padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -821,9 +857,11 @@ describe('structureSetup', () => {
});
test('false: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -839,9 +877,11 @@ describe('structureSetup', () => {
});
test('true: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="viewport">${content}</div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -857,9 +897,11 @@ describe('structureSetup', () => {
});
test('false: content | assigned: padding & viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -876,9 +918,11 @@ describe('structureSetup', () => {
});
test('true: content | assigned: padding & viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const snapshot = fillBody(
isTextarea,
(content, hostId) =>
`<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`
);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
@@ -1,438 +0,0 @@
import { validateOptions, optionsTemplateTypes as oTypes, OptionsTemplate } from 'support/options';
import { assignDeep, isEmptyObject } from 'support/utils';
type TestOptionsObj = { propA: 'propA'; null: null };
type TestOptionsEnum = 'A' | 'B' | 'C';
type TestOptions = {
str: string;
strArrNull: string | Array<string> | null;
nullbool: boolean | null;
nested: {
num: number;
switch: boolean;
abc: TestOptionsEnum;
};
obj: TestOptionsObj | null;
abc: TestOptionsEnum;
arr: Array<any>;
func: () => void;
};
const options: TestOptions = {
str: 'hi',
strArrNull: null,
nullbool: true,
nested: {
num: 1,
switch: false,
abc: 'B',
},
obj: { propA: 'propA', null: null },
abc: 'A',
arr: [1, 2, 3],
func: () => {},
};
const template: OptionsTemplate<TestOptions> = {
str: oTypes.string,
strArrNull: [oTypes.string, oTypes.array, oTypes.null],
nullbool: [oTypes.boolean, oTypes.null],
nested: {
num: oTypes.number,
switch: oTypes.boolean,
abc: 'A B C',
},
obj: [oTypes.object, oTypes.null],
abc: 'A B C',
arr: oTypes.array,
func: oTypes.function,
};
describe('options validation', () => {
describe('object return & mutation', () => {
test('foreign properties wont affect validated object', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, { nested: foreignObj }, foreignObj);
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).toEqual(options);
});
test('passed objects arent mutated', () => {
const clonedOptions = assignDeep({}, options);
validateOptions(clonedOptions, template, clonedOptions);
expect(clonedOptions).toEqual(options);
});
test('passed object isnt returned object', () => {
const clonedOptions = assignDeep({}, options);
const result = validateOptions(clonedOptions, template);
expect(result._validated).not.toBe(clonedOptions);
});
});
describe('foreign property return', () => {
test('return no foreign property', () => {
const result = validateOptions(options, template);
expect(isEmptyObject(result._foreign)).toBe(true);
});
test('return signle non-object foreign property', () => {
const foreignObj = { foreignProp: 'foreign' };
const modifiedOptions = assignDeep({}, options, foreignObj);
const result = validateOptions(modifiedOptions, template);
const { _foreign } = result;
expect(_foreign).toEqual(foreignObj);
});
test('return complex foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, foreignObj);
const result = validateOptions(modifiedOptions, template);
const { _foreign } = result;
expect(_foreign).toEqual(foreignObj);
});
test('return nested complex foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep({}, options, { nested: foreignObj }, foreignObj);
const result = validateOptions(modifiedOptions, template);
const { _foreign } = result;
expect(_foreign.nested).toEqual(foreignObj);
delete _foreign.nested;
expect(_foreign).toEqual(foreignObj);
});
});
describe('diff property return', () => {
test('one value changed', () => {
const modifiedOptions = assignDeep({}, options, { str: 'newvaluetest' });
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(_validated.str).toBe('newvaluetest');
delete _validated.str;
expect(isEmptyObject(_validated)).toBe(true);
});
test('multiple values changed', () => {
const modifiedOptions = assignDeep({}, options, {
str: 'newvaluetest',
nullbool: null,
});
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(_validated.str).toBe('newvaluetest');
expect(_validated.nullbool).toBe(null);
delete _validated.str;
delete _validated.nullbool;
expect(isEmptyObject(_validated)).toBe(true);
});
test('one nested value changed', () => {
const modifiedOptions = assignDeep({}, options, { nested: { num: -1293 } });
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(_validated.nested?.num).toBe(-1293);
delete _validated.nested?.num;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
test('multiple nested values changed', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: -1293, abc: 'C' },
});
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(_validated.nested?.num).toBe(-1293);
expect(_validated.nested?.abc).toBe('C');
delete _validated.nested?.num;
delete _validated.nested?.abc;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
test('various values changed', () => {
const newFunc = () => {};
const modifiedOptions = assignDeep({}, options, {
str: 'newstrvalue',
func: newFunc,
abc: 'C',
nested: { num: -1293, abc: 'C' },
});
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(_validated.str).toBe('newstrvalue');
expect(_validated.func).toBe(newFunc);
expect(_validated.abc).toBe('C');
delete _validated.str;
delete _validated.func;
delete _validated.abc;
expect(_validated.nested?.num).toBe(-1293);
expect(_validated.nested?.abc).toBe('C');
delete _validated.nested?.num;
delete _validated.nested?.abc;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
test('various values changed with foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const newFunc = () => {};
const modifiedOptions = assignDeep(
{},
options,
{
str: 'newstrvalue',
func: newFunc,
abc: 'C',
nested: { num: -1293, abc: 'C' },
},
foreignObj,
{ nested: foreignObj }
);
const result = validateOptions(modifiedOptions, template, options);
const { _validated } = result;
expect(_validated.str).toBe('newstrvalue');
expect(_validated.func).toBe(newFunc);
expect(_validated.abc).toBe('C');
delete _validated.str;
delete _validated.func;
delete _validated.abc;
expect(_validated.nested?.num).toBe(-1293);
expect(_validated.nested?.abc).toBe('C');
delete _validated.nested?.num;
delete _validated.nested?.abc;
expect(isEmptyObject(_validated.nested)).toBe(true);
delete _validated.nested;
expect(isEmptyObject(_validated)).toBe(true);
});
});
describe('value validity', () => {
test('single value doesnt match template', () => {
const modifiedOptions = assignDeep({}, options, { str: 1 });
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('str');
});
test('single enum value doesnt match template', () => {
const modifiedOptions = assignDeep({}, options, { abc: 'testval' });
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('abc');
});
test('multiple values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
str: 1,
abc: 'testval',
nullbool: 'string',
});
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('str');
expect(_validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('nullbool');
});
test('single nested value dont match template', () => {
const modifiedOptions = assignDeep({}, options, { nested: { num: 'hi' } });
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated.nested).not.toHaveProperty('num');
});
test('single nested enum value dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { abc: 'testabc' },
});
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated.nested).not.toHaveProperty('abc');
});
test('multiple nested values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: 'hi', abc: 'testabc' },
});
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated.nested).not.toHaveProperty('num');
expect(_validated.nested).not.toHaveProperty('abc');
});
test('all nested values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { num: 'hi', abc: 'testabc', switch: 1 },
});
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('nested');
});
test('all nested values dont match template with foreign property', () => {
const modifiedOptions = assignDeep({}, options, {
nested: {
foreign: 'foreign',
num: 'hi',
abc: 'testabc',
switch: 1,
},
});
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('nested');
});
test('various values dont match template', () => {
const modifiedOptions = assignDeep({}, options, {
nested: { switch: null },
obj: 1,
abc: 'testest',
func: {},
});
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated.nested).not.toHaveProperty('switch');
expect(_validated).not.toHaveProperty('obj');
expect(_validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('func');
});
test('various values dont match template with foreign properties', () => {
const foreignObj = {
foreignProp: 'foreign',
foreignDeep: { a: 'A', b: 'B' },
};
const modifiedOptions = assignDeep(
{},
options,
{
nested: { switch: null },
obj: 1,
abc: 'testest',
func: {},
},
foreignObj,
{ nested: foreignObj }
);
const result = validateOptions(modifiedOptions, template);
const { _validated, _foreign } = result;
expect(_foreign.nested).toEqual(foreignObj);
delete _foreign.nested;
expect(_foreign).toEqual(foreignObj);
expect(_validated.nested).not.toHaveProperty('switch');
expect(_validated).not.toHaveProperty('obj');
expect(_validated).not.toHaveProperty('abc');
expect(_validated).not.toHaveProperty('func');
});
test('nested object is string', () => {
const modifiedOptions = assignDeep({}, options, { nested: 'string' });
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('nested');
});
test('nested object is null', () => {
const modifiedOptions = assignDeep({}, options, { nested: null });
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('nested');
});
test('nested object is undefined', () => {
const modifiedOptions: Partial<TestOptions> = assignDeep({}, options);
modifiedOptions.nested = undefined;
const result = validateOptions(modifiedOptions, template);
const { _validated } = result;
expect(_validated).not.toHaveProperty('nested');
});
});
describe('error logging', () => {
test('dont log error if nothing is wrong', () => {
const { warn } = console;
console.warn = jest.fn();
validateOptions(options, template, null, true);
expect(console.warn).not.toBeCalled();
console.warn = warn;
});
test('dont log error if something is wrong and flag is false', () => {
const { warn } = console;
console.warn = jest.fn();
const modifiedOptions = assignDeep({}, options, { str: 1 });
validateOptions(modifiedOptions, template, null, false);
expect(console.warn).not.toBeCalled();
console.warn = warn;
});
test('log error if something is wrong and flag is true', () => {
const { warn } = console;
console.warn = jest.fn();
// str must be string
validateOptions(assignDeep({}, options, { str: 1 }), template, null, true);
expect(console.warn).toBeCalledTimes(1);
// abc must be A | B | C
validateOptions(assignDeep({}, options, { abc: 'some string' }), template, null, true);
expect(console.warn).toBeCalledTimes(2);
// everthing OK
validateOptions(assignDeep({}, options, { abc: 'C' }), template, null, true);
expect(console.warn).toBeCalledTimes(2);
console.warn = warn;
});
});
});
+9 -4
View File
@@ -1,3 +1,6 @@
type PartialOptions<T> = {
[P in keyof T]?: T[P] extends Record<string, unknown> ? PartialOptions<T[P]> : T[P];
};
type OSTargetElement = HTMLElement | HTMLTextAreaElement;
/**
* Static elements MUST be present.
@@ -38,10 +41,6 @@ interface ScrollbarsInitialization {
interface OSInitializationObject extends StructureInitialization, ScrollbarsInitialization {
}
type OSTarget = OSTargetElement | OSInitializationObject;
type OptionsObjectType = Record<string, unknown>;
type PartialOptions<T> = {
[P in keyof T]?: T[P] extends OptionsObjectType ? PartialOptions<T[P]> : T[P];
};
type ResizeBehavior = "none" | "both" | "horizontal" | "vertical";
type OverflowBehavior = "hidden" | "scroll" | "visible" | "visible-hidden";
type VisibilityBehavior = "visible" | "hidden" | "auto";
@@ -85,8 +84,14 @@ interface OSOptions {
onUpdated: (() => any) | null;
};
}
type OSPluginInstance = Record<string, unknown> | ((staticObj: OverlayScrollbarsStatic, instanceObj: OverlayScrollbars) => void);
type OSPlugin<T extends OSPluginInstance = OSPluginInstance> = [
string,
T
];
interface OverlayScrollbarsStatic {
(target: OSTarget | OSInitializationObject, options?: PartialOptions<OSOptions>, extensions?: any): OverlayScrollbars;
extend(osPlugin: OSPlugin | OSPlugin[]): void;
}
interface OverlayScrollbars {
options(): OSOptions;