chore(build): rename ionic directory to src and update all references in the build process.

This commit is contained in:
Josh Thomas
2016-05-19 13:20:59 -05:00
parent 8470ae04ac
commit c8f760f080
595 changed files with 73 additions and 87 deletions

54
src/util/click-block.ts Normal file
View File

@@ -0,0 +1,54 @@
import {nativeTimeout} from './dom';
const CSS_CLICK_BLOCK = 'click-block-active';
const DEFAULT_EXPIRE = 330;
let cbEle, fallbackTimerId;
let isShowing = false;
/**
* @private
*/
export class ClickBlock {
private _enabled: boolean = false;
enable() {
cbEle = document.createElement('click-block');
document.body.appendChild(cbEle);
cbEle.addEventListener('touchmove', function(ev) {
ev.preventDefault();
ev.stopPropagation();
});
this._enabled = true;
}
show(shouldShow, expire) {
if (this._enabled) {
if (shouldShow) {
show(expire);
} else {
hide();
}
}
}
}
function show(expire) {
clearTimeout(fallbackTimerId);
fallbackTimerId = nativeTimeout(hide, expire || DEFAULT_EXPIRE);
if (!isShowing) {
cbEle.classList.add(CSS_CLICK_BLOCK);
isShowing = true;
}
}
function hide() {
clearTimeout(fallbackTimerId);
if (isShowing) {
cbEle.classList.remove(CSS_CLICK_BLOCK);
isShowing = false;
}
}

503
src/util/datetime-util.ts Normal file
View File

@@ -0,0 +1,503 @@
import {isBlank, isPresent, isString, isObject, assign} from './util';
export function renderDateTime(template: string, value: DateTimeData, locale: LocaleData) {
if (isBlank(value)) {
return '';
}
let tokens = [];
let hasText = false;
FORMAT_KEYS.forEach((format, index) => {
if (template.indexOf(format.f) > -1) {
var token = '{' + index + '}';
var text = renderTextFormat(format.f, value[format.k], value, locale);
if (!hasText && text && isPresent(value[format.k])) {
hasText = true;
}
tokens.push(token, text);
template = template.replace(format.f, token);
}
});
if (!hasText) {
return '';
}
for (var i = 0; i < tokens.length; i += 2) {
template = template.replace(tokens[i], tokens[i + 1]);
}
return template;
}
export function renderTextFormat(format: string, value: any, date: DateTimeData, locale: LocaleData): string {
if (format === FORMAT_DDDD || format === FORMAT_DDD) {
try {
value = (new Date(date.year, date.month - 1, date.day)).getDay();
if (format === FORMAT_DDDD) {
return (isPresent(locale.dayNames) ? locale.dayNames : DAY_NAMES)[value];
}
return (isPresent(locale.dayShortNames) ? locale.dayShortNames : DAY_SHORT_NAMES)[value];
} catch (e) {}
return '';
}
if (format === FORMAT_A) {
return date ? date.hour < 12 ? 'AM' : 'PM' : isPresent(value) ? value.toUpperCase() : '';
}
if (format === FORMAT_a) {
return date ? date.hour < 12 ? 'am' : 'pm' : isPresent(value) ? value : '';
}
if (isBlank(value)) {
return '';
}
if (format === FORMAT_YY || format === FORMAT_MM ||
format === FORMAT_DD || format === FORMAT_HH ||
format === FORMAT_mm) {
return twoDigit(value);
}
if (format === FORMAT_YYYY) {
return fourDigit(value);
}
if (format === FORMAT_MMMM) {
return (isPresent(locale.monthNames) ? locale.monthNames : MONTH_NAMES)[value - 1];
}
if (format === FORMAT_MMM) {
return (isPresent(locale.monthShortNames) ? locale.monthShortNames : MONTH_SHORT_NAMES)[value - 1];
}
if (format === FORMAT_hh || format === FORMAT_h) {
if (value === 0) {
return '12';
}
if (value > 12) {
value -= 12;
}
if (format === FORMAT_hh && value < 10) {
return ('0' + value);
}
}
return value.toString();
}
export function dateValueRange(format: string, min: DateTimeData, max: DateTimeData): any[] {
let opts: any[] = [];
let i: number;
if (format === FORMAT_YYYY || format === FORMAT_YY) {
// year
i = max.year;
while (i >= min.year) {
opts.push(i--);
}
} else if (format === FORMAT_MMMM || format === FORMAT_MMM ||
format === FORMAT_MM || format === FORMAT_M ||
format === FORMAT_hh || format === FORMAT_h) {
// month or 12-hour
for (i = 1; i < 13; i++) {
opts.push(i);
}
} else if (format === FORMAT_DDDD || format === FORMAT_DDD ||
format === FORMAT_DD || format === FORMAT_D) {
// day
for (i = 1; i < 32; i++) {
opts.push(i);
}
} else if (format === FORMAT_HH || format === FORMAT_H) {
// 24-hour
for (i = 0; i < 24; i++) {
opts.push(i);
}
} else if (format === FORMAT_mm || format === FORMAT_m) {
// minutes
for (i = 0; i < 60; i++) {
opts.push(i);
}
} else if (format === FORMAT_A || format === FORMAT_a) {
// AM/PM
opts.push('am', 'pm');
}
return opts;
}
export function dateSortValue(year: number, month: number, day: number): number {
return parseInt(`1${fourDigit(year)}${twoDigit(month)}${twoDigit(day)}`, 10);
}
export function dateDataSortValue(data: DateTimeData): number {
if (data) {
return dateSortValue(data.year, data.month, data.day);
}
return -1;
}
export function daysInMonth(month: number, year: number): number {
return (month === 4 || month === 6 || month === 9 || month === 11) ? 30 : (month === 2) ? isLeapYear(year) ? 29 : 28 : 31;
}
export function isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
const ISO_8601_REGEXP = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
const TIME_REGEXP = /^((\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/;
export function parseDate(val: any): DateTimeData {
// manually parse IS0 cuz Date.parse cannot be trusted
// ISO 8601 format: 1994-12-15T13:47:20Z
let parse: any[];
if (isPresent(val) && val !== '') {
// try parsing for just time first, HH:MM
parse = TIME_REGEXP.exec(val);
if (isPresent(parse)) {
// adjust the array so it fits nicely with the datetime parse
parse.unshift(undefined, undefined);
parse[2] = parse[3] = undefined;
} else {
// try parsing for full ISO datetime
parse = ISO_8601_REGEXP.exec(val);
}
}
if (isBlank(parse)) {
// wasn't able to parse the ISO datetime
return null;
}
// ensure all the parse values exist with at least 0
for (var i = 1; i < 8; i++) {
parse[i] = (parse[i] !== undefined ? parseInt(parse[i], 10) : null);
}
var tzOffset = 0;
if (isPresent(parse[9]) && isPresent(parse[10])) {
// hours
tzOffset = parseInt(parse[10], 10) * 60;
if (isPresent(parse[11])) {
// minutes
tzOffset += parseInt(parse[11], 10);
}
if (parse[9] === '-') {
// + or -
tzOffset *= -1;
}
}
return {
year: parse[1],
month: parse[2],
day: parse[3],
hour: parse[4],
minute: parse[5],
second: parse[6],
millisecond: parse[7],
tzOffset: tzOffset,
};
}
export function updateDate(existingData: DateTimeData, newData: any) {
if (isPresent(newData) && newData !== '') {
if (isString(newData)) {
// new date is a string, and hopefully in the ISO format
// convert it to our DateTimeData if a valid ISO
newData = parseDate(newData);
if (newData) {
// successfully parsed the ISO string to our DateTimeData
assign(existingData, newData);
return;
}
} else if ((isPresent(newData.year) || isPresent(newData.hour))) {
// newData is from of a datetime picker's selected values
// update the existing DateTimeData data with the new values
// do some magic for 12-hour values
if (isPresent(newData.ampm) && isPresent(newData.hour)) {
if (newData.ampm.value === 'pm') {
newData.hour.value = (newData.hour.value === 12 ? 12 : newData.hour.value + 12);
} else {
newData.hour.value = (newData.hour.value === 12 ? 0 : newData.hour.value);
}
}
// merge new values from the picker's selection
// to the existing DateTimeData values
for (var k in newData) {
existingData[k] = newData[k].value;
}
return;
}
// eww, invalid data
console.warn(`Error parsing date: "${newData}". Please provide a valid ISO 8601 datetime format: https://www.w3.org/TR/NOTE-datetime`);
}
}
export function parseTemplate(template: string): string[] {
let formats: string[] = [];
template = template.replace(/[^\w\s]/gi, ' ');
FORMAT_KEYS.forEach(format => {
if (format.f.length > 1 && template.indexOf(format.f) > -1 && template.indexOf(format.f + format.f.charAt(0)) < 0) {
template = template.replace(format.f, ' ' + format.f + ' ');
}
});
let words = template.split(' ').filter(w => w.length > 0);
words.forEach((word, i) => {
FORMAT_KEYS.forEach(format => {
if (word === format.f) {
if (word === FORMAT_A || word === FORMAT_a) {
// this format is an am/pm format, so it's an "a" or "A"
if ((formats.indexOf(FORMAT_h) < 0 && formats.indexOf(FORMAT_hh) < 0) ||
(words[i - 1] !== FORMAT_m && words[i - 1] !== FORMAT_mm)) {
// template does not already have a 12-hour format
// or this am/pm format doesn't have a minute format immediately before it
// so do not treat this word "a" or "A" as an am/pm format
return;
}
}
formats.push(word);
}
});
});
return formats;
}
export function getValueFromFormat(date: DateTimeData, format: string) {
if (format === FORMAT_A || format === FORMAT_a) {
return (date.hour < 12 ? 'am' : 'pm');
}
if (format === FORMAT_hh || format === FORMAT_h) {
return (date.hour > 12 ? date.hour - 12 : date.hour);
}
return date[convertFormatToKey(format)];
}
export function convertFormatToKey(format: string): string {
for (var k in FORMAT_KEYS) {
if (FORMAT_KEYS[k].f === format) {
return FORMAT_KEYS[k].k;
}
}
return null;
}
export function convertDataToISO(data: DateTimeData): string {
// https://www.w3.org/TR/NOTE-datetime
let rtn = '';
if (isPresent(data)) {
if (isPresent(data.year)) {
// YYYY
rtn = fourDigit(data.year);
if (isPresent(data.month)) {
// YYYY-MM
rtn += '-' + twoDigit(data.month);
if (isPresent(data.day)) {
// YYYY-MM-DD
rtn += '-' + twoDigit(data.day);
if (isPresent(data.hour)) {
// YYYY-MM-DDTHH:mm:SS
rtn += `T${twoDigit(data.hour)}:${twoDigit(data.minute)}:${twoDigit(data.second)}`;
if (data.millisecond > 0) {
// YYYY-MM-DDTHH:mm:SS.SSS
rtn += '.' + threeDigit(data.millisecond);
}
if (data.tzOffset === 0) {
// YYYY-MM-DDTHH:mm:SSZ
rtn += 'Z';
} else {
// YYYY-MM-DDTHH:mm:SS+/-HH:mm
rtn += (data.tzOffset > 0 ? '+' : '-') + twoDigit(Math.floor(data.tzOffset / 60)) + ':' + twoDigit(data.tzOffset % 60);
}
}
}
}
} else if (isPresent(data.hour)) {
// HH:mm
rtn = twoDigit(data.hour) + ':' + twoDigit(data.minute);
if (isPresent(data.second)) {
// HH:mm:SS
rtn += ':' + twoDigit(data.second);
if (isPresent(data.millisecond)) {
// HH:mm:SS.SSS
rtn += '.' + threeDigit(data.millisecond);
}
}
}
}
return rtn;
}
function twoDigit(val: number): string {
return ('0' + (isPresent(val) ? val : '0')).slice(-2);
}
function threeDigit(val: number): string {
return ('00' + (isPresent(val) ? val : '0')).slice(-3);
}
function fourDigit(val: number): string {
return ('000' + (isPresent(val) ? val : '0')).slice(-4);
}
export interface DateTimeData {
year?: number;
month?: number;
day?: number;
hour?: number;
minute?: number;
second?: number;
millisecond?: number;
tzOffset?: number;
}
export interface LocaleData {
monthNames?: string[];
monthShortNames?: string[];
dayNames?: string[];
dayShortNames?: string[];
}
const FORMAT_YYYY = 'YYYY';
const FORMAT_YY = 'YY';
const FORMAT_MMMM = 'MMMM';
const FORMAT_MMM = 'MMM';
const FORMAT_MM = 'MM';
const FORMAT_M = 'M';
const FORMAT_DDDD = 'DDDD';
const FORMAT_DDD = 'DDD';
const FORMAT_DD = 'DD';
const FORMAT_D = 'D';
const FORMAT_HH = 'HH';
const FORMAT_H = 'H';
const FORMAT_hh = 'hh';
const FORMAT_h = 'h';
const FORMAT_mm = 'mm';
const FORMAT_m = 'm';
const FORMAT_A = 'A';
const FORMAT_a = 'a';
const FORMAT_KEYS = [
{ f: FORMAT_YYYY, k: 'year' },
{ f: FORMAT_MMMM, k: 'month' },
{ f: FORMAT_DDDD, k: 'day' },
{ f: FORMAT_MMM, k: 'month' },
{ f: FORMAT_DDD, k: 'day' },
{ f: FORMAT_YY, k: 'year' },
{ f: FORMAT_MM, k: 'month' },
{ f: FORMAT_DD, k: 'day' },
{ f: FORMAT_HH, k: 'hour' },
{ f: FORMAT_hh, k: 'hour' },
{ f: FORMAT_mm, k: 'minute' },
{ f: FORMAT_M, k: 'month' },
{ f: FORMAT_D, k: 'day' },
{ f: FORMAT_H, k: 'hour' },
{ f: FORMAT_h, k: 'hour' },
{ f: FORMAT_m, k: 'minute' },
{ f: FORMAT_A, k: 'ampm' },
{ f: FORMAT_a, k: 'ampm' },
];
const FORMAT_REGEX = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|DD?D?D?|ddd?d?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|.)/g;
const DAY_NAMES = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
const DAY_SHORT_NAMES = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const MONTH_SHORT_NAMES = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];

