-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathccm.js
3857 lines (3087 loc) · 140 KB
/
ccm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @overview ccm framework
* @author André Kless <[email protected]> 2014-2018
* @license The MIT License (MIT)
* @version latest (16.1.0)
* @changes
* version 16.1.0 (14.04.2018):
* - ccm.helper.formData with integrated escaping of HTML characters
* - ccm.helper.fillForm with integrated unescaping of HTML characters
* version 16.0.1 (12.04.2018): bugfix in ccm.helper.dataset
* version 16.0.0 (08.04.2018): update service for ccm data management
* - uses ES6 syntax
* - no caching on higher data levels
* - datastore settings are not optional
* - code refactoring
* - working with datastores is always with callbacks (no return values)
* - only data dependencies are solved in results of store.get() calls
* - result of store.set() calls is the dataset key on success (not the hole dataset)
* - result of store.del() calls is TRUE on success (not the dataset)
* - datastore objects have a 'parent' property (they are now part of ccm contexts)
* - datastores are sending 'get', 'set', 'del' instead of 'key', 'dataset', 'del' to server interface
* - user token from the highest ccm user instance is automatically passed on data operations (get, set, del)
* - queries and datastore settings passed as parameters will be cloned
* - add ccm.helper.log(*) -> logs a ccm-specific message in the browser console
* - error messages from a data server and invalid dataset keys are logged in the browser console
* - add ccm.helper.isResourceDataObject(*):boolean -> checks if a value is an resource data object
* - add ccm.helper.isKey(*):boolean -> checks if a value is a valid ccm dataset key
* - character '-' is allowed in ccm dataset keys
* - add ccm.helper.isDatastoreSettings(*):boolean -> checks if the value is datastore settings
* - ccm.helper.onfinish uses highest ccm user instance
* - bugfix for HTML attribute readonly in ccm.helper.html
* - update help functions 'dataset', 'isDatastore' and 'isKey'
* - help functions 'fillForm' and 'formData' support any element with a name attribute (not just input fields)
* - support of Polymer v2 web components
* - declarative onfinish supports alert message
* - updated ccm.context.find()
* (for older version changes see ccm-15.0.2.js)
*/
( function () {
/**
* contains the already loaded resources
* @type {Object}
* @example
* // example of a cache containing two resources already loaded
* {
* 'https://akless.github.io/ccm/unit_tests/dummy/hello.html': "Hello, <b>World</b>!",
* 'https://akless.github.io/ccm/unit_tests/dummy/script.js': { foo: 'bar' }
* }
*/
let cache = {};
/**
* registered ccm component objects
* @type {Object.<ccm.types.index, ccm.types.component>}
*/
const components = {};
/**
* for creating ccm datastores
* @lends ccm.Datastore
* @constructor
*/
const Datastore = function () {
/**
* websocket communication callbacks
* @type {function[]}
*/
const callbacks = [];
/**
* own reference for inner functions
* @type {ccm.Datastore}
*/
const that = this;
/**
* privatized instance members
* @type {Object}
*/
let my;
/**
* is called once after for the initialization and is then deleted
* @param {function} callback - when datastore is initialized
*/
this.init = callback => {
// privatize security relevant members
my = self.helper.privatize( that, 'local', 'store', 'url', 'db', 'method', 'datasets' );
// prepare IndexedDB if necessary
my.store && !my.url ? prepareDB( proceed ) : proceed();
/**
* prepares the ccm database in the IndexedDB
* @param {function} callback
*/
function prepareDB( callback ) {
// open ccm database if necessary
!db ? openDB( proceed ) : proceed();
/**
* open ccm database
* @param {function} callback
*/
function openDB( callback ) {
/**
* the request for opening the ccm database
* @type {Object}
*/
const request = indexedDB.open( 'ccm' );
// set success callback
request.onsuccess = function () {
// set database object
db = this.result;
// perform callback
callback();
};
}
function proceed() {
// needed object store in IndexedDB not exists? => update ccm database
!db.objectStoreNames.contains( my.store ) ? updateDB( callback ) : callback();
/**
* updates the ccm database
* @param {function} callback
*/
function updateDB( callback ) {
/**
* current ccm database version number
* @type {number}
*/
let version = parseInt( localStorage.getItem( 'ccm' ) );
// no version number? => start with 1
if ( !version ) version = 1;
// close ccm database
db.close();
/**
* request for reopening ccm database
* @type {Object}
*/
const request = indexedDB.open( 'ccm', version + 1 );
// set callback for event when update is needed
request.onupgradeneeded = () => {
// set database object
db = this.result;
// save new ccm database version number in local storage
localStorage.setItem( 'ccm', db.version );
// create new object store
db.createObjectStore( my.store, { keyPath: 'key' } );
};
// set success callback => perform callback
request.onsuccess = callback;
}
}
}
function proceed() {
// is ccm realtime datastore?
my.url && my.url.indexOf( 'ws' ) === 0 ? prepareRealtime( callback ) : callback();
/**
* prepares the realtime functionality
* @param {function} callback
*/
function prepareRealtime( callback ) {
// prepare initial message
let message = [ my.db, my.store ];
if ( my.datasets )
message = message.concat( my.datasets );
// connect to server
my.socket = new WebSocket( my.url, 'ccm-cloud' );
// set server notification callback
my.socket.onmessage = message => {
// parse server message to JSON
message = JSON.parse( message.data );
// own request? => perform callback
if ( message.callback ) callbacks[ message.callback - 1 ]( message.data );
// notification about changed data from other client?
else {
// perform change callback
that.onchange && that.onchange( message.data );
}
};
// send initial message
my.socket.onopen = function () { this.send( message ); callback(); };
}
}
};
/**
* returns the source of the datastore
* @returns {Object} datastore source
*/
this.source = () => self.helper.filterProperties( my, 'url', 'db', 'store' );
/** clears the local cache */
this.clear = () => my.local = {};
/**
* requests one or more datasets
* @param {ccm.types.key|Object} [key_or_query={}] - unique key of the dataset or alternative a query (default: query all datasets)
* @param {function} [callback] - when operation is finished (result is passed as first parameter)
*/
this.get = ( key_or_query={}, callback ) => {
// first parameter is skippable
if ( typeof key_or_query === 'function' ) { callback = key_or_query; key_or_query = {}; }
// was a query passed? => clone query
if ( self.helper.isObject( key_or_query ) ) key_or_query = self.helper.clone( key_or_query );
// has an invalid key been passed? => abort and perform callback without a result
else if ( !self.helper.isKey( key_or_query ) && !( my.url && key_or_query === '{}' ) ) { self.helper.log( 'This value is not a valid dataset key:', key_or_query ); return null; }
// detect managed data level
my.url ? serverDB() : ( my.store ? clientDB() : localCache() );
/** requests dataset(s) from local cache */
function localCache() {
solveDependencies( self.helper.isObject( key_or_query ) ? runQuery( key_or_query ) : self.helper.clone( my.local[ key_or_query ] ), callback );
/**
* finds datasets in local cache by query
* @param {Object} query
* @returns {ccm.types.dataset[]}
*/
function runQuery( query ) {
/**
* found datasets
* @type {ccm.types.dataset[]}
*/
const results = [];
// find the datasets in the local cache that satisfy the query
for ( const key in my.local ) self.helper.isSubset( query, my.local[ key ] ) && results.push( self.helper.clone( my.local[ key ] ) );
return results;
}
}
/** requests dataset(s) from client-side database */
function clientDB() {
getStore().get( key_or_query ).onsuccess = event => solveDependencies( event.target.result, callback );
}
/** requests dataset(s) from server-side database */
function serverDB() {
( my.socket ? useWebsocket : useHttp )( prepareParams( { get: key_or_query } ), response => solveDependencies( response, callback ) );
}
/**
* solves all ccm data dependencies inside a value
* @param {*} value - value
* @param {function} callback - when all data dependencies are solved (first parameter is the object)
*/
function solveDependencies( value, callback ) {
// no array or object passed? => abort and perform callback with NULL
if ( !Array.isArray() && !self.helper.isObject() ) return callback( value );
/**
* unfinished asynchronous operations
* @type {number}
*/
let counter = 1;
recursive( value );
function recursive( arr_or_obj ) {
// iterate over object/array
for ( const i in arr_or_obj ) {
const value = arr_or_obj[ i ];
// is a data dependency? => solve it
if ( Array.isArray( value && value.length > 0 && value[ 0 ] === 'ccm.get' ) ) solveDependency( value, arr_or_obj, i );
// is an array or object? => search it recursively
else if ( Array.isArray( value ) || ( self.helper.isObject( value ) && !self.helper.isNode( value ) && !self.helper.isInstance( value ) ) ) recursive( value );
}
// check if all data dependencies are solved (in case none exist)
check();
}
/**
* solves a ccm data dependency
* @param {ccm.types.action} dependency - data dependency
* @param {Array|Object} arr_or_obj - array or object that contains the data dependency
* @param {string|number} i - index/key in the array/object containing the data dependency
*/
function solveDependency( dependency, arr_or_obj, i ) {
// start of a new asynchronous operation
counter++;
// solve dependency, search result recursively and check if all data dependencies are solved
self.get( dependency[ 1 ], dependency[ 2 ], result => { arr_or_obj[ i ] = result; recursive( result ); check(); } );
}
/** checks if all data dependencies are resolved and calls the callback, if so */
function check() {
!--counter && callback && callback( value );
}
}
};
/**
* creates or updates a dataset
* @param {Object} priodata - priority data
* @param {function} [callback]
*/
this.set = ( priodata, callback ) => {
// clone priority data
priodata = self.helper.clone( priodata );
// priority data has no key? => generate unique key
if ( !priodata.key ) priodata.key = self.helper.generateKey();
// priority data does not contain a valid key? => abort
if ( !self.helper.isKey( priodata.key ) ) return self.helper.log( 'This value is not a valid dataset key:', priodata.key );
// detect managed data level
my.url ? serverDB() : ( my.store ? clientDB() : localCache() );
/** creates/updates dataset in local cache */
function localCache() {
// does the dataset already exist? => update it
if ( my.local[ priodata.key ] ) self.helper.integrate( priodata, my.local[ priodata.key ] );
// dataset not exist? => create it
else my.local[ priodata.key ] = priodata;
// perform callback
callback && callback();
}
/** creates/updates dataset in client-side database */
function clientDB() {
getStore().put( priodata ).onsuccess = event => callback && callback( !!event.target.result );
}
/**
* creates/updates dataset in server-side database
* @returns {ccm.types.dataset} created or updated dataset
*/
function serverDB() {
( my.socket ? useWebsocket : useHttp )( prepareParams( { set: priodata } ), response => self.helper.isKey( response ) && callback && callback( response ) );
}
};
/**
* deletes a dataset
* @param {ccm.types.key} key - dataset key
* @param {function} [callback]
*/
this.del = ( key, callback ) => {
// invalid key? => abort
if ( !self.helper.isKey( key ) ) return self.helper.log( 'This value is not a valid dataset key:', key );
// detect managed data level
my.url ? serverDB() : ( my.store ? clientDB() : localCache() );
/** deletes dataset in local cache */
function localCache() {
delete my.local[ key ]; callback && callback( true );
}
/** deletes dataset in client-side database */
function clientDB() {
getStore().delete( key ).onsuccess = event => callback && callback( !!event.target.result );
}
/** deletes dataset in server-side database */
function serverDB() {
( my.socket ? useWebsocket : useHttp )( prepareParams( { del: key } ), response => response === true && callback && callback( response ) );
}
};
/**
* gets the object store from IndexedDB
* @returns {Object}
*/
function getStore() {
return db.transaction( [ my.store ], 'readwrite' ).objectStore( my.store );
}
/**
* prepares HTTP parameters
* @param {Object} [params] - individual HTTP parameters
* @returns {Object} complete HTTP parameters
*/
function prepareParams( params={} ) {
if ( my.db ) params.db = my.db;
params.store = my.store;
const user = self.context.find( that, 'user' );
if ( user && user.isLoggedIn() ) {
params.realm = user.getRealm();
params.token = user.data().token;
}
return params;
}
/**
* sends HTTP parameters to server interface via websocket connection
* @param {Object} params - HTTP parameters
* @param {function} callback - callback for server response
*/
function useWebsocket( params, callback ) {
callbacks.push( callback );
params.callback = callbacks.length;
my.socket.send( JSON.stringify( params ) );
}
/**
* sends HTTP parameters to server interface via HTTP request
* @param {Object} params - HTTP parameters
* @param {function} callback - callback for server response
*/
function useHttp( params, callback ) {
self.load( { url: my.url, params: params, method: my.method }, callback );
}
};
/**
* ccm database in IndexedDB
* @type {Object}
*/
let db;
/**
* @summary Contains the waiting lists of the resources being loaded.
* @description A wait list contains the ccm.load calls that will be run again after the resource is loaded.
* @type {Object.<string,ccm.types.action[]>}
* @example
* // example of a wait list for the resource "style.css" for which two ccm.load calls are waiting
* {
* 'https://akless.github.io/ccm/unit_tests/dummy/style.css': [
* [ ccm.load,
* 'https://akless.github.io/ccm/unit_tests/dummy/style.css',
* 'https://akless.github.io/ccm/unit_tests/dummy/hello.html'
* ],
* [ ccm.load,
* 'https://akless.github.io/ccm/unit_tests/dummy/script.js',
* 'https://akless.github.io/ccm/unit_tests/dummy/style.css'
* ]
* ]
* }
*/
const waiting_lists = {};
/*--------------------------------------------- public ccm namespaces ----------------------------------------------*/
// set global ccm namespace
if ( !window.ccm ) ccm = {
/**
* global namespaces for the registered ccm components
* @type {Object.<ccm.types.index, object>}
*/
components: {},
/**
* callbacks for cross domain data exchanges via ccm.load
* @type {Object.<string, function>}
*/
callbacks: {},
/**
* globally stored data of the JavaScript files downloaded via ccm.load
* @type {Object}
*/
files: {}
};
/**
* global ccm object of the framework
* @type {Object}
*/
const self = {
/**
* version number of the framework
* @returns {ccm.types.version}
*/
version: () => '16.1.0',
/** clears the cache of already loaded resources */
clear: () => { cache = {}; },
/**
* asynchronous loading of resources
* @see https://github.com/akless/ccm/wiki/Loading-of-Resources
* @param {...ccm.types.resource} resources - resources data
* @param {function} [callback] - when all resources are loaded (first parameter are the results)
* @returns {*} result(s) of the ccm.load call (only if no asynchronous operations were required)
*/
load: function () {
/**
* arguments of this ccm.load call
* @type {Array}
*/
const args = [ ...arguments ];
/**
* current ccm.load call
* @type {ccm.types.action}
*/
const call = args.slice( 0 ); call.unshift( self.load );
/**
* result(s) of this ccm.load call
* @type {*}
*/
let results = [];
/**
* indicates if this ccm.load call is waiting for resources being loaded
* @type {boolean}
*/
let waiting = false;
/**
* number of resources being loaded
* @type {number}
*/
let counter = 1;
/**
* when all resources have been loaded
* @type {function}
*/
const callback = typeof args[ args.length - 1 ] === 'function' ? args.pop() : null;
// iterate over resources data => load resource
args.map( ( resource, i ) => {
// increase number of resources being loaded
counter++;
// no manipulation of passed original parameters
resource = self.helper.clone( resource );
// resource data is an array? => load resources serially
if ( Array.isArray( resource ) ) { results[ i ] = []; serial( null ); return; }
// has resource URL instead of resource data? => use resource data which contains only the URL information
if ( !self.helper.isObject( resource ) ) resource = { url: resource };
/**
* file extension from the URL of the resource
* @type {string}
*/
const suffix = resource.url.split( '.' ).pop().toLowerCase();
// no given resource context or context is 'head'? => load resource in global <head> context (no Shadow DOM)
if ( !resource.context || resource.context === 'head' ) resource.context = document.head;
// given resource context is a ccm instance? => load resource in the shadow root context of that instance
if ( self.helper.isInstance( resource.context ) ) resource.context = resource.context.element.parentNode;
// determine the operation for loading the resource
const operation = getOperation();
// loading of CSS, but not in the global <head>? => ignore cache (to support loading the same CSS file in different contexts)
if ( operation === loadCSS && resource.context !== document.head ) resource.ignore_cache = true;
// by default, no caching for loading data
if ( operation === loadData && resource.ignore_cache === undefined ) resource.ignore_cache = true;
// avoid loading a resource twice
if ( caching() ) return;
// is the resource loaded for the first time? => mark the resource as "loading" in the cache
if ( cache[ resource.url ] === undefined ) cache[ resource.url ] = null;
// start loading of the resource
operation();
/**
* loads resources serially (recursive function)
* @param {*} result - result of the last serially loaded resource (is null on the first call)
*/
function serial( result ) {
// not the first call? => add result of the last call
if ( result !== null ) results[ i ].push( result );
// are there any more resources to load serially?
if ( resource.length > 0 ) {
/**
* next resource to be serially loaded
* @type {ccm.types.resource}
*/
const next = resource.shift();
// next resource is an array of resources? => load these resources in parallel (recursive call of ccm.load)
if ( Array.isArray( next ) ) { next.push( serial ); self.load.apply( null, next ); }
// normal resource data is given for the next resource => load the next resource serially (recursive call of ccm.load and this function)
else self.load( next, serial );
}
// serially loading of resources completed => check if all resources of this ccm.load call are loaded
else check();
}
/**
* determines the operation for loading the resource
* @returns {function}
*/
function getOperation() {
switch ( resource.type ) {
case 'css': return loadCSS;
case 'image': return loadImage;
case 'data': return loadData;
case 'js': return loadJS;
}
switch ( suffix ) {
case 'css':
return loadCSS;
case 'jpg':
case 'jpeg':
case 'gif':
case 'png':
case 'svg':
case 'bmp':
return loadImage;
case 'js':
return loadJS;
default:
return loadData;
}
}
/**
* avoids loading a resource twice
* @returns {boolean} resource does not need to be reloaded
*/
function caching() {
// no caching for this resource? => resource must be loaded (unless this ccm.load call is already waiting for a resource)
if ( resource.ignore_cache ) return waiting;
// is the resource already loading?
if ( cache[ resource.url ] === null ) {
// is this ccm.load call already waiting? => abort this ccm.load call (counter will not decrement)
if ( waiting ) return true;
// mark this ccm.load call as waiting
waiting = true;
// is there no waiting list for this resource yet? => create waitlist
if ( !waiting_lists[ resource.url ] ) waiting_lists[ resource.url ] = [];
// put this ccm.load call on the waiting list for this resource
waiting_lists[ resource.url ].push( call );
// abort this ccm.load call (counter will not decrement)
return true;
}
// has the resource already been loaded?
if ( cache[ resource.url ] !== undefined ) {
// the result is the already cached value for this resource
results[ i ] = cache[ resource.url ];
// resource does not need to be reloaded
success(); return true;
}
// resource must be loaded
return false;
}
/** loads (and executes) a CSS file */
function loadCSS() {
// load the CSS file via a <link> element
let element = { tag: 'link', rel: 'stylesheet', type: 'text/css', href: resource.url };
if ( resource.attr ) self.helper.integrate( resource.attr, element );
element = self.helper.html( element );
resource.context.appendChild( element );
element.onload = success;
}
/** (pre)loads an image file */
function loadImage() {
// (pre)load the image file via an image object
const image = new Image();
image.onload = success;
image.src = resource.url;
}
/** loads (and executes) a JavaScript file */
function loadJS() {
/**
* filename of the JavaScript file (without '.min')
* @type {string}
*/
const filename = resource.url.split( '/' ).pop().replace( '.min.', '.' );
// mark JavaScript file as loading
ccm.files[ filename ] = null;
// load the JavaScript file via a <script> element
let element = { tag: 'script', src: resource.url };
if ( resource.attr ) self.helper.integrate( resource.attr, element );
element = self.helper.html( element );
resource.context.appendChild( element );
element.onload = () => {
/**
* data globally stored by the loaded JavaScript file
* @type {*}
*/
const data = ccm.files[ filename ];
// remove the stored data from the global context
delete ccm.files[ filename ];
// perform success callback
if ( data !== null ) successData( data ); else success();
};
}
/** performs a data exchange */
function loadData() {
// no HTTP method set? => use 'GET'
if ( !resource.method ) resource.method = 'GET';
// should JSONP be used? => load data via JSONP, otherwise via AJAX request
if ( resource.method === 'JSONP' ) jsonp(); else ajax();
/** performs a data exchange via JSONP */
function jsonp() {
// prepare callback function
const callback = 'callback' + self.helper.generateKey();
if ( !resource.params ) resource.params = {};
resource.params.callback = 'ccm.callbacks.' + callback;
ccm.callbacks[ callback ] = data => {
resource.context.removeChild( element );
delete ccm.callbacks[ callback ];
successData( data );
};
// prepare the <script> element for data exchange
let element = { tag: 'script', src: buildURL( resource.url, resource.params ) };
if ( resource.attr ) self.helper.integrate( resource.attr, element );
element = self.helper.html( element );
element.src = element.src.replace( /&/g, '&' ); // TODO: Why is this "&" happening in ccm.helper.html?
// start the data exchange
resource.context.appendChild( element );
}
/** performs an AJAX request */
function ajax() {
const request = new XMLHttpRequest();
request.open( resource.method, resource.method === 'GET' ? buildURL( resource.url, resource.params ) : resource.url, true );
request.onreadystatechange = () => {
if ( request.readyState === 4 && request.status === 200 )
successData( self.helper.regex( 'json' ).test( request.responseText ) ? JSON.parse( request.responseText ) : request.responseText );
};
request.send( resource.method === 'POST' ? JSON.stringify( resource.params ) : undefined );
}
/**
* adds the parameters in the URL
* @param {string} url - URL
* @param {Object} data - HTTP parameters
* @returns {string} finished URL
*/
function buildURL( url, data ) {
return data ? url + '?' + params( data ).slice( 0, -1 ) : url;
function params( obj, prefix ) {
let result = '';
for ( const i in obj ) {
const key = prefix ? prefix + '[' + encodeURIComponent( i ) + ']' : encodeURIComponent( i );
if ( typeof( obj[ i ] ) === 'object' )
result += params( obj[ i ], key );
else
result += key + '=' + encodeURIComponent( obj[ i ] ) + '&';
}
return result;
}
}
}
/**
* when a data exchange has been completed successfully
* @param {*} data - received data
*/
function successData( data ) {
// received data is a JSON string? => parse it to JSON
if ( typeof data === 'string' && ( data.charAt( 0 ) === '[' || data.charAt( 0 ) === '{' ) ) data = JSON.parse( data );
// add received data to the results of the ccm.load call and to the cache
results[ i ] = cache[ resource.url ] = self.helper.protect( data );
// perform success callback
success();
}
/** when a resource is loaded successfully */
function success() {
// is there no result value yet? => use the URL as the result of the ccm.load call and the cache
if ( results[ i ] === undefined ) results[ i ] = cache[ resource.url ] = resource.url;
// is there a waiting list for the loaded resource? => perform waiting ccm.load calls
if ( waiting_lists[ resource.url ] )
while ( waiting_lists[ resource.url ].length > 0 )
self.helper.action( waiting_lists[ resource.url ].shift() );
// check if all resources are loaded
check();
}
} );
// check if all resources are loaded (important if all resources are already loaded)
return check();
/**
* checks if all resources are loaded
* @returns {*} result of this ccm.load call
*/
function check() {
// decrease number of resources being loaded
counter--;
// are all resources loaded now?
if ( counter === 0 ) {
// only one result? => do not use a field
if ( results.length <= 1 ) results = results[ 0 ];
// finish this ccm.load call
if ( callback ) callback( results );
return results;
}
}
},
/**
* @summary registers a <i>ccm</i> component in the <i>ccm</i> framework
* @memberOf ccm
* @param {ccm.types.component|ccm.types.url|ccm.types.index} component - object, URL or index of a <i>ccm</i> component
* @param {ccm.types.config} [config] - default <i>ccm</i> instance configuration (check documentation of associated <i>ccm</i> component to see which properties could be set)
* @param {function} [callback] - callback when <i>ccm</i> component is registered (first parameter is the object of the registered <i>ccm</i> component)
* @returns {boolean} true if component is registered
* @example ccm.component( { index: 'chat-2.1.3', Instance: function () { ... } } );
* @example ccm.component( { name: 'chat', version: [2,1,3], Instance: function () {...} } );
* @example ccm.component( { name: 'blank', Instance: function () {...} } );
* @example ccm.component( 'ccm.blank.js );
* @example ccm.component( 'http://akless.github.io/ccm-developer/resources/ccm.chat.min.js' );
*/
component: function ( component, config, callback ) {
// config parameter is a function? => config skipped
if ( typeof config === 'function' ) { callback = config; config = undefined; }
// component is given as string?
if ( typeof component === 'string' ) {
// given string is component index? => proceed with already registered component object
if ( component.indexOf( '.js' ) === -1 ) proceed( components[ component ] );
// given string is component URL
else {
/**
* @type {ccm.types.component_index}
*/
var index = self.helper.getIndex( component );
// is already registered component? => proceed with already registered component object
if ( components[ index ] ) return proceed( components[ index ] );
// not registered component => load component file and proceed with resulting component object
else self.load( component, proceed );
}
}
// component is given as object => proceed with given object
else proceed( component );
// check and return if component is already registered
return typeof component === 'string' && component.indexOf( '.js' ) === -1 && components[ component ] && true;