-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathccm.js
3239 lines (2658 loc) · 125 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
* Core script of _ccmjs_ that is automatically loaded as soon as a component is integrated into a webpage.
* The core script sets an object in the namespace [window.ccm]{@link ccm} that represents the loaded _ccmjs_ version
* and defines the Custom Element <code>\<ccm-app\></code>.
* @author André Kless <[email protected]> (https://github.com/akless)
* @license The MIT License (MIT)
* @version 27.5.0
* @domain https://ccmjs.github.io/ccm/
* @changes
* version 27.5.0 (23.03.2023)
* - added third parameter: ccm.start(component,config,element)
* version 27.4.2 (04.11.2022)
* - bugfix in ccm.helper.html2json: Preserve whitespaces and line breaks in pre-tags.
* version 27.4.1 (20.10.2022)
* - bugfix in ccm.helper.html for checked checkboxes
* version 27.4.0 (17.06.2022)
* - ccm.helper.generateKey returns a Universally Unique Identifier (UUID)
* - better error handling when using IndexedDB
* version 27.3.1 (14.02.2022)
* - store.set() and store.del() returns original operation result
* version 27.3.0 (14.02.2022)
* - ccm.helper.html() accepts a instance reference and returns it as result
* version 27.2.0 (17.01.2022)
* - ccm.helper.isSubset() can check if a property not exists with value 'null'
* version 27.1.2 (27.12.2021)
* - highestByProperty() and nearestByProperty() returns null if there is no start instance
* version 27.1.1 (28.09.2021)
* - an instance created with ccm.start() is ready AFTER instance.start() is finished
* version 27.1.0 (27.09.2021)
* - added attribute 'ccm' for <ccm-app> to define used version of ccmjs (<ccm-app ccm="27.1.0" component="..." src="...">)
* version 27.0.0 (24.09.2021)
* - a source configuration is stored at property 'src' instead of 'key'
* - an instance configuration can have recursive source configurations
* (for older version changes see ccm-26.4.4.js)
*/
( () => {
/**
* Contains the registered components within this _ccmjs_ version.
* @memberOf ccm
* @private
* @type {Object.<ccm.types.component_index, ccm.types.component_obj>}
*/
const _components = {};
/**
* for creating ccm datastores
* @private
* @constructor
*/
const Datastore = function () {
/**
* websocket communication callbacks
* @type {Function[]}
*/
const callbacks = [];
/**
* own reference for inner functions
* @type {ccm.Datastore}
*/
let that;
/**
* instance for user authentication
* @type {Object}
*/
let user;
/**
* is called once after for the initialization and is then deleted
* @returns {Promise}
*/
this.init = async () => {
// remember own reference for inner functions
that = this;
// prepare ccm database in IndexedDB
await prepareDB();
// prepare websocket connection
await prepareRealtime();
// one-time call
delete that.init;
/**
* prepares ccm database if data is managed in IndexedDB
* @returns {Promise}
*/
async function prepareDB() {
// data is not managed in IndexedDB? => abort
if ( !that.name || that.url ) return;
await openDB(); // open database
await createStore(); // create object store
/**
* opens ccm database if not already open
* @returns {Promise}
*/
function openDB() {
return new Promise( ( resolve, reject ) => {
if ( db ) return resolve();
const idb = indexedDB.open( 'ccm' );
idb.onsuccess = function () { db = this.result; resolve(); }
idb.onerror = reject;
} );
}
/**
* creates object store if not already exists
* @returns {Promise}
*/
function createStore() { return new Promise( ( resolve, reject ) => {
// object store already exists? => abort
if ( db.objectStoreNames.contains( that.name ) ) return resolve();
/**
* current database version number
* @type {number}
*/
let version = parseInt( localStorage.getItem( 'ccm' ) );
// no version number? => start with 1
if ( !version ) version = 1;
// close database
db.close();
/**
* request for reopening database
* @type {Object}
*/
const request = indexedDB.open( 'ccm', version + 1 );
// set callback for event when update is needed
request.onupgradeneeded = function () {
// remember ccm database object
db = this.result;
// remember new database version number in local storage
localStorage.setItem( 'ccm', db.version );
// create new object store
db.createObjectStore( that.name, { keyPath: 'key' } );
};
request.onsuccess = resolve;
request.onerror = reject;
} ); }
}
};
/** clears local cache */
this.clear = () => that.local = {};
/**
* returns datastore source information
* @returns {{name: string, url: string, db: string}}
*/
this.source = () => { return { name: that.name, url: that.url, db: that.db } };
/**
* requests one or more datasets
* @param {ccm.types.key|Object} [key_or_query={}] - dataset key or query (default: query all datasets)
* @returns {Promise}
*/
this.get = ( key_or_query={} ) => new Promise( ( resolve, reject ) => {
// no manipulation of passed original parameter (avoids unwanted side effects)
key_or_query = ccm.helper.clone( key_or_query );
// invalid key? => abort
if ( !ccm.helper.isObject( key_or_query ) && !ccm.helper.isKey( key_or_query ) ) return reject( new Error( 'invalid dataset key: ' + key_or_query ) );
// detect managed data level
that.url ? serverDB() : ( that.name ? clientDB() : localCache() );
/** requests dataset(s) from local cache */
function localCache() {
// get local dataset(s) from local cache
resolve( ccm.helper.clone( ccm.helper.isObject( key_or_query ) ? runQuery( key_or_query ) : that.local[ key_or_query ] ) );
/**
* finds datasets in local cache by query
* @param {Object} query
* @returns {ccm.types.dataset[]}
*/
function runQuery( query ) {
const results = [];
for ( const key in that.local ) ccm.helper.isSubset( query, that.local[ key ] ) && results.push( that.local[ key ] );
return results;
}
}
/** requests dataset(s) from client-side database */
function clientDB() {
const store = getStore();
const request = ccm.helper.isObject( key_or_query ) ? store.getAll() : store.get( key_or_query );
request.onsuccess = event => resolve( event.target.result || null );
request.onerror = event => reject( event.target.errorCode );
}
/** requests dataset(s) from server-side database */
function serverDB() {
( that.socket ? useWebsocket : useHttp )( prepareParams( { get: key_or_query } ) ).then( resolve ).catch( error => checkError( error, reject ) );
}
} );
/**
* creates or updates a dataset
* @param {Object} priodata - priority data
* @returns {Promise}
*/
this.set = priodata => new Promise( ( resolve, reject ) => {
// no manipulation of passed original parameter (avoids unwanted side effects)
priodata = ccm.helper.toJSON( priodata );
// priority data has no key? => generate unique key
if ( !priodata.key ) priodata.key = ccm.helper.generateKey();
// priority data contains invalid key? => abort
if ( !ccm.helper.isKey( priodata.key ) && !ccm.helper.isObject( priodata.key ) ) return reject( new Error( 'invalid dataset key: ' + priodata.key ) );
// detect managed data level
that.url ? serverDB() : ( that.name ? clientDB() : localCache() );
/** creates/updates dataset in local cache */
async function localCache() {
// dataset already exists? => update
if ( that.local[ priodata.key ] ) that.local[ priodata.key ] = await ccm.helper.integrate( priodata, that.local[ priodata.key ] );
// dataset not exists? => create
else that.local[ priodata.key ] = priodata;
resolve( priodata );
}
/** creates/updates dataset in client-side database */
function clientDB() {
const request = getStore().put( priodata );
request.onsuccess = event => resolve( event.target.result );
request.onerror = event => reject( event.target.errorCode );
}
/** creates/updates dataset in server-side database */
function serverDB() {
( that.socket ? useWebsocket : useHttp )( prepareParams( { set: priodata } ) ).then( resolve ).catch( error => checkError( error, reject ) );
}
} );
/**
* deletes a dataset
* @param {ccm.types.key} key - dataset key
* @returns {Promise}
*/
this.del = key => new Promise( ( resolve, reject ) => {
// invalid key? => abort
if ( !ccm.helper.isKey( key ) ) return reject( new Error( 'invalid dataset key: ' + key ) );
// detect managed data level
that.url ? serverDB() : ( that.name ? clientDB() : localCache() );
/** deletes dataset in local cache */
function localCache() {
const dataset = that.local[ key ];
delete that.local[ key ];
resolve( dataset );
}
/** deletes dataset in client-side database */
function clientDB() {
const request = getStore().delete( key );
request.onsuccess = event => resolve( event.target.result );
request.onerror = event => reject( event.target.errorCode );
}
/** deletes dataset in server-side database */
function serverDB() {
( that.socket ? useWebsocket : useHttp )( prepareParams( { del: key } ) ).then( resolve ).catch( error => checkError( error, reject ) );
}
} );
/**
* counts number of stored datasets
* @param {Object} [query] - count how many stored datasets match the query (not supported for IndexedDB)
* @returns {Promise<number>}
*/
this.count = query => new Promise( ( resolve, reject ) => {
// detect managed data level
that.url ? serverDB() : ( that.name ? clientDB() : localCache() );
function localCache() {
resolve( query ? that.get( query ).then( datasets => datasets.length ) : Object.keys( that.local ).length );
}
function clientDB() {
const request = getStore().count();
request.onsuccess = event => ( !isNaN( event.target.result ) ? resolve : reject )( event.target.result );
request.onerror = event => reject( event.target.errorCode );
}
function serverDB() {
( that.socket ? useWebsocket : useHttp )( prepareParams( query ? { count: query } : {} ) ).then( response => ( !isNaN( response ) ? resolve( parseInt( response ) ) : reject( response ) ) ).catch( error => checkError( error, reject ) );
}
} );
/**
* gets object store from IndexedDB
* @returns {Object}
*/
function getStore() {
return db.transaction( [ that.name ], 'readwrite' ).objectStore( that.name );
}
/**
* prepares data to be sent to server
* @param {Object} [params] - data to be sent to server
* @returns {Object} prepared data
*/
function prepareParams( params = {} ) {
if ( that.db ) params.db = that.db;
params.store = that.name;
if ( that.realm === null ) return params;
user = ccm.context.find( that, 'user' );
if ( that.token && that.realm ) {
params.realm = that.realm;
params.token = that.token;
}
else if ( user && user.isLoggedIn() ) {
params.realm = user.getRealm();
params.token = ( user.getValue ? user.getValue() : user.data() ).token;
}
return params;
}
/**
* checks server error
* @param error
* @param reject
* @returns {Promise<void>}
*/
async function checkError( error, reject ) {
// token has expired? => user must login again and app restarts
if ( error && ( error === 401 || error === 403 || error.data && ( error.data.status === 401 || error.data.status === 403 ) ) && user ) {
try {
await user.logout();
await user.login();
await ccm.context.root( user ).start();
}
catch ( e ) {
await ccm.context.root( user ).start();
}
}
else
reject( error );
}
/**
* prepares the realtime functionality
* @returns {Promise}
*/
function prepareRealtime() { return new Promise( resolve => {
// is no ccm realtime datastore? => abort
if ( !that.url || that.url.indexOf( 'ws' ) !== 0 ) return resolve();
// no change callback and not a standalone datastore? => set default change callback: restart parent
if ( !that.onchange && that.parent ) that.onchange = that.parent.start;
// prepare initial message
let message = [ that.db, that.name ];
if ( that.dataset ) { that.observe = that.dataset; delete that.dataset; }
if ( that.observe ) {
if ( !Array.isArray( that.observe ) ) that.observe = [ that.observe ];
that.observe = that.observe.map( key_or_query => ccm.helper.isObject( key_or_query ) ? JSON.stringify( key_or_query ) : key_or_query );
message = message.concat( that.observe );
}
// connect to server
that.socket = new WebSocket( that.url, 'ccm-cloud' );
// set server notification callback
that.socket.onmessage = message => {
// parse server message to JSON
const {callback,data} = ccm.helper.parse( message.data );
// own request? => perform callback
if ( callback ) { callbacks[ callback ]( data ); delete callbacks[ callback ]; }
// notification about changed data from other client? => perform change callback
else that.onchange && that.onchange( data );
};
// send initial message
that.socket.onopen = () => { that.socket.send( message ); resolve(); };
} ); }
/**
* sends data to server interface via websocket connection
* @param {Object} params - data to be sent to server
* @returns {Promise}
*/
function useWebsocket( params ) { return new Promise( ( resolve, reject ) => {
const key = ccm.helper.generateKey();
callbacks[ key ] = result => Number.isInteger( result ) ? checkError( result, reject ) : resolve( result );
params.callback = key;
try {
if ( that.socket.readyState > 1 )
prepareRealtime().then( () => that.socket.send( ccm.helper.stringify( params ) ) );
else
that.socket.send( ccm.helper.stringify( params ) );
}
catch ( e ) {
prepareRealtime().then( () => that.socket.send( ccm.helper.stringify( params ) ) );
}
} ); }
/**
* sends data to server interface via HTTP request
* @param {Object} params - data to be sent to server
* @returns {Promise}
*/
function useHttp( params ) {
return ccm.load( { url: that.url, params: params, method: that.method } );
}
};
/**
* ccm database in IndexedDB
* @type {Object}
*/
let db;
// set global namespace
if ( !window.ccm ) window.ccm = {
/**
* @description
* JSONP callbacks for cross domain data exchanges via {@link ccm.load} are temporarily stored here (is always emptied directly).
* This global namespace <code>ccm.callbacks</code> is also used for dynamic loading of JavaScript modules.
* The namespace is only used internally by _ccmjs_ and should not used by component developers.
* @memberOf ccm
* @type {Object.<string,function>}
* @tutorial loading-of-resources
*/
callbacks: {},
/**
* @description
* Result data of loaded JavaScript files via {@link ccm.load} are temporarily stored here (is always emptied directly).
* The namespace is only used internally by _ccmjs_ and should not used by component developers.
* @memberOf ccm
* @type {Object}
* @tutorial loading-of-resources
*/
files: {}
};
/**
* Everything around _ccmjs_ is capsuled in the single global namespace <code>window.ccm</code>.
* The namespace contains the latest version of _ccmjs_ that has been loaded so far within the webpage.
* In the webpage a _ccmjs_ version is represented as a JavaScript object.
* The object provides methods for [using components]{@tutorial usage-of-components}, [loading of resources]{@tutorial loading-of-resources} and [data management]{@tutorial data-management}.
* For [backwards compatibility]{@tutorial backwards-compatibility} each _ccmjs_ version loaded on the webpage so far has its own inner namespace within <code>window.ccm</code>.
* This ensures that different versions of _ccmjs_ can be used without conflict within the same webpage.
* @global
* @namespace
*/
const ccm = {
/**
* @description Returns the _ccmjs_ version.
* @returns {ccm.types.version_nr}
*/
version: () => '27.5.0',
/**
* @summary loads resources
* @description
* _ccmjs_ provides a service for asynchronous loading of resources. It could be used with the method <code>ccm.load</code>.
* You can load resources like HTML, CSS, Images, JavaScript, Modules, JSON and XML data on-demand and cross-domain.
* On a single call several resources can be loaded at once. It can be flexibly controlled which resources are loaded in serial and which in parallel.
* See {@tutorial loading-of-resources} to learn everything about this method. There are also more examples how to use it.
* This method can be used to define dependencies to other resources in [instance configurations]{@link ccm.types.instance_config}.
* @param {...(string|ccm.types.resource_obj)} resources - resources data
* @returns {Promise<*>}
* @tutorial loading-of-resources
*/
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( ccm.load );
/**
* result(s) of this ccm.load call
* @type {*}
*/
let results = [];
/**
* number of resources being loaded
* @type {number}
*/
let counter = 1;
/**
* indicates whether loading of at least one resource failed
* @type {boolean}
*/
let failed = false;
return new Promise( ( resolve, reject ) => {
// iterate over resources data => load resource(s)
args.forEach( ( resource, i ) => {
// increase number of resources being loaded
counter++;
// no manipulation of passed original parameters (avoids unwanted side effects)
resource = ccm.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 ( !ccm.helper.isObject( resource ) ) resource = { url: resource };
/**
* file extension from the URL of the resource
* @type {string}
*/
const suffix = resource.url.split( '.' ).pop().split( '?' ).shift().split( '#' ).shift().toLowerCase();
// ensuring lowercase on HTTP method
if ( resource.method ) resource.method = resource.method.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 shadow root context of that instance
if ( ccm.helper.isInstance( resource.context ) ) resource.context = resource.context.element.parentNode;
/**
* operation for loading resource
* @type {Function}
*/
const operation = getOperation();
// timeout check
let timeout; ccm.timeout && window.setTimeout( () => timeout === undefined && ( timeout = true ) && error( 'timeout' ), ccm.timeout );
// start loading of resource
operation();
/**
* loads resources serially (recursive function)
* @param {*} result - result of last serially loaded resource (is null on first call)
*/
function serial( result ) {
// not the first call? => add result of last call to serially results
if ( result !== null ) results[ i ].push( result );
// serially loading of resources completed? => finish serially loading and check if all resources of this ccm.load call are loaded
if ( resource.length === 0 ) return check();
// load next resource serially (recursive call of ccm.load and this function)
let next = resource.shift(); if ( !Array.isArray( next ) ) next = [ next ];
ccm.load.apply( null, next ).then( serial ).catch( serial );
// if next resource is an array, contained resources are loaded in parallel
}
/**
* determines operation for loading resource
* @returns {Function}
*/
function getOperation() {
switch ( resource.type ) {
case 'html': return loadHTML;
case 'css': return loadCSS;
case 'image': return loadImage;
case 'js': return loadJS;
case 'module': return loadModule;
case 'json': return loadJSON;
case 'xml': return loadXML;
}
switch ( suffix ) {
case 'html':
return loadHTML;
case 'css':
return loadCSS;
case 'jpg':
case 'jpeg':
case 'gif':
case 'png':
case 'svg':
case 'bmp':
return loadImage;
case 'js':
return loadJS;
case 'mjs':
return loadModule;
case 'xml':
return loadXML;
default:
return loadJSON;
}
}
/** loads a HTML file */
function loadHTML() {
// load HTML as string via HTTP GET request
resource.type = 'html';
resource.method = 'get';
loadJSON();
}
/** loads (and executes) a CSS file */
function loadCSS() {
// already exists in same context? => abort
if ( resource.context.querySelector( 'link[rel="stylesheet"][type="text/css"][href="' + resource.url + '"]' ) ) return success();
// load the CSS file via a <link> element
let element = { tag: 'link', rel: 'stylesheet', type: 'text/css', href: resource.url };
if ( resource.attr ) element = Object.assign( element, resource.attr );
element = ccm.helper.html( element );
element.onload = success;
element.onerror = event => { element.parentNode.removeChild( element ); error( element, event ); };
resource.context.appendChild( element );
}
/** (pre)loads an image file */
function loadImage() {
// (pre)load the image file via an image object
const image = new Image();
image.onload = success;
image.onerror = event => error( image, event );
image.src = resource.url;
}
/** loads (and executes) a JavaScript file */
function loadJS() {
/**
* filename of JavaScript file (without '.min')
* @type {string}
*/
const filename = resource.url.split( '/' ).pop().split( '?' ).shift().replace( '.min.', '.' );
// mark JavaScript file as loading
window.ccm.files[ filename ] = null; window.ccm.files[ '#' + filename ] = window.ccm.files[ '#' + filename ] ? window.ccm.files[ '#' + filename ] + 1 : 1;
// load the JavaScript file via a <script> element
let element = { tag: 'script', src: resource.url, async: true };
if ( resource.attr ) element = Object.assign( element, resource.attr );
element = ccm.helper.html( element );
element.onload = () => {
/**
* data globally stored by loaded JavaScript file
* @type {*}
*/
const data = window.ccm.files[ filename ];
// remove stored data from global context
if ( !--window.ccm.files[ '#' + filename ] ) { delete window.ccm.files[ filename ]; delete window.ccm.files[ '#' + filename ]; }
// remove no more needed <script> element
element.parentNode.removeChild( element );
// perform success callback
data !== null ? successData( data ) : success();
};
element.onerror = event => { element.parentNode.removeChild( element ); error( element, event ); };
resource.context.appendChild( element );
}
/** loads a JavaScript module */
function loadModule() {
let [ url, ...keys ] = resource.url.split( '#' );
if ( url.startsWith( './' ) ) url = url.replace( './', location.href.substring( 0, location.href.lastIndexOf( '/' ) + 1 ) );
import( url ).then( result => {
if ( keys.length === 1 ) result = result[ keys[ 0 ] ]
if ( keys.length > 1 ) {
const obj = {};
keys.forEach( key => obj[ key ] = result[ key ] );
result = obj;
}
successData( result );
} );
}
/** loads JSON data */
function loadJSON() {
// load data using desired method
switch ( resource.method ) {
case 'jsonp':
jsonp();
break;
case 'get':
case 'post':
case 'put':
case 'delete':
ajax();
break;
case 'fetch':
fetchAPI();
break;
default:
resource.method = 'post';
ajax();
}
/** performs a data exchange via JSONP */
function jsonp() {
// prepare callback function
const callback = 'callback' + ccm.helper.generateKey();
if ( !resource.params ) resource.params = {};
resource.params.callback = 'window.ccm.callbacks.' + callback;
window.ccm.callbacks[ callback ] = data => {
element.parentNode.removeChild( element );
delete window.ccm.callbacks[ callback ];
successData( data );
};
// prepare <script> element for data exchange
let element = { tag: 'script', src: buildURL( resource.url, resource.params ) };
if ( resource.attr ) element = Object.assign( element, resource.attr );
element = ccm.helper.html( element );
element.onerror = event => { element.parentNode.removeChild( element ); error( element, event ); };
element.src = element.src.replace( /&/g, '&' ); // TODO: Why is this "&" happening in ccm.helper.html?
// start data exchange
resource.context.appendChild( element );
}
/** performs a data exchange via AJAX request */
function ajax() {
const request = new XMLHttpRequest();
request.open( resource.method, resource.method === 'get' && resource.params ? buildURL( resource.url, resource.params ) : resource.url, true );
if ( resource.headers )
for ( const key in resource.headers ) {
request.setRequestHeader( key, resource.headers[ key ] );
if ( key.toLowerCase() === 'authorization' )
request.withCredentials = true;
}
( resource.method === 'post' || resource.method === 'put' ) && request.setRequestHeader( 'Content-Type', 'application/json' );
request.onreadystatechange = () => {
if ( request.readyState === 4 )
request.status >= 200 && request.status < 300 ? successData( request.responseText ) : error( request );
};
request.send( resource.method === 'post' || resource.method === 'put' ? ccm.helper.stringify( resource.params ) : undefined );
}
/** performs a data exchange via fetch API */
function fetchAPI() {
if ( !resource.init ) resource.init = {};
if ( resource.params ) resource.init.method.toLowerCase() === 'post' ? resource.init.body = ccm.helper.stringify( resource.params) : resource.url = buildURL( resource.url, resource.params );
fetch( resource.url, resource.init ).then( response => response.text() ).then( successData ).catch( error );
}
/**
* adds HTTP parameters in URL
* @param {string} url - URL
* @param {Object} data - HTTP parameters
* @returns {string} URL with added HTTP parameters
*/
function buildURL( url, data ) {
if ( ccm.helper.isObject( data.json ) ) data.json = ccm.helper.stringify( data.json );
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;
}
}
}
/** loads a XML file */
function loadXML() {
if ( !resource.method ) resource.method = 'post';
const request = new XMLHttpRequest();
request.overrideMimeType( 'text/xml' );
request.onreadystatechange = () => {
if ( request.readyState === 4 )
request.status === 200 ? successData( request.responseXML ) : error( request );
};
request.open( resource.method, resource.url, true );
request.send();
}
/**
* when a data exchange has been completed successfully
* @param {*} data - received data
*/
function successData( data ) {
// timeout already occurred? => abort (counter will not decrement)
if ( checkTimeout() ) return;
// received data is a JSON string? => parse it to JSON
try { if ( typeof data !== 'object' ) data = ccm.helper.parse( data ); } catch ( e ) {}
// received data is loaded HTML? => look for <ccm-template> tags
if ( resource.type === 'html' ) {
const regex = /<ccm-template key="(\w*?)">([^]*?)<\/ccm-template>/g;
const result = {}; let array;
while ( array = regex.exec( data ) )
result[ array[ 1 ] ] = array[ 2 ];
if ( Object.keys( result ).length ) data = result;
}
// add received data to results of ccm.load call and to cache
results[ i ] = data;
// perform success callback
success();
}
/** when a resource is loaded successfully */
function success() {
// timeout already occurred? => abort (counter will not decrement)
if ( checkTimeout() ) return;
// is there no result value yet? => use URL as result
if ( results[ i ] === undefined ) results[ i ] = resource.url;
// check if all resources are loaded
check();
}
/**
* checks if timeout already occurred
* @returns {boolean}
*/
function checkTimeout() {
return timeout ? ccm.helper.log( 'loading of ' + resource.url + ' succeeded after timeout (' + ccm.timeout + 'ms)' ) || true : timeout = false;
}
/**
* when loading of a resource failed
* @param {...*} data - relevant process data
*/
function error() {
// loading of at least one resource failed
failed = true;
// create load error data
results[ i ] = {
error: new Error( 'loading of ' + resource.url + ' failed' ), // error object
resource: resource, // resource data
data: [ ...arguments ], // relevant process data
call: call // ccm.load call
};
if ( results[ i ].data.length <= 1 ) results[ i ].data = results[ i ].data[ 0 ];
// check if all resources are loaded
check();
}
} );
// check if all resources are loaded (important if all resources are already loaded)
check();
/** checks if all resources are loaded */
function check() {
// still more loading resources left? => abort
if ( --counter ) return;
// only one result? => do not use an array
if ( results.length === 1 )
results = results[ 0 ];
// finish this ccm.load call
( failed ? reject : resolve )( results );
}
} );
},
/**
* @summary registers a component
* @description
* Registers a component within this _ccmjs_ version. The returned [component object]{@link ccm.types.component_obj} can than be used for flexible [instance]{@link ccm.types.instance} creation.
* This method can be used to define dependencies to other components in [instance configurations]{@link ccm.types.instance_config}.
* After registration, the component is ready to use on the webpage.
* If a URL to a component file is passed instead of a [component object]{@link ccm.types.component_obj}, the object is determined by loading this file.
* If the component uses a different _ccmjs_ version, this version is loaded (if not already present) and then the component is registered in that _ccmjs_ version instead.
* With the `config` parameter you can pass a default [instance configuration]{@link ccm.types.instance_config} that will be integrated with higher priority in the default [instance configuration]{@link ccm.types.instance_config} that is defined by the component.
* The resulting default configuration applies for all [instances]{@link ccm.types.instance} that are created via the returned [component object]{@link ccm.types.component_obj}.
* @param {ccm.types.component_obj|string} component - component object or URL of a component file
* @param {ccm.types.instance_config} [config] - default configuration for instances that are created out of the component (check documentation of associated component to see which properties could be set)
* @returns {Promise<ccm.types.component_obj>} cloned component object (the original cannot be reached from the outside for security reasons)
* @example
* const component_obj = await ccm.component( {
* name: 'blank',
* ccm: 'https://ccmjs.github.io/ccm/ccm.js',
* Instance: function () {
* this.start = async () => {
* this.element.innerHTML = 'Hello, World!';
* };