307
src/util/dom.ts Normal file
View File

@@ -0,0 +1,307 @@
// RequestAnimationFrame Polyfill (Android 4.3 and below)
/*! @author Paul Irish */
/*! @source https://gist.github.com/paulirish/1579671 */
(function() {
var rafLastTime = 0;
const win: any = window;
if (!win.requestAnimationFrame) {
win.requestAnimationFrame = function(callback, element) {
var currTime = Date.now();
var timeToCall = Math.max(0, 16 - (currTime - rafLastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
rafLastTime = currTime + timeToCall;
return id;
};
}
if (!win.cancelAnimationFrame) {
win.cancelAnimationFrame = function(id) { clearTimeout(id); };
}
})();
// use native raf rather than the zone wrapped one
let originalRaf = (window[window['Zone']['__symbol__']('requestAnimationFrame')] || window[window['Zone']['__symbol__']('webkitRequestAnimationFrame')]);
// if the originalRaf from the Zone symbol is not available, we need to provide the polyfilled version
export const nativeRaf = originalRaf !== undefined ? originalRaf['bind'](window) : window.requestAnimationFrame.bind(window);
// zone wrapped raf
export const raf = window.requestAnimationFrame.bind(window);
export const cancelRaf = window.cancelAnimationFrame.bind(window);
export const nativeTimeout = window[window['Zone']['__symbol__']('setTimeout')]['bind'](window);
export const clearNativeTimeout = window[window['Zone']['__symbol__']('clearTimeout')]['bind'](window);
export function rafFrames(framesToWait, callback) {
framesToWait = Math.ceil(framesToWait);
if (framesToWait < 2) {
nativeRaf(callback);
} else {
nativeTimeout(() => {
nativeRaf(callback);
}, (framesToWait - 1) * 16.6667);
}
}
export let CSS: {
transform?: string,
transition?: string,
transitionDuration?: string,
transitionDelay?: string,
transitionTimingFn?: string,
transitionStart?: string,
transitionEnd?: string,
} = {};
(function() {
// transform
var i, keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform',
'-moz-transform', 'moz-transform', 'MozTransform', 'mozTransform', 'msTransform'];
for (i = 0; i < keys.length; i++) {
if (document.documentElement.style[keys[i]] !== undefined) {
CSS.transform = keys[i];
break;
}
}
// transition
keys = ['webkitTransition', 'mozTransition', 'msTransition', 'transition'];
for (i = 0; i < keys.length; i++) {
if (document.documentElement.style[keys[i]] !== undefined) {
CSS.transition = keys[i];
break;
}
}
// The only prefix we care about is webkit for transitions.
var isWebkit = CSS.transition.indexOf('webkit') > -1;
// transition duration
CSS.transitionDuration = (isWebkit ? '-webkit-' : '') + 'transition-duration';
// transition timing function
CSS.transitionTimingFn = (isWebkit ? '-webkit-' : '') + 'transition-timing-function';
// transition delay
CSS.transitionDelay = (isWebkit ? '-webkit-' : '') + 'transition-delay';
// To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
CSS.transitionEnd = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend';
})();
export function transitionEnd(el: HTMLElement, callback: Function) {
if (el) {
CSS.transitionEnd.split(' ').forEach(eventName => {
el.addEventListener(eventName, onEvent);
});
return unregister;
}
function unregister() {
CSS.transitionEnd.split(' ').forEach(eventName => {
el.removeEventListener(eventName, onEvent);
});
}
function onEvent(ev) {
if (el === ev.target) {
unregister();
callback(ev);
}
}
}
export function ready(callback?: Function) {
let promise = null;
if (!callback) {
// a callback wasn't provided, so let's return a promise instead
promise = new Promise(resolve => { callback = resolve; });
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
callback();
} else {
document.addEventListener('DOMContentLoaded', completed, false);
window.addEventListener('load', completed, false);
}
return promise;
function completed() {
document.removeEventListener('DOMContentLoaded', completed, false);
window.removeEventListener('load', completed, false);
callback();
}
}
export function windowLoad(callback?: Function) {
let promise = null;
if (!callback) {
// a callback wasn't provided, so let's return a promise instead
promise = new Promise(resolve => { callback = resolve; });
}
if (document.readyState === 'complete') {
callback();
} else {
window.addEventListener('load', completed, false);
}
return promise;
function completed() {
window.removeEventListener('load', completed, false);
callback();
}
}
export function pointerCoord(ev: any): {x: number, y: number} {
// get coordinates for either a mouse click
// or a touch depending on the given event
let c = { x: 0, y: 0 };
if (ev) {
const touches = ev.touches && ev.touches.length ? ev.touches : [ev];
const e = (ev.changedTouches && ev.changedTouches[0]) || touches[0];
if (e) {
c.x = e.clientX || e.pageX || 0;
c.y = e.clientY || e.pageY || 0;
}
}
return c;
}
export function hasPointerMoved(threshold, startCoord, endCoord) {
return startCoord && endCoord &&
(Math.abs(startCoord.x - endCoord.x) > threshold || Math.abs(startCoord.y - endCoord.y) > threshold);
}
export function isActive(ele) {
return !!(ele && (document.activeElement === ele));
}
export function hasFocus(ele) {
return isActive(ele) && (ele.parentElement.querySelector(':focus') === ele);
}
export function isTextInput(ele) {
return !!ele &&
(ele.tagName === 'TEXTAREA' ||
ele.contentEditable === 'true' ||
(ele.tagName === 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset|color|image|button)$/i).test(ele.type)));
}
export function hasFocusedTextInput() {
let ele = document.activeElement;
if (isTextInput(ele)) {
return (ele.parentElement.querySelector(':focus') === ele);
}
return false;
}
const skipInputAttrsReg = /^(value|checked|disabled|type|class|style|id|autofocus|autocomplete|autocorrect)$/i;
export function copyInputAttributes(srcElement, destElement) {
// copy attributes from one element to another
// however, skip over a few of them as they're already
// handled in the angular world
var attrs = srcElement.attributes;
for (var i = 0; i < attrs.length; i++) {
var attr = attrs[i];
if (!skipInputAttrsReg.test(attr.name)) {
destElement.setAttribute(attr.name, attr.value);
}
}
}
let matchesFn: string;
let matchesMethods: Array<string> = ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector'];
matchesMethods.some((fn: string) => {
if (typeof document.documentElement[fn] === 'function') {
matchesFn = fn;
return true;
}
});
export function closest(ele: HTMLElement, selector: string, checkSelf?: boolean) {
if (ele && matchesFn) {
// traverse parents
ele = (checkSelf ? ele : ele.parentElement);
while (ele !== null) {
if (ele[matchesFn](selector)) {
return ele;
}
ele = ele.parentElement;
}
}
return null;
}
/**
* Get the element offsetWidth and offsetHeight. Values are cached
* to reduce DOM reads. Cache is cleared on a window resize.
*/
export function getDimensions(ele: HTMLElement, id: string): {
width: number, height: number, left: number, top: number
} {
let dimensions = dimensionCache[id];
if (!dimensions) {
// make sure we got good values before caching
if (ele.offsetWidth && ele.offsetHeight) {
dimensions = dimensionCache[id] = {
width: ele.offsetWidth,
height: ele.offsetHeight,
left: ele.offsetLeft,
top: ele.offsetTop
};
} else {
// do not cache bad values
return { width: 0, height: 0, left: 0, top: 0 };
}
}
return dimensions;
}
export function clearDimensions(id: string) {
delete dimensionCache[id];
}
export function windowDimensions(): {width: number, height: number} {
if (!dimensionCache.win) {
// make sure we got good values before caching
if (window.innerWidth && window.innerHeight) {
dimensionCache.win = {
width: window.innerWidth,
height: window.innerHeight
};
} else {
// do not cache bad values
return { width: 0, height: 0 };
}
}
return dimensionCache.win;
}
export function flushDimensionCache() {
dimensionCache = {};
}
let dimensionCache: any = {};

102
src/util/events.ts Normal file
View File

