UNPKG

tattica

Version:

Tactical and adaptive asset loading library

1 lines 16.9 kB
{"version":3,"file":"tattica.mjs","sources":["../src/lib/makeArr.mjs","../src/lib/loadSingle.mjs","../src/lib/error.mjs","../src/lib/waitIdle.mjs","../src/lib/loader.mjs","../src/lib/loadBlock.mjs","../src/tattica.mjs","../src/lib/connection.mjs","../src/lib/placeholder.mjs","../src/lib/loadIntersections.mjs","../src/lib/makeQueue.mjs"],"sourcesContent":["// return the value of the attribute {string} if the attribute exists\n// return null otherwise\nconst attribute = (a, string) => {\n if (a[string]) return a[string].value;\n return null;\n};\n\n// Making an array referencing both the dom element and all his attributes is useful\n// because you otherwise have to read from the DOM every time you need to know some value.\n\n/**\n * @param {Array} elements - Collection of DOM nodes flagged by target attribute. eg: `data-flag`\n * @param {Object} config - Configuration object for tattica. Better explaination in `Tattica.mjs`\n */\nconst makeArr = (elements, config) => {\n const arr = [];\n elements.forEach((e, i) => {\n const attr = e.attributes;\n arr.push({\n el: e,\n key: i,\n timestamp: config.timestamp,\n timeout: config.timeout !== undefined ? config.timeout : 1000,\n priority: Number(attribute(attr, 'data-priority') || attribute(attr, 'data-priority-block')) || null,\n order: {\n dataPriority: attribute(attr, 'data-priority'),\n dataPriorityBlock: attribute(attr, 'data-priority-block'),\n dataBlock: attribute(attr, 'data-block'),\n },\n src: {\n initial: attribute(attr, 'src'),\n default: attribute(attr, 'data-src'),\n medium: attribute(attr, 'data-src-medium'),\n slow: attribute(attr, 'data-src-slow'),\n fall: attribute(attr, 'data-src-fall'),\n },\n callback: config.callback || null,\n });\n });\n return arr;\n};\n\nexport default makeArr;\n","/**\n * Loads a single image\n * return a Promise that is:\n * - resolved after image load\n * - rejected if an error occurs. eg: wrong URL provided\n * note: timestamp is printed before and after `onload`\n * => when testing, is possible to know exactly when the images easier\n *\n * @param {Object} node - Object containing all the info needed to load the image\n * @param {Object} connection - Tells which URL to pass to the `src` attribute\n */\n\nconst loadSingle = (node, connection) => {\n // destructuring\n const {\n el,\n src,\n timestamp,\n timeout,\n callback,\n } = node;\n\n // falling back to non-null URLs\n const asset = src[connection.string] || src.medium || src.slow || src.default;\n\n // Check if the image is already loaded by `loadIntersections.js`\n const isLoaded = el.attributes['data-is-loaded'];\n return new Promise((resolve, reject) => {\n if (isLoaded) resolve();\n if (timeout) setTimeout(resolve, timeout);\n el.onload = () => {\n el.style.visibility = 'visible';\n el.setAttribute('data-is-loaded', true);\n if (timestamp) el.setAttribute('data-timestamp-loaded', Date.now());\n if (callback) callback(el);\n resolve();\n };\n el.onerror = (err) => {\n // if no fallback is provided a 1px gif weighting 3bytes is loaded => no broken images\n const fallback = src.fall || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';\n const error = {\n err,\n ref: connection.ref,\n fall: src.fall,\n fallUsed: fallback,\n key: node.key,\n };\n if (timestamp) el.setAttribute('data-timestamp-loaded', Date.now());\n el.src = fallback;\n\n // error is catched by `error.mjs`\n reject(error);\n };\n if (timestamp) el.setAttribute('data-timestamp-start', Date.now());\n el.src = asset;\n });\n};\n\nexport default loadSingle;\n","/**\n * @param {Object} err - error object sent by `loadSingle.js` if image loading fails\n * @param {string} ref - The attribute which the URL results null\n * @param {number} key - Key identifier, unique for every element\n * @param {string} fall - URL provided by the element attribute\n * @param {string} fallUsed - URL string effectively used. Defaults to a 1px gif weighting 2bytes\n */\n\nconst errormsg = (err) => {\n const { error } = console;\n error(`\n The '${err.ref}' attribute you inserted in element at key '${err.key}' is invalid. \n The 'data-src-fall' provided is: ${err.fall}\n Fallback used: ${err.fallUsed}\n `);\n error(err.err.target);\n};\n\nexport default errormsg;\n","/**\n * resolve a promise when browser is in `idle` state\n */\n\nconst waitIdle = () => new Promise((resolve) => {\n window.requestIdleCallback(() => {\n resolve();\n }, {\n timeout: 2000,\n });\n});\n\nexport default waitIdle;\n","import loadSingle from './loadSingle';\nimport loadBlock from './loadBlock';\nimport waitIdle from './waitIdle';\nimport error from './error';\n\n/**\n * Recursively loading all elements.\n * If:\n * - element at current index is part of a block => the block is loaded\n * else:\n * - is loaded the single element\n *\n * The connection type is always passed\n *\n * @param {Array} elements - refer to `makeArr.mjs`\n * @param {Object} connection - refer to `connection.mjs`\n * @param {function} resolve - Promise resolve method attached\n * @param {number} index - Index used for recursively load every element. Starts at `0`\n */\n\nconst loader = async (elements, connection, resolve, index = 0) => {\n const newIndex = index + 1;\n const element = elements[index];\n\n // resolve if every element is loaded\n if (!element) {\n resolve();\n return;\n }\n const {\n dataBlock,\n dataPriorityBlock,\n } = element.order;\n const hasBlock = dataBlock || dataPriorityBlock;\n if (hasBlock) {\n // filter the elements belonging to the same block of the elements at current index\n const block = elements\n .filter(e => e.order.dataBlock === hasBlock || e.order.dataPriorityBlock === hasBlock);\n await loadBlock(block, connection);\n } else {\n await loadSingle(element, connection).catch(error);\n }\n\n // wait for idle before next request\n await waitIdle();\n loader(elements, connection, resolve, newIndex);\n};\n\n// Attach a promise to loader => It can be executed with `await` in an async block => cleaner syntax\nconst loaderWithPromise = (elements, connection) => new Promise(resolve => loader(elements, connection, resolve));\n\nexport default loaderWithPromise;\n","import loadSingle from './loadSingle';\nimport error from './error';\n\n/**\n * Load multiple images (block) simultaneously\n * @param {Array} block - Already filtered array containing images belonging to the same block\n * @param {Object} connection - refer to `connection.mjs` or `loadSingle.mjs`\n */\n\nconst loadBlock = (block, connection) => new Promise((resolve) => {\n const promises = [];\n\n // requesting every image at the same time\n for (const img of block) {\n promises.push(loadSingle(img, connection).catch(error));\n }\n\n // waiting for completion\n Promise.all(promises).then(() => resolve());\n});\n\n\nexport default loadBlock;\n","// Refer to single modules for functions explanations\nimport makeArr from './lib/makeArr';\nimport makeQueue from './lib/makeQueue';\nimport loader from './lib/loader';\nimport loadIntersections from './lib/loadIntersections';\nimport placeholder from './lib/placeholder';\nimport connection from './lib/connection';\n\n/**\n*@param {Object} config - Tells tattica how it should behave\n*@param {string} config.flag - Attribute to search in the DOM to select elements for the loading queue\n*@param {string} config.string - String to use to set non-empty src attribute to every image (no broken images)\n*@param {boolean} config.loadIntersections - Defaults to TRUE. Detect elements that enter the\n* viewport and load image if not already loaded\n*@param {number} config.timeout - Sets maximum loading time (in ms) for an image to load sync. Defaults to 1000ms.\n* Can be set to FALSE. However setting it to false is discouraged in a real-world scenario.\n*@param {function} config.callback - Pass callback to be executed after an image loading resolve. Defaults to null\n*@param {boolean} config.timestamp - If set to TRUE prints Date.now() in `timestamp` image attribute.\n* Useful for testing purpose. eg: check if images are loading synchronously\n*/\n\nconst tattica = (config = {}) => {\n const flag = config.flag || 'data-flag';\n const flags = document.querySelectorAll(`[${flag}]`);\n // array-like object containing all the attributes useful for every single element\n const elements = makeArr(flags, config);\n const connectionType = connection();\n\n // set non-empty src attribute\n placeholder(elements, config.string);\n\n // `load` event is dispatched only after every element and every asset requested initially from the page is laoded\n // By starting after the `load` event means that (hopefully) everything critical is loaded.\n // We can start our background work without saturating the connection.\n window.addEventListener('load', () => {\n // The intersectionObserver is fired anyway after the `load` event.\n // So we don't need to worry about images in viewport that don't load.\n // However, we are waiting for an `idleCallback` before starting to load the queue.\n if (config.loadIntersections) loadIntersections(flags);\n window.requestIdleCallback(async () => {\n // array of elements ordered in two queues (`withPriority`, `others`)\n const queue = makeQueue(elements);\n await loader(queue.withPriority, connectionType);\n await loader(queue.others, connectionType);\n }, {\n timeout: 2000,\n });\n });\n};\n\nexport default tattica;\n","/**\n * check `navigator.connection.effectivetype` to control on what type of connection the user is on\n * => return an object referencing the:\n * @param {number} num - number corresponding to the connection type\n * @param {string} string - the status corresponding to the number. eg: `slow`, `medium`,\n * `default` (for faster than 3g connections)\n * @param {string} ref - the corresponding string used to match the correct attribute on the html element\n */\nconst connection = () => {\n const type = navigator.connection.effectiveType.split('g')[0];\n const typeNum = Number(type.match(/\\d/)[0]);\n let typeString;\n let ref;\n if (typeNum < 3) {\n typeString = 'slow';\n ref = 'data-src-slow';\n } else if (typeNum < 4) {\n typeString = 'medium';\n ref = 'data-src-medium';\n } else {\n typeString = 'default';\n ref = 'data-src';\n }\n return {\n num: typeNum,\n string: typeString,\n ref,\n };\n};\n\nexport default connection;\n","/**\n * Passing a placeholder at every elements that has empty src => no broken images\n * @param {Array} elements - elements array\n * @param {string} string - placeholder\n */\n\nconst placeholder = (elements, string = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=') => {\n elements.forEach((node) => {\n const { el } = node;\n if (!el.src || el.src === '') {\n el.style.visibility = 'hidden';\n el.src = string;\n }\n });\n};\n\nexport default placeholder;\n","/**\n * Load elements that intersect the viewport if not already loaded\n * @param {Array} elements - elements flagged\n */\n\nconst waitIntersections = (elements) => {\n const observables = Object.keys(elements).map(e => elements[e]);\n const options = {\n root: null,\n rootMargin: '0px',\n threshold: 0.2,\n };\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n const isLoaded = entry.target.attributes['data-is-loaded'];\n if (!isLoaded) {\n const { target } = entry;\n target.style.visibility = 'visible';\n target.src = target.attributes['data-src'].value;\n\n // set attribute `data-is-loaded` so `loadSingle` avoid unnecessary request\n target.setAttribute('data-is-loaded', true);\n }\n }\n });\n }, options);\n\n observables.forEach((node) => {\n observer.observe(node);\n });\n};\n\nexport default waitIntersections;\n","/**\n * Filter elements by priority\n * return two queues:\n * - withPriority, containing `data-priority` and `data-priority-block`\n * - others, all the others in DOM order\n * @param {Array} elements - Array containing objects with elements properties\n */\n\nconst makeQueue = (elements) => {\n const withPriority = elements\n .filter(e => e.order.dataPriority || e.order.dataPriorityBlock)\n .sort((a, b) => a.priority - b.priority);\n const others = elements\n .filter(e => !e.priority);\n return {\n withPriority,\n others,\n };\n};\n\nexport default makeQueue;\n"],"names":["const","attribute","a","string","value","loadSingle","node","connection","asset","src","medium","slow","default","isLoaded","el","attributes","Promise","resolve","reject","timeout","setTimeout","onload","style","visibility","setAttribute","timestamp","Date","now","callback","onerror","err","fallback","fall","error","ref","fallUsed","key","errormsg","target","waitIdle","window","requestIdleCallback","loader","elements","index","newIndex","element","order","hasBlock","block","filter","e","dataBlock","dataPriorityBlock","promises","push","catch","all","then","loadBlock","loaderWithPromise","config","typeString","type","typeNum","flags","document","querySelectorAll","flag","arr","forEach","i","attr","undefined","priority","Number","dataPriority","initial","makeArr","connectionType","navigator","effectiveType","split","match","num","placeholder","addEventListener","loadIntersections","observables","Object","keys","map","observer","IntersectionObserver","entries","entry","isIntersecting","root","rootMargin","threshold","observe","queue","sort","b","makeQueue","withPriority","others"],"mappings":"AAEAA,IAAMC,WAAaC,EAAGC,UAChBD,EAAEC,GAAgBD,EAAEC,GAAQC,MACzB,MCQHC,WAAcC,EAAMC,6DAWlBC,EAAQC,EAAIF,EAAWJ,SAAWM,EAAIC,QAAUD,EAAIE,MAAQF,EAAIG,QAGhEC,EAAWC,EAAGC,WAAW,yBACxB,IAAIC,iBAASC,EAASC,GACvBL,GAAUI,IACVE,GAASC,WAAWH,EAASE,GACjCL,EAAGO,kBACDP,EAAGQ,MAAMC,WAAa,UACtBT,EAAGU,aAAa,kBAAkB,GAC9BC,GAAWX,EAAGU,aAAa,wBAAyBE,KAAKC,OACzDC,GAAUA,EAASd,GACvBG,KAEFH,EAAGe,iBAAWC,OAENC,EAAWtB,EAAIuB,MAAQ,6DACvBC,EAAQ,KACZH,EACAI,IAAK3B,EAAW2B,IAChBF,KAAMvB,EAAIuB,KACVG,SAAUJ,EACVK,IAAK9B,EAAK8B,KAERX,GAAWX,EAAGU,aAAa,wBAAyBE,KAAKC,OAC7Db,EAAGL,IAAMsB,EAGTb,EAAOe,IAELR,GAAWX,EAAGU,aAAa,uBAAwBE,KAAKC,OAC5Db,EAAGL,IAAMD,KC9CP6B,WAAYP,GACRG,oBACRA,gBACSH,qDAAsDA,8DAC1BA,+BAClBA,mBAEnBG,EAAMH,EAAIA,IAAIQ,SCXVC,oBAAiB,IAAIvB,iBAASC,GAClCuB,OAAOC,+BACLxB,KACC,CACDE,QAAS,SCYPuB,WAAgBC,EAAUpC,EAAYU,EAAS2B,kBAAQ,2CAwBrDL,qBACNG,EAAOC,EAAUpC,EAAYU,EAAS4B,SAxBhCA,EAAWD,EAAQ,EACnBE,EAAUH,EAASC,OAGpBE,SACH7B,4BAME6B,EAAQC,MACNC,mDACFA,OAEIC,EAAQN,EACXO,gBAAOC,UAAKA,EAAEJ,MAAMK,YAAcJ,GAAYG,EAAEJ,MAAMM,oBAAsBL,oCC5BhEC,EAAO1C,UAAe,IAAIS,iBAASC,WAC9CqC,EAAW,SAGCL,kBAChBK,EAASC,KAAKlD,OAAgBE,GAAYiD,MAAMvB,IAIlDjB,QAAQyC,IAAIH,GAAUI,uBAAWzC,QDoBzB0C,CAAUV,EAAO1C,8CAEjBF,EAAWyC,EAASvC,GAAYiD,MAAMvB,8GAS1C2B,WAAqBjB,EAAUpC,UAAe,IAAIS,iBAAQC,UAAWyB,EAAOC,EAAUpC,EAAYU,8BE5BvF4C,kBAAS,QCVpBC,EACA5B,EAHE6B,EACAC,EDaAC,EAAQC,SAASC,sBADVN,EAAOO,MAAQ,kBAGtBzB,WNXSA,EAAUkB,OACnBQ,EAAM,UACZ1B,EAAS2B,iBAASnB,EAAGoB,OACbC,EAAOrB,EAAEpC,WACfsD,EAAId,KAAK,CACPzC,GAAIqC,EACJf,IAAKmC,EACL9C,UAAWoC,EAAOpC,UAClBN,aAA4BsD,IAAnBZ,EAAO1C,QAAwB0C,EAAO1C,QAAU,IACzDuD,SAAUC,OAAO1E,EAAUuE,EAAM,kBAAoBvE,EAAUuE,EAAM,yBAA2B,KAChGzB,MAAO,CACL6B,aAAc3E,EAAUuE,EAAM,iBAC9BnB,kBAAmBpD,EAAUuE,EAAM,uBACnCpB,UAAWnD,EAAUuE,EAAM,eAE7B/D,IAAK,CACHoE,QAAS5E,EAAUuE,EAAM,OACzB5D,QAASX,EAAUuE,EAAM,YACzB9D,OAAQT,EAAUuE,EAAM,mBACxB7D,KAAMV,EAAUuE,EAAM,iBACtBxC,KAAM/B,EAAUuE,EAAM,kBAExB5C,SAAUiC,EAAOjC,UAAY,SAG1ByC,EMdUS,CAAQb,EAAOJ,GAC1BkB,GCjBAhB,EAAOiB,UAAUzE,WAAW0E,cAAcC,MAAM,KAAK,IACrDlB,EAAUW,OAAOZ,EAAKoB,MAAM,MAAM,KAG1B,GACZrB,EAAa,OACb5B,EAAM,iBACG8B,EAAU,GACnBF,EAAa,SACb5B,EAAM,oBAEN4B,EAAa,UACb5B,EAAM,YAED,CACLkD,IAAKpB,EACL7D,OAAQ2D,MACR5B,cCpBiBS,EAAUxC,kBAAS,8DACtCwC,EAAS2B,iBAAShE,GACRQ,WACHA,EAAGL,KAAkB,KAAXK,EAAGL,MAChBK,EAAGQ,MAAMC,WAAa,SACtBT,EAAGL,IAAMN,KFkBbkF,CAAY1C,EAAUkB,EAAO1D,QAK7BqC,OAAO8C,iBAAiB,kBAIlBzB,EAAO0B,4BGjCY5C,OACnB6C,EAAcC,OAAOC,KAAK/C,GAAUgD,aAAIxC,UAAKR,EAASQ,KAMtDyC,EAAW,IAAIC,8BAAsBC,GACzCA,EAAQxB,iBAASyB,MACXA,EAAMC,iBACSD,EAAMzD,OAAOvB,WAAW,kBAC1B,CACLuB,eACRA,EAAOhB,MAAMC,WAAa,UAC1Be,EAAO7B,IAAM6B,EAAOvB,WAAW,YAAYX,MAG3CkC,EAAOd,aAAa,kBAAkB,OAf9B,CACdyE,KAAM,KACNC,WAAY,MACZC,UAAW,KAkBbX,EAAYlB,iBAAShE,GACnBsF,EAASQ,QAAQ9F,KHSaiF,CAAkBtB,GAChDzB,OAAOC,uCAEC4D,WIjCO1D,SAMV,cALcA,EAClBO,gBAAOC,UAAKA,EAAEJ,MAAM6B,cAAgBzB,EAAEJ,MAAMM,oBAC5CiD,cAAMpG,EAAGqG,UAAMrG,EAAEwE,SAAW6B,EAAE7B,kBAClB/B,EACZO,gBAAOC,UAAMA,EAAEuB,YJ4BA8B,CAAU7D,0BAClBD,EAAO2D,EAAMI,aAAc1B,2CAC3BrC,EAAO2D,EAAMK,OAAQ3B,6DAC1B,CACD5D,QAAS"}