UNPKG

pickerjs

Version:
526 lines (452 loc) 13.2 kB
import { IS_BROWSER, WINDOW, } from './constants'; const { hasOwnProperty, toString } = Object.prototype; /** * Detect the type of the given value. * @param {*} value - The value to detect. * @returns {string} Returns the type. */ export function typeOf(value) { return toString.call(value).slice(8, -1).toLowerCase(); } /** * Check if the given value is a string. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a string, else `false`. */ export function isString(value) { return typeof value === 'string'; } /** * Check if the given value is finite. */ export const isFinite = Number.isFinite || WINDOW.isFinite; /** * Check if the given value is not a number. */ export const isNaN = Number.isNaN || WINDOW.isNaN; /** * Check if the given value is a number. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a number, else `false`. */ export function isNumber(value) { return typeof value === 'number' && !isNaN(value); } /** * Check if the given value is an object. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is an object, else `false`. */ export function isObject(value) { return typeof value === 'object' && value !== null; } /** * Check if the given value is a plain object. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a plain object, else `false`. */ export function isPlainObject(value) { if (!isObject(value)) { return false; } try { const { constructor } = value; const { prototype } = constructor; return constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf'); } catch (error) { return false; } } /** * Check if the given value is a function. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a function, else `false`. */ export function isFunction(value) { return typeof value === 'function'; } /** * Check if the given value is a date. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a date, else `false`. */ export function isDate(value) { return typeOf(value) === 'date'; } /** * Check if the given value is a valid date. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a valid date, else `false`. */ export function isValidDate(value) { return isDate(value) && value.toString() !== 'Invalid Date'; } /** * Iterate the given data. * @param {*} data - The data to iterate. * @param {Function} callback - The process function for each element. * @returns {*} The original data. */ export function forEach(data, callback) { if (data && isFunction(callback)) { if (Array.isArray(data) || isNumber(data.length)/* array-like */) { const { length } = data; let i; for (i = 0; i < length; i += 1) { if (callback.call(data, data[i], i, data) === false) { break; } } } else if (isObject(data)) { Object.keys(data).forEach((key) => { callback.call(data, data[key], key, data); }); } } return data; } /** * Recursively assigns own enumerable properties of source objects to the target object. * @param {Object} target - The target object. * @param {Object[]} sources - The source objects. * @returns {Object} The target object. */ export function deepAssign(target, ...sources) { if (isObject(target) && sources.length > 0) { sources.forEach((source) => { if (isObject(source)) { Object.keys(source).forEach((key) => { if (isPlainObject(target[key]) && isPlainObject(source[key])) { target[key] = deepAssign({}, target[key], source[key]); } else { target[key] = source[key]; } }); } }); } return target; } /** * Check if the given element has a special class. * @param {Element} element - The element to check. * @param {string} value - The class to search. * @returns {boolean} Returns `true` if the special class was found. */ export function hasClass(element, value) { return element.classList ? element.classList.contains(value) : element.className.indexOf(value) > -1; } /** * Add classes to the given element. * @param {Element} element - The target element. * @param {string} value - The classes to be added. */ export function addClass(element, value) { if (!value) { return; } if (isNumber(element.length)) { forEach(element, (elem) => { addClass(elem, value); }); return; } if (element.classList) { element.classList.add(value); return; } const className = element.className.trim(); if (!className) { element.className = value; } else if (className.indexOf(value) < 0) { element.className = `${className} ${value}`; } } /** * Remove classes from the given element. * @param {Element} element - The target element. * @param {string} value - The classes to be removed. */ export function removeClass(element, value) { if (!value) { return; } if (isNumber(element.length)) { forEach(element, (elem) => { removeClass(elem, value); }); return; } if (element.classList) { element.classList.remove(value); return; } if (element.className.indexOf(value) >= 0) { element.className = element.className.replace(value, ''); } } /** * Add or remove classes from the given element. * @param {Element} element - The target element. * @param {string} value - The classes to be toggled. * @param {boolean} added - Add only. */ export function toggleClass(element, value, added) { if (!value) { return; } if (isNumber(element.length)) { forEach(element, (elem) => { toggleClass(elem, value, added); }); return; } // IE10-11 doesn't support the second parameter of `classList.toggle` if (added) { addClass(element, value); } else { removeClass(element, value); } } const REGEXP_HYPHENATE = /([a-z\d])([A-Z])/g; /** * Transform the given string from camelCase to kebab-case * @param {string} value - The value to transform. * @returns {string} The transformed value. */ export function hyphenate(value) { return value.replace(REGEXP_HYPHENATE, '$1-$2').toLowerCase(); } /** * Get data from the given element. * @param {Element} element - The target element. * @param {string} name - The data key to get. * @returns {string} The data value. */ export function getData(element, name) { if (isObject(element[name])) { return element[name]; } if (element.dataset) { return element.dataset[name]; } return element.getAttribute(`data-${hyphenate(name)}`); } /** * Set data to the given element. * @param {Element} element - The target element. * @param {string} name - The data key to set. * @param {string} data - The data value. */ export function setData(element, name, data) { if (isObject(data)) { element[name] = data; } else if (element.dataset) { element.dataset[name] = data; } else { element.setAttribute(`data-${hyphenate(name)}`, data); } } /** * Remove data from the given element. * @param {Element} element - The target element. * @param {string} name - The data key to remove. */ export function removeData(element, name) { if (isObject(element[name])) { try { delete element[name]; } catch (error) { element[name] = undefined; } } else if (element.dataset) { // #128 Safari not allows to delete dataset property try { delete element.dataset[name]; } catch (error) { element.dataset[name] = undefined; } } else { element.removeAttribute(`data-${hyphenate(name)}`); } } const REGEXP_SPACES = /\s\s*/; const onceSupported = (() => { let supported = false; if (IS_BROWSER) { let once = false; const listener = () => {}; const options = Object.defineProperty({}, 'once', { get() { supported = true; return once; }, /** * This setter can fix a `TypeError` in strict mode * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only} * @param {boolean} value - The value to set */ set(value) { once = value; }, }); WINDOW.addEventListener('test', listener, options); WINDOW.removeEventListener('test', listener, options); } return supported; })(); /** * Remove event listener from the target element. * @param {Element} element - The event target. * @param {string} type - The event type(s). * @param {Function} listener - The event listener. * @param {Object} options - The event options. */ export function removeListener(element, type, listener, options = {}) { let handler = listener; type.trim().split(REGEXP_SPACES).forEach((event) => { if (!onceSupported) { const { listeners } = element; if (listeners && listeners[event] && listeners[event][listener]) { handler = listeners[event][listener]; delete listeners[event][listener]; if (Object.keys(listeners[event]).length === 0) { delete listeners[event]; } if (Object.keys(listeners).length === 0) { delete element.listeners; } } } element.removeEventListener(event, handler, options); }); } /** * Add event listener to the target element. * @param {Element} element - The event target. * @param {string} type - The event type(s). * @param {Function} listener - The event listener. * @param {Object} options - The event options. */ export function addListener(element, type, listener, options = {}) { let handler = listener; type.trim().split(REGEXP_SPACES).forEach((event) => { if (options.once && !onceSupported) { const { listeners = {} } = element; handler = (...args) => { delete listeners[event][listener]; element.removeEventListener(event, handler, options); listener.apply(element, args); }; if (!listeners[event]) { listeners[event] = {}; } if (listeners[event][listener]) { element.removeEventListener(event, listeners[event][listener], options); } listeners[event][listener] = handler; element.listeners = listeners; } element.addEventListener(event, handler, options); }); } /** * Dispatch event on the target element. * @param {Element} element - The event target. * @param {string} type - The event type(s). * @param {Object} data - The additional event data. * @returns {boolean} Indicate if the event is default prevented or not. */ export function dispatchEvent(element, type, data) { let event; // Event and CustomEvent on IE9-11 are global objects, not constructors if (isFunction(Event) && isFunction(CustomEvent)) { event = new CustomEvent(type, { detail: data, bubbles: true, cancelable: true, }); } else { event = document.createEvent('CustomEvent'); event.initCustomEvent(type, true, true, data); } return element.dispatchEvent(event); } /** * Check if the given year is a leap year. * @param {number} year - The year to check. * @returns {boolean} Returns `true` if the given year is a leap year, else `false`. */ export function isLeapYear(year) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } /** * Get days number of the given month. * @param {number} year - The target year. * @param {number} month - The target month. * @returns {number} Returns days number. */ export function getDaysInMonth(year, month) { return [31, (isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; } /** * Add leading zeroes to the given value * @param {number} value - The value to add. * @param {number} [length=1] - The number of the leading zeroes. * @returns {string} Returns converted value. */ export function addLeadingZero(value, length = 1) { const str = String(Math.abs(value)); let i = str.length; let result = ''; if (value < 0) { result += '-'; } while (i < length) { i += 1; result += '0'; } return result + str; } /** * Map token to type name * @param {string} token - The token to map. * @returns {string} Returns mapped type name. */ export function tokenToType(token) { return { Y: 'year', M: 'month', D: 'day', H: 'hour', m: 'minute', s: 'second', S: 'millisecond', }[token.charAt(0)]; } export const REGEXP_TOKENS = /(Y|M|D|H|m|s|S)\1*/g; /** * Parse date format. * @param {string} format - The format to parse. * @returns {Object} Returns parsed format data. */ export function parseFormat(format) { let tokens = format.match(REGEXP_TOKENS); if (!tokens) { throw new Error('Invalid format.'); } // Remove duplicate tokens (#22) tokens = tokens.filter((token, index) => tokens.indexOf(token) === index); const result = { tokens, }; tokens.forEach((token) => { result[tokenToType(token)] = token; }); return result; }