@@ -0,0 +1,102 @@
/**
* @name Events
* @description
* Events is a publish-subscribe style event system for sending and responding to application-level
* events across your app.
*
* @usage
* ```ts
* import {Events} from 'ionic-angular';
*
* constructor(public events: Events) {}
*
* // first page (publish an event when a user is created)
* function createUser(user) {
* console.log('User created!')
* events.publish('user:created', user);
* }
*
* // second page (listen for the user created event)
* events.subscribe('user:created', (user) => {
* console.log('Welcome', user);
* });
*
* ```
* @demo /docs/v2/demos/events/
*/
export class Events {
private _channels: Array<any> = [];
/**
* Subscribe to an event topic. Events that get posted to that topic will trigger the provided handler.
*
* @param {string} topic the topic to subscribe to
* @param {function} handler the event handler
*/
subscribe(topic, ...handlers) {
if (!this._channels[topic]) {
this._channels[topic] = [];
}
handlers.forEach((handler) => {
this._channels[topic].push(handler);
});
}
/**
* Unsubscribe from the given topic. Your handler will no longer receive events published to this topic.
*
* @param {string} topic the topic to unsubscribe from
* @param {function} handler the event handler
*
* @return true if a handler was removed
*/
unsubscribe(topic, handler) {
let t = this._channels[topic];
if (!t) {
// Wasn't found, wasn't removed
return false;
}
if (!handler) {
// Remove all handlers for this topic
delete this._channels[topic];
return true;
}
// We need to find and remove a specific handler
let i = t.indexOf(handler);
if (i < 0) {
// Wasn't found, wasn't removed
return false;
}
t.splice(i, 1);
// If the channel is empty now, remove it from the channel map
if (!t.length) {
delete this._channels[topic];
}
return true;
}
/**
* Publish an event to the given topic.
*
* @param {string} topic the topic to publish to
* @param {any} eventData the data to send as the event
*/
publish(topic, ...args) {
var t = this._channels[topic];
if (!t) {
return null;
}
let responses = [];
t.forEach((handler) => {
responses.push(handler(args));
});
return responses;
}
}

View File

@@ -0,0 +1,51 @@
export class FeatureDetect {
private _results: any = {};
run(window, document) {
for (let name in featureDetects) {
this._results[name] = featureDetects[name](window, document, document.body);
}
}
has(featureName) {
return !!this._results[featureName];
}
static add(name, fn) {
featureDetects[name] = fn;
}
}
let featureDetects = {};
// FeatureDetect.add('sticky', function(window, document) {
// // css position sticky
// let ele = document.createElement('div');
// ele.style.cssText = 'position:-webkit-sticky;position:sticky';
// return ele.style.position.indexOf('sticky') > -1;
// });
FeatureDetect.add('hairlines', function(window, document, body) {
/**
* Hairline Shim
* Add the "hairline" CSS class name to the body tag
* if the browser supports subpixels.
*/
let canDo = false;
if (window.devicePixelRatio >= 2) {
var hairlineEle = document.createElement('div');
hairlineEle.style.border = '.5px solid transparent';
body.appendChild(hairlineEle);
if (hairlineEle.offsetHeight === 1) {
body.classList.add('hairlines');
canDo = true;
}
body.removeChild(hairlineEle);
}
return canDo;
});

84
src/util/form.ts Normal file
View File

@@ -0,0 +1,84 @@
import {Injectable} from '@angular/core';
/**
* @private
*/
@Injectable()
export class Form {
private _blur: HTMLElement;
private _focused = null;
private _ids: number = -1;
private _inputs: Array<any> = [];
constructor() {
this.focusCtrl(document);
}
register(input) {
this._inputs.push(input);
}
deregister(input) {
let index = this._inputs.indexOf(input);
if (index > -1) {
this._inputs.splice(index, 1);
}
if (input === this._focused) {
this._focused = null;
}
}
focusCtrl(document) {
// raw DOM fun
let focusCtrl = document.createElement('focus-ctrl');
focusCtrl.setAttribute('aria-hidden', true);
this._blur = document.createElement('button');
this._blur.tabIndex = -1;
focusCtrl.appendChild(this._blur);
document.body.appendChild(focusCtrl);
}
focusOut() {
console.debug('focusOut');
let activeElement: any = document.activeElement;
if (activeElement) {
activeElement.blur();
}
this._blur.focus();
}
setAsFocused(input) {
this._focused = input;
}
/**
* Focuses the next input element, if it exists.
*/
tabFocus(currentInput) {
let index = this._inputs.indexOf(currentInput);
if (index > -1 && (index + 1) < this._inputs.length) {
let nextInput = this._inputs[index + 1];
if (nextInput !== this._focused) {
console.debug('tabFocus, next');
return nextInput.initFocus();
}
}
index = this._inputs.indexOf(this._focused);
if (index > 0) {
let previousInput = this._inputs[index - 1];
if (previousInput) {
console.debug('tabFocus, previous');
previousInput.initFocus();
}
}
}
nextId() {
return ++this._ids;
}
}

269
src/util/functions.scss Normal file
View File

