-
Notifications
You must be signed in to change notification settings - Fork 5
/
caldom.js
1963 lines (1674 loc) · 68.5 KB
/
caldom.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
/*
* CalDom 1.0.7 - Reactive++
* Copyright (c) 2021 Dumi Jay
* Released under the MIT license - https://github.com/dumijay/CalDom/
*/
// Uncomment below line to support ES6 module import/export.
// export default
(function(){
var _window = window;
var _array_prototype = Array.prototype;
var _slice = _array_prototype.slice;
var _node_prototype = _window.Node.prototype;
var _insertFunc_appendChild = _node_prototype.appendChild;
var _insertFunc_insertBefore = _node_prototype.insertBefore;
var _isNodeConnected = !("isConnected" in Node.prototype)
? function(node){
return !(node.ownerDocument.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_DISCONNECTED);
}
: function(node){
return node.isConnected;
}
var is_proxy_supported = _window.Proxy != undefined;
//RequstAnimationFrame polyfill
var _requestAnimationFramePolyfill = _window.requestAnimationFrame
|| _window.webkitRequestAnimationFrame
|| _window.mozRequestAnimationFrame
|| function(callback){
return setTimeout( callback, 1000 / 60);
}
//_window.Element.prototype.matches polyfill
_window.Element.prototype.matches = _window.Element.prototype.matches
|| _window.Element.prototype.webkitMatchesSelector
|| _window.Element.prototype.mozkitMatchesSelector
|| _window.Element.prototype.msMatchesSelector
|| _window.Element.prototype.oMatchesSelector;
/**
* @category Initiate
* @constructor
* @title _( query_or_elems )
* @description Initiates a CalDom instance.
* @param {String | CalDom | Node | Array<Node> | NodeList | HTMLCollection} [query_or_elems] (Optional) "+tag" creates a new Element.
* '<tag></tag>' creates specified HTML structure. "~svg_tag" creates a SVG element.
* Otherwise, it can be a CSS Selector, an XPath query starting with "$", a CalDom instance, a single Node/Element, an array of Nodes or a NodeList/HTMLCollection.
* @param {CalDom | Node | String | Array<CalDom | Node | String>} [children] (Optional) Child elements to be passed into append(). Can be CalDOM/Node/String or an array of them. See append() for all possibilities.
* This might be useful for code-clarity at render() function for reactive components. See examples at react().
* @param {Window} [parentWindow=window] (Optional) parent_window default to current window. Use this to work with iframes or external windows.
* @returns {CalDom} A new CalDom instance with created/found elements.
* @example
* //Create a new password input element with a placeholder
* var passwordInput = _("+input")
* .attr({
* type: "password",
* placeholder: "Enter your password"
* });
*
* //Create a password input element using full HTML syntax
* var passwordInput = _( '<input type="password" placeholder="Type your password here" />' );
*
* //Create a new SVG element
* var svgRectangle = _("~rect")
* .attr({
* width: 100,
* height: 100
* });
*
* //Get element by ID using CSS Selector
* var container = _("#container-id");
*
* //Find anchor elements inside <article> with href containing "wikipedia.org" using an XPath query
* var wikipedia_links = _("$//article/a[contains(@href, 'wikipedia.org')]");
*
* //Create a CalDom instance with a single Node
* var container = _( document.body.children[3] );
*
* //Create a CalDom instance with a NodeList or array of Nodes
* var bodyChildren = _(document.body.childNodes);
*
* //Create a new DIV element and provide an array of children as the 2nd argument.
* //CalDom accepts a wide variety of input types as children. See append() for all possibilities.
* var something = _(
* "+div",
* [
* document.createElement("h1"), //Node
*
* _("+p").text("I'm a paragraph inside DIV."), //CalDom
*
* '<img width="120" height="120" src="something.png" />', //HTML syntax
*
* "Text Node inside DIV"
* ]
* );
*/
var CalDom = function(query_or_elems, children, parentWindow){
this.init(query_or_elems, children, parentWindow);
};
CalDom.prototype = {
/**
* @private
* @description Internal init(). Refer CalDom's constructor
*/
init: function(selector_xpath_caldom_elems, children, parentWindow){
this._w = parentWindow || window;
this.$ = {};
if( selector_xpath_caldom_elems ){
// if(selector_xpath_caldom_elems instanceof CalDom){
// this.elems = selector_xpath_caldom_elems.elems;
// }
// else
if( _isArrayLike(selector_xpath_caldom_elems) ){
this.elems = _slice.call( selector_xpath_caldom_elems );
}
else if(typeof selector_xpath_caldom_elems == 'object' ){
this.elems = [selector_xpath_caldom_elems];
}
else{
this.elems = q(selector_xpath_caldom_elems, this._w.document);
}
this.elem = this.elems[0];
if( this.elems.length != 1 ) _setMultipleMode.call(this);
}
if( children ) this.append( children );
return this;
},
/**
* @category Traverse
* @title elems: Array<Node>
* @description Nodes/Elements of this CalDom instance.
* (Note that directly changing items here is not recommended.)
* @example
*
* //Get 3rd img element (zero-based index)
* var third_img_elem = _("img").elems[2];
*/
"elems": [],
/**
* @category Traverse
* @title elem: Node
* @description First Node of this CalDom instance. .elem == .elems[0]
* @example
*
* //Get first img element
* var first_img_elem = _("img").elem;
*/
"elem": undefined,
/**
* @category Traverse
* @title find(selector_or_xpath)
* @description Get a new CalDom instance with matching descendent elements for all elements in this CalDom instance.
* Note: This could return duplicates when there are inter-connected elements in this CalDom instance.
* @param {String} selector_or_xpath CSS Selector to find or XPath query starting with "$"
* @returns {CalDom} A new CalDom instance with found elements.
* @example
* //Find child elements by the CSS selector
* var slide_photos = calDomInstance.find(".slide-photo");
*
* //Find child anchor elements with href containing "wikipedia.org" using an XPath query.
* //(Note relative XPath queries starts with './' instead of root '/')
* var wikipedia_links = calDomInstance.find("$./a[contains(@href, 'wikipedia.org')]");
*
* //Find following siblings of a <li> using an XPath query.
* var li_next_siblings = _("#li-id").find("$./following-sibling::li");
*/
"find": _findSingle,
/**
* @category Traverse
* @description Get a new CalDom instance with i-th element in this CalDom instance.
* @param {Number} index Can be a zero-based index or a minus(-i) index from end (Eg: -1 returns last element)
* @returns {CalDom} A new CalDom instance with the i-th element
* @example
* //Get a new CalDom instance for 5th paragraph
* var fifth_para = _("p").eq(4);
*
* //Get a new Caldom instance for 2nd last paragraph
* var second_last_para = _("p").eq(-2);
*/
"eq": function(index){
return new CalDom( this.elems[ index < 0 ? this.elems.length + index : index ], undefined, this._w );
},
/**
* @category Traverse
* @description Get a new CalDom instance with immediate parent(s) or n-th parent(s) or parent(s) matching the CSS selector. (If this CalDom instance has more than 1 element, parents of all of them are included.)
* @param {Number | String} [n_or_selector] (Optional) Default is immediate parent. If a number is given, n-th parent is returned (zero-based). If a string is given, first parent(s) to match the CSS selector is returned.
* @returns {CalDom} A new CalDom instance with matched parents. Multiple parents will be returned if there are 2 or more elements in this CalDom instance.
* @throws {TypeError} Throws if Element.matches() is not supported in the browser. Use the polyfill at https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
* @example
* //Get a new CalDom instance for immediate parent
* var para_parent = _("#para").parent();
*
* //Get a new CalDom instance for 2nd-level parent(grand-parent)
* var grand_parent = _("#para").parent(1);
*
* //Get parents with the class "container" for all img elements
* var img_parents = _("img").parent(".container");
*/
"parent": function(n_or_selector){
var _this = this;
var output = [];
if( !n_or_selector || typeof n_or_selector == 'number' ){
this.each(function(elem){
var cur_node = elem.parentNode;
for(var parent_i = 0; parent_i < n_or_selector; parent_i++){
cur_node = cur_node.parentNode;
}
output.push( cur_node );
});
}
else{
this.each(function(elem){
var cur_node = elem.parentNode;
var is_document = cur_node == _this._w.document;
while( !is_document && !cur_node.matches(n_or_selector) ){
cur_node = cur_node.parentNode;
is_document = cur_node == _this._w.document;
}
if( !is_document ) output.push( cur_node );
});
}
return new CalDom( output, undefined, this._w );
},
/**
* @category Traverse
* @description Get a new CalDom instance with children of all elements in this CalDom instance. (If this CalDom instance has more than 1 element, children of all of them are included.)
* @param {String} [match_selector] (Optional) CSS selector to match
* @throws {TypeError} Throws if Element.matches() is not supported in the browser. Use the polyfill at https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
* @returns {CalDom} A new CalDom instance with children.
* @example
*
* //Get all children of element with id "container-id"
* var container_children = _("#container-id").children();
*
* //Get checked elements
* var checked_elements = _("#form-id").children(":checked");
*/
"children": function(match_selector){
var output = [];
this.each(function(elem){
var children = _slice.call( elem.childNodes ); //Because NodeList enumeration is damn slow \_/ https://jsben.ch/1HYYe
for( var i = 0, len = children.length; i < len; i++ ){
var child = children[i];
if( !match_selector || (child.matches && child.matches(match_selector)) ) output.push(child);
}
});
return new CalDom( output, undefined, this._w );
},
/**
* @category Iterate
* @title each(callback)
* @description Iterate each element in this CalDom instance with a callback.
* @param {Function} callback Function to callback. Callback is called with 2 arguments: callback(elem: Node, index: Number). The enumeration stops if the callback returns false.
* @returns {CalDom} Returns current CalDom instance
* @example
* //Iterate through all img elements with the class "slide" and log its index and src
* _("img.slide").each( function(elem, i){
* console.log( i, elem.src );
* });
*/
"each": _eachSingle,
/**
* @category Iterate
* @title map(callback)
* @description Iterate each element in this CalDom instance with a callback and get an array of callback's returned values.
* @param {Function} callback Function to callback. Callback is called with 2 arguments: callback(elem: Node, index: Number).
* @returns {Array} An array of values returned by the callback.
* @example
* //Get value length of text input elements
* var lengths = _("input[type=text]")
* .map( function(elem, i) {
* return elem.value.length;
* })
*/
"map": _mapSingle,
/**
* @category Manipulate/Retrieve Content
* @description Get or set innerHTML of all elements in this CalDom instance.
* (WARNING: This might lead to XSS vulnerabilities. Use text() or append("text_content") if you're only updating text).
* @param {String | Array<String>} [html_or_html_array] (Optional) A HTML string or an array of HTML strings to set at corresponding n-th element.
* If not given, an array of innerHTML for all elements in this CalDom instance is returned.
* @returns { CalDom | Array<String> } If html_or_html_array param is given, current CalDom instance is returned.
* Otherwise, an array of innerHTML from all elements in this CalDom instance is returned.
* @example
* //Set HTML code to a DIV element
* _("#div-id").html( '<p>Bla Bla</p>' );
*
* //Set an array of HTML code to corresponding n-th element
* _(".container").html( ['<p id="para-1"></p>', '<p id="para-2"></p>'] );
*
* //Get HTML code of all div elements as an array
* var h3_html_array = _("div").html();
*/
"html": function(html_or_html_array){
// TODO: Make this direct for performance?
return this.prop("innerHTML", html_or_html_array, true);
},
/**
* @category Manipulate/Retrieve Content
* @description Get or set textContent of all elements in this CalDom instance.
* @param {String | Array<String>} [text_or_text_array] (Optional) Text or an array of text to set at corresponding n-th element.
* If not given, an array of textContent is returned.
* @returns { CalDom | Array<String> } If text_or_text_array is given, current CalDom instance is returned.
* Otherwise, an array of textContent from all elements in this CalDom instance is returned.
* @example
* //Set text of all anchor elements to "Click Here"
* _("a").text("Click Here");
*
* //Set an array of text to n-th paragraph
* _("p").text( ["Para One", "Para Two"] )
*
* //Get an array of text from span elements with the class "caption"
* var captions_array = _("span.caption").text();
*/
"text": function(text_or_text_array){
// TODO: Make this direct for performance?
return this.prop("textContent", text_or_text_array, true);
},
/**
* @category Manipulate/Retrieve Content
* @description Get or set value property of elements in this CalDom instance.
* @param {String | Number | Array<String | Number>} [val_or_val_array] (Optional) Value or array of values to to be set at corresponding n-th element.
* If not given, array of values will be returned.
* @returns { CalDom | Array<String> } If val_or_val_array is given, current CalDom instance is returned.
* Otherwise, array of value from all elements in this CalDom instance is returned.
* @example
*
* //Get an value array from all text input elements
* var input_text_array = _('input[type="text"]').val();
*
* //Make all password fields empty
* _('input[type="password"]').val("");
*
* //Set an array of values to n-th input element
* _('input[type="text"]').val( ["One", "Two", "Three"] );
*/
"val": function(val_or_val_array){
// TODO: Make this direct for performance
return this.prop("value", val_or_val_array, true);
},
/**
* @category Manipulate/Retrieve Content
* @title attr(...)
* @description Get or set attribute(s) of elements in this CalDom instance.
* @param {String | Object} key_or_key_values Attribute name as a String or { key: value, ... } object to set multiple attributes
* @param {any | Array<String | Number>} [val_or_val_array] (Optional) Value or array of values to be assigned at corresponding n-th element.
* If not given, an array of attribute values for the given key is returned.
* @returns { CalDom | Array<String | Number> } If it's a set request, current CalDom instance is returned.
* Otherwise, an attribute value array from all elements in this CalDom instance is returned.
* @example
* //Set scrolling attribute of all iframes
* _("iframe").attr("scrolling", "no");
*
* //Get an array of href attribute from all <a> tags
* var links_array = _("a").attr("href");
*
* //Set width and height of img elements
* _("img").attr({
* width: 400,
* height: 300
* });
*
* //Set an array of type attribute to n-th input elements
* _("input").attr( "type", ["text", "password", "date"] );
*/
"attr": _attrSingle,
/**
* @category Manipulate/Retrieve Content
* @title prop(...)
* @description Get or set variable(s) at elements' root. Other than assigning custom data of any type at elements' root, this can be used to access all properties/attributes of elements as well.
* Warning: Look out for untracked circular references that might lead to memory leaks.
* @param {String | Object} key_or_key_values Variable name as a String or { key: value, ... } object to set multiple variables.
* @param {any | Array} [val_or_val_array] (Optional) Value or an array of values to be to be assigned at the corresponding n-th element. If empty, an array of given key's values is returned.
* @returns {CalDom | Array} If it's a set request, this chainable CalDom instance is returned.
* Otherwise, a value array from all elements in this CalDom instance is returned.
* @example
* //Get custom data
* var custom_data_array = _(".class-name").prop("customData");
*
* //Set click event handler
* _("#elem-id").prop("onclick", (e) => { console.log("clicked") });
*
* //Set an array of custom_data to n-th <p> separately
* _("p").prop( "custom_data", [custom_data_one, custom_data_two, null] );
*
* //Set multiple variables
* _("input").prop({
* type: "text",
* value: "Hello World!",
* custom_data: 44,
* other_data: { name: "Jane" }
* });
*/
"prop": _propSingle,
/**
* @category Manipulate/Retrieve Content
* @description Store & retrieve { key: value, ... } data from elements in this CalDom instance.
* (Data is stored at element._data. However, _data location might change over time, avoid accessing it directly as possible.)
* Warning: Look out for untracked circular references that might lead to memory leaks.
* @param {Object} key Can be any javascript Object, usually a string.
* @param {any | Array} [value] (Optional) Can be any javascript Object or an array of Objects to be to be assigned at the corresponding n-th element.
* @returns If it's a set request, this CalDom instance is returned.
* Otherwise, an array of values for the provided key from all elements is returned.
*/
"data": function(key, value){
if( value == undefined ){
return this.map(function(elem){
if( elem["_data"] ) return elem["_data"][key];
});
}
else{
if( Array.isArray(value) ){
this.each(function(elem, i){
if( !elem["_data"] ) elem["_data"] = {};
elem["_data"][key] = value[i];
});
}
else{
this.each(function(elem){
if( !elem["_data"] ) elem["_data"] = {};
elem["_data"][key] = value;
});
}
return this;
}
},
/**
* @category CSS Styling
* @title css(....)
* @description Get & set CSS style rule(s) of elements in this CalDom instance.
* @param {String | Object} key_or_key_values CSS property name or { property: value, ... } object to set multiple rules
* @param {String | Number | Array<String | Number>} val_or_val_array CSS value or an array of values to be set at n-th element for the given CSS property name.
* @returns {CalDom | Array<String | Number>} If it's a set request, this CalDom instance is returned.
* Otherwise, a CSS value array for the given property from all elements in this CalDom instance is returned.
* @example
*
* //Set one rule
* _(".class-name").css("background-color", "blue");
*
* //Set multiple rules
* _("#div-id").css({
* backgroundColor: "red",
* "font-size": "2em"
* });
*
* //Set CSS value using an array for n-th element
* _(".box")
* .css( "background-color", [ "red", "yello", "#555"] );
*
* //Get CSS value
* var rule_value = _("#something").css("display")[0];
*/
"css": _cssSingle,
/**
* @category CSS Styling
* @title addClass(class_names)
* @description Add class name(s) to elements in this CalDom instance.
* @param {String | Array} class_names A single class name or multiple class names separated by spaces or as an array.
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Add a single class
* _("#container").addClass("wide-view");
*
* //Add multiple classes (space-separated)
* _("#container").addClass("visible dark-theme narrow");
*
* //Add multiple classes (array)
* _("#container").addClass( ["visible", "dark-theme", "narrow"] );
*/
"addClass": _addClassSingle,
/**
* @category CSS Styling
* @title removeClass(class_names)
* @description Remove class name(s) from elements in this CalDom instance.
* @param {String | Array} class_names A single class name or multiple class names separated by spaces or as an array.
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Remove a single class
* _("#container").removeClass("visible");
*
* //Remove multiple classes (space-separated)
* _("#container").removeClass("dark-theme narrow");
*
* //Remove multiple classes (array)
* _("#container").removeClass( ["dark-theme", "narrow"] );
*/
"removeClass": _removeClassSingle,
/**
* @category CSS Styling
* @title show([display_value])
* @description Set display CSS property of all elements in this CalDom instance.
* @param {String} [display_value] (optional) Display value. Default to "block"
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Set CSS display property of #container to "block"
* _("#container").show();
*
* //Set CSS display property of #gallery to "flex"
* _("#gallery").show("flex");
*/
"show": function(display_value){
return this.css("display", display_value || "block");
},
/**
* @category CSS Styling
* @title hide()
* @description Set display CSS property of all elements in this CalDom instance to "None".
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Set CSS display property of #container to "none"
* _("#container").hide();
*/
"hide": function(){
return this.css("display", "none");
},
/**
* @category Event Handling
* @description Add event listeners to elements in this CalDom instance.
* @param {String | Array} event_names A single event name or multiple event names separated by spaces or as an array.
* @param {Function} handler Callback function to handle the event.
* @param {any} [options] (Optional) options to pass into addEventListener's 3rd param.
* @returns {CalDom} Returns this CalDom instance.
* @example
* //Add a click event listener
* _("div-id").on( "click", function(e){ console.log("clicked") } );
*
* //Add mousemove and touchmove event listeners
* _("div-id").on("mousemove touchmove", moveHandler);
*
* //Event names as an array
* _("div-id").on(["mousemove", "touchmove"], moveHandler);
*
*/
"on": function(event_names, handler, options){
var events = _getSpaceSeparatedArray(event_names);
this.each(function(elem){
for( var event_i = 0, events_len = events.length; event_i < events_len; event_i++ ){
elem.addEventListener(events[event_i], handler, options);
}
});
return this;
},
/**
* @category Event Handling
* @description Remove event listeners from elements in this CalDom instance.
* @param {String | Array} event_names A single event name or multiple event names separated by spaces or as an array.
* @param {Function} handler Callback. The same callback provided at on() or Node.addEventListener() should be provided.
* @param {any} [options] (Optional) options to pass into removeEventListener's 3rd param.
* @returns {CalDom} Returns this CalDom instance.
* @example
* //Remove click event listener
* _("div-id").off("click", clickEventHandler);
*
* //Remove mousemove and touchmove event listeners
* _("div-id").off("mousemove touchmove", moveHandler);
*
* //Event names as an array
* _("div-id").off(["mousemove", "touchmove"], moveHandler);
*/
"off": function(event_names, handler, options){
var events = _getSpaceSeparatedArray(event_names);
this.each(function(elem){
for( var event_i = 0, events_len = events.length; event_i < events_len; event_i++ ){
elem.removeEventListener(events[event_i], handler, options);
}
});
return this;
},
/**
* @category Manipulate DOM Tree
* @title append( elems_caldom_generator, ...elems )
* @description Append/Move elements to first element of this CalDom instance.
* Null and undefined inputs are silently ignored. Note that if you append an existing element, it is moved to the new destination (not cloning).
* @param {Node | String | CalDom | Array | NodeList | HTMLCollection} elems_or_caldom First argument can be a CalDom instance, a Node/String or an array of Node/String.
* Provided elements are added to the first element of this CalDom instance. This is XSS safe as it's considering String inputs as a text node. Use .html() or init _("<h1></h1>") to inject HTML.
* See the examples for wide range of possibilities.
* @param {Node | String | CalDom } [...elems] (Optional) If the first argument is not an array-type, all ...arguments are added to the first element of this CalDom instance.
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Append a new <div> to body (Element created by CalDom)
* _("body").append( _("+div") );
*
* //Append a new <div> to body (Element created by document.createElement)
* _("body").append( document.createElement("div") );
*
* //Append multiple elements, including nested elements and Node arrays
* _("#container-id").append(
* _("+div")
* .append( //Nested
* _("+p").text("Paragraph inside the div.")
* ),
*
* _("p").text("Another paragraph as a first-level child."),
*
* document.createElement("img"),
*
* null, undefined, //Silently ignored
*
* "Text Node"
*
* );
*
* //Append an array of Nodes and CalDom instances to <body>
* _("body").append(
* [
* _("+h1"),
*
* _("+div").append(
* _("+p").text("Paragraph inside the div.")
* ),
*
* document.createElement("p"),
*
* "Text Node"
* ]
* );
*
* //Move existing <p> elements from "container-a" to "container-b"
* _("#container-b").append( _("#container-a p") );
*
* //Short form of append:
*
* //Appending empty Paragrapgh to the body
* _( "body", _("+p") );
*
* //Appending multiple elements to the body
* _( "body", [
* _("+p", "Para Text"),
* document.createElement("div"),
* "Text Node"
* ]);
*/
"append": function(elems_or_caldom){
return insertBefore.call(this, _isArrayLike(elems_or_caldom) ? elems_or_caldom : arguments, null, _insertFunc_appendChild);
},
/**
* @category Manipulate DOM Tree
* @description Prepend/Move elements to the first element of this CalDom instance.
* (Same as append(), except 2nd argument is reserved to identify before element.)
* Null and undefined inputs are silently ignored. Note that if you prepend an existing element, it is moved to the new destination (not cloning).
* @param {Node | CalDom | Array | NodeList | HTMLCollection} elems_or_caldom First argument can be a CalDom instance, a Node/String or an array of Node/String.
* Items are prepended to the first element of this CalDom instance. This is XSS safe as it's considering String inputs as a text node. Use .html() or init _("<h1></h1>") to inject HTML.
* @param {Node | String | CalDom} [before_elem_or_caldom] (Optional) If provided, items are inserted before this element instead before the firstChild.
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Prepend a new <p> element as the first child
* _("#container-id")
* .prepend(
* _("+p").text("I'm the first child now.")
* );
*
* //Prepend a new <p> element before the 3rd paragraph in the container
* _("#container-id")
* .prepend(
* _("+p").text("I'm the 3rd paragraph now."),
* _("#container-id p").eq(2) //Before element
* );
*
* //First argument of prepend() is the same as append().
* //Refer its examples for a wide variety of possibilities.
*/
"prepend": function(elems_or_caldom, before_elem_or_caldom){
return insertBefore.call(this, _isArrayLike(elems_or_caldom) ? elems_or_caldom : [elems_or_caldom], before_elem_or_caldom, _insertFunc_insertBefore);
},
/**
* @category Manipulate DOM Tree
* @description Remove all elements of this CalDom instance from the DOM.
* @returns {CalDom} Returns this empty CalDom instance.
* @example
*
* //Remove all <p> elements
* _("p").remove();
*/
"remove": function(){
this._willUnmount(true);
this.each(function(elem){
elem.parentNode.removeChild(elem);
});
this.elems = [];
this.elem = undefined;
_setMultipleMode.call(this);
this._didUnmount(true);
return this;
},
/**
* @category Manipulate DOM Tree
* @description Replaces current elements in this CalDom instance with new elements returned by the render_callback function.
* The algorithm compares the elements(including their descendents) and only replace the nodes if there is a difference against the actual DOM. This is useful to minimize expensive browser layout repaints.
* (This is the same algorithm used by react() to compare & apply DOM changes.)
* @param {Function} render_callback Render function that returns the new replacement element.
* The callback receieves 3 parameters: render_callback( current_elem: Node, index: Number, caldom_instance: CalDom ): {Node || CalDom}
* @returns {CalDom} Returns this CalDom instance.
* @example
*
* //Imagine this HTML document:
* // <div>
* // <p>ABC</p>
* // <p>XYZ</p>
* // </div>
*
* //Replace all paragraph elements using a new _("+p")
* _("p")
* .replace(
* function(old_elem, index, caldom_instance){
* //In this case, only the 1st paragraph will be replaced
* //since the 2nd paragraph is the same.
*
* return _("+p").text( "XYZ" );
* }
* );
*
* //Replace using a new element created by createElement()
* _("p")
* .replace(
* function(old_elem, index, caldom_instance){
* //Both paragraphs will be replaced by this empty DIV
* return document.createElement("div");
* }
* );
*/
"replace": function(render_callback){
this.each(function(elem, i){
var new_elem = render_callback(elem, i, this);
if( new_elem instanceof CalDom ) new_elem = new_elem.elems[0];
_replace(
new_elem,
elem,
elem
);
});
return this;
},
/**
* @private
* @description Used to call willMount() in reactive mode
* This function name is not preserved in minified version.
*/
_willMount: function(){
if( this["render"] || this["update"] ){
if( this["willMount"] && !this._mounted ) this["willMount"](this);
this.react(undefined, undefined, undefined, undefined, undefined, true);
}
},
/**
* @private
* @description Used to call didMount() in reactive mode.
* This function name is not preserved in minified version.
*/
_didMount: function(){
var already_mounted = this._mounted == true;
this._mounted = true;
if( this["didMount"] && !already_mounted ) this["didMount"](this);
},
/**
* @private
* @description Used to call willUnmount() in reactive mode.
* @param {Boolean} [directly_removed] Whether executed through .remove().
* This is used to differentiate component removal through replace() that might get re-connected at a different position in the DOM tree.
*/
_willUnmount: function(directly_removed){
if( this["willUnmount"] ) this["willUnmount"]( this, directly_removed );
},
/**
* @private
* @description Used to call didUnmount() in reactive mode.
* This function name is not preserved in minified version.
* @param {Boolean} [directly_removed] Whether executed through .remove().
* This is used to differentiate component removal through replace() that might get re-connected at a different position in the DOM tree.
*/
_didUnmount: function(directly_removed){
this._mounted = false;
if( this["didUnmount"] ) this["didUnmount"]( this, directly_removed );
},
/**
* @private
* @description Used to call _didUpdate() in reactive mode.
* This function name is not preserved in minified version.
*/
_didUpdate: function(){
//Resetting batched and changed keys after update/render
this._watch_state_changed_keys = {};
this._watch_state_change_count = 0;
if( this["didUpdate"] ) this["didUpdate"]( this );
},
/**
* @category Manipulate DOM Tree
* @description Make this CalDom instance a reactive component by providing a state, render() or an update() function. CalDom compares differences and efficiently updates relevant DOM Node changes.
* Alternatively, this can be also achieved by extending _.Component class. See below examples.
* @param {any} [state] (Optional, but requires to initiate) Any value or an { key: value, ... } object.
* @param {Object} [config] (Optional, but requires to initiate) Reactive configuration containing render(), update(), etc.
* @param {Function} [config.render] (Optional, if update() is given) Should return a CalDom or a Node/Element. Return false to terminate render process.
* render(state: any, component: CalDom): receieves state and component(this CalDom instance) as arguments.
* All CalDom methods & properties (find(), text(), css(), etc.) can be accessed via component. Eg: component.text("Hello World!").
* CalDom sync child nodes (including text), attributes, CSS, value, checked, indeterminate, selected & _data property set by .data() between connected DOM & virtual DOM. It doesn't sync changed events & other custom properties directly attached to DOM nodes.
* To force a complete Node replacement with all events and properties, set a different/incremental "caldom-v" attribute to the element at render() ("v" as in version of the element).
*
* The first render() gets executed synchronously. After that, render() gets executed asynchronously through requestAnimationFrame when the state changes. This is asynchronous in both watched = true mode or when react() is called manually.
* Calling react() with a brand new state execute render() synchronously. Eg: app.react( new_state );
*
* @param {Function} [config.update] (Optional, if render() is given) If an update() function is present, CalDom will execute it instead of render(). (Intial render() get executed regardless).
* Manually updating DOM changes directly by accessing the native DOM API is obviously more performant than a render() based virtual-DOM approach. Set this.$ references at render() to access the native Nodes directly. Refer this.$ below for details.
* Also, update() is useful to update existing non-javascript originated(.html file based) HTML structures reactively.
* If the update() returns true, CalDom will also execute render() and proceed with virtual-real DOM comparison and apply subsequent changes.
*
* update(
* state: any,
* component: CalDom,
* state_changed_keys: Object,
* state_change_count: Number
* );
*
* Arguments are state, component(this CalDom instance) and batched and changed state key names & change count since the last render()/update().
* state_changed_keys & state_change_count is useful to do select direct DOM changes or fall back to render() by returning true if the changes are complex.
* state_changed_keys is an Object. Eg: { key_name: String, value_is_available: Boolean }. If the state change is a deletion, value_is_available is set to false.
* state_changed_keys is one-dimentional. If the same key_name is used more than once at deeper levels of state object, only the last key-value change is represented here. Use this.state for deeper analysis.
* If the same key change multiple times, state_change_count is increased accordingly. state_changed_keys & state_change_count are only populated in watched = true mode.
*
* All CalDom methods & properties (find(), text(), css(), etc,) can be accessed via component. Eg: component.css( "color", "green" )
*
* @param {Boolean} [config.watched=true] (Optional) True by default. If false, CalDom will not react to state changes automatically. Call react() manually after changing state.
* CalDom is using Javascript Proxies to detect state changes. Browser versions released before 2016 may not support it natively. Implement a pollyfil for older browsers or you can call react() manually after state changes.
* (Note that Proxy pollyfils require variable to be defined in advance.)
* If Proxy is not supported caldom.watched will set to false despite initially setting config.watched = true.
* If you chose not to use the Proxy for some reason, you can call .react() after state changes. There is no performance hit by calling it repetedly because CalDom is using requestAnimationFrame to batch DOM updates efficiently.
* Also, calling react() manually immediately after state changes does not cause a re-render even when Proxy is supported.
*
* @param {Function} [config.willMount] (Optional) Called before the Component is mounted(appended) into a parent Node. Receives current CalDom instance as the only argument.
* @param {Function} [config.didMount] (Optional) Called after the Component is mounted(appended) into a parent Node. Receives current CalDom instance as the only argument.
* @param {Function} [config.willUnmount] (Optional) Called before the Component is removed.
* Callback receives two argument as callback( component: CalDom, [directly_removed: Boolean] ). directly_removed is set to true if removed directly by calling .remove().
* Otherwise, set to undefined if removed while applying virtual-DOM changes to the real DOM. The removed component might get re-connected at a different position in the DOM tree.
* @param {Function} [config.didUnmount] (Optional) Called after the Component is removed.
* Callback receives two argument as callback( component: CalDom, [directly_removed: Boolean] ). directly_removed is set to true if removed directly by calling .remove().
* Otherwise, set to undefined if removed while applying virtual-DOM changes to the real DOM. The removed component might get re-connected at a different position in the DOM tree.
* @param {Function} [config.didUpdate] (Optional) Called after the Component is updated and/or rendered and all virtual-DOM changes applied to the real DOM. Receives current CalDom instance as the only argument.
*
* @returns {CalDom} Returns this CalDom instance.
*
* @example
*
* //Hello World Component (non-class based approach)
* var helloWorldApp = _().react(
* { name: "World!" },
* {
* render: (state) => _("+h1", "Hello " + state.name )
* }
* );
*
* _("body").append( helloWorldApp );
*
* helloWorldApp.state.name = "CalDom!";
*
* //Hello World Component (Class based approach)
* class HelloWorldApp extends _.Component{
*
* constructor(state){
* super();
* this.react(state); //this == CalDom instance
* }
*
* render(state){ //state is a shortcut to this.state
* return _("+h1", "Hello " + state.name );
* }
* }
*
* var app = new HelloWorldApp({ name: "World!" });
* _("body", app );
*
* app.state.name = "CalDom";
*
* //Make an existing HTML H1 reactive
* var helloWorld = _("#main-heading").react(
* { name : "World!" },
* {
* update: (state, component) => component.text( "Hello " + state.name )
* }
* );
*
* helloWorld.state.name = "CalDom!";
*
* //Combine the power of both render() & update()
* var helloWorld = _("#main-heading").react(
* { name : "World!", visible: false },
* {
* render: function(state){
* return this.$.h1 = _("+h1", "Hello " + state.name )
* .addClass( state.visible ? "visible" : "").elem;
* },
*
* update: function(state, component, changed_keys, state_change_count){
* if( state_change_count == 1 && "visible" in changed_keys ){
*
* //Directly & efficiently update DOM without going through Virtual-DOM
* if( state.visible ) this.$.h1.classList.add("visible");
* else this.$.h1.classList.remove("visible");
* }
* else{
* //If the changes are too complex, proceed to render() via Virtual-DOM
* return true;
* }
* }
* }
* );
*
* setTimeout( () =>
* helloWorld.state.visible = true //This is handled by update()
* , 1000);
*
* //Manual reactive approach (without automatic update on state change)
* var helloWorldApp = _().react(
* { name: "World!" },
* {