// dna.js ~~ MIT License
const dna = {
version: '[VERSION]',
// API:
// dna.clone()
// dna.cloneSub()
// dna.createTemplate()
// dna.getModel()
// dna.empty()
// dna.insert()
// dna.refresh()
// dna.refreshAll()
// dna.updateField()
// dna.recount()
// dna.destroy()
// dna.getClone()
// dna.getClones()
// dna.getIndex()
// dna.up()
// dna.down()
// dna.bye()
// dna.registerInitializer()
// dna.clearInitializers()
// dna.registerContext()
// dna.info()
// See: https://dnajs.org/docs/#api
clone: (name, data, options) => {
// Generates a copy of the template and populates the fields, attributes, and
// classes from the supplied data.
const defaults = {
fade: false,
top: false,
container: null,
empty: false,
clones: 1,
html: false,
transform: null,
callback: null
};
const settings = { ...defaults, ...options };
const template = dna.store.getTemplate(name);
if (template.nested && !settings.container)
dna.core.berserk('Container missing for nested template', name);
if (settings.empty)
dna.empty(name);
const list = [].concat(...Array(settings.clones).fill(data));
let clones = $();
const addClone = (data, index) =>
clones = clones.add(dna.core.replicate(template, data, index, settings));
list.forEach(addClone);
dna.placeholder.setup();
dna.panels.initialize(clones.first().closest('.dna-panels'));
clones.first().parents('.dna-hide').removeClass('dna-hide').addClass('dna-unhide');
return clones;
},
cloneSub: (holderClone, arrayField, data, options) => {
// Clones a sub-template to append onto an array loop.
const name = dna.compile.subTemplateName(holderClone, arrayField);
const selector = '.dna-contains-' + name;
const settings = { container: holderClone.find(selector).addBack(selector) };
const clones = dna.clone(name, data, { ...settings, ...options });
dna.core.updateArray(clones);
return clones;
},
createTemplate: (name, html, holder) => {
// Generates a template from an HTML string.
$(html).attr({ id: name }).addClass('dna-template').appendTo(holder);
return dna.store.getTemplate(name);
},
getModel: (elemOrName, options) => {
// Returns the underlying data of the clone.
const getAllModels = (name) => {
const model = [];
const addToModel = (i, elem) => model.push(dna.getModel($(elem)));
dna.getClones(name).each(addToModel);
return model;
};
const getOneModel = (elem) => dna.getClone($(elem), options).data('dnaModel');
return typeof elemOrName === 'string' ? getAllModels(elemOrName) : getOneModel(elemOrName);
},
empty: (name, options) => {
// Deletes all clones generated from the template.
const defaults = { fade: false, callback: null };
const settings = { ...defaults, ...options };
const template = dna.store.getTemplate(name);
const clones = template.container.children('.dna-clone');
if (template.container.data().dnaCountsMap)
template.container.data().dnaCountsMap[name] = 0;
const fadeDelete = () => dna.ui.slideFadeDelete(clones, settings.callback);
return settings.fade ? fadeDelete() : dna.core.remove(clones, settings.callback);
},
insert: (name, data, options) => {
// Updates the first clone if it already exists otherwise creates the first clone.
const clone = dna.getClones(name).first();
return clone.length ? dna.refresh(clone, { data: data, html: options && options.html }) :
dna.clone(name, data, options);
},
refresh: (clone, options) => {
// Updates an existing clone to reflect changes to the data model.
const defaults = { html: false };
const settings = { ...defaults, ...options };
const elem = dna.getClone(clone, options);
const data = settings.data ? settings.data : dna.getModel(elem);
return dna.core.inject(elem, data, elem.data().dnaCount, settings);
},
refreshAll: (name, options) => {
// Updates all the clones of the specified template.
const refresh = (i, elem) => dna.refresh($(elem), options);
return dna.getClones(name).each(refresh);
},
updateField: (inputElem, value) => {
const field = inputElem.data() && inputElem.data().dnaField;
const update = () => {
if (inputElem.is('input:checkbox'))
inputElem.prop('checked', value);
else if (inputElem.is('input:radio'))
inputElem.prop('checked', value); //TOOD: if true, deselect other buttons in model
else if (inputElem.is('input, select, textarea'))
inputElem.val(value);
dna.getModel(inputElem)[field] = value;
};
if (field)
update();
return inputElem;
},
recount: (clone, options) => {
// Renumbers the counters starting from 1 for the clone and its siblings based on DOM order.
clone = dna.getClone(clone);
const renumber = () => {
const name = clone.data().dnaRules.template;
const update = (i, elem) => {
elem = $(elem);
elem.data().dnaCount = i + 1;
dna.refresh(elem, options);
};
const container = clone.parent();
const clones = container.children('.dna-clone.' + name).each(update);
container.data().dnaCountsMap = container.data().dnaCountsMap || {};
container.data().dnaCountsMap[name] = clones.length;
};
if (clone.length)
renumber();
return clone;
},
destroy: (clone, options) => {
// Removes an existing clone from the DOM.
const defaults = { fade: false, callback: null };
const settings = { ...defaults, ...options };
clone = dna.getClone(clone, options);
const arrayField = dna.core.getArrayName(clone);
if (arrayField)
dna.getModel(clone.parent())[arrayField].splice(dna.getIndex(clone), 1);
const fadeDelete = () => dna.ui.slideFadeDelete(clone, settings.callback);
return settings.fade ? fadeDelete() : dna.core.remove(clone, settings.callback);
},
getClone: (elem, options) => {
// Returns the clone (or sub-clone) for the specified element.
const defaults = { main: false };
const settings = { ...defaults, ...options };
const selector = settings.main ? '.dna-clone:not(.dna-sub-clone)' : '.dna-clone';
return elem instanceof $ ? elem.closest(selector) : $();
},
getClones: (name) => {
// Returns an array of all the existing clones for the given template.
return dna.store.getTemplate(name).container.children('.dna-clone.' + name);
},
getIndex: (elem, options) => {
// Returns the index of the clone.
const clone = dna.getClone(elem, options);
return clone.parent().children('.dna-clone.' + clone.data().dnaRules.template).index(clone);
},
up: function(elemOrEventOrIndex, callback) {
// Smoothly moves a clone up one slot effectively swapping its position with the previous
// clone.
return dna.ui.smoothMoveUp(dna.getClone(dna.ui.toElem(elemOrEventOrIndex, this)), callback);
},
down: function(elemOrEventOrIndex, callback) {
// Smoothly moves a clone down one slot effectively swapping its position with the next
// clone.
return dna.ui.smoothMoveDown(dna.getClone(dna.ui.toElem(elemOrEventOrIndex, this)), callback);
},
bye: function(elemOrEventOrIndex, callback) {
// Performs a sliding fade out effect on the clone and then removes the element.
const elem = dna.ui.toElem(elemOrEventOrIndex, this);
const options = { fade: true, callback: typeof callback === 'function' ? callback : null };
return dna.destroy(elem, options);
},
registerInitializer: (func, options) => {
// Adds a callback function to the list of initializers that are run on all DOM elements.
const defaults = { onDocumentLoad: true };
const settings = { ...defaults, ...options };
const getElems = (selector) => !selector ? $(window.document) :
$(selector).not('.dna-template ' + selector).addClass('dna-initialized');
if (settings.onDocumentLoad)
dna.util.apply(func, [getElems(settings.selector)].concat(settings.params));
const initializer = { func: func, selector: settings.selector, params: settings.params };
dna.events.initializers.push(initializer);
return dna.events.initializers;
},
clearInitializers: () => {
// Deletes all initializers.
dna.events.initializers = [];
},
registerContext: (contextName, contextObjOrFn) => {
// Registers an application object or individual function to enable it to be used for event
// callbacks. Registration is needed when global namespace is not available to dna.js, such
// as when using webpack to load dna.js as a module.
dna.events.context[contextName] = contextObjOrFn;
return dna.events.context;
},
info: () => {
// Returns status information about templates on the current web page.
const names = Object.keys(dna.store.templates);
const panels = $('.dna-menu.dna-panels-initialized');
return {
version: dna.version,
templates: names.length,
clones: $('.dna-clone:not(.dna-sub-clone)').length,
subs: $('.dna-sub-clone').length,
names: names,
store: dna.store.templates,
initializers: dna.events.initializers,
panels: panels.toArray().map(elem => $(elem).attr('data-nav'))
};
}
};
dna.array = {
find: (array, value, key) => {
// Returns the index and a reference to the first array element with a key equal to the
// supplied value. The default key is "code".
// Example:
// const array = [{ code: 'a', word: 'Ant' }, { code: 'b', word: 'Bat' }];
// dna.array.find(array, 'b').item.word === 'Bat';
// dna.array.find(array, 'b').index === 1;
// dna.array.find(array, 'x').item === undefined;
key = key || 'code';
const valid = Array.isArray(array);
let i = 0;
if (valid)
while (i < array.length && array[i][key] !== value)
i++;
return valid && i < array.length ?
{ item: array[i], index: i } :
{ item: undefined, index: -1 };
},
last: (array) => {
// Returns the last element of the array (or undefined if not possible).
// Example:
// dna.array.last([3, 21, 7]) === 7;
return Array.isArray(array) && array.length ? array[array.length - 1] : undefined;
},
fromMap: (map, options) => {
// Converts an object (hash map) into an array of objects. The default key is "code".
// Example:
// dna.array.fromMap({ a: { word: 'Ant' }, b: { word: 'Bat' } })
// converts:
// { a: { word: 'Ant' }, b: { word: 'Bat' } }
// to:
// [{ code: 'a', word: 'Ant' }, { code: 'b', word: 'Bat' }]
const defaults = { key: 'code', kebabCodes: false };
const settings = { ...defaults, ...options };
const array = [];
const toObj = (item) => item instanceof Object ? item : { value: item };
for (let property in map)
array[array.push(toObj(map[property])) - 1][settings.key] =
settings.kebabCodes ? dna.util.toKebab(property) : property;
return array;
},
toMap: (array, options) => {
// Converts an array of objects into an object (hash map). The default key is "code".
// Example:
// dna.array.toMap([{ code: 'a', word: 'Ant' }, { code: 'b', word: 'Bat' }])
// converts:
// [{ code: 'a', word: 'Ant' }, { code: 'b', word: 'Bat' }]
// to:
// { a: { code: 'a', word: 'Ant' }, b: { code: 'b', word: 'Bat' } }
const defaults = { key: 'code', camelKeys: false };
const settings = { ...defaults, ...options };
const map = {};
const addObj = (obj) => map[obj[settings.key]] = obj;
const addObjCamelKey = (obj) => map[dna.util.toCamel(obj[settings.key])] = obj;
array.forEach(settings.camelKeys ? addObjCamelKey : addObj);
return map;
},
wrap: (objOrArray) => {
// Returns the given array if it is an array or returns a new array with the given object as
// the first item.
return !objOrArray ? [] : objOrArray instanceof Array ? objOrArray : [objOrArray];
}
};
dna.browser = {
getUrlParams: () => {
// Returns the query parameters as an object literal.
// Example:
// https://example.com?lang=jp&code=7 ==> { lang: 'jp', code: 7 }
const params = {};
const addParam = (pair) => params[pair.split('=')[0]] = pair.split('=')[1];
window.location.search.slice(1).split('&').forEach(pair => pair && addParam(pair));
return params;
}
};
dna.pageToken = {
// A simple key/value store specific to the page (URL path) that is cleared out when the
// user's browser session ends.
put: (key, value) => {
// Example:
// dna.pageToken.put('favorite', 7); //saves 7
window.sessionStorage[key + window.location.pathname] = JSON.stringify(value);
return value;
},
get: (key, defaultValue) => {
// Example:
// dna.pageToken.get('favorite', 0); //returns 0 if not set
const value = window.sessionStorage[key + window.location.pathname];
return value === undefined ? defaultValue : JSON.parse(value);
}
};
dna.ui = {
deleteElem: function(elemOrEventOrIndex, callback) {
// A flexible function for removing a jQuery element.
// Example:
// $('.box').fadeOut(dna.ui.deleteElem);
callback = typeof callback === 'function' ? callback : null;
return dna.core.remove(dna.ui.toElem(elemOrEventOrIndex, this), callback);
},
focus: (elem) => {
// Sets focus on an element.
return elem.trigger('focus');
},
getAttrs: (elem) => {
// Returns the attributes of the DOM node in a regular array.
return elem[0] ? Object.values(elem[0].attributes) : [];
},
getComponent: (elem) => {
// Returns the component (container element with a data-component
attribute) to
// which the element belongs.
return elem.closest('[data-component]');
},
pulse: (elem, options) => {
// Fades in an element after hiding it to create a single smooth flash effect. The optional
// interval fades out the element.
const defaults = { duration: 400, interval: null, out: 5000 };
const settings = { ...defaults, ...options };
const css = { hide: { opacity: 0 }, show: { opacity: 1 } };
elem.stop(true).slideDown().css(css.hide).animate(css.show, settings.duration);
if (settings.interval)
elem.animate(css.show, settings.interval).animate(css.hide, settings.out);
return elem;
},
slideFade: (elem, callback, show) => {
// Smooth slide plus fade effect.
const obscure = { opacity: 0, transition: 'opacity 0s' };
const easeIn = { opacity: 1, transition: 'opacity 400ms' };
const easeOut = { opacity: 0, transition: 'opacity 400ms' };
const reset = { transition: 'opacity 0s' };
const doEaseIn = () => elem.css(easeIn);
const clearTransition = () => elem.css(reset);
if (show && window.setTimeout(doEaseIn, 200))
elem.css(obscure).hide().delay(100).slideDown(callback);
else
elem.css(easeOut).delay(100).slideUp(callback);
elem.delay(200).promise().then(clearTransition); //keep clean for other animations
return elem;
},
slideFadeIn: (elem, callback) => {
// Smooth slide plus fade effect.
return dna.ui.slideFade(elem, callback, true);
},
slideFadeOut: (elem, callback) => {
// Smooth slide plus fade effect.
return dna.ui.slideFade(elem, callback, false);
},
slideFadeToggle: (elem, callback) => {
// Smooth slide plus fade effect.
return dna.ui.slideFade(elem, callback, elem.is(':hidden'));
},
slideFadeDelete: (elem, callback) => {
// Smooth slide plus fade effect.
return dna.ui.slideFadeOut(elem, () => dna.ui.deleteElem(elem, callback));
},
smoothHeightSetBaseline: (container) => {
// See: smoothHeightAnimate below
dna.ui.$container = container = container || $('body');
const height = container.outerHeight();
return container.css({ minHeight: height, maxHeight: height, overflow: 'hidden' });
},
smoothHeightAnimate: (delay, container) => {
// Smoothly animates the height of a container element from a beginning height to a final
// height.
container = container || dna.ui.$container;
const animate = () => {
container.css({ minHeight: 0, maxHeight: '100vh' });
const turnOffTransition = () => container.css({ transition: 'none', maxHeight: 'none' });
window.setTimeout(turnOffTransition, 1000); //allow 1s transition to finish
};
window.setTimeout(animate, delay || 50); //allow container time to draw
const setAnimationLength = () => container.css({ transition: 'all 1s' });
window.setTimeout(setAnimationLength, 10); //allow baseline to lock in height
return container;
},
smoothMove: (elem, up, callback) => {
// Uses animation to smoothly slide an element up or down one slot amongst its siblings.
callback = typeof callback === 'function' ? callback : null;
const move = () => {
const ghostElem = submissiveElem.clone(true);
if (up)
elem.after(submissiveElem.hide()).before(ghostElem);
else
elem.before(submissiveElem.hide()).after(ghostElem);
let finishes = 0;
const finish = () => finishes++ && callback && callback(elem);
const animate = () => {
dna.ui.slideFadeIn(submissiveElem, finish);
dna.ui.slideFadeDelete(ghostElem, finish);
};
window.setTimeout(animate);
};
const submissiveElem = up ? elem.prev() : elem.next();
if (submissiveElem.length)
move();
else if (callback)
callback(elem);
return elem;
},
smoothMoveUp: (elem, callback) => {
// Uses animation to smoothly slide an element up one slot amongst its siblings.
return dna.ui.smoothMove(elem, true, callback);
},
smoothMoveDown: (elem, callback) => {
// Uses animation to smoothly slide an element down one slot amongst its siblings.
return dna.ui.smoothMove(elem, false, callback);
},
toElem: (elemOrEventOrIndex, that) => {
// A flexible way to get the jQuery element whether it is passed in directly, is a DOM
// element, is the target of an event, or comes from the jQuery context.
const elem = elemOrEventOrIndex instanceof $ && elemOrEventOrIndex;
const target = elemOrEventOrIndex && elemOrEventOrIndex.target;
return elem || $(target || elemOrEventOrIndex || that);
},
};
dna.util = {
apply: (fn, params) => {
// Calls fn (string name or actual function) passing in params.
// Usage:
// dna.util.apply('app.cart.buy', 7); ==> app.cart.buy(7);
const args = params === undefined ? [] : [].concat(params);
const elem = args[0];
let result;
const contextApply = (context, names) => {
if (!context || (names.length === 1 && typeof context[names[0]] !== 'function'))
dna.core.berserk('Callback function not found', fn);
else if (names.length === 1)
result = context[names[0]].apply(elem, args); //'app.cart.buy' ==> window['app']['cart']['buy']
else
contextApply(context[names[0]], names.slice(1));
};
const findFn = (names) => {
if (elem instanceof $)
args.push(dna.ui.getComponent(elem));
const name = names[0];
const identifierPattern = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/;
const unknown = (name) => window[name] === undefined && !dna.events.context[name];
const topLevelGet = (null, eval);
const callable = (name) => ['object', 'function'].includes(topLevelGet('typeof ' + name));
if (identifierPattern.test(name) && unknown(name) && callable(name))
dna.registerContext(name, topLevelGet(name));
contextApply(dna.events.context[name] ? dna.events.context : window, names);
};
if (elem instanceof $ && elem.length === 0) //noop for emply list of elems
result = elem;
else if (typeof fn === 'function') //run regular function with supplied arguments
result = fn.apply(elem, args);
else if (elem && elem[fn]) //run element's jQuery function
result = elem[fn](args[1], args[2], args[3]);
else if (fn === '' || { number: true, boolean: true}[typeof fn])
dna.core.berserk('Invalid callback function', fn);
else if (typeof fn === 'string' && fn.length > 0)
findFn(fn.split('.'));
return result;
},
assign: (data, field, value) => {
// Sets the field in the data object to the new value and returns the updated data object.
// Example:
// dna.util.assign({ a: { b: 7 } }, 'a.b', 21); //{ a: { b: 21 } }
const fields = typeof field === 'string' ? field.split('.') : field;
const name = fields[0];
const dataObj = $.isPlainObject(data) ? data : {};
const nestedData = () => dataObj[name] === undefined ? dataObj[name] = {} : dataObj[name];
if (fields.length === 1)
dataObj[name] = value;
else
dna.util.assign(nestedData(), fields.slice(1), value);
return dataObj;
},
printf: (format, ...values) => {
// Builds a formatted string by replacing the format specifiers with the supplied arguments.
// Usage:
// dna.util.printf('%s: %s', 'Items in cart', 3) === 'Items in cart: 3';
return values.reduce((str, value) => str.replace(/%s/, value), format);
},
realTruth: (value) => {
// Returns the "real" boolean truth of a value.
// Examples:
// const trues = [true, 1, '1', 't', 'T', 'TRue', 'Y', 'yes', 77, [5], {}, 'Colbert', Infinity];
// const falses = [false, 0, '0', 'f', 'F', 'faLSE', 'N', 'no', '', [], null, undefined, NaN];
const falseyStr = () => /^(f|false|n|no|0)$/i.test(value);
const emptyArray = () => value instanceof Array && value.length === 0;
return value ? !emptyArray() && !falseyStr() : false;
},
toCamel: (kebabStr) => {
// Converts a kebab-case string (a code made of lowercase letters and dashes) to camelCase.
// Example:
// dna.util.toCamel('ready-set-go') === 'readySetGo'
const hump = (match, letter) => letter.toUpperCase();
return ('' + kebabStr).replace(/\-(.)/g, hump);
},
toKebab: (camelStr) => {
// Converts a camelCase string to kebab-case (a code made of lowercase letters and dashes).
// Example:
// dna.util.toKebab('readySetGo') === 'ready-set-go'
const dash = (word) => '-' + word.toLowerCase();
return ('' + camelStr).replace(/([A-Z]+)/g, dash).replace(/\s|^-/g, '');
},
value: (data, field) => {
// Returns the value of the field from the data object.
// Example:
// dna.util.value({ a: { b: 7 } }, 'a.b') === 7
if (typeof field === 'string')
field = field.split('.');
return (data === null || data === undefined || field === undefined) ? null :
(field.length === 1 ? data[field[0]] : dna.util.value(data[field[0]], field.slice(1)));
}
};
dna.placeholder = { //TODO: optimize
// A template placeholder is only shown when its corresponding template is empty (has zero
// clones). The "data-placeholder" attribute specifies the name of the template.
setup: () => {
$('option.dna-template').closest('select').addClass('dna-hide');
const fade = (i, elem) => {
const input = $(elem).stop(true);
return dna.getClones(input.data().placeholder).length ? input.fadeOut() : input.fadeIn();
};
$('[data-placeholder]').each(fade);
}
};
dna.panels = {
// Each click of a menu item displays its corresponding panel and optionally passes the panel
// element and hash to the function specified by the "data-callback" attribute.
// Usage:
//
//