forked from franciscop/umbrella
-
Notifications
You must be signed in to change notification settings - Fork 0
/
umbrella.js
629 lines (497 loc) · 15.8 KB
/
umbrella.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
// Umbrella JS
// -----------
// Covers your basic javascript needs
// Small, lightweight jQuery alternative
// @author Francisco Presencia Fandos http://francisco.io/
// @inspiration http://youmightnotneedjquery.com/
// INIT
// It should make sure that there's at least one element in nodes
var u = function(parameter, context) {
// Make sure that we are always working with the u object
// This is only so we can avoid selector = new u("whatever");
// and use u("whatever").bla();
// Reference: http://stackoverflow.com/q/24019863
if (!(this instanceof u)) { // !() http://stackoverflow.com/q/8875878
return new u(parameter, context);
}
// Check if it's a selector or an object
if (typeof parameter == "string") {
// Store the nodes
parameter = this.select(parameter, context);
}
// If we're referring a specific node as in click(){ u(this) }
// or the select() returned only one node
if (parameter && parameter.nodeName) {
// Store the node as an array
parameter = [parameter];
}
// Make anything an array
if (!Array.isArray(parameter)) {
parameter = this.slice(parameter);
}
this.nodes = parameter;
return this;
};
// Force it to be an array AND also it clones them
// Store all the nodes as an array
// http://toddmotto.com/a-comprehensive-dive-into-nodelists-arrays-converting-nodelists-and-understanding-the-dom/
u.prototype.slice = function(pseudo, temp) {
return pseudo ? [].slice.call(pseudo, 0) : [];
};
// Normalize the arguments to an array
// Allow for several class names like "a b, c" and several parameters
// toString() is to flatten the array: http://stackoverflow.com/q/22920305
u.prototype.args = function(args){
return ((typeof args === 'string') ? args : this.slice(args).toString())
.split(/[\s,]+/).filter(function(e){ return e.length; });
};
// Make the nodes unique
u.prototype.unique = function(){
return u(this.nodes.reduce(function(clean, node){
return (node && clean.indexOf(node) === -1) ? clean.concat(node) : clean;
}, []));
};
// Parametize an object
u.prototype.param = function(obj){
// Encode the values https://gist.github.com/brettz9/7147458
function en(str) {
return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A').replace(/%20/g, '+');
}
var query = '';
for(var key in obj) {
query += '&' + en(key) + '=' + en(obj[key]);
}
return query.slice(1);
}
// This also made the code faster
// Read "Initializing instance variables" in https://developers.google.com/speed/articles/optimizing-javascript
// Default selector
// Default value
u.prototype.nodes = [];
// Options
u.options = {};
/**
* .addClass(name1, name2, ...)
*
* Add a class to the matched nodes
* Possible polyfill: https://github.com/eligrey/classList.js
* @return this Umbrella object
*/
u.prototype.addClass = function(args){
// Normalize the arguments to a simple array
args = this.args(arguments);
// Loop through all the nodes
return this.each(function(el){
// Loop and add each of the classes
args.forEach(function(name){
el.classList.add(name);
});
});
};
/**
* .adjacent(position, text)
*
* Add text in the specified position. It is used by other functions
*/
u.prototype.adjacent = function(position, text) {
// Loop through all the nodes
return this.each(function(node) {
// http://stackoverflow.com/a/23589438
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element.insertAdjacentHTML
node.insertAdjacentHTML(position, text);
});
};
/**
* .after(html)
*
* Add child after all of the current nodes
* @param String html to be inserted
* @return this Umbrella object
*/
u.prototype.after = function(text) {
return this.adjacent('afterend', text);
};
/**
* .ajax(success, error, before)
*
* Create a POST request for whenever the matched form submits
* @param function success called when response is received
* @param function before called function before sending the request
*/
u.prototype.ajax = function(done, before) {
// Loop through all the nodes
return this.on("submit", function(e) {
// Stop the browser from sending the request
e.preventDefault();
// Post the actual data
ajax(u(this).attr("method"), u(this).attr("action"), u(this).serialize(), done, before);
});
};
/**
* .append(html)
*
* Add child the last thing inside each node
* @param String html to be inserted
* @return this Umbrella object
*/
u.prototype.append = function(html) {
return this.adjacent('beforeend', html);
};
/**
* .attr(name, value)
*
* Retrieve or set the data for an attribute of the first matched node
* @param String name the attribute to search
* @param String value optional atribute to set
* @return this|String
*/
// ATTR
// Return the fist node attribute
u.prototype.attr = function(name, value) {
if (value !== undefined){
var nm = name;
name = {};
name[nm] = value;
}
if (typeof name === 'object') {
return this.each(function(node){
for(var key in name) {
if (name[key] !== null){
node.setAttribute(key, name[key]);
} else {
node.removeAttribute(key);
}
}
});
}
return this.nodes.length ? this.first().getAttribute(name) : "";
};
/**
* .before(html)
*
* Add child before all of the current nodes
* @param String html to be inserted
* @return this Umbrella object
*/
u.prototype.before = function(html) {
return this.adjacent('beforebegin', html);
};
/**
* .children()
*
* Travel the matched elements one node down
* @return this Umbrella object
*/
u.prototype.children = function(selector) {
var self = this;
return this.join(function(node){
return self.slice(node.children);
}).filter(selector);
};
/**
* .closest()
*
* Find a node that matches the passed selector
* @return this Umbrella object
*/
u.prototype.closest = function(selector) {
return this.join(function(node) {
// Keep going up and up on the tree
// First element is also checked
do {
if (u(node).is(selector)) {
return node;
}
} while (node = node.parentNode)
});
};
/**
* .each()
* Loops through every node from the current call
* it accepts a callback that will be executed on each node
* The context for 'this' within the callback is the html node
* The callback has two parameters, the node and the index
*/
u.prototype.each = function(callback) {
// Loop through all the nodes
this.nodes.forEach(function(node, i){
// Perform the callback for this node
// By doing callback.call we allow "this" to be the context for
// the callback (see http://stackoverflow.com/q/4065353 precisely)
callback.call(this, node, i);
}, this);
return this;
};
// .filter(selector)
// Delete all of the nodes that don't pass the selector
u.prototype.filter = function(selector){
// Just a native filtering function for ultra-speed
return u(this.nodes.filter(function(node){
// Accept a function to filter the nodes
if (typeof selector === 'function') {
return selector(node);
}
// Make it compatible with some other browsers
node.matches = node.matches || node.msMatchesSelector || node.webkitMatchesSelector;
// Check if it's the same element (or any element if no selector was passed)
return node.matches(selector || "*");
}));
};
/**
* Find all the nodes children of the current ones matched by a selector
*/
u.prototype.find = function(selector) {
return this.join(function(node){
return u(selector || "*", node).nodes;
});
};
/**
* Get the first of the nodes
* @return htmlnode the first html node in the matched nodes
*/
u.prototype.first = function() {
return this.nodes[0] || false;
};
/**
* ajax(url, data, success, error, before);
*
* Perform a POST request to the given url
* @param String method the method to send the data, defaults to GET
* @param String url the place to send the request
* @param String data the ready to send string of data
* @param function success optional callback if everything goes right
* @param function error optional callback if anything goes south
* @param function before optional previous callback
*/
function ajax(method, url, data, done, before) {
// To avoid repeating it
done = done || Function;
// Create and send the actual request
var request = new XMLHttpRequest;
// An error is just an error
// This uses a little hack of passing an array to u() so it handles it as
// an array of nodes, hence we can use 'on'. However a single element wouldn't
// work since it a) doesn't have nodeName and b) it will be sliced, failing
u([request]).on('error timeout abort', function(){
done(new Error, null, request);
}).on('load', function() {
// Also an error if it doesn't start by 2 or 3...
// This is valid as there's no code 2x nor 2, nor 3x nor 3, only 2xx and 3xx
var err = !/^(2|3)/.test(request.status) ? new Error(request.status) : null;
// Attempt to parse the body into JSON
var body = parseJson(request.response) || request.response;
return done(err, body, request);
});
// Create a request of type POST to the URL and ASYNC
request.open(method || 'GET', url);
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// Load the callback before sending the data
if (before) before(request);
request.send(typeof data == 'string' ? data : u().param(data));
return request;
}
/**
* parseJson(json)
*
* Parse JSON without throwing an error
* @param String json the string to check
* @return object from the json or false
*/
function parseJson(jsonString){
try {
var o = JSON.parse(jsonString);
// Handle non-exception-throwing cases:
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking
// so we must check for that, too.
if (o && typeof o === "object") {
return o;
}
} catch (e) {}
return false;
}
/**
* .hasClass(name)
*
* Find out whether the matched elements have a class or not
* @param String name the class name we want to find
* @return boolean wether the nodes have the class or not
*/
u.prototype.hasClass = function(names) {
names = this.args(arguments);
// Attempt to find a node that passes the conditions
return this.nodes.some(function(node){
// Check if the current node has all of the classes
return names.every(function(name){
// Check whether
return node.classList.contains(name)
});
});
};
/**
* .html(text)
*
* Set or retrieve the html from the matched node(s)
* @param text optional some text to set as html
* @return this|html Umbrella object
*/
u.prototype.html = function(text) {
// Needs to check undefined as it might be ""
if (text === undefined)
return this.first().innerHTML || "";
// If we're attempting to set some text
// Loop through all the nodes
return this.each(function(node) {
// Set the inner html to the node
node.innerHTML = text;
});
};
// .is(selector)
// Check whether any of the nodes matches the selector
u.prototype.is = function(selector){
return this.filter(selector).nodes.length > 0;
};
/**
* Merge all of the nodes that the callback returns
*/
u.prototype.join = function(callback) {
return u(this.nodes.reduce(function(newNodes, node, i){
return newNodes.concat(callback(node, i));
}, [])).unique();
};
/**
* .on(event, callback)
*
* Attach the callback to the event listener for each node
* @param String event(s) the type of event ('click', 'submit', etc)
* @param function callback function called when the event triggers
* @return this Umbrella object
*/
u.prototype.on = function(events, callback) {
return this.each(function(node){
this.args(events).forEach(function(event){
node.addEventListener(event, callback);
});
});
};
/**
* .parent()
*
* Travel the matched elements one node up
* @return this Umbrella object
*/
u.prototype.parent = function(selector) {
return this.join(function(node){
return node.parentNode;
}).filter(selector);
};
/**
* .prepend(html)
*
* Add child the first thing inside each node
* @param String html to be inserted
* @return this Umbrella object
*/
u.prototype.prepend = function(html) {
return this.adjacent('afterbegin', html);
};
/**
* .remove()
*
* Delete the matched nodes from the html tree
*/
u.prototype.remove = function() {
// Loop through all the nodes
return this.each(function(node) {
// Perform the removal
node.parentNode.removeChild(node);
});
};
/**
* .removeClass(name)
*
* Removes a class from all of the matched nodes
* @param String name the class name we want to remove
* @return this Umbrella object
*/
u.prototype.removeClass = function(args) {
// Normalize the arguments to a simple array
args = this.args(arguments);
// Loop through all the nodes
return this.each(function(el){
// Loop and add each of the classes
args.forEach(function(name){
el.classList.remove(name);
});
});
};
// Select the adecuate part from the context
u.prototype.select = function(parameter, context) {
// querySelector is the only one that accepts documentFragment
return context ? this.select.byCss(parameter, context)
// If we're matching a class
: /^\.[\w\-]+$/.test(parameter) ? this.select.byClass(parameter.substring(1))
// If we're matching a tag
: /^\w+$/.test(parameter) ? this.select.byTag(parameter)
// If we match an id
: /^\#[\w\-]+$/.test(parameter) ? this.select.byId(parameter.substring(1))
// A full css selector
: this.select.byCss(parameter);
};
// The tag nodes
u.prototype.select.byTag = document.getElementsByTagName.bind(document);
// Find some html nodes using an Id
u.prototype.select.byId = document.getElementById.bind(document);
// Find some html nodes using a Class
u.prototype.select.byClass = document.getElementsByClassName.bind(document);
// Select some elements using a css Selector
u.prototype.select.byCss = function(parameter, context) {
return (context || document).querySelectorAll(parameter);
};
/**
* .serialize()
*
* Convert al html form elements into an object
* The <input> and <button> without type will be parsed as default
* NOTE: select-multiple for <select> is disabled on purpose
* Source: http://stackoverflow.com/q/11661187
* @return String the string to be sent through a Post or Get
*/
u.prototype.serialize = function() {
var obj = {};
// Store the class in a variable for manipulation
u(this.first().elements).each(function(el) {
// We only want to match elements with names, but not files
if (el.name && el.type !== 'file'
// Ignore the checkboxes that are not checked
&& (!/(checkbox|radio)/.test(el.type) || el.checked)) {
// Add the element to the object
obj[el.name] = el.value;
}
});
return this.param(obj);
};
/**
* .trigger(name)
* ----------
* Call an event manually on all the nodes
* @param event: the event or event name to call
* @return u: an instance of umbrella
*/
u.prototype.trigger = function(event) {
// Allow the event to bubble up and to be cancelable (default)
var opts = { bubbles: true, cancelable: true };
try {
// Accept different types of event names or an event itself
event = (typeof event == 'string') ? new Event(event, opts) : event;
} catch(e) {
var name = event;
event = document.createEvent('Event');
event.initEvent(name, opts.bubbles, opts.cancelable);
}
// Loop all of the nodes
return this.each(function(node){
// Actually trigger the event
node.dispatchEvent(event);
});
};