-
Notifications
You must be signed in to change notification settings - Fork 0
/
wn.js
597 lines (516 loc) · 17.1 KB
/
wn.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
/*!*
* WebNicer (WN) JavaScript Library v1.0.4
* http://bitbucket.org/jciolek/webnicer
*
* @preserve
* @author Jacek Ciolek <[email protected]>
* @licence Copyright 2012-2013, Jacek Ciolek
* Dual licensed under the MIT or GPL Version 3 licenses.
* http://bitbucket.org/jciolek/webnicer/wiki/Licence
*/
(function (global){
var _wn = global.wn,
wn;
/**
* Standalone, unobtrusive library which provides:
* - module-style namespaces for organizing libraries, objects and values globally
* - support for defining dependencies between namespaces and loading required files seamlessly
* - support for classical single inheritance of constructor functions
*/
function Webnicer()
{
var webnicer = function (ns, obj) {
return webnicer.ns(ns, obj);
};
// populate all the properties
Webnicer.extend(webnicer, Webnicer);
// fill in the gaps - instance specific
webnicer.nsObj = {};
webnicer.loader = new Loader();
return webnicer;
}
/**
* Extends objects with other objects - deep or shallow, in- or excluding properties from prototype.
*
* @param {Object} child - Object to be extended (extendee)
* @param {Object} parent - Object extending the child (extender)
* @param {Boolean} [deep] - (optional) deep extension, false by default
* @param {Boolean} [overwrite] - (optional) overwrite objects by parent's clones in deep, false by default
*
* @description Modifies provided child. Works on pairs of any object, even a function and an array.
* However function always overwrites an object, even in deep extention.
* Always overwrites primitives with objects and vice versa.
* This function has no internal dependecies, can be easily borrowed.
*/
Webnicer.extend = function extend(child, parent, deep, overwrite)
{
var cType = typeof child,
pType = typeof parent,
allowedTypes = {
'object': true,
'function': true
},
F = function () {},
i, p, c;
deep = deep || false;
overwrite = overwrite || false;
// check input parameters
if (!(pType in allowedTypes && cType in allowedTypes)) {
throw new TypeError('WN::extend(): Type mismatch. Trying to extend ' + cType + ' with ' + pType + '.');
}
if (child === null || parent === null) {
throw new TypeError('WN::extend(): child and parent cannot be null.');
}
for (i in parent) {
if (parent.hasOwnProperty(i)) {
p = parent[i];
c = child[i];
// shallow copy
// always overwrite when parent's property is null, not an object
// or a function where the child's property is not a function
if (!deep || p === null || !(typeof p in allowedTypes)) {
child[i] = p;
continue;
}
// deep copy
if (overwrite || !(typeof c in allowedTypes)) {
if (typeof p === 'function') {
c = (function () {
var _p = p.__wnCloneOf || p,
fn = function () {
return _p.apply(this, arguments);
};
fn.__wnCloneOf = _p;
return fn;
})();
} else {
// initialize compatible type
// WARNING! instances of some built-in constructors made this way may not work, e.g. Date()
// if the constructor requires parameters it won't work either
// unless the properties are public
try {
c = new p.constructor();
} catch (e) {
F.prototype = p.constructor.prototype;
c = new F();
}
}
}
// extend child's property
child[i] = extend(c, p, deep, overwrite);
}
}
return child;
};
// now the method is in place, extend own prototype
Webnicer.extend(Webnicer, {
// has to be filled in by the constructor
nsObj: null,
loader: null,
sandbox: Webnicer,
/**
* Requires the presence of given namespaces.
* Optionally sets callback to be called when all required namespaces are present.
*
* @param {array} nsArr - Array of required namespaces
* @param {function} [callback] - (optional) Function to be called when all required namespaces are present.
*/
require: function (nsArr, callback)
{
var reqObj = new Request(callback),
nsStr;
// ns may be a string or an array - unify handling
if (typeof nsArr === 'string') {
nsArr = [nsArr];
}
// ns has to be an array at this point
if (!(nsArr instanceof Array)) {
throw new TypeError('WN::require(): nsArr parameter is expected to be a String or an Array. ' + typeof ns + ' given.');
}
// if callback is present it has to be a function
if (callback !== undefined && typeof callback !== 'function') {
throw new TypeError('WN::require(): callback parameter is expected to be a Function. ' + typeof callback + ' given.');
}
while (nsStr = nsArr.shift()) {
// collect missing namespaces - might be null or undefined
if (this.ns(nsStr) == undefined) {
if (nsStr.charAt(0) !== '.') {
nsStr = '.' + nsStr;
}
reqObj.add(nsStr);
}
}
// send request to the loader only if it cannot be finished straightaway
if (!reqObj.finish()) {
this.loader.request(reqObj);
}
},
/**
* Sets or gets a value on give namespace path.
*
* @param {Object} ns - Dot separated namespace path, excluding WN
* @param {Object} [obj] - (optional) Object to be set as the last segment of the namespace path
*
* @description Creates the namespace path recursively and sets given object as the last segment of the path.
* If the second parameter is not defined function acts as a getter.
* Allows for setting leaf and parent nodes with the same name as an aid to inheritance, e.g. 'User' and 'User.Admin'
*/
ns: function (nsStr, obj)
{
var nsArr = [],
l = 0,
i = 0,
j,
nsRootObj = this.nsObj,
nsObj = nsRootObj,
isGet = (obj === undefined),
isLeaf = true,
isLast = false,
nsCurr = '',
nsCurrPath = '';
// don't accept rubbish parameters
if (typeof nsStr != 'string' || nsStr.length === 0) {
throw new TypeError('WN::ns(): nsStr parameter is expected to be a non-zero length String. ' + typeof nsStr + ': "' + nsStr + '" given.');
}
// make sure there is '.' at the beginning of the namespace string
if (nsStr.charAt(0) !== '.') {
nsStr = '.' + nsStr;
}
// if called as a getter just search in the index
if (isGet) {
return nsObj[nsStr];
}
// acting as a setter from this point onwards
// check if we are dealing with a leaf or a parent node
if (nsStr.charAt(nsStr.length - 1) == '.') {
isLeaf = false;
nsStr = nsStr.substr(0, nsStr.length - 1);
}
// if we are setting a parent node it has to be an object
if (!isLeaf && (typeof obj != 'object' || obj instanceof Array)) {
throw new TypeError('WN::ns(): obj parameter is expected to be a non-Array Object, ' + typeof obj + ' given.');
}
nsArr = nsStr.split('.');
// go through the path and create missing objects along the way
for (i = 0, l = nsArr.length; i < l; i++) {
isLast = (l - 1 <= i);
nsCurr = nsArr[i] + (!isLast || !isLeaf ? '.' :'');
nsCurrPath += nsCurr;
// object on the path does not exist
if (nsObj[nsCurr] === undefined) {
if (!isLast || !isLeaf) {
nsRootObj[nsCurrPath] = nsObj[nsCurr] = {};
}
}
// on the last path segment and setting
if (isLast) {
// extend the parent node with new properties
if (!isLeaf) {
for (j in obj) {
// TODO: re-index if the object defines ns parent nodes
if (obj.hasOwnProperty(j) && obj[j] !== undefined) {
nsObj[nsCurr][j] = obj[j];
nsRootObj[nsCurrPath + j] = obj[j];
// notify the loader about new arrival for each child namespace
this.loader.notify(nsCurrPath + j);
}
}
// or simply set the leaf
} else {
nsRootObj[nsCurrPath] = nsObj[nsCurr] = obj;
}
// notify the loader about new arrival
// for a leaf or a parent node
this.loader.notify(nsCurrPath);
}
nsObj = nsObj[nsCurr];
}
return nsObj;
},
/**
* Provides classical inheritance for constructors
*
* @param {function} C - Child constructor function
* @param {function|string} P - Parent constructor function or namespace for it
* @param {boolean} copyStatic - Indicates whether constructor function properties should be copied as well
*
* @description Preserves inheritance chain, therefore:
* Child instanceof Parent == true
* Child instanceof GrandParent == true
*/
inherit: (function () {
var F = function () {};
return function (C, P, copyStatic) {
var PProto,
CProto;
P = typeof P === 'string' && this.ns(P) || P;
C = typeof C === 'string' && this.ns(C) || C;
copyStatic = copyStatic || false;
// be strict about the types of parameters given
if (typeof C !== 'function') {
throw new TypeError('WN::inherit(): C parameter is expected to be a function, ' + typeof C + ' given.');
}
if (typeof P !== 'function') {
throw new TypeError('WN::inherit(): P parameter is expected to be a function, ' + typeof P + ' given.');
}
PProto = P.prototype,
CProto = C.prototype;
F.prototype = PProto;
C.prototype = new F();
// copy instance properties
this.extend(C.prototype, CProto);
C.prototype.constructor = C;
C.prototype.parent = PProto;
// copy constructor properties
if (copyStatic) {
this.extend(C, P);
}
C.parent = P;
};
})()
});
/**
* Takes care of loading requested scripts and notifying interested parties.
*
* @description Can notify on script load or when required namespace has been set.
* @see methods notify() and addMapping()
*/
function Loader()
{
this.required = {};
this.requested = {};
this.nsMapping = {};
}
Webnicer.extend(Loader.prototype, {
// required: null,
// requested: null,
urlPrefix: '/js/',
urlSuffix: '.js',
// nsMapping: null,
/**
* Converts a namespace into a url
*
* @param {string} ns - The namespace to be converted into a url.
* @returns {string} - The resulting url.
*
* @description This method can be easily replaced in order to provide environment specific and more sophisticated conversion.
*/
ns2url: function(ns)
{
// internally all namespaces start with '.' - get rid of it
// namespace ending with '.' means parent - file should end with '_'
// namespace path reflects directory path so all remaining '.'s are replaced with '/'s
return this.urlPrefix + ns.replace(/^\./, '').replace(/\.$/, '_').replace(/\./g, '/') + this.urlSuffix;
},
/**
* Adds custom ns -> url mapping which does not follow general rules implemented by ns2url().
*
* @param {string} ns - The namespace to be mapped.
* @param {string} url - The url to be mapped onto.
* @param {boolean} noNS - Indicates whether the requested script does not set required namespace.
*
* @description Allows for adding files which do not follow gerneal location pattern
* or do not set namespaces.
* @see method ns2url()
*/
addMapping: function(ns, url, noNs)
{
if (typeof ns !== 'string' || ns.length === 0 || ns === '.') {
throw new TypeError('WN::addMapping(): ns parameter is expected to be a non-zero length string. ' + typeof ns + ': "' + ns + '" + given.');
}
// internally all namespaces start with '.'
if (ns.charAt(0) !== '.') {
ns = '.' + ns;
}
this.nsMapping[ns] = new RequestURL(url, noNs);
},
/**
* Request namespaces from given Request object to be loaded.
*
* @param {object} reqObj - Request object containing required namespaces.
* @return {boolean} - true if the request could be finished, false otherwise.
*
* @description Converts namespaces to urls and request all files that has not been already loaded.
* @see method Request::finish()
*/
request: function(reqObj)
{
var _this = this,
scriptNode,
scriptNodeFirst,
nsObj = reqObj.nsObj,
ns,
urlObj,
url;
for (ns in nsObj) {
urlObj = this.nsMapping[ns] || new RequestURL(this.ns2url(ns));
url = urlObj.url;
// cater for non-namespace requests
// if the url corresponding with the current namespace has been loaded remove the namespace from the request object
if (this.requested[url] === true) {
reqObj.del(ns);
continue;
}
// add request object to the notification list
if (this.required[ns] === undefined) {
this.required[ns] = [];
}
this.required[ns].push(reqObj);
// request include file if not requested before
if (this.requested[url] === undefined) {
this.requested[url] = false;
scriptNode = document.createElement('script');
scriptNode.type = 'text/javascript';
scriptNode.src = url;
scriptNode.async = true;
// provide support for scripts which do not set any namespaces
if (urlObj.noNs) {
scriptNode.onload = (function() {
var _ns = ns,
_url = url;
return function(e) {
_this.requested[_url] = true;
_this.notify.call(_this, _ns);
};
})();
// one browser needs special treatment - have a guess
scriptNode.onreadystatechange = (function() {
var _scriptNode = scriptNode;
return function() {
if (_scriptNode.readyState === 'loaded' || _scriptNode.readyState === 'complete') {
_scriptNode.onreadystatechange = null;
_scriptNode.onload();
}
};
})();
}
scriptNodeFirst = document.getElementsByTagName('script')[0];
scriptNodeFirst.parentNode.insertBefore(scriptNode, scriptNodeFirst);
}
}
// if requested only loaded files then there is nothing to wait for
// otherwise notify() will pick up
return reqObj.finish();
},
/**
* Notifies all Request objects in the collection about new namespace arrival.
*
* @param {string} newNs - The new namespace.
*
* @description Attempts to finish all Requests for the given namespace.
*/
notify: function(newNs)
{
var reqArr,
reqObj;
// anyone interested in the new namespace?
reqArr = this.required[newNs];
if (reqArr !== undefined) {
// go through all request objects subsribed to the current namespace
while (reqObj = reqArr.shift()) {
// remove new namespace from the wish list
reqObj.del(newNs);
// try to finish the current request
reqObj.finish();
}
// everyone notified, list no longer useful
delete this.required[newNs];
}
}
});
/**
* Represents URL to be requested
*
* @constructor
* @param {string} url - The URL
* @param {boolean} noNs - Indicates whether the requested script does not set required namespace.
*/
function RequestURL(url, noNs)
{
if (typeof url !== 'string' || url.length === 0) {
throw new TypeError('RequestURL(): url parameter is expected to be a non-zero length string. ' + typeof url + ': "' + url + '" given.');
}
this.url = url || '';
this.noNs = Boolean(noNs || false);
}
/**
* Represents a request for multiple namespaces
*
* @constructor
* @param {function} callback - The callback function to be called when the request is satisfied
*/
function Request(callback) {
this.nsObj = {};
this.callback = callback;
}
Webnicer.extend(Request.prototype, {
nsObj: null,
callback: null,
finished: false,
length: 0,
/**
* Remove namespace from the collection
*
* @param {string} ns - The namespace to be removed
* @return {boolean} - true if the namespace was deleted, false otherwise (it didn't exist)
*/
del: function(ns)
{
var status = false;
if (this.nsObj.hasOwnProperty(ns)) {
delete this.nsObj[ns];
this.length--;
status = true;
}
return status;
},
/**
* Add a namespace to the collection
*
* @param {string} ns - The namespace to be added
* @return {boolean} - true if the namespace was added, false otherwise (it did exist)
*
* @description If the Request if finished throws an error.
* @see method finish()
*/
add: function(ns)
{
var status = false;
if (this.fihised) {
throw new Error('Request::add(): tried to add the namespace ' + ns + ' to finished Request.');
}
if (!this.nsObj.hasOwnProperty(ns)) {
this.nsObj[ns] = null;
this.length++;
status = true;
}
return status;
},
/**
* Attempts to finish the request
*
* @return {boolean} - true if the request has been finished, false otherwise
* @description Tries to call the callback funcion but only after there are no namespaces left in the collection.
* This method can be called multiple times. Subsequential calls after it is has succeeded will just return true.
*/
finish: function()
{
// run callback only if not finished before and there all requests are satisfied
if (!this.finished && this.length === 0) {
if (typeof this.callback === 'function') {
// don't block the execution thread, callbacks may be heavy
setTimeout(this.callback, 0);
}
this.finished = true;
}
return this.finished;
}
});
wn = Webnicer.sandbox();
wn.noConflict = function () {
global.wn = _wn;
return wn;
};
// add wn to the global namespace
global.wn = wn;
})(window);