@cxco/ui-faq
Version:
FAQ Module using @cxco default packages
374 lines (312 loc) • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.delay = delay;
exports.scroll = scroll;
exports.scrollToEnd = scrollToEnd;
exports.setNavigationButtons = setNavigationButtons;
exports.onCreateAnimate = onCreateAnimate;
exports.onRemoveAnimate = onRemoveAnimate;
exports.toggle = toggle;
exports.isDifferentArray = isDifferentArray;
exports.urlRegexBuilder = urlRegexBuilder;
exports.findCategoriesByFaqId = findCategoriesByFaqId;
exports.prepareAndCategoriesByFaqId = prepareAndCategoriesByFaqId;
exports.addLightBoxListeners = addLightBoxListeners;
exports.getCategoryById = exports.toggleCategory = exports.ANIMATION_DURATION = void 0;
var basicLightBox = require('basiclightbox');
/* eslint-disable no-useless-escape */
// refers to the CSS animation duration
var ANIMATION_DURATION = 400;
/**
* Selects or Deselects a category in a list.
* Unselected categories are marked as -1.
* Selected categories are marked with their category ID.
* @param {Object} category - the category object containing ID and depth.
* @param {number[]} selectedCategories - the array containing the selected categories.
* @returns {number[]} the modified selected categories list.
*/
exports.ANIMATION_DURATION = ANIMATION_DURATION;
var toggleCategory = function toggleCategory(category, selectedCategories) {
// -1 when not selected,
if (selectedCategories[category.depth] === -1) {
selectedCategories = selectCategoryAtDepth(category, selectedCategories);
} else {
selectedCategories = selectedCategories.splice(0, category.depth + 1); // if is already the selected category, deselect it.
if (selectedCategories[category.depth] === category.id) {
selectedCategories[category.depth] = -1;
} else {
selectedCategories = selectCategoryAtDepth(category, selectedCategories);
}
}
return selectedCategories;
};
/**
* Add the category ID at the correct depth in the selectedCategories category list.
* If the category has sub-categories add the next unselected step (-1) to the list.
* @param {Object} category - the category object containing ID and depth.
* @param {number[]} selectedCategories - the array containing the selected categories.
* @returns {number[]} the modified selected categories list.
*/
exports.toggleCategory = toggleCategory;
var selectCategoryAtDepth = function selectCategoryAtDepth(category, selectedCategories) {
selectedCategories[category.depth] = category.id; // if it's possible to continue to drill down
if (category.subCategories.length > 0) {
selectedCategories.push(-1);
}
return selectedCategories;
};
/**
* Calculates total height of children for a given element collection
* @param {HTMLElement[]} HTMLCollection
*/
var checkContentHeight = function checkContentHeight(HTMLCollection) {
var acc = 0;
for (var i = 0; i < HTMLCollection.length; i++) {
var elm = HTMLCollection[i];
var elmStyle = window.getComputedStyle(elm);
var verticalMargins = parseInt(elmStyle.getPropertyValue('margin-top')) + parseInt(elmStyle.getPropertyValue('margin-bottom'));
var height = elm.offsetHeight || 0;
acc += height + verticalMargins;
}
return acc;
};
/**
* Returns a property value when being animated at a specific point in time.
* @param {number} time - current time in milliseconds
* @param {number} beginVal - initial property value (e.g. height)
* @param {number} endVal - end property value (e.g. height)
* @param {number} duration - total animation time
*
* @return {number} - computed property at the specific point in time `{time}`
*/
var easeOutQuad = function easeOutQuad(time, beginVal, endVal, duration) {
return beginVal > endVal ? beginVal - (time /= duration) * beginVal : -endVal * (time /= duration) * (time - 2) + beginVal;
};
/**
* Animates property `styleAttr` in element `elm`.
* @param {HTMLElement} elm
* @param {string} styleAttr - css property to be animated
* @param {number} beginVal
* @param {number} endVal
* @param {number} duration
*/
var animate = function animate(elm, styleAttr, beginVal, endVal, duration) {
var startTime = new Date().getTime();
var request;
var step = function step() {
var elapsedTime = new Date().getTime() - startTime;
if (elapsedTime >= duration) {
window.cancelAnimationFrame(request);
elm.removeAttribute('style');
} else {
elm.style[styleAttr] = "".concat(easeOutQuad(elapsedTime, beginVal, endVal, duration), "px");
request = window.requestAnimationFrame(step);
}
};
request = window.requestAnimationFrame(step);
};
/**
* Go through the nodes of a tree to find a category by it's ID.
* @param {number} id
* @param {Object[]} categoryTree
*/
var getCategoryById = function getCategoryById(id, categoryTree) {
var found = categoryTree.find(function (ele) {
return ele.id === id;
});
if (typeof found === 'undefined') {
var nextChildren;
if (Array.isArray(categoryTree)) {
nextChildren = categoryTree.flatMap(function (cat) {
return cat.subCategories.length > 0 ? cat.subCategories : [];
});
}
if (typeof nextChildren === 'undefined' || nextChildren.length === 0) {
return undefined;
}
return getCategoryById(id, nextChildren);
}
return found;
};
/**
* Delays the output to next() for _delay milliseconds
*
* @param {number} _delay seconds of delay
*/
exports.getCategoryById = getCategoryById;
function delay(_delay) {
return function (value) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(value);
}, _delay);
});
};
}
/**
* Calculates the scroll amount and scrolls left or right.
* Requires `.cxco-c-slide__content` and `.cxco-c-slide__item`
* @param {string} direction
*/
function scroll(direction) {
var container = document.querySelector('.cxco-c-slide__content');
var item = document.querySelector('.cxco-c-slide__item');
var itemWidth = item.getBoundingClientRect().width;
var style = window.getComputedStyle ? getComputedStyle(item, null) : item.currentStyle;
var marginLeft = parseFloat(style.marginLeft) || 0;
var marginRight = parseFloat(style.marginRight) || 0;
var outerWidth = itemWidth + marginRight + marginLeft;
return new Promise(function (resolve, reject) {
if (direction === 'right') {
container.scrollLeft = Math.ceil(container.scrollLeft + outerWidth);
} else if (direction === 'left') {
container.scrollLeft = Math.ceil(container.scrollLeft - outerWidth);
}
resolve(container);
});
}
function scrollToEnd() {
var container = document.querySelector('.cxco-c-slide__content');
new Promise(function (resolve, reject) {
container.scrollLeft = container.scrollWidth;
resolve(container);
}).then(delay(ANIMATION_DURATION)).then(setNavigationButtons(container));
}
/**
* Enables or disables the nav buttons by calculating container width and scrollwidth
* Requires `.cxco-c-slide__content`, `.cxco-c-slide__button--prev` and `.cxco-c-slide__button--next`
* @param {HTMLElement} element
*/
function setNavigationButtons() {
var container = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document.querySelector('.cxco-c-slide__content');
return new Promise(function (resolve, reject) {
var navPrev = document.querySelector('.cxco-c-slide__button--prev');
var navNext = document.querySelector('.cxco-c-slide__button--next');
var left = Math.ceil(container.scrollLeft);
var right = Math.ceil(container.offsetWidth);
var width = Math.ceil(container.scrollWidth);
var showPrev = left === 0 || left === 1;
var showNext = left + right === width || left + right === width + 1;
navPrev.disabled = showPrev;
navNext.disabled = showNext;
resolve();
});
}
/**
* Toggles `is-creating` class, with a delay in between.
* Scrolls container.
* @param {HTMLElement} element
*/
function onCreateAnimate(element, shouldScroll) {
element.setAttribute('data-state', 'creating');
return new Promise(function (resolve) {
return resolve(element);
}).then(delay(ANIMATION_DURATION)).then(function () {
element.setAttribute('data-state', 'created');
if (shouldScroll) {
scroll('right');
}
});
}
/**
* Adds `removing` value to `data-state`
* @param {HTMLElement} element
*/
function onRemoveAnimate(element) {
element.setAttribute('data-state', 'removing');
return new Promise(function (resolve) {
return resolve(element);
}).then(delay(ANIMATION_DURATION));
}
/**
*
* @param {HTMLElement} element
*/
function toggle(element) {
var isOpening = element.getAttribute('aria-hidden') === 'false';
var beginVal = isOpening ? 0 : checkContentHeight(element.children);
var endVal = isOpening ? checkContentHeight(element.children) : 0;
animate(element, 'height', beginVal, endVal, ANIMATION_DURATION);
}
/**
* Compare two arrays and check if they have the same content
* @param {number[]} arr1
* @param {number[]} arr2
*/
function isDifferentArray(arr1, arr2) {
if (arr1 === arr2) return false;
if (arr1.length !== arr2.length) return true;
for (var i = 0; i < arr1.length; ++i) {
if (arr1[i] !== arr2[i]) return true;
}
return false;
}
/**
* Creates a RegEx based on a given `formatString`.
* The `formatString` can have multiple formats such as
* "#{categorycrumbs}/{categoryname}/{faqid}/{question}" or "#crumbs={categorycrumbs}&category={categoryname}&faqid={faqid}&question={question}".
* - Replaces the separator between the {placeholders} with a non-capturing group
* - Escapes "/" character
* - Replaces the `{categorycrumbs}`, `{categoryname}`, `{faqid}` and `{question}` placeholders with a correct regex groups
* @param {string} formatString
*/
function urlRegexBuilder(formatString) {
return formatString.replace(/#([\?a-z=]*)/i, function (match, g1) {
return g1 ? '#(?:' + g1 + ')' : '#';
}).replace(/(?:})([^{]*)(?:{)/gmi, '}(?:$1)\?{').replace(/\//gmi, '\\/').replace('{categorycrumbs}', '((?:(?:\\\d)+-?)*)').replace('{categoryname}', '([A-Z0-9 %]+)').replace('{faqid}', '(\\\d+)?').replace('{question}', '(.+)?');
}
/**
* Find the `categoryName` and `categoryCrumbs` for a FAQ in a Category tree.
* It finds the first match, so if a FAQ is in multiple categories it only return the first.
* This recursive function is best used when the categories start from a single node, and for that we can create a fake node and add the categories as it's children.
* That fake node needs some fake properties, like the id=0 and thus '0-' is removed from the crumbs.
* @param {Object} category
* @param {Number} faqId
* @param {String} crumbs
*/
function findCategoriesByFaqId(category, faqId, crumbs) {
var _FAQ = category.items.find(function (faq) {
return faq.id === faqId;
});
if (_FAQ) {
return {
categoryCrumbs: (crumbs === '' && category.id !== 0 ? category.id : crumbs + '-' + category.id).replace(/^0-/, ''),
categoryName: category.name
};
} else if (category.subCategories.length > 0) {
var result;
for (var i = 0; result === undefined && i < category.subCategories.length; i++) {
result = findCategoriesByFaqId(category.subCategories[i], faqId, crumbs === '' ? String(category.id) : crumbs + '-' + category.id);
}
return result;
}
return undefined;
}
/**
* create a fake root node for the tree and add categories as it's children
* @param {*} categories
* @param {*} faqId
* @param {*} crumbs
*/
function prepareAndCategoriesByFaqId(categories, faqId, crumbs) {
var category = {
id: 0,
name: 'root',
items: []
};
category.subCategories = categories;
return findCategoriesByFaqId(category, faqId, crumbs);
}
function addLightBoxListeners(element) {
element.querySelectorAll('.cxco-lightbox').forEach(function (element) {
element.addEventListener('click', function (e) {
try {
basicLightBox.create(e.target.outerHTML).show();
} catch (e) {
console.error(e);
}
});
});
}