-
Notifications
You must be signed in to change notification settings - Fork 0
/
kobayashi.js
986 lines (895 loc) · 37.1 KB
/
kobayashi.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
KOBAYASHI_VERSION = '2009-08-20';
// Debugging tools
;;; function alert_dump(obj, name) {
;;; var s = name ? name + ":\n" : obj.toString ? obj.toString() + ":\n" : '';
;;; for (var i in obj) s += i + ': ' + obj[i] + "\n";
;;; alert(s);
;;; }
function carp() {
try {
$('#debug').append($('<p>').text($.makeArray(arguments).join(' ')));
} catch(e) { }
try {
console.log.apply(this, arguments);
} catch(e) {
try {
console.log(arguments);
} catch(e) { }
}
}
function arr2map(arr) {
var rv = {};
for (var i = 0; i < arr.length; i++) rv[ arr[i] ] = 1;
return rv;
}
var BASE_PATH = window.BASE_URL ? BASE_URL.replace(/\/$/,'') : '';
function prepend_base_path_to(url) {
if (url.charAt(0) == '/') url = BASE_PATH + url;
return url;
}
//// URL management
var URLS = {
/*
test: function($target) {
alert($target.attr('id'));
location.href = 'http://google.com';
}
*/
};
// mapping for containers to the URLs that give out their base content (what's there when the page loads)
var BASES = {};
//// End of URL management
var Kobayashi = {};
// ID of the element where AJAX'ed stuff is placed if no target specified
Kobayashi.DEFAULT_TARGET = 'kobayashi-default-target';
Kobayashi.LOADED_MEDIA = {};
( function($) { $(document).ready( function() {
// We need to remember what URL is loaded in which element,
// so we can load or not load content appropriately on hash change.
var LOADED_URLS = Kobayashi.LOADED_URLS = {};
// We also need to keep track of what's been loaded from a hashchange
// to be able to distinguish what's affected by
// no longer being mentioned in the hash.
var URL_LOADED_BY_HASH = Kobayashi.URL_LOADED_BY_HASH = {};
// If the hash changes before all ajax requests complete,
// we want to cancel the pending requests. MAX_REQUEST is actually the number
// of the last hashchange event. Each ajax request then remembers the state
// of this variable when it was issued and if it's obsolete by the time it's
// finished, the results are discarded. It's OK to discard it because it
// never gets into LOADED_URLS.
var MAX_REQUEST = 0;
// When a sequence of URLs to load into various elements is given,
// the requests are stored in this fifo and their results are
// rendered into the document as they become ready, but always in order.
var LOAD_BUF = [];
// These are the indices into the LOAD_BUF array -- MIN_LOAD is the index
// of the request to be processed next (there should never be any defined
// fields in LOAD_BUF at position less than MIN_LOAD).
// MAX_LOAD should pretty much always be LOAD_BUF.length - 1.
var MIN_LOAD, MAX_LOAD = -1;
// When something is loaded into an element that has no base view (in urls.js),
// and the user hits back, we need to reload. But then we don't want to reload again,
// so keep information about whether we manipulated the content, so we can
// abstain from reloading if we have not.
var PAGE_CHANGED = 0;
function object_empty(o) {
for (var k in o) return false;
return true;
}
function keys(o) {
var rv = [];
for (var k in o) rv.push(k);
return rv;
}
// Returns the closest parent that is a container for a dynamically loaded piece of content, along with some information about it.
function closest_loaded(el) {
while ( el && (!el.id || !LOADED_URLS[ el.id ]) ) {
el = el.parentNode;
}
if (!el) return null;
return { container: el, id: el.id, url: LOADED_URLS[ el.id ], toString: function(){return this.id} };
}
Kobayashi.closest_loaded = closest_loaded;
function inject_content($target, data, address, extras) {
// whatever was loaded inside, remove it from LOADED_URLS
if (!object_empty(LOADED_URLS)) {
var sel = '#'+keys(LOADED_URLS).join(',#');
$target.find(sel).each(function() {
delete LOADED_URLS[ this.id ];
});
}
var redirect_to;
if (redirect_to = extras.xhr.getResponseHeader('Redirect-To')) {
var arg = {};
for (k in extras) { arg[k] = extras[k]; }
delete arg.xhr;
var literal_address;
if (redirect_to.substr(0, BASE_PATH.length) == BASE_PATH) {
literal_address = redirect_to.substr(BASE_PATH.length);
}
else {
literal_address = redirect_to;
}
arg.address = literal_address;
arg.is_priority_request = true;
load_content(arg);
if ( extras.by_hash ) {
var literal_target_id = extras.target_id + '::';
if (extras.target_id == Kobayashi.DEFAULT_TARGET) {
if (location.hash.indexOf('#'+extras.target_id+'::') < 0) {
literal_target_id = '';
}
}
adr(literal_target_id + literal_address, {just_set: true, nohistory: true});
}
}
else {
$target.removeClass('loading').html(data);
if ( ! $target.hasClass('js-noautoshow') ) $target.show();
if (address != undefined) {
LOADED_URLS[ $target.attr('id') ] = address;
if (extras && extras.by_hash) URL_LOADED_BY_HASH[ $target.attr('id') ] = true;
}
if ($.isFunction( extras.success_callback )) try {
extras.success_callback.call(extras);
} catch(e) { carp('Failed success callback (load_content)', e, extras) };
$target.trigger('content_added', extras);
}
$(document).trigger('dec_loading');
PAGE_CHANGED++;
}
// argument is a LOAD_BUF item
function inject_error_message(info) {
if (!info) {
carp('inject_error_message expect an object with target_id, xhr and address. Received:', info);
return;
}
var $target = $('#'+info.target_id);
$target.text(gettext('Rendering error...'));
var response_text = info.xhr.responseText;
var $err_div = $('<div class="error-code"></div>')/*.append(
$('<a>reload</a>').css({display:'block'}).click(function(){
load_content(info);
return false;
})
)*/;
LOADED_URLS[ info.target_id ] = 'ERROR:'+info.address;
try {
$err_div.append( JSON.parse(response_text).message );
} catch(e) {
// Render the income HTML
if (response_text.indexOf('<html') >= 0) {
// Render the HTML document in an <object>
$obj = $(
'<object type="text/html" width="'
+ ($target.width() - 6)
+ '" height="'
+ Math.max($target.height(), 300)
+ '"></object>'
);
function append_error_data() {
$obj.attr({ data:
'data:text/html;base64,'
+ Base64.encode(response_text)
}).appendTo( $err_div );
}
if (window.Base64) {
append_error_data();
}
else {
request_media(MEDIA_URL + 'js/base64.js');
$(document).one('media_loaded', append_error_data);
}
}
else {
$err_div.append( response_text );
}
}
$target.empty().append($err_div);
}
Kobayashi.inject_error_message = inject_error_message;
// get new index to LOAD_BUF (the request queue), either on its end (normal) or at the beginning (is_priority == true)
function alloc_loadbuf(is_priority) {
if (MIN_LOAD == undefined || MAX_LOAD+1 < MIN_LOAD) {
return (MIN_LOAD = ++MAX_LOAD);
}
if (is_priority) {
if (LOAD_BUF[MIN_LOAD] == undefined) return MIN_LOAD;
if (MIN_LOAD <= 0) {
carp(new Error('Cannot make a priority request when request queue is not empty before first request has been finished.'));
return ++MAX_LOAD;
}
return --MIN_LOAD;
}
else {
return ++MAX_LOAD;
}
}
// Check if the least present request has finished and if so, shift it
// from the queue and render the results, and then call itself recursively.
// This effectively renders all finished requests from the first up to the
// first pending one, where it stops. If all requests are finished,
// the queue gets cleaned and the indices reset.
function draw_ready() {
// Slide up to the first defined request or to the end of the queue
while (!LOAD_BUF[ MIN_LOAD ] && LOAD_BUF.length > MIN_LOAD+1) MIN_LOAD++;
// If the queue is empty, clean it
if (!LOAD_BUF[ MIN_LOAD ]) {
// ;;; carp("Emptying buffer");
LOAD_BUF = [];
MIN_LOAD = undefined;
MAX_LOAD = -1;
return;
}
var info = LOAD_BUF[ MIN_LOAD ];
if (!info.data) return; // Not yet ready
delete LOAD_BUF[ MIN_LOAD ];
while (LOAD_BUF.length > MIN_LOAD+1 && !LOAD_BUF[ ++MIN_LOAD ]) {}
var $target = $('#'+info.target_id);
if ($target && $target.jquery && $target.length) {} else {
carp('Could not find target element: #'+info.target_id);
if ($.isFunction(info.error_callback)) try {
info.error_callback.call(info);
} catch(e) { carp('Failed error callback (load_content)', e, info); }
$(document).trigger('dec_loading');
draw_ready();
return;
}
inject_content($target, info.data, info.address, info);
// Check next request
draw_ready();
}
// This removes a request from the queue
function cancel_request( load_id ) {
var info = LOAD_BUF[ load_id ];
delete LOAD_BUF[ load_id ];
$('#'+info.target_id).removeClass('loading');
$(document).trigger('dec_loading');
carp('Failed to load '+info.address+' into '+info.target_id);
}
// Take a container and a URL. Give the container the "loading" class,
// fetch the URL, push the request into the queue, and when it finishes,
// check for requests ready to be loaded into the document.
function load_content(arg) {
var target_id = arg.target_id;
var address = arg.address;
if (target_id == undefined) {
carp('ERROR: Kobayashi.load_content must get target_id field in its argument.');
return;
}
;;; carp('loading '+address+' into #'+target_id);
delete arg.xhr; // just in case there was one
// An empty address means we should revert to the base state.
// If one is not set up for the given container, reload the whole page.
if (address.length == 0) {
if (BASES[ target_id ]) {
address = BASES[ target_id ];
} else {
if (PAGE_CHANGED) location.reload();
return;
}
}
$('#'+target_id).addClass('loading');
$(document).trigger('show_loading');
var url = prepend_base_path_to(address);
url = $('<a>').attr('href', url).get(0).href;
var load_id = alloc_loadbuf(arg.is_priority_request);
LOAD_BUF[ load_id ] = {
target_id: target_id,
address: address
};
if (arg.by_hash) LOAD_BUF[ load_id ].by_hash = arg.by_hash;
$.ajax({
url: url,
type: 'GET',
complete: function(xhr) {
LOAD_BUF[ this.load_id ].xhr = xhr;
if (this.succeeded) {
this._success(xhr);
}
else {
this._error(xhr);
}
},
success: function() { this.succeeded = true; },
error: function() { this.succeeded = false; },
_success: function(xhr) {
if (this.request_no < MAX_REQUEST) {
cancel_request( this.load_id );
}
else {
LOAD_BUF[ this.load_id ].data = xhr.responseText;
}
if (this.success_callback) {
LOAD_BUF[ this.load_id ].success_callback = this.success_callback;
}
if (this.error_callback) {
LOAD_BUF[ this.load_id ].error_callback = this.error_callback;
}
draw_ready();
},
_error: function(xhr) {
inject_error_message( LOAD_BUF[ this.load_id ] );
cancel_request( this.load_id );
$(document).trigger('load_content_failed', [xhr]);
draw_ready();
if ($.isFunction(this.error_callback)) try {
this.error_callback();
} catch(e) { carp('Failed error callback (load_content)', e, this); }
},
load_id: load_id,
request_no: MAX_REQUEST,
success_callback: arg.success_callback,
error_callback: arg.error_callback,
original_options: arg
});
}
Kobayashi.load_content = load_content;
function reload_content(container_id) {
var addr = LOADED_URLS[ container_id ] || '';
load_content({
target_id: container_id,
address: addr
});
}
Kobayashi.reload_content = reload_content;
function unload_content(container_id, options) {
if (!options) options = {};
delete LOADED_URLS[ container_id ];
delete URL_LOADED_BY_HASH[ container_id ];
var $container = $('#'+container_id);
if (!options.keep_content) {
$container.empty();
}
}
Kobayashi.unload_content = unload_content;
// We want location.hash to exactly describe what's on the page.
// #url means that the result of $.get(url) be loaded into the default target div.
// #id::url means that the result of $.get(url) be loaded into the #id element.
// Any number of such specifiers can be concatenated, e.g. #/some/page/#header::/my/header/
// If URLS[ foo ] is set (in urls.js), and #foo is present,
// then the function is called given the $target as argument
// and nothing else is done for this specifier.
function load_by_hash() {
var hash = location.hash.substr(1);
// ;;; carp('load #'+MAX_REQUEST+'; hash: '+hash)
// Figure out what should be reloaded and what not by comparing the requested things with the loaded ones.
var requested = {};
var specifiers = hash.split('#');
var ids_map = {};
var ids_arr = [];
for (var i = 0; i < specifiers.length; i++) {
var spec = specifiers[ i ];
var address = spec;
var target_id = Kobayashi.DEFAULT_TARGET;
if (spec.match(/^([-\w]+)::(.*)/)) {
target_id = RegExp.$1;
address = RegExp.$2;
}
requested[ target_id ] = address;
ids_map[ target_id ] = 1;
ids_arr.push(target_id);
}
for (var k in LOADED_URLS) if (!ids_map[ k ]) {
ids_map[ k ] = 1;
ids_arr.push(k);
}
var is_ancestor = {};
for (var ai = 0; ai < ids_arr.length; ai++) {
for (var di = 0; di < ids_arr.length; di++) {
if (ai == di) continue;
var aid = ids_arr[ai];
var did = ids_arr[di];
var $d = $('#'+did);
if ($d && $d.length) {} else continue;
var $anc = $d.parent().closest('#'+aid);
if ($anc && $anc.length) {
is_ancestor[ aid+','+did ] = 1;
}
}
}
var processed = {};
var reload_target = {};
while (!object_empty(ids_map)) {
// draw an element that's independent on any other in the list
var ids = [];
for (var id in ids_map) ids.push(id);
var indep;
for (var i = 0; i < ids.length; i++) {
var top_el_id = ids[i];
var is_independent = true;
for (var j = 0; j < ids.length; j++) {
var low_el_id = ids[j];
if (low_el_id == top_el_id) continue;
if (is_ancestor[ low_el_id + ',' + top_el_id ]) {
is_independent = false;
break;
}
}
if (is_independent) {
indep = top_el_id;
delete ids_map[ top_el_id ];
break;
}
}
if (!indep) {
carp(ids_map);
throw('Cyclic graph of elements???');
}
var result = {};
for (var par in processed) {
// if we went over an ancestor of this element
if (is_ancestor[ par+','+indep ]) {
// and we marked it for reload
if (processed[ par ].to_reload) {
// and we're not just recovering
if (requested[ indep ]) {
// then reload no matter if url changed or not
result.to_reload = true;
break;
}
else {
// no need to recover when parent gets reloaded
result.to_reload = false;
break;
}
}
}
}
// If parent didn't force reload or delete,
if (result.to_reload == undefined) {
// and the thing is no longer requested and we don't have the base loaded,
if (!requested[ indep ] && LOADED_URLS[ indep ] != '') {
// and it's been loaded via URL hash change
if (URL_LOADED_BY_HASH[ indep ]) {
// then reload the base
result.to_reload = 1;
}
else {
// else prevent it from being reloaded
result.to_reload = false;
}
}
}
if (result.to_reload == undefined) {
// If the requested url changed,
if (requested[ indep ] != LOADED_URLS[ indep ]) {
// mark for reload
result.to_reload = 1;
}
}
// If we want to reload but no URL is set, default to the base
if (result.to_reload && !requested[ indep ]) {
requested[ indep ] = '';
}
processed[ indep ] = result;
}
// Now we figured out what to reload:
// The things that are in requested AND that have processed[ $_ ].to_reload set to a true value
for (var target_id in requested) {
if (!processed[ target_id ].to_reload) {
continue;
}
var address = requested[ target_id ];
// A specially treated specifier. The callback should set up LOADED_URLS properly.
// FIXME: Rewrite
if (URLS[address]) {
URLS[address](target_id);
continue;
}
load_content({
target_id: target_id,
address: address,
by_hash: true
});
}
}
// Fire hashchange event when location.hash changes
window.CURRENT_HASH = '';
$(document).bind('hashchange', function() {
// carp('hash: ' + location.hash);
MAX_REQUEST++;
$('.loading').removeClass('loading');
$(document).trigger('hide_loading');
load_by_hash();
});
setTimeout( function() {
try {
if (location.hash != CURRENT_HASH) {
CURRENT_HASH = location.hash;
$(document).trigger('hashchange');
}
} catch(e) { carp(e); }
setTimeout(arguments.callee, 50);
}, 50);
// End of hash-driven content management
// Loads stuff from an URL to an element like load_by_hash but:
// - Only one specifier (id-url pair) can be given.
// - URL hash doesn't change.
// - The specifier is interpreted by adr to get the URL from which to ajax.
// This results in support of relative addresses and the target_id::rel_base::address syntax.
function simple_load(specifier) {
var target_id;
var colon_index = specifier.indexOf('::');
if (colon_index < 0) {
target_id = Kobayashi.DEFAULT_TARGET;
}
else {
target_id = specifier.substr(0, colon_index);
}
var address = get_hashadr(specifier);
if (LOADED_URLS[target_id] == address) {
$('#'+target_id).slideUp('fast');
unload_content(target_id);
return;
}
load_content({target_id:target_id, address:address});
}
Kobayashi.simple_load = simple_load;
// Set up event handlers
$('.js-simpleload,.js-simpleload-container a').live('click', function(evt) {
if (evt.button != 0) return true; // just interested in left button
if ( $(this).data('hashadred') ) return true;
simple_load($(this).attr('href'));
$(this).data('simpleloaded', true);
evt.preventDefault();
});
$('.js-hashadr,.js-hashadr-container a').live('click', function(evt) {
if (evt.button != 0) return true; // just interested in left button
if ( $(this).data('simpleloaded') ) return true;
if ($(this).is('.js-nohashadr')) return true; // override hashadr-container
adr($(this).attr('href'));
$(this).data('hashadred', true);
evt.preventDefault();
});
})})(jQuery);
// Manipulate the hash address.
//
// We use http://admin/#/foo/ instead of http://admin/foo/.
// Therefore, <a href="bar/"> won't lead to http://admin/#/foo/bar/ as we need but to http://admin/bar/.
// To compensate for this, use <a href="javascript:adr('bar/')> instead.
// adr('id::bar/') can be used too.
//
// adr('bar/#id::baz/') is the same as adr('bar/'); adr('id::baz/').
// Absolute paths and ?var=val strings work too.
//
// Alternatively, you can use <a href="bar/" class="js-hashadr">.
// The js-hashadr class says clicks should be captured and delegated to function adr.
// A third way is to encapsulate a link (<a>) into a .js-hashadr-container element.
//
// The target_id::rel_base::address syntax in a specifier means that address is taken as relative
// to the one loaded to rel_base and the result is loaded into target_id.
// For example, suppose that location.hash == '#id1::/foo/'. Then calling
// adr('id2::id1::bar/') would be like doing location.hash = '#id1::/foo/#id2::/foo/bar/'.
//
// The second argument is an object where these fields are recognized:
// - hash: a custom hash string to be used instead of location.hash,
// - just_get: 'address' Instructs the function to merely return the modified address (without the target_id).
// - just_get: 'hash' Instructs the function to return the modified hash instead of applying it to location.
// Using this option disables the support of multiple '#'-separated specifiers.
// Other than the first one are ignored.
// - just_set: Instructs the function to change the URL without triggering the hashchange event.
// - nohistory: When true, the change of the location will not create a record in the browser history
// (location.replace will be used).
// - _hash_preproc: Internal. Set when adr is used to preprocess the hash
// to compensate for hash and LOADED_URLS inconsistencies.
function adr(address, options) {
if (address == undefined) {
carp('No address given to adr()');
return;
}
function set_location_hash(newhash, options) {
if (newhash.charAt(0) != '#') newhash = '#' + newhash;
if (options.just_set) {
CURRENT_HASH = newhash;
}
if (options.nohistory) {
location.replace( newhash );
}
else {
location.hash = newhash;
}
}
// '#' chars in the address separate invividual requests for hash modification.
// First deal with the first one and then recurse on the subsequent ones.
if (address.charAt(0) == '#') address = address.substr(1);
var hashpos = (address+'#').indexOf('#');
var tail = address.substr(hashpos+1);
address = address.substr(0, hashpos);
if (!options) options = {};
var hash = (options.hash == undefined) ? location.hash : options.hash;
// Figure out which specifier is concerned.
var target_id = '';
// But wait, if target_id::rel_base::address was specified,
// then get the modifier address and insert it then as appropriate.
var new_address, reg_res;
if (reg_res = address.match(/([-\w]*)::([-\w]*)::(.*)/)) {
var rel_base;
target_id = reg_res[1];
rel_base = reg_res[2];
address = reg_res[3];
if (rel_base.length) rel_base += '::';
new_address = adr(rel_base+address, {hash:hash, just_get:'address'})
if (options.just_get == 'address') return new_address;
}
// OK, go on figuring out which specifier is concerned.
else if (reg_res = address.match(/([-\w]*)::(.*)/)) {
target_id = reg_res[1];
address = reg_res[2];
}
// If no hash is present, simply use the address.
if (hash.length <= 1) {
var newhash;
if (target_id.length == 0) {
newhash = address;
}
else {
newhash = target_id + '::' + address
}
if (options.just_get == 'address') return address;
if (options.just_get == 'hash') return newhash;
else {
set_location_hash(newhash, options);
return;
}
}
// In case we're modifying a target that has something loaded
// in it which is not in the hash, correct it first
if (!options._hash_preproc) {
var acc2hash = get_hashadr(target_id+'::', {_hash_preproc:true});
var acc2record = Kobayashi.LOADED_URLS[ target_id ];
if (acc2record && acc2hash != acc2record) {
hash = get_hash(target_id+'::'+acc2record, {_hash_preproc:true});
}
}
// Figure out the span in the current hash where the change applies.
var start = 0;
var end;
var specifier_prefix = '';
if (target_id.length == 0) {
for (; start >= 0; start = hash.indexOf('#', start+1)) {
end = (hash+'#').indexOf('#', start+1);
if (hash.substring(start, end).indexOf('::') < 0) {
start++;
break;
}
}
if (start < 0) {
hash += '#';
start = end = hash.length;
}
}
else {
var idpos = hash.indexOf(target_id+'::');
if (idpos == -1) {
hash += '#';
start = end = hash.length;
specifier_prefix = target_id + '::';
}
else {
start = idpos + target_id.length + '::'.length;
end = (hash+'#').indexOf('#', start);
}
}
// Now, hash.substring(start,end) is the address we need to modify.
// Figure out whether we replace the address, append to it, or what.
// Move start appropriately to denote where the part to replace starts.
var newhash;
var addr_start = start;
var old_address = hash.substring(start,end);
// We've not gotten the address from a previous recursive call, thus modify the address as needed.
if (new_address == undefined) {
new_address = address;
// empty address -- remove the specifier
if (address.length == 0) {
// but in case of just_get:address, return the original address for the container (relative "")
if (options.just_get == 'address') new_address = hash.substring(start,end);
start = hash.lastIndexOf('#',start);
start = Math.max(start,0);
addr_start = start;
}
// absolute address -- replace what's in there.
else if (address.charAt(0) == '/') {
}
// set a get parameter
else if (address.charAt(0) == '&') {
var qstart = old_address.indexOf('?');
if (qstart < 0) qstart = old_address.length;
var oldq = old_address.substr(qstart);
var newq = oldq;
if (oldq.length == 0) {
newq = '?' + address.substr(1);
}
else {
var assignments = address.substr(1).split(/&/);
for (var i = 0; i < assignments.length; i++) {
var ass = assignments[i];
var vname = (ass.indexOf('=') < 0) ? ass : ass.substr(0, ass.indexOf('='));
if (vname.length == 0) {
carp('invalid assignment: ' + ass);
continue;
}
var vname_esc = vname.replace(/\W/g, '\\$1');
var vname_re = new RegExp('(^|[?&])' + vname_esc + '(?:=[^?&]*)?(&|$)');
var changedq = newq.replace(vname_re, '\$1' + ass + '\$2');
// vname was not in oldq -- append
// the second condition is there so that when we have ?v and call &v we won't get ?v&v but still ?v
if (changedq == newq && !vname_re.test(newq)) {
newq = newq + '&' + ass;
}
else {
newq = changedq;
}
}
}
new_address = old_address.substr(0, qstart) + newq;
}
// relative address -- append to the end, but no farther than to a '?'
else {
var left_anchor = hash.lastIndexOf('#', start)+1;
start = (hash.substr(0, end)+'?').indexOf('?', start);
// cut off the directories as appropriate when the address starts with ../
while (new_address.substr(0,3) == '../' && hash.substring(left_anchor,start-1).indexOf('/') >= 0) {
new_address = new_address.substr(3);
start = hash.lastIndexOf('/', start-2)+1;
}
}
}
newhash = hash.substr(0, start) + specifier_prefix + new_address + hash.substr(end);
if (options.just_get == 'address') {
return hash.substring(addr_start, start) + new_address;
}
else if (tail) {
return adr(tail, {hash:newhash});
}
else if (options.just_get == 'hash') {
return newhash;
}
else {
set_location_hash(newhash, options);
}
}
// returns address for use in hash, i.e. without BASE_PATH
function get_hashadr(address, options) {
if (!options) options = {};
options.just_get = 'address';
return adr(address, options);
}
// returns address for use in requests, i.e. with BASE_PATH prepended
function get_adr(address, options) {
var hashadr = get_hashadr(address, options);
// if (hashadr.charAt(0) != '/') hashadr = get_hashadr(hashadr);
return prepend_base_path_to(hashadr);
}
// returns the hash instead of assigning it to location
function get_hash(address, options) {
if (!options) options = {};
options.just_get = 'hash';
return adr(address, options);
}
// Dynamic media (CSS, JS) loading
(function() {
// Get an URL to a CSS or JS file, attempt to load it into the document and call callback on success.
function load_media(url, callbacks) {
var succ_fn = callbacks.succ_fn;
var err_fn = callbacks.err_fn;
var next_fn = callbacks.next_fn;
if (Kobayashi.LOADED_MEDIA[ url ]) {
if ($.isFunction(succ_fn)) succ_fn(url);
if ($.isFunction(next_fn)) next_fn(url);
;;; carp('Skipping loaded medium: '+url);
return true;
}
;;; carp('loading medium '+url);
url.match(/(?:.*\/\/[^\/]*)?([^?]+)(?:\?.*)?/);
$(document).data('loaded_media')[ RegExp.$1 ] = url;
if (url.match(/\.(\w+)(?:$|\?)/))
var ext = RegExp.$1;
else throw('Unexpected URL format: '+url);
var abs_url = $('<a>').attr({href:url}).get(0).href;
function stylesheet_present(url) {
for (var i = 0; i < document.styleSheets.length; i++) {
if (document.styleSheets[i].href == url) return document.styleSheets[i];
}
return false;
}
function get_css_rules(stylesheet) {
try {
if (stylesheet.cssRules) return stylesheet.cssRules;
if (stylesheet.rules ) return stylesheet.rules;
} catch(e) { carp(e); }
carp('Could not get rules from: ', stylesheet);
return;
}
if (ext == 'css') {
if (stylesheet_present(abs_url)) {
if ($.isFunction(succ_fn)) succ_fn(url);
if ($.isFunction(next_fn)) next_fn(url);
;;; carp('Stylesheet already present: '+url);
return true;
}
var tries = 100;
setTimeout(function() {
if (--tries < 0) {
Kobayashi.LOADED_MEDIA[ url ] = false;
carp('Timed out loading CSS: '+url);
if ($.isFunction(err_fn)) err_fn(url);
return;
}
var ss;
if (ss = stylesheet_present(abs_url)) {
var rules = get_css_rules(ss);
if (rules && rules.length) {
Kobayashi.LOADED_MEDIA[ url ] = true;
if ($.isFunction(succ_fn)) succ_fn(url);
;;; carp('CSS Successfully loaded: '+url);
}
else {
Kobayashi.LOADED_MEDIA[ url ] = false;
if (rules) carp('CSS stylesheet empty.');
if ($.isFunction(err_fn)) err_fn(url);
return;
}
}
else setTimeout(arguments.callee, 100);
}, 100);
var $csslink = $('<link rel="stylesheet" type="text/css" href="'+url+'" />').appendTo($('head'));
if ($.isFunction(next_fn)) next_fn(url);
return $csslink;
}
else if (ext == 'js') {
var $scripts = $('script');
for (var i = 0; i < $scripts.length; i++) {
if ($scripts.get(i).src == abs_url) {
if ($.isFunction(succ_fn)) succ_fn(url);
if ($.isFunction(next_fn)) next_fn(url);
;;; carp('Script already present: '+url);
return true;
}
}
return $.ajax({
url: url,
type: 'GET',
dataType: 'script',
success: function() {
Kobayashi.LOADED_MEDIA[ this.url ] = true;
if ($.isFunction(succ_fn)) succ_fn(url);
if ($.isFunction(next_fn)) next_fn(url);
;;; carp('JS Successfully loaded: '+this.url);
},
error: function() {
Kobayashi.LOADED_MEDIA[ this.url ] = false;
if ($.isFunction( err_fn)) err_fn(url);
if ($.isFunction(next_fn)) next_fn(url);
carp('Failed to load JS: '+url, this);
},
cache: true
});
}
else throw('Unrecognized media type "'+ext+'" in URL: '+url);
}
Kobayashi.load_media = load_media;
var media_queue = [];
$(document).data('loaded_media', {});
function init_media() {
$(document).trigger('media_loaded').data('loaded_media', {});
}
function draw_media() {
if (media_queue.length == 0) {
init_media();
return true;
}
var url = media_queue.shift();
load_media(url, {next_fn: draw_media});
}
// Load a CSS / JavaScript file (given an URL) after previously requested ones have been loaded / failed loading.
function request_media(url) {
var do_start = media_queue.length == 0;
media_queue.push(url);
if (do_start) {
setTimeout(draw_media,20);
$(document).trigger('media_loading_start');
}
}
window.request_media = request_media;
})();