@@ -0,0 +1,269 @@
// Color Functions
// --------------------------------------------------
@function color-brightness($color-value) {
@if (type-of($color-value) != color) {
@return color-error($color-value);
}
@return (red($color-value) * .299 + green($color-value) * .587 + blue($color-value) * .114) / 255 * 100%;
}
@function color-inverse($color-value, $dark: #000, $light: #fff) {
@if (type-of($color-value) != color) {
@return color-error($color-value);
}
$brightness: color-brightness($color-value);
$red: red($color-value);
$green: green($color-value);
@if ($brightness > 79) {
@return $dark;
}
@if ($green > 240) {
@return $dark;
}
@if ($red > 220 and $green > 180) {
@return $dark;
}
@return $light;
}
// Pass dark and light colors based on the mode
// this is mostly used for toolbar buttons/titles
//
// @param {String} $color-value - color to get the inverse of
// @param {Boolean} $custom-contrast-mode - the mode to use
// in order to pass the custom colors
//
// @return {Color}
// --------------------------------------------------
@function mode-inverse($color-value, $custom-contrast-mode) {
$dark: #000;
$light: #fff;
@if ($custom-contrast-mode == md) {
$dark: #424242;
$light: #fff;
} @else if ($custom-contrast-mode == ios) {
$dark: color($colors-ios, primary);
$light: #fff;
}
@return color-inverse($color-value, $dark, $light);
}
@function color-shade($color-value, $amount:8%) {
@if (type-of($color-value) != color) {
@return color-error($color-value);
}
$lightness: lightness($color-value);
$shade: #fff;
@if ($lightness > 50) {
$shade: #000;
}
@return mix($shade, $color-value, $amount);
}
@function color-error($color-value) {
@error "
The value `#{$color-value}` must be a color.
If you are setting the value as a map make sure
both the base and contrast are defined as colors.
For example:
$colors: (
primary: #327eff,
secondary: (base: #32db64, contrast: #000),
);";
@return false;
}
// Copy Colors Map
// --------------------------------------------------
@function copy-colors($colors-map) {
@return map-merge($colors-map, ());
}
// String Replace Function
// --------------------------------------------------
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
// URL Encode Function
// --------------------------------------------------
@function url-encode($val) {
@return str-replace($val, " ", "%20");
}
// Fetch nested keys
// @param {Map} $map - Map
// @param {Arglist} $keys - Keys to fetch
// @return {*}
// --------------------------------------------------
@function map-fetch($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
// Fetch map color value
// @param {Map} $map - Map
// @param {String} $color-name - Color name to get
// @param {String} $color-key - Color key (optional), default base
// @return {Color}
// --------------------------------------------------
@function color($map, $color-name, $color-key:null) {
// Get the value from $color-name in the map
// this can be of type color or a map
$color-value: map-get($map, $color-name);
// If we were given a map we need to grab the value
// of the key that is passed or the base key
@if(type-of($color-value) == map) {
@if($color-key) {
$color-value: map-fetch($map, $color-name, $color-key);
} @else {
$color-value: map-fetch($map, $color-name, base);
}
}
// If it isn't a map then return the value
@return $color-value;
}
// Get the color map key based on the value
// if it doesn't exist then return the value
// --------------------------------------------------
@function color-key($colors, $value) {
@each $color-name, $color-value in $colors {
$base-value: color($colors, $color-name);
@if ($base-value == $value) {
@return map-get($colors, $color-name);
}
}
@return $value;
}
// Fetch map color contrast
// @param {Map} $colors - colors map
// @param {String} $value - color value or color name
//
// Example values
// --------------------------------------------------
// primary | #327eff | #444
// map key | map value | hex color not in map
// --------------------------------------------------
//
// @param {Boolean} $custom-contrast-mode - use custom
// contrast function
// @return {Color}
// --------------------------------------------------
@function color-contrast($colors, $value, $custom-contrast-mode: null) {
$color-value: null;
// If the value is a color (e.g. #fff)
// we need to call color-key to see if
// it exists in the color map or not
@if (type-of($value) == color) {
$color-value: color-key($colors, $value);
// If the value is a string (e.g. primary)
// we want to get the value from the map
// where it is the key
} @else {
$color-value: map-get($colors, $value);
}
// If the value is a map then get the contrast
// from the map (e.g. (base: #327eff, contrast: blue))
@if (type-of($color-value) == map) {
// If the map has the contrast key then use that
// otherwise it is a map with just a base so get
// the inverse of that base color
@if map-has-key($color-value, contrast) {
$color-value: map-get($color-value, contrast);
} @else {
$color-value: color-inverse(map-get($color-value, base));
}
// If a mode was passed we need to call
// the custom inverse function to get the inverse
// color based on the mode
} @elseif ($custom-contrast-mode) {
$color-value: mode-inverse($color-value, $custom-contrast-mode);
// Otherwise we were passed a color and can use the
// function to get the inverse of that color
// (e.g. #f4f4f4)
} @else {
$color-value: color-inverse($color-value);
}
// If the final value being returned is not a color
// we should error
@if (type-of($color-value) != color) {
@return color-error($color-value);
}
@return $color-value;
}
// Create a list using the colors map
// @param {Map} $colors - colors map
// @return {List} $color-name, $color-base, $color-contrast
// ----------------------------------------------------------
@function get-colors($colors) {
$colors-list: ();
@each $color-name, $color-value in $colors {
$color-base: null;
$color-contrast: null;
@if(type-of($color-value) == map) {
$color-base: map-get($color-value, base);
$color-contrast: map-get($color-value, contrast);
} @else {
$color-base: $color-value;
$color-contrast: color-inverse($color-value);
}
$colors-list: append($colors-list, ($color-name, $color-base, $color-contrast), comma);
}
@return $colors-list;
}

186
src/util/keyboard.ts Normal file
View File

@@ -0,0 +1,186 @@
import {Injectable, NgZone} from '@angular/core';
import {Config} from '../config/config';
import {Form} from './form';
import {hasFocusedTextInput, nativeRaf, rafFrames, nativeTimeout} from './dom';
/**
* @name Keyboard
* @description
* The `Keyboard` class allows you to work with the keyboard events provided by the Ionic keyboard plugin.
*
* @usage
* ```ts
* export class MyClass{
* constructor(keyboard: Keyboard){
* this.keyboard = keyboard;
* }
* }
*
* ```
*/
@Injectable()
export class Keyboard {
constructor(config: Config, private _form: Form, private _zone: NgZone) {
_zone.runOutsideAngular(() => {
this.focusOutline(config.get('focusOutline'), document);
});
}
/**
* Check to see if the keyboard is open or not.
*
* ```ts
* export class MyClass{
* constructor(keyboard: Keyboard){
* this.keyboard = keyboard;
* }
* keyboardCheck(){
* setTimeout(() => console.log('is the keyboard open ', this.keyboard.isOpen()));
* }
* }
*
* ```
*
* @return {boolean} returns a true or flase value if the keyboard is open or not
*/
isOpen() {
return hasFocusedTextInput();
}
/**
* When the keyboard is closed, call any methods you want
*
* ```ts
* export class MyClass{
* constructor(keyboard: Keyboard){
* this.keyboard = keyboard;
* this.keyboard.onClose(this.closeCallback);
* }
* closeCallback(){
* // call what ever functionality you want on keyboard close
* console.log('Closing time');
* }
* }
*
* ```
* @param {function} callback method you want to call when the keyboard has been closed
* @return {function} returns a callback that gets fired when the keyboard is closed
*/
onClose(callback, pollingInternval = KEYBOARD_CLOSE_POLLING, pollingChecksMax = KEYBOARD_POLLING_CHECKS_MAX) {
console.debug('keyboard onClose');
const self = this;
let checks = 0;
let promise = null;
if (!callback) {
// a callback wasn't provided, so let's return a promise instead
promise = new Promise(resolve => { callback = resolve; });
}
function checkKeyboard() {
console.debug('keyboard isOpen', self.isOpen());
if (!self.isOpen() || checks > pollingChecksMax) {
rafFrames(30, () => {
self._zone.run(() => {
console.debug('keyboard closed');
callback();
});
});
} else {
nativeTimeout(checkKeyboard, pollingInternval);
}
checks++;
}
nativeTimeout(checkKeyboard, pollingInternval);
return promise;
}
/**
* Programmatically close the keyboard
*
*/
close() {
console.debug('keyboard close()');
nativeRaf(() => {
if (hasFocusedTextInput()) {
// only focus out when a text input has focus
this._form.focusOut();
}
});
}
/**
* @private
*/
focusOutline(setting, document) {
/* Focus Outline
* --------------------------------------------------
* By default, when a keydown event happens from a tab key, then
* the 'focus-outline' css class is added to the body element
* so focusable elements have an outline. On a mousedown or
* touchstart event, then the 'focus-outline' css class is removed.
*
* Config default overrides:
* focusOutline: true - Always add the focus-outline
* focusOutline: false - Do not add the focus-outline
*/
let self = this;
let isKeyInputEnabled = false;
function cssClass() {
nativeRaf(() => {
document.body.classList[isKeyInputEnabled ? 'add' : 'remove']('focus-outline');
});
}
if (setting === true) {
isKeyInputEnabled = true;
return cssClass();
} else if (setting === false) {
return;
}
// default is to add the focus-outline when the tab key is used
function keyDown(ev) {
if (!isKeyInputEnabled && ev.keyCode === 9) {
isKeyInputEnabled = true;
enableKeyInput();
}
}
function pointerDown() {
isKeyInputEnabled = false;
enableKeyInput();
}
function enableKeyInput() {
cssClass();
self._zone.runOutsideAngular(() => {
document.removeEventListener('mousedown', pointerDown);
document.removeEventListener('touchstart', pointerDown);
if (isKeyInputEnabled) {
document.addEventListener('mousedown', pointerDown);
document.addEventListener('touchstart', pointerDown);
}
});
}
document.addEventListener('keydown', keyDown);
}
}
const KEYBOARD_CLOSE_POLLING = 150;
const KEYBOARD_POLLING_CHECKS_MAX = 100;

54
src/util/mixins.scss Normal file
View File

@@ -0,0 +1,54 @@
// Appearance
// --------------------------------------------------
@mixin appearance($val) {
-webkit-appearance: $val;
-moz-appearance: $val;
}
// Calc
// --------------------------------------------------
@mixin calc($property, $expression) {
#{$property}: -webkit-calc(#{$expression});
#{$property}: calc(#{$expression});
}
// Input Placeholder
// --------------------------------------------------
@mixin placeholder($color: #999, $text-indent: 0) {
&::-moz-placeholder { // Firefox 19+
color: $color;
}
&:-ms-input-placeholder {
color: $color;
}
&::-webkit-input-placeholder {
// Safari placeholder margin issue
text-indent: $text-indent;
color: $color;
}
}
// SVG Background Image Mixin
// --------------------------------------------------
@mixin svg-background-image($svg) {
$url: url-encode($svg);
background-image: url("data:image/svg+xml;charset=utf-8,#{$url}");
}
// User Select None
// --------------------------------------------------
@mixin user-select-none() {
user-select: none;
}

283
src/util/scroll-view.ts Normal file
View File

@@ -0,0 +1,283 @@
import {CSS, pointerCoord, nativeRaf, cancelRaf} from '../util/dom';
export class ScrollView {
private _el: HTMLElement;
private _js: boolean = false;
private _top: number = 0;
private _pos: Array<number>;
private _velocity: number;
private _max: number;
private _rafId: number;
private _cb: Function;
isPlaying: boolean;
constructor(ele: HTMLElement) {
this._el = ele;
}
getTop(): number {
if (this._js) {
return this._top;
}
return this._top = this._el.scrollTop;
}
setTop(top: number) {
this._top = top;
if (this._js) {
this._el.style[CSS.transform] = `translate3d(0px,${top * -1}px,0px)`;
} else {
this._el.scrollTop = top;
}
}
scrollTo(x: number, y: number, duration: number): Promise<any> {
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
let self = this;
if (!self._el) {
// invalid element
return Promise.resolve();
}
x = x || 0;
y = y || 0;
let fromY = self._el.scrollTop;
let fromX = self._el.scrollLeft;
let xDistance = Math.abs(x - fromX);
let yDistance = Math.abs(y - fromY);
let maxAttempts = (duration / 16) + 100;
return new Promise(resolve => {
let startTime: number;
let attempts = 0;
// scroll loop
function step() {
attempts++;
if (!self._el || !self.isPlaying || attempts > maxAttempts) {
self.isPlaying = false;
resolve();
return;
}
let time = Math.min(1, ((Date.now() - startTime) / duration));
// where .5 would be 50% of time on a linear scale easedT gives a
// fraction based on the easing method
let easedT = (--time) * time * time + 1;
if (fromY !== y) {
self.setTop((easedT * (y - fromY)) + fromY);
}
if (fromX !== x) {
self._el.scrollLeft = Math.floor((easedT * (x - fromX)) + fromX);
}
if (easedT < 1) {
nativeRaf(step);
} else {
// done
resolve();
}
}
// start scroll loop
self.isPlaying = true;
// chill out for a frame first
nativeRaf(() => {
startTime = Date.now();
nativeRaf(step);
});
});
}
scrollToTop(duration: number): Promise<any> {
return this.scrollTo(0, 0, duration);
}
scrollToBottom(duration: number): Promise<any> {
let y = 0;
if (this._el) {
y = this._el.scrollHeight - this._el.clientHeight;
}
return this.scrollTo(0, y, duration);
}
stop() {
this.isPlaying = false;
}
/**
* @private
* JS Scrolling has been provided only as a temporary solution
* until iOS apps can take advantage of scroll events at all times.
* The goal is to eventually remove JS scrolling entirely. This
* method may be removed in the future.
*/
jsScroll(onScrollCallback: Function): Function {
this._js = true;
this._cb = onScrollCallback;
this._pos = [];
this._el.addEventListener('touchstart', this._start.bind(this));
this._el.addEventListener('touchmove', this._move.bind(this));
this._el.addEventListener('touchend', this._end.bind(this));
this._el.parentElement.classList.add('js-scroll');
return () => {
this._el.removeEventListener('touchstart', this._start.bind(this));
this._el.removeEventListener('touchmove', this._move.bind(this));
this._el.removeEventListener('touchend', this._end.bind(this));
this._el.parentElement.classList.remove('js-scroll');
};
}
/**
* @private
* Used for JS scrolling. May be removed in the future.
*/
private _start(ev) {
this._velocity = 0;
this._pos.length = 0;
this._max = null;
this._pos.push(pointerCoord(ev).y, Date.now());
}
/**
* @private
* Used for JS scrolling. May be removed in the future.
*/
private _move(ev) {
if (this._pos.length) {
let y = pointerCoord(ev).y;
// ******** DOM READ ****************
this._setMax();
this._top -= (y - this._pos[this._pos.length - 2]);
this._top = Math.min(Math.max(this._top, 0), this._max);
this._pos.push(y, Date.now());
// ******** DOM READ THEN DOM WRITE ****************
this._cb(this._top);
// ******** DOM WRITE ****************
this.setTop(this._top);
}
}
/**
* @private
* Used for JS scrolling. May be removed in the future.
*/
private _setMax() {
if (!this._max) {
// ******** DOM READ ****************
this._max = (this._el.offsetHeight - this._el.parentElement.offsetHeight + this._el.parentElement.offsetTop);
}
}
/**
* @private
* Used for JS scrolling. May be removed in the future.
*/
private _end(ev) {
// figure out what the scroll position was about 100ms ago
let positions = this._pos;
this._velocity = 0;
cancelRaf(this._rafId);
if (!positions.length) return;
let y = pointerCoord(ev).y;
positions.push(y, Date.now());
let endPos = (positions.length - 1);
let startPos = endPos;
let timeRange = (Date.now() - 100);
// move pointer to position measured 100ms ago
for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 2) {
startPos = i;
}
if (startPos !== endPos) {
// compute relative movement between these two points
let timeOffset = (positions[endPos] - positions[startPos]);
let movedTop = (positions[startPos - 1] - positions[endPos - 1]);
// based on XXms compute the movement to apply for each render step
this._velocity = ((movedTop / timeOffset) * FRAME_MS);
// verify that we have enough velocity to start deceleration
if (Math.abs(this._velocity) > MIN_VELOCITY_START_DECELERATION) {
// ******** DOM READ ****************
this._setMax();
this._rafId = nativeRaf(this._decelerate.bind(this));
}
}
positions.length = 0;
}
/**
* @private
* Used for JS scrolling. May be removed in the future.
*/
private _decelerate() {
var self = this;
if (self._velocity) {
self._velocity *= DECELERATION_FRICTION;
// update top with updated velocity
// clamp top within scroll limits
self._top = Math.min(Math.max(self._top + self._velocity, 0), self._max);
// ******** DOM READ THEN DOM WRITE ****************
self._cb(self._top);
// ******** DOM WRITE ****************
self.setTop(self._top);
if (self._top > 0 && self._top < self._max && Math.abs(self._velocity) > MIN_VELOCITY_CONTINUE_DECELERATION) {
self._rafId = nativeRaf(self._decelerate.bind(self));
}
}
}
/**
* @private
*/
destroy() {
this._velocity = 0;
this.stop();
this._el = null;
}
}
const MAX_VELOCITY = 150;
const MIN_VELOCITY_START_DECELERATION = 4;
const MIN_VELOCITY_CONTINUE_DECELERATION = 0.12;
const DECELERATION_FRICTION = 0.97;
const FRAME_MS = (1000 / 60);

View File

@@ -0,0 +1,843 @@
import * as datetime from '../../../src/util/datetime-util';
export function run() {
describe('convertDataToISO', () => {
it('should convert DateTimeData to datetime string, +330 tz offset', () => {
var data: datetime.DateTimeData = {
year: 1994,
month: 12,
day: 15,
hour: 13,
minute: 47,
second: 20,
millisecond: 789,
tzOffset: 330,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('1994-12-15T13:47:20.789+05:30');
});
it('should convert DateTimeData to datetime string, Z timezone', () => {
var data: datetime.DateTimeData = {
year: 1994,
month: 12,
day: 15,
hour: 13,
minute: null,
second: null,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('1994-12-15T13:00:00Z');
});
it('should convert DateTimeData to YYYY-MM-DD', () => {
var data: datetime.DateTimeData = {
year: 1994,
month: 1,
day: 1,
hour: null,
minute: null,
second: null,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('1994-01-01');
});
it('should convert DateTimeData to YYYY-MM', () => {
var data: datetime.DateTimeData = {
year: 1994,
month: 1,
day: null,
hour: null,
minute: null,
second: null,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('1994-01');
});
it('should convert DateTimeData to YYYY', () => {
var data: datetime.DateTimeData = {
year: 1994,
month: null,
day: null,
hour: null,
minute: null,
second: null,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('1994');
});
it('should convert DateTimeData to HH:mm:SS.SSS', () => {
var data: datetime.DateTimeData = {
year: null,
month: null,
day: null,
hour: 13,
minute: 47,
second: 20,
millisecond: 789,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('13:47:20.789');
});
it('should convert DateTimeData to HH:mm:SS string', () => {
var data: datetime.DateTimeData = {
year: null,
month: null,
day: null,
hour: 13,
minute: 47,
second: 20,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('13:47:20');
});
it('should convert DateTimeData to HH:mm string', () => {
var data: datetime.DateTimeData = {
year: null,
month: null,
day: null,
hour: 13,
minute: 47,
second: null,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('13:47');
});
it('should not convert DateTimeData with null data', () => {
var data: datetime.DateTimeData = {
year: null,
month: null,
day: null,
hour: null,
minute: null,
second: null,
millisecond: null,
tzOffset: 0,
};
var str = datetime.convertDataToISO(data);
expect(str).toEqual('');
var str = datetime.convertDataToISO({});
expect(str).toEqual('');
});
});
describe('convertFormatToKey', () => {
it('should convert year formats to their DateParse key', () => {
expect(datetime.convertFormatToKey('YYYY')).toEqual('year');
expect(datetime.convertFormatToKey('YY')).toEqual('year');
});
it('should convert month formats to their DateParse key', () => {
expect(datetime.convertFormatToKey('MMMM')).toEqual('month');
expect(datetime.convertFormatToKey('MMM')).toEqual('month');
expect(datetime.convertFormatToKey('MM')).toEqual('month');
expect(datetime.convertFormatToKey('M')).toEqual('month');
});
it('should convert day formats to their DateParse key', () => {
expect(datetime.convertFormatToKey('DDDD')).toEqual('day');
expect(datetime.convertFormatToKey('DDD')).toEqual('day');
expect(datetime.convertFormatToKey('DD')).toEqual('day');
expect(datetime.convertFormatToKey('D')).toEqual('day');
});
it('should convert hour formats to their DateParse key', () => {
expect(datetime.convertFormatToKey('HH')).toEqual('hour');
expect(datetime.convertFormatToKey('H')).toEqual('hour');
expect(datetime.convertFormatToKey('hh')).toEqual('hour');
expect(datetime.convertFormatToKey('h')).toEqual('hour');
});
it('should convert minute formats to their DateParse key', () => {
expect(datetime.convertFormatToKey('mm')).toEqual('minute');
expect(datetime.convertFormatToKey('m')).toEqual('minute');
});
it('should convert am/pm formats to their DateParse key', () => {
expect(datetime.convertFormatToKey('A')).toEqual('ampm');
expect(datetime.convertFormatToKey('a')).toEqual('ampm');
});
});
describe('getValueFromFormat', () => {
it('should convert 24 hour to am value', () => {
var d = datetime.parseDate('00:47');
expect(datetime.getValueFromFormat(d, 'hh')).toEqual(0);
expect(datetime.getValueFromFormat(d, 'h')).toEqual(0);
var d = datetime.parseDate('11:47');
expect(datetime.getValueFromFormat(d, 'hh')).toEqual(11);
expect(datetime.getValueFromFormat(d, 'h')).toEqual(11);
});
it('should convert 24 hour to pm value', () => {
var d = datetime.parseDate('12:47');
expect(datetime.getValueFromFormat(d, 'hh')).toEqual(12);
expect(datetime.getValueFromFormat(d, 'h')).toEqual(12);
var d = datetime.parseDate('13:47');
expect(datetime.getValueFromFormat(d, 'hh')).toEqual(1);
expect(datetime.getValueFromFormat(d, 'h')).toEqual(1);
});
it('should convert am hours to am value', () => {
var d = datetime.parseDate('00:47');
expect(datetime.getValueFromFormat(d, 'A')).toEqual('am');
expect(datetime.getValueFromFormat(d, 'a')).toEqual('am');
var d = datetime.parseDate('11:47');
expect(datetime.getValueFromFormat(d, 'A')).toEqual('am');
expect(datetime.getValueFromFormat(d, 'a')).toEqual('am');
});
it('should convert pm hours to pm value', () => {
var d = datetime.parseDate('12:47');
expect(datetime.getValueFromFormat(d, 'A')).toEqual('pm');
expect(datetime.getValueFromFormat(d, 'a')).toEqual('pm');
var d = datetime.parseDate('23:47');
expect(datetime.getValueFromFormat(d, 'A')).toEqual('pm');
expect(datetime.getValueFromFormat(d, 'a')).toEqual('pm');
});
it('should convert date formats to values', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.getValueFromFormat(d, 'YYYY')).toEqual(1994);
expect(datetime.getValueFromFormat(d, 'M')).toEqual(12);
expect(datetime.getValueFromFormat(d, 'DDDD')).toEqual(15);
});
});
describe('parseTemplate', () => {
it('should not parse D thats apart of a word', () => {
var formats = datetime.parseTemplate('D Days');
expect(formats.length).toEqual(1);
expect(formats[0]).toEqual('D');
});
it('should not parse D thats apart of a word', () => {
var formats = datetime.parseTemplate('DD Days');
expect(formats.length).toEqual(1);
expect(formats[0]).toEqual('DD');
});
it('should not parse m thats apart of a word', () => {
var formats = datetime.parseTemplate('m mins');
expect(formats.length).toEqual(1);
expect(formats[0]).toEqual('m');
});
it('should not parse M thats apart of a word', () => {
var formats = datetime.parseTemplate('mm Minutes');
expect(formats.length).toEqual(1);
expect(formats[0]).toEqual('mm');
});
it('should not pickup "a" within 12-hour, but its not the am/pm', () => {
var formats = datetime.parseTemplate('hh:mm is a time');
expect(formats.length).toEqual(2);
expect(formats[0]).toEqual('hh');
expect(formats[1]).toEqual('mm');
});
it('should allow am/pm when using 12-hour and no spaces', () => {
var formats = datetime.parseTemplate('hh:mma');
expect(formats.length).toEqual(3);
expect(formats[0]).toEqual('hh');
expect(formats[1]).toEqual('mm');
expect(formats[2]).toEqual('a');
});
it('should allow am/pm when using 12-hour', () => {
var formats = datetime.parseTemplate('hh:mm a');
expect(formats.length).toEqual(3);
expect(formats[0]).toEqual('hh');
expect(formats[1]).toEqual('mm');
expect(formats[2]).toEqual('a');
});
it('should not add am/pm when not using 24-hour', () => {
var formats = datetime.parseTemplate('HH:mm a');
expect(formats.length).toEqual(2);
expect(formats[0]).toEqual('HH');
expect(formats[1]).toEqual('mm');
});
it('should get formats from template "m mm h hh H HH D DD DDD DDDD M MM MMM MMMM YY YYYY"', () => {
var formats = datetime.parseTemplate('m mm h hh H HH D DD DDD DDDD M MM MMM MMMM YY YYYY');
expect(formats[0]).toEqual('m');
expect(formats[1]).toEqual('mm');
expect(formats[2]).toEqual('h');
expect(formats[3]).toEqual('hh');
expect(formats[4]).toEqual('H');
expect(formats[5]).toEqual('HH');
expect(formats[6]).toEqual('D');
expect(formats[7]).toEqual('DD');
expect(formats[8]).toEqual('DDD');
expect(formats[9]).toEqual('DDDD');
expect(formats[10]).toEqual('M');
expect(formats[11]).toEqual('MM');
expect(formats[12]).toEqual('MMM');
expect(formats[13]).toEqual('MMMM');
expect(formats[14]).toEqual('YY');
expect(formats[15]).toEqual('YYYY');
});
it('should get formats from template YYMMMMDDHHmm', () => {
var formats = datetime.parseTemplate('YYMMMMDDHHmm');
expect(formats[0]).toEqual('YY');
expect(formats[1]).toEqual('MMMM');
expect(formats[2]).toEqual('DD');
expect(formats[3]).toEqual('HH');
expect(formats[4]).toEqual('mm');
});
it('should get formats from template MM/DD/YYYY', () => {
var formats = datetime.parseTemplate('MM/DD/YYYY');
expect(formats[0]).toEqual('MM');
expect(formats[1]).toEqual('DD');
expect(formats[2]).toEqual('YYYY');
});
});
describe('renderDateTime', () => {
it('should show correct month and day name defaults', () => {
var d = datetime.parseDate('2016-05-12');
var r = datetime.renderDateTime('DDDD MMM D YYYY', d, {});
expect(r).toEqual('Thursday May 12 2016');
});
it('should format h:mm a, PM', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('h:mm a', d, {})).toEqual('1:47 pm');
});
it('should get empty text for format without data', () => {
var emptyObj = {};
expect(datetime.renderDateTime('MMMM D, YYYY h:mm a', emptyObj, {})).toEqual('');
var dataWithNulls: datetime.DateTimeData = {
year: null,
month: null,
day: null,
hour: null,
minute: null,
second: null,
millisecond: null,
tzOffset: 0,
};
expect(datetime.renderDateTime('MMMM D, YYYY h:mm a', dataWithNulls, {})).toEqual('');
});
it('should format h:mm a, AM', () => {
var d = datetime.parseDate('1994-12-15T00:47:20.789Z');
expect(datetime.renderDateTime('h:mm a', d, {})).toEqual('12:47 am');
});
it('should format HH:mm', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('HH:mm', d, {})).toEqual('13:47');
});
it('should format MMMM D, YYYY', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('MMMM D, YYYY', d, {})).toEqual('December 15, 1994');
});
it('should format MM/DD/YYYY', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('MM/DD/YYYY', d, {})).toEqual('12/15/1994');
});
it('should format DD-MM-YY', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('DD-MM-YY', d, {})).toEqual('15-12-94');
});
it('should format YYYY', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('DD-MM-YY', d, {})).toEqual('15-12-94');
});
it('should format YYYY$MM.DD*HH?mm', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('YYYY$MM.DD*HH?mm', d, {})).toEqual('1994$12.15*13?47');
});
it('should return empty when template invalid', () => {
var d = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(datetime.renderDateTime('', d, {})).toEqual('');
});
it('should return empty when date invalid', () => {
var d = datetime.parseDate(null);
expect(datetime.renderDateTime('YYYY', d, {})).toEqual('');
});
});
describe('renderTextFormat', () => {
it('should return a', () => {
var d = datetime.parseDate('00:47');
expect(datetime.renderTextFormat('a', 'am', d, {})).toEqual('am');
expect(datetime.renderTextFormat('a', 'am', null, {})).toEqual('am');
var d = datetime.parseDate('11:47');
expect(datetime.renderTextFormat('a', 'am', d, {})).toEqual('am');
expect(datetime.renderTextFormat('a', 'am', null, {})).toEqual('am');
var d = datetime.parseDate('12:47');
expect(datetime.renderTextFormat('a', 'pm', d, {})).toEqual('pm');
expect(datetime.renderTextFormat('a', 'pm', null, {})).toEqual('pm');
});
it('should return A', () => {
var d = datetime.parseDate('00:47');
expect(datetime.renderTextFormat('A', 'am', d, {})).toEqual('AM');
expect(datetime.renderTextFormat('A', 'am', null, {})).toEqual('AM');
var d = datetime.parseDate('11:47');
expect(datetime.renderTextFormat('A', 'am', d, {})).toEqual('AM');
expect(datetime.renderTextFormat('A', 'am', null, {})).toEqual('AM');
var d = datetime.parseDate('12:47');
expect(datetime.renderTextFormat('A', 'pm', d, {})).toEqual('PM');
expect(datetime.renderTextFormat('A', 'pm', null, {})).toEqual('PM');
});
it('should return m', () => {
expect(datetime.renderTextFormat('m', 1, null, {})).toEqual('1');
expect(datetime.renderTextFormat('m', 12, null, {})).toEqual('12');
});
it('should return mm', () => {
expect(datetime.renderTextFormat('mm', 1, null, {})).toEqual('01');
expect(datetime.renderTextFormat('mm', 12, null, {})).toEqual('12');
});
it('should return hh', () => {
expect(datetime.renderTextFormat('hh', 0, null, {})).toEqual('12');
expect(datetime.renderTextFormat('hh', 1, null, {})).toEqual('01');
expect(datetime.renderTextFormat('hh', 11, null, {})).toEqual('11');
expect(datetime.renderTextFormat('hh', 12, null, {})).toEqual('12');
expect(datetime.renderTextFormat('hh', 13, null, {})).toEqual('01');
expect(datetime.renderTextFormat('hh', 21, null, {})).toEqual('09');
expect(datetime.renderTextFormat('hh', 23, null, {})).toEqual('11');
});
it('should return h', () => {
expect(datetime.renderTextFormat('h', 0, null, {})).toEqual('12');
expect(datetime.renderTextFormat('h', 1, null, {})).toEqual('1');
expect(datetime.renderTextFormat('h', 11, null, {})).toEqual('11');
expect(datetime.renderTextFormat('h', 12, null, {})).toEqual('12');
expect(datetime.renderTextFormat('h', 13, null, {})).toEqual('1');
expect(datetime.renderTextFormat('h', 21, null, {})).toEqual('9');
expect(datetime.renderTextFormat('h', 23, null, {})).toEqual('11');
});
it('should return hh', () => {
expect(datetime.renderTextFormat('hh', 1, null, {})).toEqual('01');
expect(datetime.renderTextFormat('hh', 12, null, {})).toEqual('12');
});
it('should return H', () => {
expect(datetime.renderTextFormat('H', 1, null, {})).toEqual('1');
expect(datetime.renderTextFormat('H', 12, null, {})).toEqual('12');
});
it('should return HH', () => {
expect(datetime.renderTextFormat('HH', 1, null, {})).toEqual('01');
expect(datetime.renderTextFormat('HH', 12, null, {})).toEqual('12');
});
it('should return D', () => {
expect(datetime.renderTextFormat('D', 1, null, {})).toEqual('1');
expect(datetime.renderTextFormat('D', 12, null, {})).toEqual('12');
});
it('should return DD', () => {
expect(datetime.renderTextFormat('DD', 1, null, {})).toEqual('01');
expect(datetime.renderTextFormat('DD', 12, null, {})).toEqual('12');
});
it('should return DDD', () => {
var d: datetime.DateTimeData = {
year: 2016,
month: 5,
day: 12,
};
expect(datetime.renderTextFormat('DDD', null, d, {})).toEqual('Thu');
});
it('should return DDD with custom locale', () => {
var d: datetime.DateTimeData = {
year: 2016,
month: 5,
day: 12,
};
expect(datetime.renderTextFormat('DDD', null, d, customLocale)).toEqual('qui');
});
it('should return DDDD', () => {
var d: datetime.DateTimeData = {
year: 2016,
month: 5,
day: 12,
};
expect(datetime.renderTextFormat('DDDD', null, d, {})).toEqual('Thursday');
});
it('should return DDDD with custom locale', () => {
var d: datetime.DateTimeData = {
year: 2016,
month: 5,
day: 12,
};
expect(datetime.renderTextFormat('DDDD', null, d, customLocale)).toEqual('quinta-feira');
});
it('should return M', () => {
expect(datetime.renderTextFormat('M', 1, null, {})).toEqual('1');
expect(datetime.renderTextFormat('M', 12, null, {})).toEqual('12');
});
it('should return MM', () => {
expect(datetime.renderTextFormat('MM', 1, null, {})).toEqual('01');
expect(datetime.renderTextFormat('MM', 12, null, {})).toEqual('12');
});
it('should return MMM', () => {
expect(datetime.renderTextFormat('MMM', 1, null, {})).toEqual('Jan');
expect(datetime.renderTextFormat('MMM', 12, null, {})).toEqual('Dec');
});
it('should return MMM with custom locale', () => {
expect(datetime.renderTextFormat('MMM', 1, null, customLocale)).toEqual('jan');
});
it('should return MMMM', () => {
expect(datetime.renderTextFormat('MMMM', 1, null, {})).toEqual('January');
expect(datetime.renderTextFormat('MMMM', 12, null, {})).toEqual('December');
});
it('should return MMMM with custom locale', () => {
expect(datetime.renderTextFormat('MMMM', 1, null, customLocale)).toEqual('janeiro');
});
it('should return YY', () => {
expect(datetime.renderTextFormat('YY', 1994, null, {})).toEqual('94');
expect(datetime.renderTextFormat('YY', 94, null, {})).toEqual('94');
});
it('should return YYYY', () => {
expect(datetime.renderTextFormat('YYYY', 1994, null, {})).toEqual('1994');
expect(datetime.renderTextFormat('YYYY', 0, null, {})).toEqual('0000');
});
it('should return empty when blank', () => {
expect(datetime.renderTextFormat(null, null, null, {})).toEqual('');
expect(datetime.renderTextFormat(null, 1994, null, {})).toEqual('1994');
});
});
describe('parseISODate', () => {
it('should get HH:MM:SS.SSS+HH:MM', () => {
var parsed = datetime.parseDate('13:47:20.789+05:30');
expect(parsed.year).toEqual(null);
expect(parsed.month).toEqual(null);
expect(parsed.day).toEqual(null);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(789);
expect(parsed.tzOffset).toEqual(330);
});
it('should get HH:MM:SS.SSS', () => {
var parsed = datetime.parseDate('13:47:20.789');
expect(parsed.year).toEqual(null);
expect(parsed.month).toEqual(null);
expect(parsed.day).toEqual(null);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(789);
expect(parsed.tzOffset).toEqual(0);
});
it('should get HH:MM:SS', () => {
var parsed = datetime.parseDate('13:47:20');
expect(parsed.year).toEqual(null);
expect(parsed.month).toEqual(null);
expect(parsed.day).toEqual(null);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should get HH:MM', () => {
var parsed = datetime.parseDate('13:47');
expect(parsed.year).toEqual(null);
expect(parsed.month).toEqual(null);
expect(parsed.day).toEqual(null);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(null);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should get YYYY-MM-DDTHH:MM:SS.SSS+HH:MM', () => {
var parsed = datetime.parseDate('1994-12-15T13:47:20.789+05:30');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(789);
expect(parsed.tzOffset).toEqual(330);
});
it('should get YYYY-MM-DDTHH:MM:SS.SSS-HH:MM', () => {
var parsed = datetime.parseDate('1994-12-15T13:47:20.789-11:45');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(789);
expect(parsed.tzOffset).toEqual(-705);
});
it('should get YYYY-MM-DDTHH:MM:SS.SSS-HH', () => {
var parsed = datetime.parseDate('1994-12-15T13:47:20.789-02');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(789);
expect(parsed.tzOffset).toEqual(-120);
});
it('should get YYYY-MM-DDTHH:MM:SS.SSSZ and set UTC offset', () => {
var parsed = datetime.parseDate('1994-12-15T13:47:20.789Z');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(789);
expect(parsed.tzOffset).toEqual(0);
});
it('should get YYYY-MM-DDTHH:MM:SS', () => {
var parsed = datetime.parseDate('1994-12-15T13:47:20');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(20);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should get YYYY-MM-DDTHH:MM', () => {
var parsed = datetime.parseDate('1994-12-15T13:47');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(13);
expect(parsed.minute).toEqual(47);
expect(parsed.second).toEqual(null);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should NOT work with YYYY-MM-DDTHH', () => {
var parsed = datetime.parseDate('1994-12-15T13');
expect(parsed).toEqual(null);
});
it('should get YYYY-MM-DD', () => {
var parsed = datetime.parseDate('1994-12-15');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(15);
expect(parsed.hour).toEqual(null);
expect(parsed.minute).toEqual(null);
expect(parsed.second).toEqual(null);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should get YYYY-MM', () => {
var parsed = datetime.parseDate('1994-12');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(12);
expect(parsed.day).toEqual(null);
expect(parsed.hour).toEqual(null);
expect(parsed.minute).toEqual(null);
expect(parsed.second).toEqual(null);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should get YYYY', () => {
var parsed = datetime.parseDate('1994');
expect(parsed.year).toEqual(1994);
expect(parsed.month).toEqual(null);
expect(parsed.day).toEqual(null);
expect(parsed.hour).toEqual(null);
expect(parsed.minute).toEqual(null);
expect(parsed.second).toEqual(null);
expect(parsed.millisecond).toEqual(null);
expect(parsed.tzOffset).toEqual(0);
});
it('should handle bad date formats', () => {
var parsed = datetime.parseDate('12/15/1994');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('12-15-1994');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('1994-1994');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('1994 12 15');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('12.15.1994');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('12\\15\\1994');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('200');
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('holla');
expect(parsed).toEqual(null);
});
it('should get nothing with null date', () => {
var parsed = datetime.parseDate(null);
expect(parsed).toEqual(null);
var parsed = datetime.parseDate(undefined);
expect(parsed).toEqual(null);
var parsed = datetime.parseDate('');
expect(parsed).toEqual(null);
});
});
// pt-br
var customLocale: datetime.LocaleData = {
dayNames: [
'domingo',
'segunda-feira',
'ter\u00e7a-feira',
'quarta-feira',
'quinta-feira',
'sexta-feira',
's\u00e1bado'
],
dayShortNames: [
'dom',
'seg',
'ter',
'qua',
'qui',
'sex',
's\u00e1b'
],
monthNames: [
'janeiro',
'fevereiro',
'mar\u00e7o',
'abril',
'maio',
'junho',
'julho',
'agosto',
'setembro',
'outubro',
'novembro',
'dezembro'
],
monthShortNames: [
'jan',
'fev',
'mar',
'abr',
'mai',
'jun',
'jul',
'ago',
'set',
'out',
'nov',
'dez'
],
};
}

