/[suikacvs]/webroot/www/js/jste/tutorial.js
Suika

Contents of /webroot/www/js/jste/tutorial.js

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.33 - (show annotations) (download) (as text)
Tue Feb 3 12:55:47 2009 UTC (16 years, 6 months ago) by wakaba
Branch: MAIN
CVS Tags: HEAD
Changes since 1.32: +28 -3 lines
File MIME type: application/javascript
defer the tutorial initialization until the simpleballoon style sheet has been loaded, maybe this will prevent ie's vml from renderred poorly

1 if (typeof (JSTE) === "undefined") var JSTE = {};
2
3 JSTE.WATNS = 'http://suika.fam.cx/ns/wat';
4 JSTE.SpaceChars = /[\x09\x0A\x0C\x0D\x20]+/;
5
6 JSTE.Class = function (constructor, prototype) {
7 return JSTE.Subclass (constructor, JSTE.EventTarget, prototype);
8 }; // Class
9
10 JSTE.Class.addClassMethods = function (classObject, methods) {
11 new JSTE.Hash (methods).forEach (function (n, v) {
12 if (!classObject[n]) {
13 classObject[n] = v;
14 }
15 });
16 }; // addClassMethods
17
18 JSTE.Subclass = function (constructor, superclass, prototype) {
19 constructor.prototype = new superclass;
20 for (var n in prototype) {
21 constructor.prototype[n] = prototype[n];
22 }
23 constructor.prototype.constructor = constructor;
24 constructor.prototype._super = superclass;
25 return constructor;
26 }; // Subclass
27
28 JSTE.EventTarget = new JSTE.Subclass (function () {
29
30 }, function () {}, {
31 addEventListener: function (eventType, handler, useCapture) {
32 if (useCapture) return;
33 if (!this.eventListeners) this.eventListeners = {};
34 if (!this.eventListeners[eventType]) {
35 this.eventListeners[eventType] = new JSTE.List;
36 }
37 this.eventListeners[eventType].push (handler);
38 }, // addEventListener
39 removeEventListener: function (eventType, handler, useCapture) {
40 if (useCapture) return;
41 if (!this.eventListeners) return;
42 if (!this.eventListeners[eventType]) return;
43 this.eventListeners[eventType].remove (handler);
44 }, // removeEventListener
45 dispatchEvent: function (e) {
46 if (!this.eventListeners) return;
47 var handlers = this.eventListeners[e.type];
48 if (!handlers) return;
49 e.currentTarget = this;
50 e.target = this;
51 var preventDefault;
52 handlers.forEach (function (handler) {
53 if (handler.apply (this, [e])) {
54 preventDefault = true;
55 }
56 });
57 return preventDefault || e.isDefaultPrevented ();
58 } // dispatchEvent
59 }); // EventTarget
60
61 JSTE.Event = new JSTE.Class (function (eventType, canBubble, cancelable) {
62 this.type = eventType;
63 this.bubbles = canBubble;
64 this.cancelable = cancelable;
65 }, {
66 preventDefault: function () {
67 this.defaultPrevented = true;
68 }, // preventDefault
69 isDefaultPrevented: function () {
70 return this.defaultPrevented;
71 } // isDefaultPrevented
72 });
73
74 JSTE.Observer = new JSTE.Class (function (eventType, target, onevent) {
75 this.eventType = eventType;
76 this.target = target;
77 if (target.addEventListener) {
78 this.code = onevent;
79 } else if (target.attachEvent) {
80 this.code = function () {
81 onevent (event);
82 };
83 } else {
84 this.code = onevent;
85 }
86 this.disabled = true;
87 this.start ();
88 }, {
89 start: function () {
90 if (!this.disabled) return;
91 if (this.target.addEventListener) {
92 this.target.addEventListener (this.eventType, this.code, false);
93 this.disabled = false;
94 } else if (this.target.attachEvent) {
95 this.target.attachEvent ("on" + this.eventType, this.code);
96 this.disabled = false;
97 }
98 }, // start
99 stop: function () {
100 if (this.disabled) return;
101 if (this.target.removeEventListener) {
102 this.target.removeEventListener (this.eventType, this.code, false);
103 this.disabled = true;
104 } else if (this.target.detachEvent) {
105 this.target.detachEvent ("on" + this.eventType, this.code);
106 this.disabled = true;
107 }
108 } // stop
109 }); // Observer
110
111 new JSTE.Observer ('load', window, function () {
112 JSTE.windowLoaded = true;
113 });
114
115
116
117 JSTE.Hash = new JSTE.Class (function (hash) {
118 this.hash = hash || {};
119 }, {
120 forEach: function (code) {
121 for (var n in this.hash) {
122 var r = code (n, this.hash[n]);
123 if (r && r.stop) break;
124 }
125 }, // forEach
126 clone: function (code) {
127 var newHash = {};
128 this.forEach (function (n, v) {
129 newHash[n] = v;
130 });
131 return new this.constructor (newHash);
132 }, // clone
133
134 getNamedItem: function (n) {
135 return this.hash[n];
136 }, // getNamedItem
137 setNamedItem: function (n, v) {
138 return this.hash[n] = v;
139 }, // setNamedItem
140
141 getNames: function () {
142 var r = new JSTE.List;
143 for (var n in this.hash) {
144 r.push (n);
145 }
146 return r;
147 }, // getNames
148
149 getByNames: function (names) {
150 var self = this;
151 return names.forEach (function (name) {
152 var value = self.getNamedItem (name);
153 if (value != null) {
154 return new JSTE.List.Return (value);
155 } else {
156 return null;
157 }
158 });
159 } // getByNames
160 }); // Hash
161
162
163 JSTE.List = new JSTE.Class (function (arrayLike) {
164 this.list = arrayLike || [];
165 }, {
166 getLast: function () {
167 if (this.list.length) {
168 return this.list[this.list.length - 1];
169 } else {
170 return null;
171 }
172 }, // getLast
173
174 forEach: function (code) {
175 var length = this.list.length;
176 for (var i = 0; i < length; i++) {
177 var r = code (this.list[i]);
178 if (r && r.stop) return r.returnValue;
179 }
180 return null;
181 }, // forEach
182 map: function (code) {
183 var newList = new this.constructor;
184 var length = this.list.length;
185 for (var i = 0; i < length; i++) {
186 newList.push (code (this.list[i]));
187 }
188 return newList;
189 }, // map
190 mapToHash: function (code) {
191 var newHash = new JSTE.Hash;
192 var length = this.list.length;
193 for (var i = 0; i < length; i++) {
194 var nv = code (this.list[i]);
195 newHash.setNamedItem (nv[0], nv[1]);
196 }
197 return newHash;
198 }, // mapToHash
199
200 numberToInteger: function () {
201 var newList = [];
202 this.forEach (function (item) {
203 if (typeof item === "number") {
204 newList.push (Math.floor (item));
205 } else {
206 newList.push (item);
207 }
208 });
209 return new this.constructor (newList);
210 }, // numberToInteger
211
212 clone: function () {
213 var newList = [];
214 this.forEach (function (item) {
215 newList.push (item);
216 });
217 return new this.constructor (newList);
218 }, // clone
219
220 grep: function (code) {
221 var newList = [];
222 this.forEach (function (item) {
223 if (code (item)) {
224 newList.push (item);
225 }
226 });
227 return new this.constructor (newList);
228 }, // grep
229 onlyNonNull: function () {
230 return this.grep (function (item) {
231 return item != null; /* Intentionally "!=" */
232 });
233 }, // onlyNonNull
234
235 uniq: function (eq) {
236 if (!eq) eq = function (i1, i2) { return i1 === i2 };
237 var prevItems = [];
238 return this.grep (function (item) {
239 for (var i = 0; i < prevItems.length; i++) {
240 if (eq (item, prevItems[i])) {
241 return false;
242 }
243 }
244 prevItems.push (item);
245 return true;
246 });
247 }, // uniq
248
249 getFirstMatch: function (code) {
250 return this.forEach (function (item) {
251 if (code (item)) {
252 return new JSTE.List.Return (item);
253 }
254 });
255 }, // getFirstMatch
256
257 switchByElementType: function () {
258 var cases = new JSTE.List (arguments);
259 this.forEach (function (n) {
260 cases.forEach (function (c) {
261 if (c.namespaceURI == n.namespaceURI) {
262 return new JSTE.List.Return (c.execute (n));
263 }
264 });
265 });
266 }, // switchByElementType
267
268 // destructive
269 push: function (item) {
270 this.list.push (item);
271 }, // push
272
273 // destructive
274 pushCloneOfLast: function () {
275 this.list.push (this.getLast ().clone ());
276 }, // pushCloneOfLast
277
278 // destructive
279 append: function (list) {
280 var self = this;
281 list.forEach (function (n) {
282 self.list.push (n);
283 });
284 return this;
285 }, // append
286
287 // destructive
288 pop: function () {
289 return this.list.pop ();
290 }, // pop
291
292 // destructive
293 remove: function (removedItem) {
294 var length = this.list.length;
295 for (var i = length - 1; i >= 0; --i) {
296 var item = this.list[i];
297 if (item == removedItem) { // Intentionally "=="
298 this.list.splice (i, 1);
299 }
300 }
301 }, // remove
302
303 // destructive
304 clear: function () {
305 this.list.splice (0, this.list.length);
306 } // clear
307
308 }); // List
309
310 JSTE.Class.addClassMethods (JSTE.List, {
311 spaceSeparated: function (v) {
312 return new JSTE.List ((v || '').split (JSTE.SpaceChars)).grep (function (v) {
313 return v.length;
314 });
315 }, // spaceSeparated
316
317 getCommonItems: function (l1, l2, cb, eq) {
318 if (!eq) eq = function (i1, i2) { return i1 === i2 };
319
320 var common = new JSTE.List;
321
322 l1 = l1.grep (function (i1) {
323 var hasI1;
324 l2 = l2.grep (function (i2) {
325 if (eq (i1, i2)) {
326 common.push (i1);
327 hasI1 = true;
328 return false;
329 } else {
330 return true;
331 }
332 });
333 return !hasI1;
334 });
335
336 cb (common, l1, l2);
337 } // getCommonItems
338 }); // List class methods
339
340 JSTE.List.Return = new JSTE.Class (function (rv) {
341 this.stop = true;
342 this.returnValue = rv;
343 }, {
344
345 }); // Return
346
347 JSTE.List.SwitchByLocalName = new JSTE.Class (function (ns, cases, ow) {
348 this.namespaceURI = ns;
349 this.cases = cases;
350 this.otherwise = ow || function (n) { };
351 }, {
352 execute: function (n) {
353 for (var ln in this.cases) {
354 if (JSTE.Element.matchLocalName (n, ln)) {
355 return this.cases[ln] (n);
356 }
357 }
358 return this.otherwise (n);
359 }
360 });
361
362 JSTE.Set = {};
363
364 JSTE.Set.Unordered = new JSTE.Class (function () {
365 // this.caseInsensitive
366 }, {
367 addFromList: function (list) {
368 var self = this;
369 list.forEach (function (name) {
370 if (self.caseInsensitive) name = name.toLowerCase ();
371 self['value-' + name] = true;
372 });
373 }, // addFromList
374
375 has: function (name) {
376 if (this.caseInsensitive) name = name.toLowerCase ();
377 return this['value-' + name] !== undefined;
378 } // has
379 }); // Unordered
380
381
382 if (!JSTE.Node) JSTE.Node = {};
383
384 JSTE.Class.addClassMethods (JSTE.Node, {
385 querySelector: function (node, selectors) {
386 if (node.querySelector) {
387 var el;
388 try {
389 el = node.querySelector (selectors);
390 } catch (e) {
391 el = null;
392 }
393 return el;
394 } else if (window.uu && uu.css) {
395 if (selectors != "") {
396 /* NOTE: uu.css return all elements for "" or ",xxx" */
397 return uu.css (selectors, node)[0];
398 } else {
399 return null;
400 }
401 } else if (window.Ten && Ten.DOM && Ten.DOM.getElementsBySelector) {
402 return Ten.DOM.getElementsBySelector (selectors)[0];
403 } else {
404 return null;
405 }
406 }, // querySelector
407 querySelectorAll: function (node, selectors) {
408 if (node.querySelectorAll) {
409 var nl;
410 try {
411 nl = node.querySelectorAll (selectors);
412 } catch (e) {
413 nl = null;
414 }
415 return new JSTE.List (nl);
416 } else if (window.uu && uu.css) {
417 if (selectors != "") {
418 /* NOTE: uu.css return all elements for "" or ",xxx". */
419 return new JSTE.List (uu.css (selectors, node));
420 } else {
421 return new JSTE.List;
422 }
423 } else if (window.Ten && Ten.DOM && Ten.DOM.getElementsBySelector) {
424 return new JSTE.List (Ten.DOM.getElementsBySelector (selectors));
425 } else {
426 return new JSTE.List;
427 }
428 } // querySelectorAll
429 });
430
431 JSTE.Document = {};
432
433 JSTE.Class.addClassMethods (JSTE.Document, {
434 getTheHTMLElement: function (doc) {
435 var el = doc.documentElement;
436 // BUG: XML support
437 if (el.nodeName.toUpperCase () == 'HTML') {
438 return el;
439 } else {
440 return null;
441 }
442 }, // getTheHTMLElement
443 getTheHeadElement: function (doc) {
444 // BUG: XML support
445 var el = JSTE.Document.getTheHTMLElement (doc);
446 if (!el) return null;
447 var elc = el.childNodes;
448 for (i = 0; i < elc.length; i++) {
449 var cel = elc[i];
450 if (cel.nodeName.toUpperCase () == 'HEAD') {
451 return cel;
452 }
453 }
454 return null;
455 }, // getTheHeadElement
456
457 getClassNames: function (doc) {
458 // BUG: XML support
459 var r = new JSTE.Set.Unordered ();
460 r.caseInsensitive = doc.compatMode != 'CSS1Compat';
461
462 var docEl = doc.documentElement;
463 if (docEl) {
464 r.addFromList (JSTE.List.spaceSeparated (docEl.className));
465 }
466
467 var bodyEl = doc.body;
468 if (bodyEl) {
469 r.addFromList (JSTE.List.spaceSeparated (bodyEl.className));
470 }
471
472 return r;
473 } // getClassNames
474 }); // JSTE.Document class methods
475
476 if (!JSTE.Element) JSTE.Element = {};
477
478 JSTE.Class.addClassMethods (JSTE.Element, {
479 getLocalName: function (el) {
480 var localName = el.localName;
481 if (!localName) {
482 localName = el.nodeName;
483 if (el.prefix) {
484 localName = localName.substring (el.prefix.length + 1);
485 }
486 }
487 return localName;
488 }, // getLocalName
489
490 match: function (el, ns, ln) {
491 if (el.nodeType !== 1) return false;
492 if (el.namespaceURI !== ns) return false;
493 return JSTE.Element.matchLocalName (el, ln);
494 }, // match
495 matchLocalName: function (el, ln) {
496 var localName = JSTE.Element.getLocalName (el);
497 if (ln instanceof RegExp) {
498 if (!localName.match (ln)) return false;
499 } else {
500 if (localName !== ln) return false;
501 }
502 return true;
503 }, // matchLocalName
504
505 getChildElement: function (el, ns, ln) {
506 return new JSTE.List (el.childNodes).getFirstMatch (function (item) {
507 return JSTE.Element.match (item, ns, ln);
508 });
509 }, // getChildElement
510 getChildElements: function (el, ns, ln) {
511 return new JSTE.List (el.childNodes).grep (function (item) {
512 return JSTE.Element.match (item, ns, ln);
513 });
514 }, // getChildElements
515 getChildrenClassifiedByType: function (el) {
516 var r = new JSTE.ElementHash;
517 new JSTE.List (el.childNodes).forEach (function (n) {
518 if (n.nodeType == 1) {
519 r.getOrCreate (n.namespaceURI, JSTE.Element.getLocalName (n)).push (n);
520 } else {
521 r.getOrCreate (null, n.nodeType).push (n);
522 }
523 });
524 return r;
525 }, // getChildrenClassifiedByType
526
527 isEmpty: function (el) {
528 // HTML5 definition of "empty"
529 return !new JSTE.List (el.childNodes).forEach (function (n) {
530 var nt = n.nodeType;
531 if (nt == 1) {
532 return new JSTE.List.Return (true /* not empty */);
533 } else if (nt == 3 || nt == 4) {
534 if (/[^\u0009\u000A\u000C\u000D\u0020]/.test (n.data)) {
535 return new JSTE.List.Return (true /* not empty */);
536 }
537 } else if (nt == 7 || nt == 8) { // comment/pi
538 // does not affect emptyness
539 } else {
540 // We don't support EntityReference.
541 return new JSTE.List.Return (true /* not empty */);
542 }
543 });
544 }, // isEmpty
545
546 appendText: function (el, s) {
547 return el.appendChild (el.ownerDocument.createTextNode (s));
548 }, // appendText
549
550 appendToHead: function (el) {
551 var doc = el.ownerDocument;
552 var head = JSTE.Document.getTheHeadElement (doc) || doc.body || doc.documentElement || doc;
553 head.appendChild (el);
554 }, // appendToHead
555
556 createTemplate: function (doc, node) {
557 var df = doc.createDocumentFragment ();
558 new JSTE.List (node.childNodes).forEach (function (n) {
559 if (n.nodeType == 1) {
560 var c = doc.createElement (JSTE.Element.getLocalName (n));
561 new JSTE.List (n.attributes).forEach (function (n) {
562 c.setAttribute (n.name, n.value);
563 });
564 c.appendChild (JSTE.Element.createTemplate (doc, n));
565 df.appendChild (c);
566 } else if (n.nodeType == 3 || n.nodeType == 4) {
567 df.appendChild (doc.createTextNode (n.data));
568 }
569 });
570 return df;
571 }, // createTemplate
572
573 hasAttribute: function (el, localName) {
574 if (el.hasAttribute) {
575 return el.hasAttribute (localName);
576 } else {
577 return el.getAttribute (localName) != null;
578 }
579 }, // hasAttribute
580
581 getClassNames: function (el) {
582 return new JSTE.List (el.className.split (JSTE.SpaceChars));
583 }, // getClassNames
584 addClassName: function (el, newClassName) {
585 el.className = el.className + ' ' + newClassName;
586 }, // deleteClassName
587 deleteClassName: function (el, oldClassName) {
588 var classNames = el.className.split (JSTE.SpaceChars);
589 var newClasses = [];
590 for (var n in classNames) {
591 if (classNames[n] != oldClassName) {
592 newClasses.push (classNames[n]);
593 }
594 }
595 el.className = newClasses.join (' ');
596 }, // deleteClassName
597 replaceClassName: function (el, oldClassName, newClassName) {
598 var classNames = el.className.split (JSTE.SpaceChars);
599 var newClasses = [newClassName];
600 for (var n in classNames) {
601 if (classNames[n] != oldClassName) {
602 newClasses.push (classNames[n]);
603 }
604 }
605 el.className = newClasses.join (' ');
606 }, // replaceClassName
607
608 getIds: function (el) {
609 return new JSTE.List (el.id != "" ? [el.id] : []);
610 }, // getIds
611
612 /*
613 NR.js <http://suika.fam.cx/www/css/noderect/NodeRect.js> must be loaded
614 before the invocation.
615 */
616 scroll: function (elements) {
617 if (!JSTE.windowLoaded) {
618 new JSTE.Observer ('load', window, function () {
619 JSTE.Element.scroll (elements);
620 });
621 return;
622 }
623
624 var top = Infinity;
625 var left = Infinity;
626 var topEl;
627 var leftEl;
628 elements.forEach (function (el) {
629 var rect = NR.Element.getRects (el, window).borderBox;
630 if (rect.top < top) {
631 top = rect.top;
632 topEl = el;
633 }
634 if (rect.left < left) {
635 left = rect.left;
636 leftEl = el;
637 }
638 });
639
640 if (!leftEl && !topEl) {
641 return;
642 }
643
644 var doc = (leftEl || topEl).ownerDocument;
645
646 var rect = NR.View.getViewportRects (window, doc).contentBox;
647 if (rect.top <= top && top <= rect.bottom) {
648 top = rect.top;
649 }
650 if (rect.left <= left && left <= rect.right) {
651 left = rect.left;
652 }
653
654 /*
655 Set scroll* of both <html> and <body> elements, to support all of
656 four browsers and two (or three) rendering modes. This might result
657 in confusing the user if both <html> and <body> elements have their
658 'overflow' properties specified to 'scroll'.
659
660 Note that this code does not do a good job if the |el| is within an
661 |overflow: scroll| element.
662 */
663 doc.body.scrollTop = top;
664 doc.body.scrollLeft = left;
665 doc.documentElement.scrollTop = top;
666 doc.documentElement.scrollLeft = left;
667 } // scroll
668 }); // Element
669
670 JSTE.ElementHash = new JSTE.Class (function () {
671 this.items = [];
672 }, {
673 get: function (ns, ln) {
674 ns = ns || '';
675 if (this.items[ns]) {
676 return this.items[ns].getNamedItem (ln) || new JSTE.List;
677 } else {
678 return new JSTE.List;
679 }
680 }, // get
681 getOrCreate: function (ns, ln) {
682 ns = ns || '';
683 if (this.items[ns]) {
684 var l = this.items[ns].getNamedItem (ln);
685 if (!l) this.items[ns].setNamedItem (ln, l = new JSTE.List);
686 return l;
687 } else {
688 var l;
689 this.items[ns] = new JSTE.Hash;
690 this.items[ns].setNamedItem (ln, l = new JSTE.List);
691 return l;
692 }
693 } // getOrCreate
694 }); // ElementHash
695
696
697
698 JSTE.Script = {};
699
700 JSTE.Class.addClassMethods (JSTE.Script, {
701 loadScripts: function (urls, onload) {
702 var number = urls.list.length;
703 var counter = 0;
704 var check = function () {
705 if (counter == number) {
706 onload ();
707 }
708 };
709 urls.forEach (function (url) {
710 if (/\.css(?:\?|$)/.test (url)) {
711 JSTE.Style.loadStyle (url, function () {
712 counter++;
713 check ();
714 });
715 return;
716 }
717
718 var script = document.createElement ('script');
719 script.src = url;
720 script.onload = function () {
721 counter++;
722 check ();
723 script.onload = null;
724 script.onreadystatechange = null;
725 };
726 script.onreadystatechange = function () {
727 if (script.readyState != 'complete' && script.readyState != 'loaded') {
728 return;
729 }
730 counter++;
731 check ();
732 script.onload = null;
733 script.onreadystatechange = null;
734 };
735 document.body.appendChild (script);
736 });
737 } // loadScripts
738 }); // Script class methods
739
740 JSTE.Style = {};
741
742 JSTE.Class.addClassMethods (JSTE.Style, {
743 loadStyle: function (url, onload) {
744 var link = document.createElement ('link');
745 link.rel = 'stylesheet';
746 link.href = url;
747 if (onload) {
748 link.onload = function () {
749 onload ();
750 link.onload = null;
751 link.onreadystatechange = null;
752 };
753 link.onreadystatechange= function () {
754 if (link.readyState != 'complete' && link.readyState != 'loaded') {
755 return;
756 }
757 onload ();
758 link.onload = null;
759 link.onreadystatechange = null;
760 };
761 }
762 JSTE.Element.appendToHead (link);
763 } // loadStyle
764 }); // Style class methods
765
766
767
768 JSTE.Prefetch = {};
769
770 JSTE.Class.addClassMethods (JSTE.Prefetch, {
771 URL: function (url) {
772 var link = document.createElement ('link');
773 link.rel = 'prefetch';
774 link.href = url;
775 JSTE.Element.appendToHead (link);
776 } // url
777 }); // JSTE.Prefetch class methods
778
779
780 JSTE.URL = {};
781
782 JSTE.Class.addClassMethods (JSTE.URL, {
783 eq: function (u1, u2) {
784 // TODO: maybe we should once decode URLs and then reencode them
785 u1 = (u1 || '').replace (/([^\x21-\x7E]+)/, function (s) { return encodeURI (s) });
786 u2 = (u2 || '').replace (/([^\x21-\x7E]+)/, function (s) { return encodeURI (s) });
787 return u1 == u2;
788 } // eq
789 }); // URL class methods
790
791
792 JSTE.XHR = new JSTE.Class (function (url, onsuccess, onerror) {
793 try {
794 this._xhr = new XMLHttpRequest ();
795 } catch (e) {
796 try {
797 this._xhr = new ActiveXObject ('Msxml2.XMLHTTP');
798 } catch (e) {
799 try {
800 this._xhr = new ActiveXObject ('Microsoft.XMLHTTP');
801 } catch (e) {
802 try {
803 this._xhr = new ActiveXObject ('Msxml2.XMLHTTP.4.0');
804 } catch (e) {
805 this._xhr = null;
806 }
807 }
808 }
809 }
810
811 this._url = url;
812 this._onsuccess = onsuccess || function () { };
813 this._onerror = onerror || function () { };
814 }, {
815 get: function () {
816 if (!this._xhr) return;
817
818 var self = this;
819 this._xhr.open ('GET', this._url, true);
820 this._xhr.onreadystatechange = function () {
821 self._onreadystatechange ();
822 }; // onreadystatechange
823 this._xhr.send (null);
824 }, // get
825
826 _onreadystatechange: function () {
827 if (this._xhr.readyState == 4) {
828 if (this.succeeded ()) {
829 this._onsuccess ();
830 } else {
831 this._onerror ();
832 }
833 }
834 }, // _onreadystatechange
835
836 succeeded: function () {
837 return (this._xhr.status < 400);
838 }, // succeeded
839
840 getDocument: function () {
841 return this._xhr.responseXML;
842 } // getDocument
843 }); // XHR
844
845 // An abstract class
846 JSTE.Storage = new JSTE.Class (function () {
847
848 }, {
849 get: function (name) {
850 throw "not implemented";
851 }, // get
852 getJSON: function (name) {
853 var value = this.get (name);
854 if (value != null) {
855 return JSTE.JSON.parse (value); // XXX: try-catch?
856 } else {
857 return value;
858 }
859 }, // getJSON
860
861 set: function (name, value) {
862 throw "not implemented";
863 }, // set
864 setJSON: function (name, obj) {
865 this.set (name, JSTE.JSON.stringify (obj));
866 }, // setJSON
867
868 has: function (name) {
869 return this.get (name) !== undefined;
870 }, // has
871
872 del: function (name) {
873 throw "del not implemented";
874 }, // del
875
876 flushGet: function (name) {
877 var v = this.get ('flush-' + name);
878 if (v !== undefined) {
879 this.del ('flush-' + name);
880 }
881 return v;
882 }, // flushGet
883 flushSet: function (name, value) {
884 this.set ('flush-' + name, value);
885 }, // flushSet
886
887 getNames: function () {
888 throw "not implemented";
889 }, // getNames
890
891 setPrefix: function (newPrefix) {
892 throw "not implemented";
893 } // setPrefix
894 }); // Storage
895
896 JSTE.Storage.PageLocal = new JSTE.Subclass (function () {
897 this.keyPrefix = '';
898 }, JSTE.Storage, {
899 get: function (name) {
900 return this['value-' + this.keyPrefix + name];
901 }, // get
902 set: function (name, value) {
903 this['value-' + this.keyPrefix + name] = value;
904 }, // set
905
906 getNames: function () {
907 var names = new JSTE.List;
908 for (var n in this) {
909 if (n.substring (0, 6 + this.keyPrefix.length) == 'value-' + this.keyPrefix) {
910 names.push (n.substring (6 + this.keyPrefix.length));
911 }
912 }
913 return names;
914 }, // getNames
915
916 setPrefix: function (newPrefix) {
917 this.keyPrefix = newPrefix;
918 } // setPrefix
919 }); // PageLocal
920
921 JSTE.Storage.Cookie = JSTE.Subclass (function () {
922 this.keyPrefix = '';
923 this.domain = null;
924 this.path = '/';
925 this.persistent = false;
926 this.expires = null; // or Date
927 }, JSTE.Storage, {
928 _parse: function () {
929 return new JSTE.List (document.cookie.split (/;/)).mapToHash (function (nv) {
930 nv = nv.replace (/^\s+/, '').replace (/\s+$/, '').split (/=/, 2);
931 nv[0] = decodeURIComponent (nv[0]);
932 nv[1] = decodeURIComponent (nv[1]);
933 return nv;
934 });
935 }, // _parse
936
937 get: function (name) {
938 return this._parse ().getNamedItem (this.keyPrefix + name);
939 }, // get
940 set: function (name, value) {
941 name = this.keyPrefix + name;
942 var r = encodeURIComponent (name) + '=' + encodeURIComponent (value);
943 if (this.domain) {
944 r += '; domain=' + this.domain;
945 }
946 if (this.path) {
947 r += '; path=' + this.path;
948 }
949 if (this.persistent) {
950 r += '; expires=' + new Date (2030, 1-1, 1).toUTCString ();
951 } else if (this.expires) {
952 r += '; expires=' + this.expires.toUTCString ();
953 }
954 document.cookie = r;
955 }, // set
956 del: function (name) {
957 var expires = this.expires;
958 var persistent = this.persistent;
959 this.expires = new Date (0);
960 this.persistent = false;
961 this.set (name, '');
962 this.expires = expires;
963 this.persistent = persistent;
964 }, // del
965
966 getNames: function () {
967 var self = this;
968 return this._parse ().getNames ().grep (function (name) {
969 return name.substring (0, self.keyPrefix.length) == self.keyPrefix;
970 }).map (function (name) {
971 return name.substring (self.keyPrefix.length);
972 });
973 }, // getNames
974
975 setPrefix: function (newPrefix) {
976 this.keyPrefix = newPrefix;
977 } // setPrefix
978 }); // Cookie
979
980 JSTE.Storage.Local = JSTE.Class (function () {
981 var self = new JSTE.Storage.Cookie;
982 self.keyPrefix = 'localStorage-';
983 self.persistent = true;
984 self.setPrefix = function (newPrefix) {
985 this.keyPrefix = 'localStorage-' + newPrefix;
986 }; // setPrefix
987 return self;
988 }); // Local
989
990 JSTE.JSON = {};
991
992 JSTE.Class.addClassMethods (JSTE.JSON, {
993 parse: function (value) {
994 if (self.JSON && JSON.parse) {
995 return JSON.parse (value); // json2.js or ES3.1
996 } else {
997 return eval ('(' + value + ')');
998 }
999 }, // parse
1000
1001 stringify: function (obj) {
1002 if (self.JSON && JSON.stringify) {
1003 return JSON.stringify (obj); // json2.js or ES3.1
1004 } else {
1005 throw "JSTE.JSON.stringify not implemented";
1006 }
1007 } // serialize
1008 }); // JSON class methods
1009
1010
1011
1012 /* Events: load, close, shown, hidden */
1013 JSTE.Message = new JSTE.Class (function (doc, template, commandTarget, availCommands) {
1014 if (!doc) return;
1015 this._targetDocument = doc;
1016 this._template = template || doc.createDocumentFragment ();
1017
1018 this._commandTarget = commandTarget;
1019 this._availCommands = availCommands || new JSTE.List;
1020
1021 this.hidden = true;
1022 this.select = "";
1023
1024 var e = new JSTE.Event ('load');
1025 this.dispatchEvent (e);
1026 }, {
1027 render: function () {
1028 var self = this;
1029 var doc = this._targetDocument;
1030
1031 var msgContainer = doc.createElement ('section');
1032 msgContainer.appendChild (this._template);
1033
1034 if (!this._availCommands.list.length) {
1035 this._availCommands.push ({name: 'back'});
1036 this._availCommands.push ({name: 'next'});
1037 }
1038
1039 this._availCommands = this._availCommands.grep (function (item) {
1040 return self._commandTarget.canExecuteCommand (item.name, item.args);
1041 });
1042
1043 this._outermostElement = this._render (msgContainer);
1044
1045 this.show ();
1046 }, // render
1047 _render: function (msgContainer, buttonContainer) {
1048 var doc = this._targetDocument;
1049
1050 var container = doc.createElement ('article');
1051
1052 container.appendChild (msgContainer);
1053
1054 var buttonContainer = this.createCommandButtons ();
1055 container.appendChild (buttonContainer);
1056
1057 doc.documentElement.appendChild (container);
1058
1059 return container;
1060 }, // _render
1061 createCommandButtons: function () {
1062 var self = this;
1063 var doc = this._targetDocument;
1064 var buttonContainer = doc.createElement ('menu');
1065 this._availCommands.forEach (function (cmd) {
1066 var label = cmd.name;
1067 if (cmd.labelTemplate) {
1068 label = JSTE.Element.createTemplate (doc, cmd.labelTemplate);
1069 }
1070
1071 var button = new JSTE.Message.Button
1072 (label, self._commandTarget, cmd.name, cmd.args, cmd.actions);
1073 buttonContainer.appendChild (button.element);
1074
1075 if (cmd.name == 'url') {
1076 JSTE.Prefetch.URL (cmd.args.url);
1077 }
1078 });
1079 return buttonContainer;
1080 }, // createCommandButtons
1081
1082 remove: function () {
1083 this.hide ();
1084
1085 this._remove ();
1086
1087 if (this._outermostElement && this._outermostElement.parentNode) {
1088 this._outermostElement.parentNode.removeChild (this._outermostElement);
1089 }
1090
1091 var e = new JSTE.Event ("close");
1092 this.dispatchEvent (e);
1093 }, // remove
1094 _remove: function () {
1095
1096 }, // remove
1097
1098 show: function () {
1099 if (!this.hidden) return;
1100 this.hidden = false;
1101 if (this._outermostElement) {
1102 JSTE.Element.replaceClassName
1103 (this._outermostElement, "jste-hidden", "jste-shown");
1104 }
1105
1106 var e = new JSTE.Event ("shown");
1107 this.dispatchEvent (e);
1108 }, // show
1109 hide: function () {
1110 if (this.hidden) return;
1111 this.hidden = true;
1112 if (this._outermostElement) {
1113 JSTE.Element.replaceClassName
1114 (this._outermostElement, "jste-shown", "jste-hidden");
1115 }
1116
1117 var e = new JSTE.Event ("hidden");
1118 this.dispatchEvent (e);
1119 }, // hide
1120
1121 setTimeout: function () {
1122 /* TODO: ... */
1123
1124 }
1125
1126 }); // Message
1127
1128 /* TODO: button label text should refer message catalog */
1129
1130 JSTE.Message.Button =
1131 new JSTE.Class (function (label, commandTarget, commandName, commandArgs, commandActions) {
1132 this._label = label != null ? label : "";
1133
1134 if (commandTarget && commandTarget instanceof Function) {
1135 this._command = commandTarget;
1136 this._classNames = new JSTE.List;
1137 } else if (commandTarget) {
1138 this._command = function () {
1139 return commandTarget.executeCommand.apply
1140 (commandTarget, [commandName, commandArgs, commandActions]);
1141 };
1142 this._classNames = new JSTE.List (['jste-command-' + commandName]);
1143 } else {
1144 this._command = function () { };
1145 this._classNames = new JSTE.List;
1146 }
1147
1148 this._createElement ();
1149 }, {
1150 _createElement: function () {
1151 try {
1152 this.element = document.createElement ('button');
1153 this.element.setAttribute ('type', 'button');
1154 } catch (e) {
1155 this.element = document.createElement ('<button type=button>');
1156 }
1157 if (this._label.nodeType) {
1158 this.element.appendChild (this._label);
1159 } else {
1160 JSTE.Element.appendText (this.element, this._label);
1161 }
1162 this.element.className = this._classNames.list.join (' ');
1163
1164 var self = this;
1165 new JSTE.Observer ("click", this.element, function (e) {
1166 self._command (e);
1167 });
1168 } // _createElement
1169 }); // Button
1170
1171 JSTE.Course = new JSTE.Class (function (doc) {
1172 this._targetDocument = doc;
1173
1174 this._entryPoints = new JSTE.List;
1175 this._entryPoints.push
1176 ({conditions: new JSTE.List ([{type: 'state', value: 'done'}]),
1177 stepUid: 'special-none'});
1178
1179 this._stepsState = new JSTE.List ([new JSTE.Hash]);
1180 this._steps = new JSTE.Hash;
1181
1182 var nullState = new JSTE.Step;
1183 nullState.uid = "special-none";
1184 this._steps.setNamedItem (nullState.uid, nullState);
1185 this._initialStepUid = nullState.uid;
1186 }, {
1187 _processStepsContent: function (el, parentSteps) {
1188 var self = this;
1189 new JSTE.List (el.childNodes).switchByElementType (
1190 new JSTE.List.SwitchByLocalName (JSTE.WATNS, {
1191 steps: function (n) { self._processStepsElement (n, parentSteps) },
1192 step: function (n) { self._processStepElement (n, parentSteps) },
1193 jump: function (n) { self._processJumpElement (n, parentSteps) },
1194 'entry-point': function (n) { self._processEntryPointElement (n, parentSteps) }
1195 })
1196 );
1197 }, // _processStepsContent
1198 _processStepsElement: function (e, parentSteps) {
1199 var steps = new JSTE.Steps ();
1200 steps.parentSteps = parentSteps;
1201 this._stepsState.pushCloneOfLast ();
1202 this._stepsState.getLast ().prevStep = null;
1203
1204 this._addConditionsFromElement (e, steps.conditions);
1205 this._processStepsContent (e, steps);
1206
1207 this._stepsState.pop ();
1208 }, // _processStepsElement
1209
1210 _processEntryPointElement: function (e, parentSteps) {
1211 var conds = parentSteps ? parentSteps.conditions.clone () : new JSTE.List;
1212 this._addConditionsFromElement (e, conds);
1213
1214 var stepUid = e.getAttribute ('step');
1215 if (stepUid != null) stepUid = 'id-' + stepUid;
1216 this._entryPoints.push ({conditions: conds, stepUid: stepUid});
1217 }, // _processEntryPointElement
1218
1219 _addConditionsFromElement: function (e, conds) {
1220 var urls = e.getAttribute ('document-url');
1221 if (urls != null) {
1222 JSTE.List.spaceSeparated (urls).forEach (function (url) {
1223 conds.push ({type: 'url', value: encodeURI (url)});
1224 // TODO: resolve relative URL, URL->URI
1225 });
1226 }
1227
1228 var urls = e.getAttribute ('not-document-url');
1229 if (urls != null) {
1230 JSTE.List.spaceSeparated (urls).forEach (function (url) {
1231 conds.push ({type: 'url', value: encodeURI (url), not: true});
1232 // TODO: resolve relative URL
1233 });
1234 }
1235
1236 var classNames = e.getAttribute ('document-class');
1237 if (classNames != null) {
1238 JSTE.List.spaceSeparated (classNames).forEach (function (className) {
1239 conds.push ({type: 'class', value: className});
1240 });
1241 }
1242
1243 var classNames = e.getAttribute ('not-document-class');
1244 if (classNames != null) {
1245 JSTE.List.spaceSeparated (classNames).forEach (function (className) {
1246 conds.push ({type: 'class', value: className, not: true});
1247 });
1248 }
1249
1250 var stateNames = e.getAttribute ('state');
1251 if (stateNames != null) {
1252 JSTE.List.spaceSeparated (stateNames).forEach (function (stateName) {
1253 conds.push ({type: 'state', value: stateName});
1254 });
1255 }
1256
1257 var stateNames = e.getAttribute ('not-state');
1258 if (stateNames != null) {
1259 JSTE.List.spaceSeparated (stateNames).forEach (function (stateName) {
1260 conds.push ({type: 'state', value: stateName, not: true});
1261 });
1262 }
1263 }, // _addConditionsFromElement
1264
1265 findEntryPoint: function (doc, states) {
1266 var self = this;
1267
1268 var td = this._targetDocument;
1269 var docURL = td.URL; // TODO: drop fragments?
1270 var docClassNames = JSTE.Document.getClassNames (td);
1271
1272 var stepUid = this._entryPoints.forEach (function (ep) {
1273 if (ep.conditions.forEach (function (cond) {
1274 var matched;
1275 if (cond.type == 'state') {
1276 matched = states.has (cond.value);
1277 } else if (cond.type == 'class') {
1278 matched = docClassNames.has (cond.value);
1279 } else if (cond.type == 'url') {
1280 matched = JSTE.URL.eq (cond.value, docURL);
1281 } else {
1282 //
1283 }
1284 if (cond.not) matched = !matched;
1285 if (!matched) return new JSTE.List.Return (true);
1286 })) return; // true = not matched
1287
1288 // matched
1289 return new JSTE.List.Return (ep.stepUid);
1290 });
1291
1292 // TODO: multiple elements with same ID
1293
1294 if (stepUid != null) {
1295 return stepUid;
1296 } else {
1297 return this._initialStepUid;
1298 }
1299 }, // findEntryPoint
1300
1301 _processStepElement: function (e, parentSteps) {
1302 var self = this;
1303
1304 var step = new JSTE.Step (e.getAttribute ('id'));
1305 step.parentSteps = parentSteps;
1306 step.setPreviousStep (this._stepsState.getLast ().prevStep);
1307 step.select = e.getAttribute ('select') || "";
1308 step.nextEvents.append
1309 (JSTE.List.spaceSeparated (e.getAttribute ('next-event')));
1310
1311 step.noHistory = JSTE.Element.hasAttribute (e, 'nohistory');
1312
1313 var cs = JSTE.Element.getChildrenClassifiedByType (e);
1314
1315 var msgEl = cs.get (JSTE.WATNS, 'message').list[0];
1316 if (msgEl) {
1317 var msg = JSTE.Element.createTemplate (this._targetDocument, msgEl);
1318 step.setMessageTemplate (msg);
1319 }
1320
1321 var nextEls = cs.get (JSTE.WATNS, 'next-step');
1322 if (nextEls.list.length) {
1323 nextEls.forEach (function (nextEl) {
1324 step.addNextStep
1325 (nextEl.getAttribute ('if'), nextEl.getAttribute ('step'));
1326 });
1327 this._stepsState.getLast ().prevStep = null;
1328 } else {
1329 this._stepsState.getLast ().prevStep = step;
1330 }
1331
1332 cs.get (JSTE.WATNS, 'command').forEach (function (bEl) {
1333 var cmd = {
1334 name: bEl.getAttribute ('type') || 'gotoStep'
1335 };
1336 if (cmd.name == 'gotoStep') {
1337 cmd.args = {stepUid: 'id-' + bEl.getAttribute ('step')};
1338 } else if (cmd.name == 'url') {
1339 // TODO: relative URL
1340 cmd.args = {url: bEl.getAttribute ('href')};
1341 }
1342 cmd.actions = {
1343 saveStateNames: JSTE.List.spaceSeparated (bEl.getAttribute ('save-state')),
1344 clearStateNames: JSTE.List.spaceSeparated (bEl.getAttribute ('clear-state'))
1345 };
1346 if (!JSTE.Element.isEmpty (bEl)) {
1347 cmd.labelTemplate = JSTE.Element.createTemplate (self._targetDocument, bEl);
1348 }
1349 step.availCommands.push (cmd);
1350 }); // wat:command
1351
1352 cs.get (JSTE.WATNS, 'save-state').forEach (function (bEl) {
1353 var ss = new JSTE.SaveState
1354 (bEl.getAttribute ('name'), bEl.getAttribute ('value'));
1355 step.saveStates.push (ss);
1356 }); // wat:save-state
1357
1358 var evs = JSTE.List.spaceSeparated (e.getAttribute ('entry-event'));
1359 if (evs.list.length) {
1360 var jump = new JSTE.Jump (step.select, evs, step.uid);
1361 if (parentSteps) parentSteps._jumps.push (jump);
1362 }
1363
1364 this._steps.setNamedItem (step.uid, step);
1365 /*if (!this._initialStepUid) {
1366 this._initialStepUid = step.uid;
1367 }*/
1368 }, // _processStepElement
1369
1370 _processJumpElement: function (e, parentSteps) {
1371 var target = e.getAttribute ('target') || '';
1372 var evs = JSTE.List.spaceSeparated (e.getAttribute ('event'));
1373 var stepName = e.getAttribute ('step') || '';
1374
1375 var jump = new JSTE.Jump (target, evs, 'id-' + stepName);
1376 if (parentSteps) parentSteps._jumps.push (jump);
1377 }, // _processJumpElement
1378
1379 getStep: function (uid) {
1380 return this._steps.getNamedItem (uid);
1381 } // getStep
1382 }); // Course
1383
1384 JSTE.Class.addClassMethods (JSTE.Course, {
1385 createFromDocument: function (doc, targetDoc) {
1386 var course = new JSTE.Course (targetDoc);
1387 var docEl = doc.documentElement;
1388 if (!docEl) return course;
1389 if (!JSTE.Element.match (docEl, JSTE.WATNS, 'course')) return course;
1390 course._processStepsContent (docEl, null);
1391 var name = docEl.getAttribute ('name');
1392 if (name != null) {
1393 course.name = name + '-';
1394 } else {
1395 course.name = '';
1396 }
1397 return course;
1398 }, // createFromDocument
1399 createFromURL: function (url, targetDoc, onload, onerror) {
1400 new JSTE.XHR (url, function () {
1401 var course = JSTE.Course.createFromDocument
1402 (this.getDocument (), targetDoc);
1403 if (onload) onload (course);
1404 }, onerror).get ();
1405 } // creatFromURL
1406 }); // Course class methods
1407
1408 JSTE.Jump = new JSTE.Class (function (selectors, eventNames, stepUid) {
1409 this.selectors = selectors;
1410 this.eventNames = eventNames;
1411 this.stepUid = stepUid;
1412 // this.parentSteps
1413 }, {
1414 startObserver: function (doc, commandTarget) {
1415 var self = this;
1416 var observers = new JSTE.List;
1417
1418 var onev = function () {
1419 commandTarget.gotoStep ({stepUid: self.stepUid});
1420 };
1421
1422 JSTE.Node.querySelectorAll (doc, this.selectors).forEach
1423 (function (el) {
1424 self.eventNames.forEach (function (evName) {
1425 var ob = new JSTE.Observer (evName, el, onev);
1426 ob._stepUid = self.stepUid;
1427 observers.push (ob);
1428 });
1429 });
1430
1431 return observers;
1432 } // startObserver
1433 }); // Jump
1434
1435 JSTE.Steps = new JSTE.Class (function () {
1436 this._jumps = new JSTE.List;
1437 this._jumpHandlers = new JSTE.List;
1438 this.conditions = new JSTE.List;
1439 }, {
1440 setCurrentStepByUid: function (uid) {
1441 this._jumpHandlers.forEach (function (jh) {
1442 if (jh._stepUid != uid && jh.disabled) {
1443 jh.start ();
1444 } else if (jh._stepUid == uid && !jh.disabled) {
1445 jh.stop ();
1446 }
1447 });
1448 }, // setCurrentStepByUid
1449
1450 installJumps: function (doc, commandTarget) {
1451 if (this._jumpHandlers.list.length) return;
1452 var self = this;
1453 this._jumps.forEach (function (j) {
1454 self._jumpHandlers.append (j.startObserver (doc, commandTarget));
1455 });
1456 }, // installJumps
1457
1458 uninstallJumps: function () {
1459 this._jumpHandlers.forEach (function (jh) {
1460 jh.stop ();
1461 });
1462 this._jumpHandlers.clear ();
1463 } // uninstallJumps
1464 }); // Steps
1465
1466 JSTE.Step = new JSTE.Class (function (id) {
1467 if (id != null && id != '') {
1468 this.uid = 'id-' + id;
1469 } else {
1470 this.uid = 'rand-' + Math.random ();
1471 }
1472 this._nextSteps = new JSTE.List;
1473 this.nextEvents = new JSTE.List;
1474 this.availCommands = new JSTE.List;
1475 this.saveStates = new JSTE.List;
1476 this.select = "";
1477 // this._messageTemplate
1478 }, {
1479 setMessageTemplate: function (msg) {
1480 this._messageTemplate = msg;
1481 }, // setMessageTemplate
1482 hasMessage: function () {
1483 return this._messageTemplate ? true : false;
1484 }, // hasMessage
1485 createMessage: function (msg, doc, commandTarget) {
1486 var msg;
1487 if (this._messageTemplate) {
1488 var clone = JSTE.Element.createTemplate (doc, this._messageTemplate);
1489 msg = new msg (doc, clone, commandTarget, this.availCommands.clone ());
1490 } else {
1491 msg = new msg (doc, null, commandTarget, this.availCommands.clone ());
1492 }
1493 msg.select = this.select;
1494 return msg;
1495 }, // createMessage
1496
1497 addNextStep: function (condition, stepId) {
1498 if (stepId != null) this._nextSteps.push ([condition, stepId]);
1499 }, // addNextStep
1500 setPreviousStep: function (prevStep) {
1501 if (!prevStep) return;
1502 if (prevStep._defaultNextStepUid) return;
1503 prevStep._defaultNextStepUid = this.uid;
1504 }, // setPreviousStep
1505
1506 getNextStepUid: function (doc) {
1507 var m = this._nextSteps.getFirstMatch (function (item) {
1508 var condition = item[0];
1509 if (condition) {
1510 return JSTE.Node.querySelector (doc, condition) != null;
1511 } else {
1512 return true;
1513 }
1514 });
1515 if (m) {
1516 return 'id-' + m[1];
1517 } else if (this._defaultNextStepUid) {
1518 return this._defaultNextStepUid;
1519 } else {
1520 return null;
1521 }
1522 }, // getNextStepUid
1523
1524 getAncestorStepsObjects: function () {
1525 var steps = new JSTE.List;
1526 var s = this.parentSteps;
1527 while (s != null) {
1528 steps.push (s);
1529 s = s.parentSteps;
1530 }
1531 return steps;
1532 } // getAncestorStepsObjects
1533 }); // Step
1534
1535 JSTE.SaveState = new JSTE.Class (function (name, value) {
1536 this.name = name || '';
1537 this.value = value || '';
1538 }, {
1539 save: function (tutorial) {
1540 var name = this.name;
1541 var value = this.value;
1542 if (name == 'back-state') return;
1543 tutorial._states.set (name, value);
1544 } // save
1545 }); // SaveState
1546
1547 /* Events: load, error, cssomready, close */
1548 JSTE.Tutorial = new JSTE.Class (function (course, doc, args) {
1549 this._course = course;
1550 this._targetDocument = doc;
1551 this._messageClass = JSTE.Message;
1552 if (args) {
1553 if (args.messageClass) this._messageClass = args.messageClass;
1554 if (args.states) this._states = args.states;
1555 }
1556 if (!this._states) this._states = new JSTE.Storage.PageLocal;
1557 this._states.setPrefix (course.name);
1558
1559 this._currentMessages = new JSTE.List;
1560 this._currentObservers = new JSTE.List;
1561 this._currentStepsObjects = new JSTE.List;
1562
1563 this._prevStepUids = new JSTE.List;
1564 this._loadBackState ();
1565
1566 var stepUid;
1567 if (this._states.flushGet ('is-back') && this._prevStepUids.list.length) {
1568 stepUid = this._prevStepUids.pop ();
1569 } else {
1570 stepUid = this._course.findEntryPoint (document, this._states);
1571 }
1572
1573 this._currentStep = this._getStepOrError (stepUid);
1574 if (this._currentStep) {
1575 var e = new JSTE.Event ('load');
1576 this.dispatchEvent (e);
1577
1578 this._saveBackState ();
1579
1580 var self = this;
1581 new JSTE.Observer ('cssomready', this, function () {
1582 self._renderCurrentStep ();
1583 });
1584 this._dispatchCSSOMReadyEvent ();
1585 return this;
1586 } else {
1587 return {};
1588 }
1589 }, {
1590 _getStepOrError: function (stepUid) {
1591 if (stepUid == 'special-none') {
1592 return null;
1593 }
1594
1595 var step = this._course.getStep (stepUid);
1596 if (step) {
1597 return step;
1598 } else {
1599 var e = new JSTE.Event ('error');
1600 e.errorMessage = 'Step not found';
1601 e.errorArguments = [this._currentStepUid];
1602 this.dispatchEvent (e);
1603 return null;
1604 }
1605 }, // _getStepOrError
1606
1607 _renderCurrentStep: function () {
1608 var self = this;
1609 var step = this._currentStep;
1610
1611 step.saveStates.forEach (function (ss) { ss.save (self) });
1612
1613 /* Message */
1614 var msg = step.createMessage
1615 (this._messageClass, this._targetDocument, this);
1616 msg.render ();
1617 this._currentMessages.push (msg);
1618
1619 /* Next-events */
1620 var selectedNodes = JSTE.Node.querySelectorAll
1621 (this._targetDocument, step.select);
1622 var handler = function () {
1623 self.executeCommand ("next");
1624 };
1625 selectedNodes.forEach (function (node) {
1626 step.nextEvents.forEach (function (eventType) {
1627 self._currentObservers.push
1628 (new JSTE.Observer (eventType, node, handler));
1629 });
1630 });
1631
1632 JSTE.List.getCommonItems (this._currentStepsObjects,
1633 step.getAncestorStepsObjects (),
1634 function (common, onlyInOld, onlyInNew) {
1635 common.forEach (function (item) {
1636 item.setCurrentStepByUid (step.uid);
1637 });
1638 onlyInOld.forEach (function (item) {
1639 item.uninstallJumps ();
1640 });
1641 onlyInNew.forEach (function (item) {
1642 item.installJumps (self._targetDocument, self);
1643 });
1644 self._currentStepsObjects = common.append (onlyInNew);
1645 });
1646 }, // _renderCurrentStep
1647 clearMessages: function () {
1648 this._currentMessages.forEach (function (msg) {
1649 msg.remove ();
1650 });
1651 this._currentMessages.clear ();
1652
1653 this._currentObservers.forEach (function (ob) {
1654 ob.stop ();
1655 });
1656 this._currentObservers.clear ();
1657 }, // clearMessages
1658 clearStepsHandlers: function () {
1659 this._currentStepsObjects.forEach (function (item) {
1660 item.uninstallJumps ();
1661 });
1662 this._currentStepsObjects.clear ();
1663 }, // clearStepsHandlers
1664
1665 executeCommand: function (commandName, commandArgs, commandActions) {
1666 if (this[commandName]) {
1667 // Common actions
1668 if (commandActions) {
1669 var self = this;
1670 if (commandActions.saveStateNames) {
1671 commandActions.saveStateNames.forEach (function (stateName) {
1672 self._states.set (stateName, '');
1673 });
1674 }
1675 if (commandActions.clearStateNames) {
1676 commandActions.clearStateNames.forEach (function (stateName) {
1677 if (stateName == 'back-state') {
1678 self._prevStateUids = new JSTE.List;
1679 self._prevPages = new JSTE.List;
1680 }
1681 self._states.del (stateName);
1682 });
1683 }
1684 }
1685
1686 return this[commandName].apply (this, [commandArgs || {}]);
1687 } else {
1688 var e = new JSTE.Event ('error');
1689 e.errorMessage = 'Command not found';
1690 e.errorArguments = [commandName];
1691 return null;
1692 }
1693 }, // executeCommand
1694 canExecuteCommand: function (commandName, commandArgs) {
1695 if (this[commandName]) {
1696 var can = this['can' + commandName.substring (0, 1).toUpperCase ()
1697 + commandName.substring (1)];
1698 if (can) {
1699 return can.apply (this, arguments);
1700 } else {
1701 return true;
1702 }
1703 } else {
1704 return false;
1705 }
1706 }, // canExecuteCommand
1707
1708 back: function () {
1709 while (this._prevStepUids.list.length == 0 &&
1710 this._prevPages.list.length > 0) {
1711 var prevPage = this._prevPages.pop ();
1712 if (!JSTE.URL.eq (prevPage.url, location.href)) { // TODO: fragment?
1713 this._saveBackState (true);
1714 this._states.flushSet ('is-back', true);
1715 if (JSTE.URL.eq (document.referrer, prevPage.url)) { // TODO: fragment?
1716 history.back ();
1717 } else {
1718 location.href = prevPage.url;
1719 }
1720 // TODO: maybe we should not return if locaton.href and prevPage.,url only differs their fragment ids?
1721 return;
1722 }
1723 this._prevStepUids = prevPage;
1724 }
1725
1726 var prevStepUid = this._prevStepUids.pop ();
1727 var prevStep = this._getStepOrError (prevStepUid);
1728 if (prevStep) {
1729 this.clearMessages ();
1730 this._saveBackState ();
1731 this._currentStep = prevStep;
1732 this._renderCurrentStep ();
1733 }
1734 }, // back
1735 canBack: function () {
1736 return this._prevStepUids.list.length > 0 || this._prevPages.list.length > 0;
1737 }, // canBack
1738 next: function () {
1739 var nextStepUid = this._currentStep.getNextStepUid (this._targetDocument);
1740 var nextStep = this._getStepOrError (nextStepUid);
1741 if (nextStep) {
1742 if (!this._currentStep.noHistory) {
1743 this._prevStepUids.push (this._currentStep.uid);
1744 }
1745 this.clearMessages ();
1746 this._saveBackState ();
1747 this._currentStep = nextStep;
1748 this._renderCurrentStep ();
1749 }
1750 }, // next
1751 canNext: function () {
1752 return this._currentStep.getNextStepUid (this._targetDocument) != null;
1753 }, // canNext
1754 gotoStep: function (args) {
1755 var nextStep = this._getStepOrError (args.stepUid);
1756 if (nextStep) {
1757 if (!this._currentStep.noHistory) {
1758 this._prevStepUids.push (this._currentStep.uid);
1759 }
1760 this._saveBackState ();
1761 this.clearMessages ();
1762 this._currentStep = nextStep;
1763 this._renderCurrentStep ();
1764 }
1765 }, // gotoStep
1766
1767 url: function (args) {
1768 location.href = args.url;
1769 }, // url
1770
1771 close: function () {
1772 this.clearMessages ();
1773 var e = new JSTE.Event ('closed');
1774 this.dispatchEvent (e);
1775 }, // close
1776
1777 _loadBackState: function () {
1778 var self = this;
1779 this._prevPages = new JSTE.List;
1780 var bs = this._states.getJSON ('back-state');
1781 new JSTE.List (bs).forEach (function (b) {
1782 var i = new JSTE.List (b.stepUids);
1783 i.url = b.url;
1784 self._prevPages.push (i);
1785 });
1786 if (JSTE.URL.eq ((this._prevPages.getLast () || {}).url, location.href)) { // TODO: fragment?
1787 this._prevStepUids = this._prevPages.pop ();
1788 }
1789 }, // loadBackState
1790 _saveBackState: function (ignoreCurrentPage) {
1791 var bs = [];
1792 this._prevPages.forEach (function (pp) {
1793 bs.push ({url: pp.url, stepUids: pp.list});
1794 });
1795 if (!ignoreCurrentPage) {
1796 var uids = this._prevStepUids.clone ();
1797 if (!this._currentStep.noHistory) {
1798 uids.push (this._currentStep.uid);
1799 }
1800 if (uids.list.length) {
1801 bs.push ({url: location.href, stepUids: uids.list});
1802 }
1803 }
1804 this._states.setJSON ('back-state', bs);
1805 }, // _saveBackState
1806
1807 // <http://twitter.com/waka/status/1129513097>
1808 _dispatchCSSOMReadyEvent: function () {
1809 var self = this;
1810 var e = new JSTE.Event ('cssomready');
1811 if (window.opera && document.readyState != 'complete') {
1812 new JSTE.Observer ('readystatechange', document, function () {
1813 if (document.readyState == 'complete') {
1814 self.dispatchEvent (e);
1815 }
1816 });
1817 } else {
1818 this.dispatchEvent (e);
1819 }
1820 } // dispatchCSSOMReadyEvent
1821
1822 }); // Tutorial
1823
1824 JSTE.Class.addClassMethods (JSTE.Tutorial, {
1825 createFromURL: function (url, doc, args, onload) {
1826 JSTE.Course.createFromURL (url, doc, function (course) {
1827 var tutorial = new JSTE.Tutorial (course, doc, args);
1828 if (onload) onload (tutorial);
1829 });
1830 } // createFromURL
1831 }); // Tutorial class methods
1832
1833
1834
1835 if (JSTE.onLoadFunctions) {
1836 new JSTE.List (JSTE.onLoadFunctions).forEach (function (code) {
1837 code ();
1838 });
1839 }
1840
1841 if (JSTE.isDynamicallyLoaded) {
1842 JSTE.windowLoaded = true;
1843 }
1844
1845 /* ***** BEGIN LICENSE BLOCK *****
1846 * Copyright 2008-2009 Wakaba <w@suika.fam.cx>. All rights reserved.
1847 *
1848 * This program is free software; you can redistribute it and/or
1849 * modify it under the same terms as Perl itself.
1850 *
1851 * Alternatively, the contents of this file may be used
1852 * under the following terms (the "MPL/GPL/LGPL"),
1853 * in which case the provisions of the MPL/GPL/LGPL are applicable instead
1854 * of those above. If you wish to allow use of your version of this file only
1855 * under the terms of the MPL/GPL/LGPL, and not to allow others to
1856 * use your version of this file under the terms of the Perl, indicate your
1857 * decision by deleting the provisions above and replace them with the notice
1858 * and other provisions required by the MPL/GPL/LGPL. If you do not delete
1859 * the provisions above, a recipient may use your version of this file under
1860 * the terms of any one of the Perl or the MPL/GPL/LGPL.
1861 *
1862 * "MPL/GPL/LGPL":
1863 *
1864 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
1865 *
1866 * The contents of this file are subject to the Mozilla Public License Version
1867 * 1.1 (the "License"); you may not use this file except in compliance with
1868 * the License. You may obtain a copy of the License at
1869 * <http://www.mozilla.org/MPL/>
1870 *
1871 * Software distributed under the License is distributed on an "AS IS" basis,
1872 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
1873 * for the specific language governing rights and limitations under the
1874 * License.
1875 *
1876 * The Original Code is JSTE code.
1877 *
1878 * The Initial Developer of the Original Code is Wakaba.
1879 * Portions created by the Initial Developer are Copyright (C) 2008
1880 * the Initial Developer. All Rights Reserved.
1881 *
1882 * Contributor(s):
1883 * Wakaba <w@suika.fam.cx>
1884 *
1885 * Alternatively, the contents of this file may be used under the terms of
1886 * either the GNU General Public License Version 2 or later (the "GPL"), or
1887 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
1888 * in which case the provisions of the GPL or the LGPL are applicable instead
1889 * of those above. If you wish to allow use of your version of this file only
1890 * under the terms of either the GPL or the LGPL, and not to allow others to
1891 * use your version of this file under the terms of the MPL, indicate your
1892 * decision by deleting the provisions above and replace them with the notice
1893 * and other provisions required by the LGPL or the GPL. If you do not delete
1894 * the provisions above, a recipient may use your version of this file under
1895 * the terms of any one of the MPL, the GPL or the LGPL.
1896 *
1897 * ***** END LICENSE BLOCK ***** */

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24