// dna.js ~~ MIT License type DnaOptionsClone = { fade?: boolean, top?: boolean, clones?: number, html?: boolean, empty?: boolean, holder?: JQuery, container?: JQuery | null, transform?: DnaCallback | null, callback?: DnaCallback | null, }; type DnaOptionsCloneSub = { fade?: boolean, top?: boolean, }; type DnaOptionsGetModel = { main?: boolean, }; type DnaOptionsEmpty = { fade?: boolean, }; type DnaOptionsInsert = { fade?: boolean, html?: boolean, transform?: DnaCallback, callback?: DnaCallback, }; type DnaOptionsRefresh = { data?: unknown, main?: boolean, html?: boolean, }; type DnaOptionsRefreshAll = { data?: unknown, main?: boolean, html?: boolean, }; type DnaOptionsRecount = { html?: boolean, }; type DnaOptionsDestroy = { main?: boolean, fade?: boolean, callback?: DnaCallback | null, }; type DnaOptionsGetClone = { main?: boolean, }; type DnaOptionsGetIndex = { main?: boolean, }; type DnaOptionsRegisterInitializer = { selector?: string | null, params?: DnaDataObject | unknown[] | null, onDocLoad?: boolean, }; type DnaPluginAction = 'bye' | 'clone-sub' | 'destroy' | 'down' | 'refresh' | 'up'; type DnaModel = unknown[] | Record; type DnaDataObject = Record; type DnaCallback = (arg1?: unknown, arg2?: unknown, arg3?: unknown, ...args: unknown[]) => unknown; type DnaElemEventIndex = JQuery | JQuery.EventBase | number; type DnaInitializer = { fn: DnaFunctionName | DnaCallback, selector: string | null, params: DnaDataObject | unknown[] | null, }; type DnaTemplate = { name: string, elem: JQuery, container: JQuery, nested: boolean, separators: number, wrapped: boolean, }; type DnaTemplateDb = { [name: string]: DnaTemplate }; type DnaTemplateName = string; type DnaContext = { [name: string]: Record | DnaCallback }; type DnaFieldName = string; type DnaFunctionName = string; type DnaClassName = string; type DnaAttrName = string; type DnaAttrItem = DnaAttrName | [string, DnaFieldName | 1 | 2, string]; type DnaLoop = { name: string, field: DnaFieldName }; type DnaRules = { template?: DnaTemplateName, array?: DnaFieldName, text?: boolean, val?: boolean, attrs?: DnaAttrItem[], props?: (string | DnaFieldName)[], option?: DnaFieldName, transform?: DnaFunctionName, callback?: DnaFunctionName, class?: [DnaFieldName, DnaClassName, DnaClassName][], require?: DnaFieldName, missing?: DnaFieldName, true?: DnaFieldName, false?: DnaFieldName, loop?: DnaLoop, }; const dnaArray = { find: (array: DnaDataObject[], value: unknown, key = 'code'): { index: number, item?: DnaDataObject } => { // Returns the index and a reference to the first array element with a key equal to the // supplied value. The default key is "code". // Examples: // const array = [{ code: 'a', word: 'Ant' }, { code: 'b', word: 'Bat' }]; // result = dna.array.find(array, 'b'); //{ item: { code: 'b', word: 'Bat' }, index: 1 } // result = dna.array.find(array, 'x'); //{ index: -1 } const valid = Array.isArray(array); let i = 0; if (valid) while (i < array.length && array[i]?.[key] !== value) i++; return valid && i < array.length ? { index: i, item: array[i] } : { index: -1 }; }, last: (array: unknown[]): unknown | null => { // 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] : null; }, fromMap: (map: DnaDataObject, options?: { key?: string, kebabCodes?: boolean }): DnaDataObject[] => { // 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 codeValue = (key: string): string => settings.kebabCodes ? dna.util.toKebab(key) : key; const toObj = (item: unknown): DnaDataObject => dna.util.isObj(item) ? item : { value: item }; return Object.keys(map).map(key => ({ ...{ [settings.key]: codeValue(key) }, ...toObj(map[key]) })); }, toMap: (array: DnaDataObject[], options?: { key: string, camelKeys: boolean }): DnaDataObject => { // 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: DnaDataObject) => map[obj[settings.key]] = obj; const addObjCamelKey = (obj: DnaDataObject) => map[dna.util.toCamel(obj[settings.key])] = obj; array.forEach(settings.camelKeys ? addObjCamelKey : addObj); return map; }, wrap: (itemOrItems: unknown): unknown[] => { // Always returns an array. const isNothing = itemOrItems === null || itemOrItems === undefined; return isNothing ? [] : Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; } }; const dnaBrowser = { getUrlParams: (): Record => { // Returns the query parameters as an object literal. // Example: // https://example.com?lang=jp&code=7 ==> { lang: 'jp', code: '7' } const params = >{}; const addParam = (parts: [string, string]) => params[parts[0]] = parts[1]; const addPair = (pair: string) => pair && addParam(<[string, string]>pair.split('=')); window.location.search.slice(1).split('&').forEach(addPair); return params; } }; const dnaPageToken = { // A simple key/value store specific to the page (URL path) that is cleared out when the // user's browser session ends. put: (key: string, value: unknown): unknown => { // Example: // dna.pageToken.put('favorite', 7); //saves 7 window.sessionStorage[key + window.location.pathname] = JSON.stringify(value); return value; }, get: (key: string, defaultValue: unknown): unknown => { // 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); } }; const dnaUi = { deleteElem: function(elemOrEventOrIndex: DnaElemEventIndex, callback?: DnaCallback | null): JQuery { // A flexible function for removing a jQuery element. // Example: // $('.box').fadeOut(dna.ui.deleteElem); const elem = dna.ui.toElem(elemOrEventOrIndex, this); return dna.core.remove(elem, callback); }, focus: (elem: JQuery): JQuery => { // Sets focus on an element. return elem.trigger('focus'); }, getAttrs: (elem: JQuery): Attr[] => { // Returns the attributes of the DOM node in a regular array. return elem[0] ? Object.values(elem[0].attributes) : []; }, getComponent: (elem: JQuery): JQuery => { // Returns the component (container element with a data-component attribute) to // which the element belongs. return elem.closest('[data-component]'); }, pulse: (elem: JQuery, options?: { duration: number, interval: number, out: number }): JQuery => { // 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: 0, 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: JQuery, callback?: DnaCallback | null, show?: boolean): JQuery => { // 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 || undefined); else elem.css(easeOut).delay(100).slideUp(callback || undefined); elem.delay(200).promise().then(clearTransition); //keep clean for other animations return elem; }, slideFadeIn: (elem: JQuery, callback?: DnaCallback | null): JQuery => { // Smooth slide plus fade effect. return dna.ui.slideFade(elem, callback, true); }, slideFadeOut: (elem: JQuery, callback?: DnaCallback | null): JQuery => { // Smooth slide plus fade effect. return dna.ui.slideFade(elem, callback, false); }, slideFadeToggle: (elem: JQuery, callback?: DnaCallback | null): JQuery => { // Smooth slide plus fade effect. return dna.ui.slideFade(elem, callback, elem.is(':hidden')); }, slideFadeDelete: (elem: JQuery, callback?: DnaCallback | null): JQuery => { // Smooth slide plus fade effect. return dna.ui.slideFadeOut(elem, () => dna.ui.deleteElem(elem, callback)); }, smoothHeightSetBaseline: (container: JQuery): JQuery => { // See: smoothHeightAnimate below const body = $('body'); const elem = body.data().dnaCurrentContainer = container || body; const height = elem.outerHeight(); return elem.css({ minHeight: height, maxHeight: height, overflow: 'hidden' }); }, smoothHeightAnimate: (delay: number, container: JQuery): JQuery => { // Smoothly animates the height of a container element from a beginning height to a final // height. const elem = container || $('body').data().dnaCurrentContainer; const animate = () => { elem.css({ minHeight: 0, maxHeight: '100vh' }); const turnOffTransition = () => elem.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 = () => elem.css({ transition: 'all 1s' }); window.setTimeout(setAnimationLength, 10); //allow baseline to lock in height return elem; }, smoothMove: (elem: JQuery, up?: boolean, callback?: DnaCallback | null): JQuery => { // Uses animation to smoothly slide an element up or down one slot amongst its siblings. const fn = 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++ && fn && fn(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 (fn) fn(elem); return elem; }, smoothMoveUp: (elem: JQuery, callback?: DnaCallback | null): JQuery => { // Uses animation to smoothly slide an element up one slot amongst its siblings. return dna.ui.smoothMove(elem, true, callback); }, smoothMoveDown: (elem: JQuery, callback?: DnaCallback | null): JQuery => { // Uses animation to smoothly slide an element down one slot amongst its siblings. return dna.ui.smoothMove(elem, false, callback); }, toElem: (elemOrEventOrIndex: DnaElemEventIndex, that?: unknown): JQuery => { // 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); } }; const dnaUtil = { apply: (fn: string | DnaCallback, params?: unknown | unknown[] | JQuery): unknown => { // Calls fn (string name or actual function) passing in params. // Usage: // dna.util.apply('app.cart.buy', 7); ==> app.cart.buy(7); const args = dna.array.wrap(params); const elem = args[0] instanceof $ ? args[0] : null; let result; const contextApply = (context: DnaCallback | Record | Window, names: string[]) => { const getFn = (): DnaCallback => (>context)[names[0]]; if (!context || names.length === 1 && typeof (context)[names[0]] !== 'function') dna.core.berserk('Callback function not found', fn); else if (names.length === 1) result = getFn().apply(elem, args); //'app.cart.buy' ==> window['app']['cart']['buy'] else contextApply(getFn(), names.slice(1)); }; const findFn = (names: string[]) => { if (elem) args.push(dna.ui.getComponent(elem)); const context = dna.events.getContextDb(); const name = names[0]; const idPattern = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; const isUnknown = (): boolean => (window)[name] === undefined && !context[name]; const topLevelGet = (null, eval); const callable = (): boolean => ['object', 'function'].includes(topLevelGet('typeof ' + name)); if (idPattern.test(name) && isUnknown() && callable()) dna.registerContext(name, topLevelGet(name)); contextApply(context[name] ? context : window, names); }; if (elem && 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 (typeof fn === 'string' && fn.length > 0) findFn(fn.split('.')); else if (fn === undefined || fn === null) result = null; else dna.core.berserk('Invalid callback function', fn); return result; }, assign: (data: DnaDataObject, field: string | string[], value: unknown): DnaDataObject => { // 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 = (): DnaDataObject => 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: string, ...values: unknown[]): string => { // Builds a formatted string by replacing the format specifiers with the supplied arguments. // Usage: // dna.util.printf('Items in %s: %s', 'cart', 3) === 'Items in cart: 3'; return values.reduce((output: string, value: unknown) => output.replace(/%s/, String(value)), format); }, realTruth: (value: unknown): boolean => { // 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(String(value)); const emptyArray = () => value instanceof Array && value.length === 0; return !!value && !emptyArray() && !falseyStr(); }, toCamel: (kebabStr: string): string => { // 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: string, letter: string): string => letter.toUpperCase(); return String(kebabStr).replace(/-(.)/g, hump); }, toKebab: (camelStr: string): string => { // 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: string) => '-' + word.toLowerCase(); return ('' + camelStr).replace(/([A-Z]+)/g, dash).replace(/\s|^-/g, ''); }, value: (data: DnaDataObject, field: string | string[]): unknown => { // 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)); }, isObj: (value: unknown): boolean => { return !!value && typeof value === 'object' && !Array.isArray(value); }, }; const dnaPlaceholder = { //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: (): JQuery => { $('option.dna-template').closest('select').addClass('dna-hide'); const fade = (node: HTMLElement) => { const elem = $(node).stop(true); dna.getClones(elem.data().placeholder).length ? elem.fadeOut() : elem.fadeIn(); }; const placeholders = $('[data-placeholder]'); placeholders.toArray().forEach(fade); return placeholders; } }; const dnaPanels = { // 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: // //
//
The X1
//
The X2
//
// The optional "data-hash" attribute specifies the hash (URL fragment ID) and updates the // location bar. The "data-nav" attributes can be omitted if the ".dna-panels" element // immediately follows the ".dna-menu" element. display: (menu: JQuery, location?: number, updateUrl?: boolean): JQuery => { // Shows the panel at the given location (index) const panels = menu.data().dnaPanels; const navName = menu.data().nav; const menuItems = menu.find('.menu-item'); const bound = (loc: number) => Math.max(0, Math.min(loc, menuItems.length - 1)); const index = bound( location === undefined ? dna.pageToken.get(navName, 0) : location); const dropDownElemType = 'SELECT'; if ((menu[0]).nodeName === dropDownElemType) (menu[0]).selectedIndex = index; menuItems.removeClass('selected').addClass('unselected'); menuItems.eq(index).addClass('selected').removeClass('unselected'); panels.hide().removeClass('displayed').addClass('hidden'); const panel = panels.eq(index).fadeIn().addClass('displayed').removeClass('hidden'); const hash = panel.data().hash; dna.pageToken.put(navName, index); if (updateUrl && hash) window.history.pushState(null, '', '#' + hash); dna.util.apply(menu.data().callback, [panel, hash]); return panel; }, clickRotate: (event: JQuery.EventBase): JQuery => { // Moves to the selected panel const item = $(event.target).closest('.menu-item'); const menu = item.closest('.dna-menu'); return dna.panels.display(menu, menu.find('.menu-item').index(item), true); }, selectRotate: (event: JQuery.EventBase): JQuery => { // Moves to the selected panel const menu = $(event.target); return dna.panels.display(menu, menu.find('option:selected').index(), true); }, initialize: (panelHolder: JQuery): JQuery => { const initialized = 'dna-panels-initialized'; const generateNavName = (): string => { const navName = 'dna-panels-' + $('body').data().dnaPanelNextNav++; panelHolder.attr('data-nav', navName).prev('.dna-menu').attr('data-nav', navName); return navName; }; const init = () => { const navName = panelHolder.data().nav || generateNavName(); const menu = $('.dna-menu[data-nav=' + navName + ']').addClass(initialized); const panels = panelHolder.addClass(initialized).children().addClass('panel'); const hash = window.location.hash.replace(/[^\w-]/g, ''); //remove leading "#" const hashIndex = (): number => panels.filter('[data-hash=' + hash + ']').index(); const savedIndex = (): number => dna.pageToken.get(navName, 0); const loc = hash && panels.first().data().hash ? hashIndex() : savedIndex(); if (!menu.length) dna.core.berserk('Menu not found for panels', navName); menu.data().dnaPanels = panels; if (!menu.find('.menu-item').length) //set .menu-item elems if not set in the html menu.children().addClass('menu-item'); dna.panels.display(menu, loc); }; const isInitialized = !panelHolder.length || panelHolder.hasClass(initialized); if (!isInitialized && !panelHolder.children().hasClass('dna-template')) init(); return panelHolder; }, setup: (): JQuery => { $('body').data().dnaPanelNextNav = 1; const panels = $('.dna-panels'); panels.toArray().forEach((node: HTMLElement) => dna.panels.initialize($(node))); $(window.document).on({ click: dna.panels.clickRotate }, '.dna-menu .menu-item'); $(window.document).on({ change: dna.panels.selectRotate }, '.dna-menu'); return panels; } }; const dnaCompile = { // Pre-compile Example Post-compile class + data().dnaRules // ----------- -------------------------------- ------------------------------------ // template

class=dna-clone // array

class=dna-nucleotide + array='tags' // field

~~tag~~

class=dna-nucleotide + text='tag' // attribute

class=dna-nucleotide + attrs=['id', ['', 'num', '']] // rule

class=dna-nucleotide + true='on' // attr rule

class=dna-nucleotide + attrs=['src', ['', 'url', '']] // prop rule class=dna-nucleotide + props=['checked', 'on'] // select rule ==> //