400
src/util/test/util.spec.ts Normal file
View File

@@ -0,0 +1,400 @@
import * as util from '../../../src/util';
export function run() {
describe('isCheckedProperty', () => {
it('should test a=undefined', () => {
expect(util.isCheckedProperty(undefined, undefined)).toBe(true);
expect(util.isCheckedProperty(undefined, null)).toBe(true);
expect(util.isCheckedProperty(undefined, '')).toBe(true);
expect(util.isCheckedProperty(undefined, 'string')).toBe(false);
expect(util.isCheckedProperty(undefined, 0)).toBe(false);
expect(util.isCheckedProperty(undefined, '0')).toBe(false);
expect(util.isCheckedProperty(undefined, 1000)).toBe(false);
expect(util.isCheckedProperty(undefined, '1000')).toBe(false);
expect(util.isCheckedProperty(undefined, -1)).toBe(false);
expect(util.isCheckedProperty(undefined, true)).toBe(false);
expect(util.isCheckedProperty(undefined, false)).toBe(false);
expect(util.isCheckedProperty(undefined, 'true')).toBe(false);
expect(util.isCheckedProperty(undefined, 'false')).toBe(false);
});
it('should test a=null', () => {
expect(util.isCheckedProperty(null, undefined)).toBe(true);
expect(util.isCheckedProperty(null, null)).toBe(true);
expect(util.isCheckedProperty(null, '')).toBe(true);
expect(util.isCheckedProperty(null, 'string')).toBe(false);
expect(util.isCheckedProperty(null, 0)).toBe(false);
expect(util.isCheckedProperty(null, '0')).toBe(false);
expect(util.isCheckedProperty(null, 1000)).toBe(false);
expect(util.isCheckedProperty(null, '1000')).toBe(false);
expect(util.isCheckedProperty(null, -1)).toBe(false);
expect(util.isCheckedProperty(null, true)).toBe(false);
expect(util.isCheckedProperty(null, false)).toBe(false);
expect(util.isCheckedProperty(null, 'true')).toBe(false);
expect(util.isCheckedProperty(null, 'false')).toBe(false);
});
it('should test a=""', () => {
expect(util.isCheckedProperty('', undefined)).toBe(true);
expect(util.isCheckedProperty('', null)).toBe(true);
expect(util.isCheckedProperty('', '')).toBe(true);
expect(util.isCheckedProperty('', 'string')).toBe(false);
expect(util.isCheckedProperty('', 0)).toBe(false);
expect(util.isCheckedProperty('', '0')).toBe(false);
expect(util.isCheckedProperty('', 1000)).toBe(false);
expect(util.isCheckedProperty('', '1000')).toBe(false);
expect(util.isCheckedProperty('', -1)).toBe(false);
expect(util.isCheckedProperty('', true)).toBe(false);
expect(util.isCheckedProperty('', false)).toBe(false);
expect(util.isCheckedProperty('', 'true')).toBe(false);
expect(util.isCheckedProperty('', 'false')).toBe(false);
});
it('should test a=true', () => {
expect(util.isCheckedProperty(true, undefined)).toBe(false);
expect(util.isCheckedProperty(true, null)).toBe(false);
expect(util.isCheckedProperty(true, '')).toBe(false);
expect(util.isCheckedProperty(true, 'string')).toBe(false);
expect(util.isCheckedProperty(true, 0)).toBe(false);
expect(util.isCheckedProperty(true, '0')).toBe(false);
expect(util.isCheckedProperty(true, 1000)).toBe(false);
expect(util.isCheckedProperty(true, '1000')).toBe(false);
expect(util.isCheckedProperty(true, -1)).toBe(false);
expect(util.isCheckedProperty(true, true)).toBe(true);
expect(util.isCheckedProperty(true, false)).toBe(false);
expect(util.isCheckedProperty(true, 'true')).toBe(true);
expect(util.isCheckedProperty(true, 'false')).toBe(false);
});
it('should test a="true"', () => {
expect(util.isCheckedProperty('true', undefined)).toBe(false);
expect(util.isCheckedProperty('true', null)).toBe(false);
expect(util.isCheckedProperty('true', '')).toBe(false);
expect(util.isCheckedProperty('true', 'string')).toBe(false);
expect(util.isCheckedProperty('true', 0)).toBe(false);
expect(util.isCheckedProperty('true', '0')).toBe(false);
expect(util.isCheckedProperty('true', 1000)).toBe(false);
expect(util.isCheckedProperty('true', '1000')).toBe(false);
expect(util.isCheckedProperty('true', -1)).toBe(false);
expect(util.isCheckedProperty('true', true)).toBe(true);
expect(util.isCheckedProperty('true', false)).toBe(false);
expect(util.isCheckedProperty('true', 'true')).toBe(true);
expect(util.isCheckedProperty('true', 'false')).toBe(false);
});
it('should test a=false', () => {
expect(util.isCheckedProperty(false, undefined)).toBe(false);
expect(util.isCheckedProperty(false, null)).toBe(false);
expect(util.isCheckedProperty(false, '')).toBe(false);
expect(util.isCheckedProperty(false, 'string')).toBe(false);
expect(util.isCheckedProperty(false, 0)).toBe(false);
expect(util.isCheckedProperty(false, '0')).toBe(false);
expect(util.isCheckedProperty(false, 1000)).toBe(false);
expect(util.isCheckedProperty(false, '1000')).toBe(false);
expect(util.isCheckedProperty(false, -1)).toBe(false);
expect(util.isCheckedProperty(false, true)).toBe(false);
expect(util.isCheckedProperty(false, false)).toBe(true);
expect(util.isCheckedProperty(false, 'true')).toBe(false);
expect(util.isCheckedProperty(false, 'false')).toBe(true);
});
it('should test a="false"', () => {
expect(util.isCheckedProperty('false', undefined)).toBe(false);
expect(util.isCheckedProperty('false', null)).toBe(false);
expect(util.isCheckedProperty('false', '')).toBe(false);
expect(util.isCheckedProperty('false', 'string')).toBe(false);
expect(util.isCheckedProperty('false', 0)).toBe(false);
expect(util.isCheckedProperty('false', '0')).toBe(false);
expect(util.isCheckedProperty('false', 1000)).toBe(false);
expect(util.isCheckedProperty('false', '1000')).toBe(false);
expect(util.isCheckedProperty('false', -1)).toBe(false);
expect(util.isCheckedProperty('false', true)).toBe(false);
expect(util.isCheckedProperty('false', false)).toBe(true);
expect(util.isCheckedProperty('false', 'true')).toBe(false);
expect(util.isCheckedProperty('false', 'false')).toBe(true);
});
it('should test a=0', () => {
expect(util.isCheckedProperty(0, undefined)).toBe(false);
expect(util.isCheckedProperty(0, null)).toBe(false);
expect(util.isCheckedProperty(0, '')).toBe(false);
expect(util.isCheckedProperty(0, 'string')).toBe(false);
expect(util.isCheckedProperty(0, 0)).toBe(true);
expect(util.isCheckedProperty(0, '0')).toBe(true);
expect(util.isCheckedProperty(0, 1000)).toBe(false);
expect(util.isCheckedProperty(0, '1000')).toBe(false);
expect(util.isCheckedProperty(0, -1)).toBe(false);
expect(util.isCheckedProperty(0, true)).toBe(false);
expect(util.isCheckedProperty(0, false)).toBe(false);
expect(util.isCheckedProperty(0, 'true')).toBe(false);
expect(util.isCheckedProperty(0, 'false')).toBe(false);
});
it('should test a="0"', () => {
expect(util.isCheckedProperty('0', undefined)).toBe(false);
expect(util.isCheckedProperty('0', null)).toBe(false);
expect(util.isCheckedProperty('0', '')).toBe(false);
expect(util.isCheckedProperty('0', 'string')).toBe(false);
expect(util.isCheckedProperty('0', 0)).toBe(true);
expect(util.isCheckedProperty('0', '0')).toBe(true);
expect(util.isCheckedProperty('0', 1000)).toBe(false);
expect(util.isCheckedProperty('0', '1000')).toBe(false);
expect(util.isCheckedProperty('0', -1)).toBe(false);
expect(util.isCheckedProperty('0', true)).toBe(false);
expect(util.isCheckedProperty('0', false)).toBe(false);
expect(util.isCheckedProperty('0', 'true')).toBe(false);
expect(util.isCheckedProperty('0', 'false')).toBe(false);
});
it('should test a=1000', () => {
expect(util.isCheckedProperty(1000, undefined)).toBe(false);
expect(util.isCheckedProperty(1000, null)).toBe(false);
expect(util.isCheckedProperty(1000, '')).toBe(false);
expect(util.isCheckedProperty(1000, 'string')).toBe(false);
expect(util.isCheckedProperty(1000, 0)).toBe(false);
expect(util.isCheckedProperty(1000, '0')).toBe(false);
expect(util.isCheckedProperty(1000, 1000)).toBe(true);
expect(util.isCheckedProperty(1000, '1000')).toBe(true);
expect(util.isCheckedProperty(1000, -1)).toBe(false);
expect(util.isCheckedProperty(1000, true)).toBe(false);
expect(util.isCheckedProperty(1000, false)).toBe(false);
expect(util.isCheckedProperty(1000, 'true')).toBe(false);
expect(util.isCheckedProperty(1000, 'false')).toBe(false);
});
it('should test a="1000"', () => {
expect(util.isCheckedProperty('1000', undefined)).toBe(false);
expect(util.isCheckedProperty('1000', null)).toBe(false);
expect(util.isCheckedProperty('1000', '')).toBe(false);
expect(util.isCheckedProperty('1000', 'string')).toBe(false);
expect(util.isCheckedProperty('1000', 0)).toBe(false);
expect(util.isCheckedProperty('1000', '0')).toBe(false);
expect(util.isCheckedProperty('1000', 1000)).toBe(true);
expect(util.isCheckedProperty('1000', '1000')).toBe(true);
expect(util.isCheckedProperty('1000', -1)).toBe(false);
expect(util.isCheckedProperty('1000', true)).toBe(false);
expect(util.isCheckedProperty('1000', false)).toBe(false);
expect(util.isCheckedProperty('1000', 'true')).toBe(false);
expect(util.isCheckedProperty('1000', 'false')).toBe(false);
});
it('should test a=-1', () => {
expect(util.isCheckedProperty(-1, undefined)).toBe(false);
expect(util.isCheckedProperty(-1, null)).toBe(false);
expect(util.isCheckedProperty(-1, '')).toBe(false);
expect(util.isCheckedProperty(-1, 'string')).toBe(false);
expect(util.isCheckedProperty(-1, 0)).toBe(false);
expect(util.isCheckedProperty(-1, '0')).toBe(false);
expect(util.isCheckedProperty(-1, 1000)).toBe(false);
expect(util.isCheckedProperty(-1, '1000')).toBe(false);
expect(util.isCheckedProperty(-1, -1)).toBe(true);
expect(util.isCheckedProperty(-1, true)).toBe(false);
expect(util.isCheckedProperty(-1, false)).toBe(false);
expect(util.isCheckedProperty(-1, 'true')).toBe(false);
expect(util.isCheckedProperty(-1, 'false')).toBe(false);
});
it('should test a="-1"', () => {
expect(util.isCheckedProperty('-1', undefined)).toBe(false);
expect(util.isCheckedProperty('-1', null)).toBe(false);
expect(util.isCheckedProperty('-1', '')).toBe(false);
expect(util.isCheckedProperty('-1', 'string')).toBe(false);
expect(util.isCheckedProperty('-1', 0)).toBe(false);
expect(util.isCheckedProperty('-1', '0')).toBe(false);
expect(util.isCheckedProperty('-1', 1000)).toBe(false);
expect(util.isCheckedProperty('-1', '1000')).toBe(false);
expect(util.isCheckedProperty('-1', -1)).toBe(true);
expect(util.isCheckedProperty('-1', true)).toBe(false);
expect(util.isCheckedProperty('-1', false)).toBe(false);
expect(util.isCheckedProperty('-1', 'true')).toBe(false);
expect(util.isCheckedProperty('-1', 'false')).toBe(false);
});
it('should test a="string"', () => {
expect(util.isCheckedProperty('string', undefined)).toBe(false);
expect(util.isCheckedProperty('string', null)).toBe(false);
expect(util.isCheckedProperty('string', '')).toBe(false);
expect(util.isCheckedProperty('string', 'string')).toBe(true);
expect(util.isCheckedProperty('string', 'otherstring')).toBe(false);
expect(util.isCheckedProperty('string', 0)).toBe(false);
expect(util.isCheckedProperty('string', '0')).toBe(false);
expect(util.isCheckedProperty('string', 1000)).toBe(false);
expect(util.isCheckedProperty('string', '1000')).toBe(false);
expect(util.isCheckedProperty('string', -1)).toBe(false);
expect(util.isCheckedProperty('string', true)).toBe(false);
expect(util.isCheckedProperty('string', false)).toBe(false);
expect(util.isCheckedProperty('string', 'true')).toBe(false);
expect(util.isCheckedProperty('string', 'false')).toBe(false);
});
});
describe('getQuerystring', () => {
it('should have no entries for empty url', () => {
expect(util.getQuerystring('')).toEqual({});
expect(util.getQuerystring(null)).toEqual({});
expect(util.getQuerystring(undefined)).toEqual({});
});
it('should have no entries when without ?', () => {
expect(util.getQuerystring('http://localhost:1234/')).toEqual({});
});
it('should have no entries with only ?', () => {
expect(util.getQuerystring('http://localhost:1234/?')).toEqual({});
});
it('should have no entries for key with no =', () => {
expect(util.getQuerystring('http://localhost:1234/?key')).toEqual({});
});
it('should have no entries with only #?', () => {
expect(util.getQuerystring('http://localhost:1234/#?')).toEqual({});
});
it('should have no entries with only #?=', () => {
expect(util.getQuerystring('http://localhost:1234/#?=')).toEqual({});
});
it('should have no entries for url with no "?" character', () => {
expect(util.getQuerystring('http://localhost:1234/#key1=1&key2=2')).toEqual({});
});
it('should contain key/value entries for all the parameters after "?" character', () => {
expect(util.getQuerystring('http://localhost:1234/#key1=1&key2x=2x?key3=3&key4=4')).toEqual({
key3: '3',
key4: '4'
});
});
it('should lowercase param keys', () => {
expect(util.getQuerystring('http://localhost:1234/#?KEY1=1&kEy2=2')).toEqual({
key1: '1',
key2: '2'
});
});
it('should not include any values when # comes after ?', () => {
expect(util.getQuerystring('http://localhost:1234/?key1=1#key2=2')).toEqual({
key1: '1'
});
});
it('should ignore empty ?& and &&', () => {
expect(util.getQuerystring('http://localhost:1234/#?&&')).toEqual({});
expect(util.getQuerystring('http://localhost:1234/#?&&key1=1&key2=2&&')).toEqual({
key1: '1',
key2: '2'
});
});
it('should get "" when key has no value', () => {
expect(util.getQuerystring('http://localhost:1234/#?key=')).toEqual({
key: ''
});
});
});
describe('isTrueProperty', () => {
it('should be true from boolean true', () => {
expect(util.isTrueProperty(true)).toBe(true);
});
it('should be true from string "true"', () => {
expect(util.isTrueProperty('true')).toBe(true);
expect(util.isTrueProperty('TRUE')).toBe(true);
expect(util.isTrueProperty(' true ')).toBe(true);
});
it('should be true from string "on"', () => {
expect(util.isTrueProperty(true)).toBe(true);
});
it('should be true from empty string ""', () => {
expect(util.isTrueProperty('')).toBe(true);
expect(util.isTrueProperty(' ')).toBe(true);
});
it('should be true from number greater than zero', () => {
expect(util.isTrueProperty(1)).toBe(true);
expect(util.isTrueProperty(999)).toBe(true);
});
it('should be false from boolean false', () => {
expect(util.isTrueProperty(false)).toBe(false);
});
it('should be false from string "off"', () => {
expect(util.isTrueProperty(true)).toBe(true);
});
it('should be false from null', () => {
expect(util.isTrueProperty(null)).toBe(false);
});
it('should be false from undefined', () => {
expect(util.isTrueProperty(undefined)).toBe(false);
});
it('should be false from string "false"', () => {
expect(util.isTrueProperty('false')).toBe(false);
expect(util.isTrueProperty(' FALSE ')).toBe(false);
expect(util.isTrueProperty('doesnt actually matter')).toBe(false);
});
});
describe('extend', () => {
it('should extend simple', () => {
var obj = { a: '0', c: '0' };
expect( util.assign(obj, { a: '1', b: '2' }) ).toBe(obj);
expect(obj).toEqual({ a: '1', b: '2', c: '0' });
});
it('should extend complex', () => {
expect(util.assign(
{ a: '0', b: '0' },
{ b: '1', c: '1' },
{ c: '2', d: '2' }
)).toEqual({
a: '0',
b: '1',
c: '2',
d: '2'
});
});
});
describe('defaults', () => {
it('should simple defaults', () => {
var obj = { a: '1' };
expect(util.defaults(obj, { a: '2', b: '2' })).toBe(obj);
expect(obj).toEqual({
a: '1', b: '2'
});
});
it('should complex defaults', () => {
expect(util.defaults(
{ a: '0', b: '0' },
{ b: '1', c: '1', e: '1' },
{ c: '2', d: '2' }
)).toEqual({
a: '0',
b: '0',
c: '2',
d: '2',
e: '1'
});
});
});
}

