/* * Copyright (c) 2015-2016, Willem Cazander * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* jslint browser: true */ /* global angular,define,sqlitePlugin */ /** * FFSpaLoader is an assets loader for single page applications. * Its build around the concept the there is only a single static index.html which * synces all its assets to local cache for offline use and or fast page loads. * * @module FFSpaLoader */ (function (root, factory) { if ( typeof define === 'function' && define.amd ) { define('FFSpaLoader', factory(root)); } else if ( typeof exports === 'object' ) { module.exports = factory(root); } else { root.FFSpaLoader = factory(root); } })(this || window, /** @lends module:FFSpaLoader */ function (rootWindow) { 'use strict'; /** * The options to customize the loader. */ var options = { debug: { enable: false, handler: null, // auto filled prefix: 'FFSpaLoader.' }, error: { handler: null, // auto filled title: 'Loader ' }, boot: { cordova: { enable: true, timeout: -1, flag: 'FFCordovaDevice' }, angular: { enable: true, modules: [] }, cleanup: { enable: true, timeout: 1000, tags: [] } }, server: { url: null, assets: null, timeout: 4096, flag: 'FFServerUrl', header: { request: { 'X-FFSpaLoader': 'sync' }, response: { } } }, question: { transport: 'http://', title: 'Question', submit: 'Start', size: 32, text: 'Please provide the server name', validate: { min: { value: 3, message: 'Server name is to short.' }, max: { value: 255, message: 'Server name is to long.' }, regex: { value: '^([a-zA-Z0-9\.\:])*$', message: 'Server name is invalid.' } } }, loader: { title: 'Loading Application', footer: '© FFSpaLoader', await: 250, progres: { items: { enable: true, size: 50, }, bar: { enable: true, percentage: true, } }, }, cache: { meta: null, js: null, css: null, dss: null } }; /** * Use single instance for websql * @private */ var cacheDB = null; /** * The factory which contains detection helpers and cache backend builders. */ var factory = { detect: { localStorage: function() { try { var testData = 'localStorageDetect'; rootWindow.localStorage.setItem(testData, testData); // throws err in private browsing mode rootWindow.localStorage.removeItem(testData); return true; } catch(e) { return false; } }, openDatabase: function() { return 'openDatabase' in rootWindow; }, sqlitePlugin: function() { return 'sqlitePlugin' in rootWindow; }, cordova: function() { return 'cordova' in rootWindow; }, cordovaDevice: function() { return options.boot.cordova.flag in rootWindow; } }, cache: { localStorage: function() { return { cacheGetValue: function(key, cb) { try { var dataRaw = rootWindow.localStorage.getItem(key); var data = JSON.parse(dataRaw); cb(null, data); } catch(e) { cb(e); } }, cacheSetValue: function(key, value, cb) { try { rootWindow.localStorage.setItem(key,JSON.stringify(value)); cb(null); } catch(e) { cb(e); } }, cacheDeleteValue: function(key, cb) { try { rootWindow.localStorage.removeItem(key); cb(null); } catch(e) { cb(e); } } }; }, websql: function(opt) { if (opt === undefined) { opt = {}; } if (opt.name === undefined) { opt.name = 'FFSpaLoader'; } if (opt.size === undefined) { opt.size = 4 * 1024 * 1024; } // reg 4MB let user do higher if (opt.version === undefined) { opt.version = '1.0'; } if (opt.table === undefined) { opt.table = 'cache_store'; } if (opt.open === undefined) { opt.open = function(dbOpt) { return rootWindow.openDatabase(dbOpt.name, dbOpt.version, dbOpt.name, dbOpt.size); }; } var nullDataHandler = function(cb) { return function () { cb(null); }; }; var executeSql = function(tx, query, values, resultHandler, errorHandler) { utilDebug('websql.executeSql '+query); tx.executeSql(query, values, resultHandler, function(tx,err) { errorHandler(new Error('Code: '+err.code+' '+err.message+' by query: '+query)); }); }; var cacheDBInit = function(cb) { cacheDB.transaction(function(tx) { var query = 'CREATE TABLE '+opt.table+'(id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, value TEXT NOT NULL)'; executeSql(tx, query, [], function(tx) { executeSql(tx, 'CREATE UNIQUE INDEX '+opt.table+'__key__udx ON '+opt.table+' (key)', [], nullDataHandler(cb), cb); }, cb); }); }; return { cacheOpen: function(cb) { if (cacheDB !== null) { return cb(null); // open once. } utilDebug('websql.cacheOpen '+JSON.stringify(opt)); try { cacheDB = opt.open(opt); } catch(e) { return cb(e); } cacheDB.transaction(function(tx) { executeSql(tx,'SELECT value FROM '+opt.table+' WHERE key = \"if-err-init\"', [], function() { cb(null); }, function() { cacheDBInit(cb); }); }); }, cacheGetValue: function(key, cb) { cacheDB.transaction(function(tx) { executeSql(tx, 'SELECT value FROM '+opt.table+' WHERE key = ?',[key], function(tx, res) { if (res.rows.length === 0) { cb(null, null); } else { var value = res.rows.item(0).value; cb(null, JSON.parse(value)); } }, cb); }); }, cacheSetValue: function(key, value, cb) { cacheDB.transaction(function(tx) { executeSql(tx, 'SELECT value FROM '+opt.table+' WHERE key = ?',[key], function(tx, res) { if (res.rows.length === 0) { var queryInsert = 'INSERT INTO '+opt.table+' (key,value) VALUES (?,?)'; executeSql(tx, queryInsert, [key,JSON.stringify(value)], nullDataHandler(cb), cb); } else { var queryUpdate = 'UPDATE '+opt.table+' SET value = ? WHERE key = ?'; executeSql(tx, queryUpdate, [JSON.stringify(value),key], nullDataHandler(cb), cb); } }, cb); }); }, cacheDeleteValue: function(key, cb) { cacheDB.transaction(function(tx) { executeSql(tx, 'DELETE FROM '+opt.table+' WHERE key = ?', [key], nullDataHandler(cb), cb); }); } }; }, } }; /** * Prints the debug message with prefix to the options.debug.handler if options.debug.enable is true. * @param {String} message The message to log. * @private */ var utilDebug = function (message) { if (options.debug.enable !== true) { return; } options.debug.handler(options.debug.prefix+message); }; /** * The default error handler which renders the error in the browser. * @param {Error|String} err The error object or message. * @private */ var utilErrorHandler = function(err) { if (!(err instanceof Error)) { err = new Error(err); } utilDebug('utilErrorHandler error '+err.name+' '+err.message); var rootTag = document.createElement('div'); rootTag.setAttribute('class','ffWrapper'); document.getElementsByTagName('body')[0].appendChild(rootTag); var titleTag = document.createElement('div'); titleTag.setAttribute('class','ffTitle'); titleTag.appendChild(document.createTextNode(options.error.title+err.name)); rootTag.appendChild(titleTag); var dialogTag = document.createElement('div'); dialogTag.setAttribute('class','ffError'); rootTag.appendChild(dialogTag); var questionTag = document.createElement('p'); questionTag.appendChild(document.createTextNode(err.message)); dialogTag.appendChild(questionTag); try { var stack = err.stack || ''; stack = stack.split('\n').map(function (line) { return line.trim()+'\n'; }); var stackText = stack.splice(stack[0] === 'Error' ? 2 : 1); var traceTag = document.createElement('pre'); traceTag.appendChild(document.createTextNode(stackText)); dialogTag.appendChild(traceTag); } catch (stackError) { utilDebug('No stack: '+stackError); } }; /** * Fetches an url resource with a XMLHttpRequest. * @param {String} url The url to fetch. * @param {function} cb Error first callback when done. * @private */ var utilHttpFetch = function (url, cb, onlyJson) { var startTime = new Date().getTime(); var httpRequest = new XMLHttpRequest(); httpRequest.onreadystatechange = function() { if (httpRequest.readyState === 4 && httpRequest.status === 200) { utilDebug('utilHttpFetch url \"'+url+'\" done in '+(new Date().getTime()-startTime)+' ms.'); var headerResponseKeys = Object.keys(options.server.header.response); for (var headerResponseKeyIdx in headerResponseKeys) { var headerResponseKey = headerResponseKeys[headerResponseKeyIdx]; var headerResponseValue = options.server.header.response[headerResponseKey]; var value = httpRequest.getResponseHeader(headerResponseKey); if (value === null) { return cb('Header missing: '+headerResponseKey); } if (headerResponseValue === null) { continue; } if (headerResponseValue !== value) { return cb('Header mismatch: '+headerResponseKey); } } cb(null, httpRequest); } else if (httpRequest.readyState === 4) { cb('Wrong status '+httpRequest.status+' of '+url); } }; httpRequest.open('GET', url, true); httpRequest.timeout = options.server.timeout; // ieX: after open() httpRequest.ontimeout = function() { cb('timeout after '+options.server.timeout+' of url '+url); }; var headerKeys = Object.keys(options.server.header.request); for (var headerKeyIdx in headerKeys) { var headerKey = headerKeys[headerKeyIdx]; var headerValue = options.server.header.request[headerKey]; httpRequest.setRequestHeader(headerKey,headerValue); } if (onlyJson === true) { httpRequest.setRequestHeader('Accept','application/json'); } httpRequest.send(); }; /** * Async helper to run each step from the stack. * @param {String} runType The runType used for debug logging. * @param {Array} stack The stack to run all steps on. * @param {function} step Stack Item + Error first callback when done function callback. * @param {function} cb Error first callback when done. * @private */ var utilRunStack = function(runType, stack, step, cb) { if (stack.length === 0) { return cb(null); } utilDebug(runType+' start'); var startTime = new Date().getTime(); var runStack = stack; var runStackStep = function(err) { if (err !== null) { return cb(err); } runStack = runStack.slice(1); if (runStack.length === 0) { utilDebug(runType+' done '+stack.length+' in '+(new Date().getTime()-startTime)+' ms.'); cb(null); } else { step(runStack[0],runStackStep); } }; step(runStack[0],runStackStep); }; var cacheGetService = function (type) { if (options.cache[type]) { return options.cache[type]; } return null; }; var cacheHasService = function (type) { return cacheGetService(type) !== null; }; var cacheCheckType = function (type, cb, action) { cacheHasService(type)?action():cb(new Error('No caching for '+type)); }; /** * Retreive a cached value. * @param {String} type The cache type to use. * @param {String} key The key to get the value for. * @param {function} cb Error first callback + value callback. * @private */ var cacheGetValue = function(type, key , cb) { cacheCheckType(type, cb, function() { var cacheKey = type+'_'+key; utilDebug('cacheGetValue key '+cacheKey); cacheGetService(type).cacheGetValue(cacheKey,cb); }); }; /** * Store a value in cache. * @param {String} type The cache type to use. * @param {String} key The key to store the value for. * @param {String} value The value to store. * @param {function} cb Error first callback. * @private */ var cacheSetValue = function(type, key, value, cb) { cacheCheckType(type, cb, function() { var cacheKey = type+'_'+key; utilDebug('cacheSetValue key '+cacheKey); cacheGetService(type).cacheSetValue(cacheKey,value,cb); }); }; /** * Deletes an cache value. * @param {String} type The cache type to use. * @param {String} key The key to delete the value. * @param {function} cb Error first callback. * @private */ var cacheDeleteValue = function(type, key, cb) { cacheCheckType(type, cb, function() { var cacheKey = type+'_'+key; utilDebug('cacheDeleteValue key '+cacheKey); cacheGetService(type).cacheDeleteValue(cacheKey,cb); }); }; var cleanupCache = function (resources, deleteAll, cb) { var typedKeys = {}; resources.forEach(function (r) { var typeKey = typedKeys[r.type]; if (typeKey === undefined) { typeKey = []; typedKeys[r.type] = typeKey; } typeKey.push(r.hash); }); var steps = []; var keys = Object.keys(typedKeys); for (var keyIdx in keys) { var type = keys[keyIdx]; if (!cacheHasService(type)) { continue; } steps.push({keys: typedKeys[type], type: type}); } utilRunStack('cleanupCacheKeys',steps, function(typeKey, cb) { cacheGetValue(typeKey.type,'keys',function (err,value) { if (err !== null) { return cb(err); } if (value === null) { return cb(null); // meta has no keys } var diff = []; for (var i=0;i shortUrlSize) { shortUrl = '...'+shortUrl.substring(shortUrl.length-shortUrlSize,shortUrl.length); } var loaderItemResourceTag = document.createElement('div'); loaderItemResourceTag.setAttribute('id',resource.hash); loaderItemResourceTag.setAttribute('class','ffLoaderItemText'); loaderItemResourceTag.appendChild(document.createTextNode(shortUrl)); loaderItemTag.appendChild(loaderItemResourceTag); }); } if (options.loader.progres.bar.enable) { var loaderBarTag = document.createElement('div'); loaderBarTag.setAttribute('class','ffLoaderBar'); rootTag.appendChild(loaderBarTag); for (var i=1;i<=10;i++) { var loaderBarStepTag = document.createElement('div'); loaderBarStepTag.setAttribute('id','barStep'+(i*10)); loaderBarStepTag.setAttribute('class','ffLoaderBarStep'); if (options.loader.progres.bar.percentage) { loaderBarStepTag.appendChild(document.createTextNode(i*10)); } loaderBarTag.appendChild(loaderBarStepTag); } } var footerTag = document.createElement('div'); footerTag.setAttribute('class','ffFooter'); footerTag.appendChild(document.createTextNode(options.loader.footer)); rootTag.appendChild(footerTag); document.getElementsByTagName('body')[0].appendChild(rootTag); }, options.loader.await); return { nextResource: function(resource) { var resourceTag = document.getElementById(resource.hash); if (resourceTag !== null) { resourceTag.setAttribute('class','ffLoaderItemTextStart'); } if (prevResource !== null) { var prevResourceTag = document.getElementById(prevResource.hash); if (prevResourceTag !== null) { prevResourceTag.setAttribute('class','ffLoaderItemTextDone'); } } prevResource = resource; step++; stepProgres = Math.round(step*100/stepMax); if (stepProgres > stepProgres10) { stepProgres10 += 10; for (var i=1;i<=10;i++) { if ((i*10) > stepProgres10) { continue; } var barStepTag = document.getElementById('barStep'+(i*10)); if (barStepTag !== null) { barStepTag.setAttribute('class','ffLoaderBarStepDone'); } } } }, done: function() { clearTimeout(createUITimeout); if (rootTag !== null) { rootTag.setAttribute('class','ffWrapper ffWrapperFadeOut'); setTimeout ( function () { document.getElementsByTagName('body')[0].removeChild(rootTag); }, 500); } } }; }; /** * Starts loader by downloading resource list or using cache version to * load/refresh/inject the resources. * * @param {function} cb Error first callback when done. * @private */ var startLoader = function (cb) { if (options.server.url === null) { cb(new Error('No url')); return; } if (options.server.assets === null) { cb(new Error('No assets')); return; } rootWindow[options.server.flag] = options.server.url; var resourcesUrl = options.server.url + options.server.assets; utilDebug('startLoader assets \"'+resourcesUrl+'\"'); utilHttpFetch(resourcesUrl, function(err, httpRequest) { if (err !== null) { utilDebug('startLoader fetch error '+err); if (cacheHasService('meta')) { cacheGetValue('meta','server_resources',function(err, value) { if (err !== null) { cb(err); } else if (value === null) { cb(new Error('Have no cache of server resouces from '+options.server.url)); } else { utilRunStack('injectResources', value, function(resource, cb) { cacheGetValue(resource.type,resource.hash,function(err,item) { injectResourceData(resource,item.data,cb); }); } , cb); } }); } else { cb(new Error('Could not fetch server resouces from '+options.server.url)); } return; } var resources = null; try { var responseObject = JSON.parse(httpRequest.responseText); if (responseObject.data === undefined) { return cb('No data in json'); } if (responseObject.data.resources === undefined) { return cb('No resources in json'); } if (responseObject.data.resources.length === 0) { return cb('Empty resources in json'); } resources = responseObject.data.resources; } catch (parseError) { return cb(parseError); } utilDebug('startLoader resources '+resources.length); var progressBar = createLoaderBar(resources); var loadResourceStep = function (resource, cb) { loadResource(resource,cb); progressBar.nextResource(resource); }; if (cacheHasService('meta')) { cacheSetValue('meta','server_resources',resources, function (err) { if (err !== null) { return cb(err); } utilRunStack('loadResources', resources, loadResourceStep , function (err) { progressBar.done(); if (err === null) { cleanupCache(resources,false,cb); // only clean when fetched + cached } else { cb(err); } }); }); } else { utilRunStack('loadResources', resources, loadResourceStep , function (err) { progressBar.done(); cb(err); // done or error }); } }, true); }; /** * Validates the user input url by downloading it. * * @param {HTMLElement} deleteTag The dom element to remove from the body tag after success. * @param {function} cb Error first callback when done. * @private */ var askUrlValidate = function (deleteTag, cb) { var inputTag = document.getElementById('serverInput'); var inputErrorTag = document.getElementById('serverInputError'); var inputValueRaw = inputTag.value; while (inputErrorTag.firstChild) { inputErrorTag.removeChild(inputErrorTag.firstChild); } var inputValueHost = null; if (inputValueRaw.indexOf("://") >= 0) { inputValueHost = inputValueRaw.split('/')[2]; } else { inputValueHost = inputValueRaw.split('/')[0]; } if (options.question.validate.min.value !== false && inputValueHost.length < options.question.validate.min.value) { inputErrorTag.appendChild(document.createTextNode(options.question.validate.min.message)); return; } if (options.question.validate.max.value !== false && inputValueHost.length > options.question.validate.max.value) { inputErrorTag.appendChild(document.createTextNode(options.question.validate.max.message)); return; } if (options.question.validate.regex.value !== false && options.question.validate.regex.value.length !== 0) { var regex = new RegExp(options.question.validate.regex.value); if (inputValueHost.match(regex) === null) { inputErrorTag.appendChild(document.createTextNode(options.question.validate.regex.message)); return; } } if (inputValueRaw.indexOf('https') === 0) { options.server.url = 'https://' + inputValueHost; // allow user to upgrade to https but not to downgrade to http } else { options.server.url = options.question.transport + inputValueHost; } var resourcesUrl = options.server.url + options.server.assets; utilDebug('askUrlStart check assets '+resourcesUrl); utilHttpFetch(resourcesUrl,function(err, httpRequest) { if (err !== null) { inputErrorTag.appendChild(document.createTextNode('Error '+err)); return; } if (httpRequest.responseText.length === 0) { inputErrorTag.appendChild(document.createTextNode('Error Got empty data.')); return; } deleteTag.setAttribute('class','ffWrapper ffWrapperFadeOut'); var clearUi = function(err) { document.getElementsByTagName('body')[0].removeChild(deleteTag); // also delete on error cb(err); }; if (cacheHasService('meta')) { cacheSetValue('meta','server_url',options.server.url, function(err) { if (err !== null) { return cb(err); } startLoader(clearUi); }); } else { startLoader(clearUi); } }, true); }; /** * Creates the ask url ui. * * @param {function} cb Callback gets called when loader is done. * @private */ var askUrl = function (cb) { utilDebug('askUrl create ui'); var rootTag = document.createElement('div'); rootTag.setAttribute('class','ffWrapper'); var formTag = document.createElement('div'); formTag.setAttribute('class','ffQuestion'); rootTag.appendChild(formTag); var titleTag = document.createElement('div'); titleTag.setAttribute('class','ffQuestionTitle'); titleTag.appendChild(document.createTextNode(options.question.title)); formTag.appendChild(titleTag); var questionTag = document.createElement('div'); questionTag.setAttribute('class','ffQuestionText'); questionTag.appendChild(document.createTextNode(options.question.text)); formTag.appendChild(questionTag); var inputTag = document.createElement('input'); inputTag.setAttribute('type','text'); inputTag.setAttribute('id','serverInput'); inputTag.setAttribute('autofocus',''); inputTag.setAttribute('onkeydown','if (event.keyCode == 13) {document.getElementById(\'serverSubmit\').click()}'); inputTag.setAttribute('size',options.question.size); formTag.appendChild(inputTag); var submitTag = document.createElement('input'); submitTag.setAttribute('id','serverSubmit'); submitTag.setAttribute('type','submit'); submitTag.setAttribute('value',options.question.submit); submitTag.onclick = function() {askUrlValidate(rootTag,cb);}; formTag.appendChild(submitTag); var serverErrorTag = document.createElement('div'); serverErrorTag.setAttribute('id','serverInputError'); serverErrorTag.setAttribute('class','ffQuestionError'); formTag.appendChild(serverErrorTag); var footerTag = document.createElement('div'); footerTag.setAttribute('class','ffFooter'); footerTag.appendChild(document.createTextNode(options.loader.footer)); rootTag.appendChild(footerTag); document.getElementsByTagName('body')[0].appendChild(rootTag); }; /** * Starts an cache type by auto selecting if needed and opening it if needed. * * @param {String} type The cache type to start. * @param {function} cb Callback gets called when loader is done. * @private */ var startCacheType = function (type,cb) { if (options.cache[type] !== null && options.cache[type] === false) { utilDebug('startCacheType '+type+' disabled'); options.cache[type] = null; return cb(null); } if (options.cache[type] === null) { if (factory.detect.cordovaDevice() && factory.detect.sqlitePlugin()) { utilDebug('startCacheType auto sqlitePlugin for '+type); options.cache[type] = factory.cache.websql({open: function(dbOpt) { return sqlitePlugin.openDatabase(dbOpt.name, dbOpt.version, dbOpt.name, dbOpt.size);}}); } else if (factory.detect.openDatabase()) { utilDebug('startCacheType auto openDatabase for '+type); options.cache[type] = factory.cache.websql(); } else if (factory.detect.localStorage()) { utilDebug('startCacheType auto localStorage for '+type); options.cache[type] = factory.cache.localStorage(); } else { utilDebug('startCacheType '+type+' none'); } } if (options.cache[type] !== null && typeof options.cache[type].cacheOpen === 'function') { options.cache[type].cacheOpen(cb); } else { cb(null); } }; /** * Starts all cache types. * * @param {function} cb Callback gets called when loader is done. * @private */ var startCache = function (cb) { // FIXME: use dynamic loop for user defined types. startCacheType('meta', function(err) { if (err !== null) { return cb(err); } startCacheType('js', function(err) { if (err !== null) { return cb(err); } startCacheType('css', function(err) { if (err !== null) { return cb(err); } startCacheType('dss', cb); }); }); }); }; /** * Starts the loader. * * @param {function} cb Optional callback gets called when loader is done. */ var start = function (cbArgu) { var startTime = new Date().getTime(); var cb = function(err) { if (err !== null) { options.error.handler(err); } else { utilDebug('start done in '+(new Date().getTime()-startTime)+' ms.'); // last debug line TODO: move bootAngular to onjsloaded bootCleanup(); // move after ang. bootAngular(function(err) { if (err !== null) { return options.error.handler(err); } if (typeof cbArgu === 'function') { cbArgu(); } }); } }; utilDebug('start spa loader'); // first debug line TODO: Add version if (options.debug.enable === true) { var optionsKeys = Object.keys(options); for (var keyId in optionsKeys) { var key = optionsKeys[keyId]; utilDebug('start config '+key+' '+JSON.stringify(options[key])); } } bootCordova(function(err) { if (err !== null) { return cb(err); } startCache(function(err) { if (err !== null) { return cb(err); } if (options.server.url !== null) { startLoader(cb); return; } if (cacheHasService('meta')) { cacheGetValue('meta','server_url',function(err, value) { if (err !== null) { cb(err); } else if (value === undefined || value === null || value === '') { askUrl(cb); } else { options.server.url = value; startLoader(cb); } }); } else { askUrl(cb); } }); }); }; /** * Clears the server url. * * @param {function} cb Optional Error first callback gets called when value is deleted. */ var clearServerUrl = function(cb) { if (cb === undefined) { cb = function() {}; } if (cacheHasService('meta')) { cacheDeleteValue('meta','server_url',function(err) { if (err !== null) { return cb(err); } setTimeout(function() {cb(null);}); // return next tick so (websql) transaction is flushed before location.reload }); } else { cb(null); } }; /** * Clears the cache. * * @param {function} cb Optional Error first callback gets called when cache is cleared. */ var clearCache = function(cb2) { if (cb2 === undefined) { cb2 = function() {}; } var cb = function(err) { setTimeout(function() {cb2(err);}); // next tick to flush transactions. }; if (cacheHasService('meta')) { cacheGetValue('meta','server_resources',function(err, value) { if (err !== null) { return cb(err); } if (value === null) { return cb(new Error('No cache value')); // TODO: check this or cb(null) or do fetch ? } cleanupCache(value,true,cb); }); } else { utilHttpFetch(options.server.url + options.server.assets,function(err, httpRequest) { if (err !== null) { return cb(err); } var resources = JSON.parse(httpRequest.responseText).data.resources; cleanupCache(resources,false,cb); }, true); // TODO: create resourcesListFetch(); } }; /** * Boot cleanup removed the html tags by id after the timeout. * * note: sync function * * @private */ var bootCleanup = function() { if (options.boot.cleanup.enable !== true) { utilDebug('bootCleanup disabled by options'); return; } if (options.boot.cleanup.tags.length === 0) { utilDebug('bootCleanup disabled by no tags ids.'); return; } utilDebug('bootCleanup after '+options.boot.cleanup.timeout+' ms.'); setTimeout ( function () { utilDebug('bootCleanup run'); for (var tagIdx in options.boot.cleanup.tags) { var tagId = options.boot.cleanup.tags[tagIdx]; var tag = document.getElementById(tagId); if (tag !== null) { tag.parentNode.removeChild(tag); utilDebug('bootCleanup removed '+tagId); } } }, options.boot.cleanup.timeout); }; /** * Boots angular modules if enabled. * * @param {function} cb Error first callback gets called done. * @private */ var bootAngular = function(cb) { if (options.boot.angular.enable !== true) { utilDebug('bootAngular disabled by options'); return cb(null); } if (options.boot.angular.modules.length === 0) { utilDebug('bootAngular disabled by no modules'); return cb(null); } utilDebug('bootAngular start '+options.boot.angular.modules); angular.bootstrap(document, options.boot.angular.modules); cb(null); }; /** * Boots cordova applications which want to use the sqllite plugin as cache. * Note: On none cordova page it will callback directly. * Note: On none cordova device page is will timeout if option is set. * * @param {function} cb Error first callback gets called device is ready. * @private */ var bootCordova = function(cb) { if (options.boot.cordova.enable !== true) { utilDebug('bootCordova disabled by options'); return cb(null); } if (factory.detect.cordova() !== true) { utilDebug('bootCordova disabled by detect'); return cb(null); } var startTime = new Date().getTime(); var bootOnce = function() { var callback = cb; cb = null; utilDebug('bootCordova done in '+(new Date().getTime()-startTime)+' ms.'); callback(null); }; if (options.boot.cordova.timeout > 0) { utilDebug('bootCordova timeout '+options.boot.cordova.timeout); setTimeout ( function () { utilDebug('bootCordova timeout'); bootOnce(); }, options.boot.cordova.timeout); } document.addEventListener('deviceready', function () { rootWindow[options.boot.cordova.flag] = true; utilDebug('bootCordova '+options.boot.cordova.flag); bootOnce(); }, false); }; // Auto fill handlers and return public object. options.debug.handler = function(msg) {console.log(msg);}; options.error.handler = utilErrorHandler; return { options: options, factory: factory, start: start, clearServerUrl: clearServerUrl, clearCache: clearCache }; });