3 * @author Jacky Nguyen <jacky@sencha.com>
6 var emptyFn = function(){},
9 head
= doc
.head
|| doc
.getElementsByTagName('head')[0],
10 addWindowListener
= global
.addEventListener
,
11 removeWindowListener
= global
.removeEventListener
,
12 jsonParse
= JSON
.parse
,
13 a
= doc
.createElement('a'),
14 documentLocation
= doc
.location
,
15 documentUri
= documentLocation
.protocol
+ '//' + documentLocation
.hostname
+ documentLocation
.pathname
+ documentLocation
.search
,
16 manifestFile
= 'app.json',
18 activeManifest
, appCache
, storage
;
21 storage
= global
.localStorage
;
22 appCache
= global
.applicationCache
;
26 function getManifestStorageKey(id
) {
27 return id
+ '-' + documentUri
+ manifestFile
;
30 function Manifest(manifest
) {
33 if (typeof manifest
== 'string') {
34 manifestContent
= manifest
;
35 manifest
= jsonParse(manifestContent
);
38 manifestContent
= JSON
.stringify(manifest
);
41 var applicationId
= manifest
.id
,
42 key
= getManifestStorageKey(applicationId
),
45 function processAsset(asset
) {
48 if (typeof asset
== 'string') {
55 asset
.version
= asset
.shared
;
56 uri
= asset
.shared
+ asset
.path
;
59 uri
= toAbsoluteUri(asset
.path
);
63 asset
.key
= applicationId
+ '-' + uri
;
64 assetMap
[uri
] = asset
;
69 function processAssets(assets
, type
) {
70 var ln
= assets
.length
,
73 for (i
= 0; i
< ln
; i
++) {
76 assets
[i
] = asset
= processAsset(asset
);
80 asset
.collection
= assets
;
82 asset
.evaluated
= false;
89 this.css
= processAssets(manifest
.css
, 'css');
90 this.js
= processAssets(manifest
.js
, 'js');
92 Ext
.microloaded
= true;
94 var filterPlatform
= window
.Ext
.filterPlatform = function(platform
) {
95 var profileMatch
= false,
96 ua
= navigator
.userAgent
,
99 platform
= [].concat(platform
);
101 function isPhone(ua
) {
102 var isMobile
= /Mobile(\/|\s)/.test(ua
);
105 // - iOS but not iPad
107 // - Android with "Mobile" in the UA
109 return /(iPhone|iPod)/.test(ua
) ||
110 (!/(Silk)/.test(ua
) && (/(Android)/.test(ua
) && (/(Android 2)/.test(ua
) || isMobile
))) ||
111 (/(BlackBerry|BB)/.test(ua
) && isMobile
) ||
112 /(Windows Phone)/.test(ua
);
115 function isTablet(ua
) {
116 return !isPhone(ua
) && (/iPad/.test(ua
) || /Android|Silk/.test(ua
) || /(RIM Tablet OS)/.test(ua
) ||
117 (/MSIE 10/.test(ua
) && /; Touch/.test(ua
)));
120 // Check if the ?platform parameter is set in the URL
121 var paramsString
= window
.location
.search
.substr(1),
122 paramsArray
= paramsString
.split("&"),
126 for (i
= 0; i
< paramsArray
.length
; i
++) {
127 var tmpArray
= paramsArray
[i
].split("=");
128 params
[tmpArray
[0]] = tmpArray
[1];
131 testPlatform
= params
.platform
;
133 return platform
.indexOf(testPlatform
) != -1;
136 for (j
= 0, jln
= platform
.length
; j
< jln
; j
++) {
137 switch (platform
[j
]) {
139 profileMatch
= isPhone(ua
);
142 profileMatch
= isTablet(ua
);
145 profileMatch
= !isPhone(ua
) && !isTablet(ua
);
148 profileMatch
= /(iPad|iPhone|iPod)/.test(ua
);
151 profileMatch
= /(Android|Silk)/.test(ua
);
154 profileMatch
= /(BlackBerry|BB)/.test(ua
);
157 profileMatch
= /Safari/.test(ua
) && !(/(BlackBerry|BB)/.test(ua
));
160 profileMatch
= /Chrome/.test(ua
);
163 profileMatch
= /MSIE 10/.test(ua
);
166 profileMatch
= /MSIE 10/.test(ua
) || /Trident/.test(ua
);
169 profileMatch
= /Tizen/.test(ua
);
172 profileMatch
= /Firefox/.test(ua
);
181 this.css
= this.css
.filter(function(css
) {
182 var platform
= css
.platform
,
183 exclude
= css
.exclude
;
188 if (filterPlatform(platform
) && !filterPlatform(exclude
)) {
192 if(!Ext
.theme
.name
) {
193 Ext
.theme
.name
= css
.theme
|| 'Default';
203 this.js
= this.js
.filter(function(js
) {
204 var platform
= js
.platform
,
205 exclude
= js
.exclude
;
210 if (filterPlatform(platform
) && !filterPlatform(exclude
)) {
221 this.assets
= this.css
.concat(this.js
);
222 this.getAsset = function(uri
) {
223 return assetMap
[uri
];
225 this.store = function() {
226 store(key
, manifestContent
);
230 if (typeof global
.Ext
=== 'undefined') {
231 var Ext
= global
.Ext
= {};
234 function toAbsoluteUri(uri
) {
239 function addMeta(name
, content
) {
240 var meta
= document
.createElement('meta');
242 meta
.setAttribute('name', name
);
243 meta
.setAttribute('content', content
);
244 head
.appendChild(meta
);
247 function request(uri
, isShared
, onSuccess
, onFailure
) {
248 (isShared
? requestIframe
: requestXhr
)(uri
, onSuccess
, onFailure
);
251 function requestXhr(uri
, onSuccess
, onFailure
) {
252 var xhr
= new XMLHttpRequest();
254 onFailure
= onFailure
|| emptyFn
;
256 uri
= uri
+ ((uri
.indexOf('?') == -1) ? '?' : '&') + Date
.now();
259 xhr
.open('GET', uri
, true);
260 xhr
.onreadystatechange = function() {
261 if (xhr
.readyState
== 4) {
262 var status
= xhr
.status
,
263 content
= xhr
.responseText
;
265 if ((status
>= 200 && status
< 300) || status
== 304 || (status
== 0 && content
.length
> 0)) {
279 function requestIframe(uri
, onSuccess
) {
280 var iframe
= doc
.createElement('iframe');
287 iframe
.src
= uri
+ '.html';
288 iframe
.style
.cssText
= 'width:0;height:0;border:0;position:absolute;z-index:-999;visibility:hidden';
289 doc
.body
.appendChild(iframe
);
292 // for remote assets, inject a script element
293 function addRemoteScript(uri
, onSuccess
, onFailure
) {
294 var script
= document
.createElement('script');
296 script
.type
= "text/javascript";
297 script
.charset
= "UTF-8";
299 script
.onerror
= onFailure
;
301 if ('addEventListener' in script
) {
302 script
.onload
= onSuccess
;
303 } else if ('readyState' in script
) {
304 script
.onreadystatechange = function() {
305 if (this.readyState
=== 'loaded' ||
306 this.readyState
=== 'complete') {
311 script
.onload
= onSuccess
;
314 head
.appendChild(script
);
317 function addRemoteLink(uri
) {
318 var link
= document
.createElement('link');
319 link
.rel
= "stylesheet";
321 head
.appendChild(link
);
324 function requestAsset(asset
, onSuccess
, onFailure
) {
325 var isRemote
= !!asset
.remote
,
326 isShared
= !!asset
.shared
;
329 if(asset
.type
=== "js") {
330 addRemoteScript(asset
.uri
, function(){
334 addRemoteLink(asset
.uri
);
340 if (!isShared
&& asset
.version
&& asset
.version
.length
) {
341 var onRequestSuccess
= onSuccess
,
342 version
= asset
.version
,
343 versionLn
= version
.length
,
344 checksumFail
, checksumType
;
346 onSuccess = function(content
) {
347 checksumType
= content
.substring(0, 1);
348 if (checksumType
== '/') {
349 if (content
.substring(2, versionLn
+ 2) !== version
) {
353 else if (checksumType
== 'f') {
354 if (content
.substring(10, versionLn
+ 10) !== version
) {
358 else if (checksumType
== '.') {
359 if (content
.substring(1, versionLn
+ 1) !== version
) {
363 if (checksumFail
=== true) {
364 if (confirm("Requested: '" + asset
.uri
+ " seems to have been changed. Attempt to refresh the application?")) {
369 onRequestSuccess(content
);
373 request(asset
.uri
, isShared
, onSuccess
, onFailure
);
376 function onMessage(e
) {
378 sourceWindow
= e
.source
.window
,
379 i
, ln
, callback
, iframe
;
381 for (i
= 0, ln
= callbacks
.length
; i
< ln
; i
++) {
382 callback
= callbacks
[i
];
383 iframe
= callback
.iframe
;
385 if (iframe
.contentWindow
=== sourceWindow
) {
386 callback
.callback(data
);
387 doc
.body
.removeChild(iframe
);
388 callbacks
.splice(i
, 1);
394 function patch(content
, delta
) {
398 if (delta
.length
=== 0) {
402 for (i
= 0,ln
= delta
.length
; i
< ln
; i
++) {
405 if (typeof chunk
=== 'number') {
406 output
.push(content
.substring(chunk
, chunk
+ delta
[++i
]));
413 return output
.join('');
416 function log(message
) {
417 if (typeof console
!= 'undefined') {
418 (console
.error
|| console
.log
).call(console
, message
);
422 function store(key
, value
) {
424 storage
.setItem(key
, value
);
427 if (storage
&& e
.code
== e
.QUOTA_EXCEEDED_ERR
&& activeManifest
) {
428 log("LocalStorage Quota exceeded, cannot store " + key
+ " locally");
429 // Quota exceeded, clean up unused items
430 // var items = activeManifest.assets.map(function(asset) {
434 // ln = storage.length,
438 // items.push(activeManifest.key);
440 // while (i <= ln - 1) {
441 // item = storage.key(i);
443 // if (items.indexOf(item) == -1) {
444 // storage.removeItem(item);
453 // Done cleaning up, attempt to store the value again
454 // If there's still not enough space, no other choice
455 // but to skip this item from being stored
457 // store(key, value);
463 function retrieve(key
) {
465 return storage
.getItem(key
);
468 // Private browsing mode
473 function retrieveAsset(asset
) {
474 return retrieve(asset
.key
);
477 function storeAsset(asset
, content
) {
478 return store(asset
.key
, content
);
484 requestXhr(manifestFile
, function(content
) {
485 new Manifest(content
).store();
486 global
.location
.reload();
491 function blink(currentManifest
) {
492 var currentAssets
= currentManifest
.assets
,
493 assetsCount
= currentAssets
.length
,
496 activeManifest
= currentManifest
;
498 addWindowListener('message', onMessage
, false);
500 function onAssetReady(asset
, content
) {
501 var assets
= asset
.collection
,
507 asset
.content
= content
;
509 for (i
= index
- 1; i
>= 0; i
--) {
511 if (!asset
.filtered
&& (!asset
.ready
|| !asset
.evaluated
)) {
516 for (i
= index
; i
< ln
; i
++) {
519 if (!asset
.evaluated
) {
520 evaluateAsset(asset
);
529 function evaluateAsset(asset
) {
530 asset
.evaluated
= true;
532 if (asset
.type
== 'js') {
537 log("Error evaluating " + asset
.uri
+ " with message: " + e
);
541 var style
= doc
.createElement('style'),
544 style
.type
= 'text/css';
545 style
.textContent
= asset
.content
;
551 if ('disabled' in asset
) {
552 style
.disabled
= asset
.disabled
;
555 base
= document
.createElement('base');
556 base
.href
= asset
.path
.replace(/\/[^\/]*$/, '/');
557 head
.appendChild(base
);
558 head
.appendChild(style
);
559 head
.removeChild(base
);
562 delete asset
.content
;
564 if (--assetsCount
== 0) {
570 var updatingAssets
= [],
571 appCacheReady
= false,
572 onAppCacheIdle = function() {},
573 onAppCacheReady = function() {
574 appCache
.swapCache();
575 appCacheReady
= true;
580 removeWindowListener('message', onMessage
, false);
582 if (appCache
.status
== appCache
.UPDATEREADY
) {
585 else if (appCache
.status
== appCache
.CHECKING
|| appCache
.status
== appCache
.DOWNLOADING
) {
586 appCache
.onupdateready
= onAppCacheReady
;
587 appCache
.onnoupdate
= appCache
.onobsolete = function() {
592 function notifyUpdateIfAppCacheReady() {
598 function notifyUpdate() {
599 var updatedCallback
= Ext
.onUpdated
|| emptyFn
;
601 if ('onSetup' in Ext
) {
602 Ext
.onSetup(updatedCallback
);
609 function doUpdate() {
612 updatingAssets
.forEach(function(asset
) {
613 storeAsset(asset
, asset
.content
);
619 function onAssetUpdated(asset
, content
) {
620 asset
.content
= content
;
622 if (--updatingCount
== 0) {
623 if (appCache
.status
== appCache
.IDLE
) {
627 onAppCacheIdle
= doUpdate
;
632 function checkForUpdate() {
633 removeWindowListener('online', checkForUpdate
, false);
634 requestXhr(manifestFile
, function(manifestContent
) {
635 activeManifest
= newManifest
= new Manifest(manifestContent
);
637 var assets
= newManifest
.assets
,
640 assets
.forEach(function(asset
) {
641 currentAsset
= currentManifest
.getAsset(asset
.uri
);
643 if (!currentAsset
|| asset
.version
!== currentAsset
.version
) {
644 updatingAssets
.push(asset
);
648 updatingCount
= updatingAssets
.length
;
650 if (updatingCount
== 0) {
651 if (appCache
.status
== appCache
.IDLE
) {
652 notifyUpdateIfAppCacheReady();
655 onAppCacheIdle
= notifyUpdateIfAppCacheReady
;
660 updatingAssets
.forEach(function(asset
) {
661 var currentAsset
= currentManifest
.getAsset(asset
.uri
),
663 update
= asset
.update
;
665 function updateFull() {
666 requestAsset(asset
, function(content
) {
667 onAssetUpdated(asset
, content
);
671 // New asset (never used before)
672 // OR Shared from CDN
673 // OR Missing local storage
675 if (!currentAsset
|| !update
|| retrieveAsset(asset
) === null || update
!= 'delta') {
679 requestXhr('deltas/' + path
+ '/' + currentAsset
.version
+ '.json',
682 onAssetUpdated(asset
, patch(retrieveAsset(asset
), jsonParse(content
)));
685 log("Malformed delta content received for " + asset
.uri
);
695 if (navigator
.onLine
!== false) {
699 addWindowListener('online', checkForUpdate
, false);
703 if (assetsCount
== 0) {
708 currentAssets
.forEach(function(asset
) {
709 var content
= retrieveAsset(asset
);
711 if (content
=== null) {
712 requestAsset(asset
, function(content
) {
714 storeAsset(asset
, content
);
716 onAssetReady(asset
, content
);
718 onAssetReady(asset
, '');
722 onAssetReady(asset
, content
);
727 function blinkOnDomReady(manifest
) {
728 if (navigator
.userAgent
.match(/IEMobile\/10\.0/)) {
729 var msViewportStyle
= document
.createElement("style");
730 msViewportStyle
.appendChild(
731 document
.createTextNode(
732 "@media screen and (orientation: portrait) {" +
733 "@-ms-viewport {width: 320px !important;}" +
735 "@media screen and (orientation: landscape) {" +
736 "@-ms-viewport {width: 560px !important;}" +
740 document
.getElementsByTagName("head")[0].appendChild(msViewportStyle
);
743 var readyStateRe
= (/MSIE 10/.test(navigator
.userAgent
)) ? /complete|loaded/ : /interactive|complete|loaded/;
744 if (doc
.readyState
.match(readyStateRe
) !== null) {
748 addWindowListener('DOMContentLoaded', function() {
749 if (navigator
.standalone
) {
750 // When running from Home Screen, the splash screen will not disappear until all
751 // external resource requests finish.
752 // The first timeout clears the splash screen
753 // The second timeout allows inital HTML content to be displayed
754 setTimeout(function() {
755 setTimeout(function() {
761 setTimeout(function() {
769 Ext
.blink = function(manifest
) {
770 var manifestContent
= retrieve(getManifestStorageKey(manifest
.id
));
772 addMeta('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no');
773 addMeta('apple-mobile-web-app-capable', 'yes');
774 addMeta('apple-touch-fullscreen', 'yes');
776 if (manifestContent
) {
777 manifest
= new Manifest(manifestContent
);
778 blinkOnDomReady(manifest
);
781 requestXhr(manifestFile
, function(content
) {
782 manifest
= new Manifest(content
);
784 blinkOnDomReady(manifest
);