140
src/util/util.scss Executable file
View File

@@ -0,0 +1,140 @@
@import "../globals.core";
.hide,
[hidden],
template {
display: none !important;
}
// Focus Outline
// --------------------------------------------------
$focus-outline-border-color: #51a7e8 !default;
$focus-outline-box-shadow: 0 0 8px 0 $focus-outline-border-color !default;
:focus,
:active {
outline: none;
}
.focus-outline {
:focus {
outline: thin dotted;
outline-offset: -1px;
}
button:focus,
[button]:focus {
border-color: $focus-outline-border-color;
outline: thin solid $focus-outline-border-color;
box-shadow: $focus-outline-box-shadow;
}
ion-input :focus {
outline: none;
}
}
// Focus Controls
// -------------------------------
focus-ctrl {
position: fixed;
input,
button {
position: fixed;
top: 1px;
left: -9999px;
z-index: 9999;
width: 9px;
pointer-events: none;
}
}
// Backdrop
// --------------------------------------------------
$backdrop-color: #000 !default;
.backdrop {
position: absolute;
top: 0;
left: 0;
z-index: $z-index-backdrop;
display: block;
width: 100%;
height: 100%;
background-color: $backdrop-color;
opacity: .01;
transform: translateZ(0);
}
// Click Block
// --------------------------------------------------
// Fill the screen to block clicks (a better pointer-events: none)
// to avoid full-page reflows and paints which can cause flickers
click-block {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: $z-index-click-block;
display: block;
opacity: 0;
transform: translate3d(0, -100%, 0);
transform: translate3d(0, calc(-100% + 1px), 0);
// background: red;
// opacity: .3;
}
.click-block-active {
transform: translate3d(0, 0, 0);
}
// Loading Icon
// --------------------------------------------------
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}
ion-loading-icon {
display: flex;
align-self: center;
margin: auto;
font-family: "Ionicons";
font-size: 128px;
color: #666;
animation: rotation 45s infinite linear;
&::before {
content: "\f44e";
}
}

230
src/util/util.ts Normal file
View File

@@ -0,0 +1,230 @@
/**
* Given a min and max, restrict the given number
* to the range.
* @param min the minimum
* @param n the value
* @param max the maximum
*/
export function clamp(min, n, max) {
return Math.max(min, Math.min(n, max));
}
/**
* The assign() method is used to copy the values of all enumerable own
* properties from one or more source objects to a target object. It will
* return the target object. When available, this method will use
* `Object.assign()` under-the-hood.
* @param target The target object
* @param source(s) The source object
*/
export function assign(...args: any[]): any {
if (typeof Object.assign !== 'function') {
// use the old-school shallow extend method
return _baseExtend(args[0], [].slice.call(args, 1), false);
}
// use the built in ES6 Object.assign method
return Object.assign.apply(null, args);
}
/**
* Do a deep extend (merge).
* @param dst the destination
* @param ... the param objects
*/
export function merge(dst: any, ...args: any[]) {
return _baseExtend(dst, [].slice.call(arguments, 1), true);
}
function _baseExtend(dst, objs, deep) {
for (var i = 0, ii = objs.length; i < ii; ++i) {
var obj = objs[i];
if (!obj || !isObject(obj) && !isFunction(obj)) continue;
var keys = Object.keys(obj);
for (var j = 0, jj = keys.length; j < jj; j++) {
var key = keys[j];
var src = obj[key];
if (deep && isObject(src)) {
if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
_baseExtend(dst[key], [src], true);
} else {
dst[key] = src;
}
}
}
return dst;
}
export function debounce(fn: Function, wait: number, immediate: boolean = false): any {
var timeout, args, context, timestamp: number, result;
return function() {
context = this;
args = arguments;
timestamp = Date.now();
var later: any = function() {
var last: any = Date.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) result = fn.apply(context, args);
}
};
var callNow = immediate && !timeout;
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) result = fn.apply(context, args);
return result;
};
}
/**
* Apply default arguments if they don't exist in
* the first object.
* @param the destination to apply defaults to.
*/
export function defaults(dest, ...args: any[]) {
for (let i = arguments.length - 1; i >= 1; i--) {
let source = arguments[i] || {};
for (let key in source) {
if (source.hasOwnProperty(key) && !dest.hasOwnProperty(key)) {
dest[key] = source[key];
}
}
}
return dest;
}
export const isBoolean = val => typeof val === 'boolean';
export const isString = val => typeof val === 'string';
export const isNumber = val => typeof val === 'number';
export const isFunction = val => typeof val === 'function';
export const isDefined = val => typeof val !== 'undefined';
export const isUndefined = val => typeof val === 'undefined';
export const isPresent = val => val !== undefined && val !== null;
export const isBlank = val => val === undefined || val === null;
export const isObject = val => typeof val === 'object';
export const isArray = Array.isArray;
export const isTrueProperty = function(val: any): boolean {
if (typeof val === 'string') {
val = val.toLowerCase().trim();
return (val === 'true' || val === 'on' || val === '');
}
return !!val;
};
export const isCheckedProperty = function(a: any, b: any): boolean {
if (a === undefined || a === null || a === '') {
return (b === undefined || b === null || b === '');
} else if (a === true || a === 'true') {
return (b === true || b === 'true');
} else if (a === false || a === 'false') {
return (b === false || b === 'false');
} else if (a === 0 || a === '0') {
return (b === 0 || b === '0');
}
// not using strict comparison on purpose
/* tslint:disable */
return (a == b);
/* tslint:enable */
};
/**
* Convert a string in the format thisIsAString to a slug format this-is-a-string
*/
export function pascalCaseToDashCase(val: string = ''): string {
return val.charAt(0).toLowerCase() + val.substring(1).replace(/[A-Z]/g, match => {
return '-' + match.toLowerCase();
});
}
let uid = 0;
export function nextUid(): number {
return ++uid;
}
export const array = {
find(arr, cb) {
for (let i = 0, ii = arr.length; i < ii; i++) {
if (cb(arr[i], i)) return arr[i];
}
},
remove(arr, itemOrIndex) {
let index = -1;
if (isNumber(itemOrIndex)) {
index = itemOrIndex;
} else {
index = arr.indexOf(itemOrIndex);
}
if (index < 0) {
return false;
}
arr.splice(index, 1);
return true;
}
};
/**
* Grab all query strings keys and values.
* @param url
*/
export function getQuerystring(url: string): any {
var queryParams = {};
if (url) {
const startIndex = url.indexOf('?');
if (startIndex !== -1) {
const queries = url.slice(startIndex + 1).split('&');
for (var i = 0; i < queries.length; i++) {
if (queries[i].indexOf('=') > 0) {
var split = queries[i].split('=');
if (split.length > 1) {
queryParams[split[0].toLowerCase()] = split[1].split('#')[0];
}
}
}
}
}
return queryParams;
}
/**
* Throttle the given fun, only allowing it to be
* called at most every `wait` ms.
*/
export function throttle(fn: Function, wait: number, options: any): any {
var context, args, result;
var timeout = null;
var previous = 0;
options || (options = {});
var later = function() {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = fn.apply(context, args);
};
return function() {
var now = Date.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = fn.apply(context, args);
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
}