/[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 - (hide 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 wakaba 1.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 wakaba 1.10 this.target = target;
77 wakaba 1.1 if (target.addEventListener) {
78     this.code = onevent;
79     } else if (target.attachEvent) {
80     this.code = function () {
81     onevent (event);
82     };
83 wakaba 1.10 } else {
84     this.code = onevent;
85 wakaba 1.1 }
86 wakaba 1.10 this.disabled = true;
87     this.start ();
88 wakaba 1.1 }, {
89 wakaba 1.10 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 wakaba 1.1 stop: function () {
100 wakaba 1.10 if (this.disabled) return;
101 wakaba 1.1 if (this.target.removeEventListener) {
102     this.target.removeEventListener (this.eventType, this.code, false);
103 wakaba 1.10 this.disabled = true;
104 wakaba 1.11 } else if (this.target.detachEvent) {
105 wakaba 1.1 this.target.detachEvent ("on" + this.eventType, this.code);
106 wakaba 1.10 this.disabled = true;
107 wakaba 1.1 }
108     } // stop
109     }); // Observer
110    
111 wakaba 1.5 new JSTE.Observer ('load', window, function () {
112     JSTE.windowLoaded = true;
113     });
114    
115 wakaba 1.10
116 wakaba 1.31
117 wakaba 1.10 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 wakaba 1.22 }, // setNamedItem
140    
141 wakaba 1.23 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 wakaba 1.22 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 wakaba 1.10
162    
163 wakaba 1.1 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 wakaba 1.23 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 wakaba 1.4
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 wakaba 1.23
212 wakaba 1.1 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 wakaba 1.11
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 wakaba 1.1
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 wakaba 1.10 // destructive
269 wakaba 1.1 push: function (item) {
270     this.list.push (item);
271     }, // push
272 wakaba 1.10
273     // destructive
274 wakaba 1.1 pushCloneOfLast: function () {
275     this.list.push (this.getLast ().clone ());
276     }, // pushCloneOfLast
277 wakaba 1.10
278     // destructive
279 wakaba 1.1 append: function (list) {
280     var self = this;
281     list.forEach (function (n) {
282     self.list.push (n);
283     });
284 wakaba 1.10 return this;
285 wakaba 1.1 }, // append
286    
287 wakaba 1.10 // destructive
288 wakaba 1.1 pop: function () {
289     return this.list.pop ();
290     }, // pop
291 wakaba 1.10
292     // destructive
293 wakaba 1.1 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 wakaba 1.10
303     // destructive
304 wakaba 1.1 clear: function () {
305     this.list.splice (0, this.list.length);
306     } // clear
307    
308     }); // List
309    
310 wakaba 1.10 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 wakaba 1.11 if (!eq) eq = function (i1, i2) { return i1 === i2 };
319 wakaba 1.10
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 wakaba 1.1 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 wakaba 1.29 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 wakaba 1.1
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 wakaba 1.11 /* NOTE: uu.css return all elements for "" or ",xxx". */
419 wakaba 1.1 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 wakaba 1.27 JSTE.Document = {};
432    
433     JSTE.Class.addClassMethods (JSTE.Document, {
434 wakaba 1.29 getTheHTMLElement: function (doc) {
435     var el = doc.documentElement;
436     // BUG: XML support
437 wakaba 1.27 if (el.nodeName.toUpperCase () == 'HTML') {
438     return el;
439     } else {
440     return null;
441     }
442     }, // getTheHTMLElement
443 wakaba 1.29 getTheHeadElement: function (doc) {
444     // BUG: XML support
445     var el = JSTE.Document.getTheHTMLElement (doc);
446 wakaba 1.27 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 wakaba 1.29 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 wakaba 1.27 }); // JSTE.Document class methods
475    
476 wakaba 1.1 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 wakaba 1.17 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 wakaba 1.18
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 wakaba 1.1
546     appendText: function (el, s) {
547     return el.appendChild (el.ownerDocument.createTextNode (s));
548     }, // appendText
549    
550 wakaba 1.29 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 wakaba 1.1 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 wakaba 1.15 new JSTE.List (n.attributes).forEach (function (n) {
562 wakaba 1.1 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 wakaba 1.5 }, // 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 wakaba 1.1
646 wakaba 1.5 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 wakaba 1.18 }); // Element
669 wakaba 1.1
670 wakaba 1.17 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 wakaba 1.31
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 wakaba 1.33 if (/\.css(?:\?|$)/.test (url)) {
711     JSTE.Style.loadStyle (url, function () {
712     counter++;
713     check ();
714     });
715     return;
716     }
717    
718 wakaba 1.31 var script = document.createElement ('script');
719     script.src = url;
720     script.onload = function () {
721     counter++;
722     check ();
723 wakaba 1.33 script.onload = null;
724     script.onreadystatechange = null;
725 wakaba 1.31 };
726     script.onreadystatechange = function () {
727     if (script.readyState != 'complete' && script.readyState != 'loaded') {
728     return;
729     }
730     counter++;
731     check ();
732 wakaba 1.33 script.onload = null;
733     script.onreadystatechange = null;
734 wakaba 1.31 };
735     document.body.appendChild (script);
736     });
737     } // loadScripts
738     }); // Script class methods
739    
740     JSTE.Style = {};
741    
742     JSTE.Class.addClassMethods (JSTE.Style, {
743 wakaba 1.33 loadStyle: function (url, onload) {
744 wakaba 1.31 var link = document.createElement ('link');
745     link.rel = 'stylesheet';
746     link.href = url;
747 wakaba 1.33 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 wakaba 1.31 JSTE.Element.appendToHead (link);
763     } // loadStyle
764     }); // Style class methods
765    
766    
767 wakaba 1.17
768 wakaba 1.27 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 wakaba 1.29 JSTE.Element.appendToHead (link);
776 wakaba 1.27 } // url
777     }); // JSTE.Prefetch class methods
778    
779 wakaba 1.32
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 wakaba 1.9 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 wakaba 1.22 // An abstract class
846     JSTE.Storage = new JSTE.Class (function () {
847    
848     }, {
849 wakaba 1.23 get: function (name) {
850     throw "not implemented";
851     }, // get
852 wakaba 1.25 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 wakaba 1.23 set: function (name, value) {
862     throw "not implemented";
863     }, // set
864 wakaba 1.25 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 wakaba 1.32 del: function (name) {
873     throw "del not implemented";
874     }, // del
875 wakaba 1.25
876     flushGet: function (name) {
877     var v = this.get ('flush-' + name);
878     if (v !== undefined) {
879 wakaba 1.32 this.del ('flush-' + name);
880 wakaba 1.25 }
881     return v;
882     }, // flushGet
883     flushSet: function (name, value) {
884     this.set ('flush-' + name, value);
885     }, // flushSet
886 wakaba 1.23
887     getNames: function () {
888     throw "not implemented";
889 wakaba 1.24 }, // getNames
890    
891     setPrefix: function (newPrefix) {
892     throw "not implemented";
893     } // setPrefix
894 wakaba 1.22 }); // Storage
895    
896     JSTE.Storage.PageLocal = new JSTE.Subclass (function () {
897 wakaba 1.24 this.keyPrefix = '';
898 wakaba 1.22 }, JSTE.Storage, {
899     get: function (name) {
900 wakaba 1.24 return this['value-' + this.keyPrefix + name];
901 wakaba 1.22 }, // get
902     set: function (name, value) {
903 wakaba 1.24 this['value-' + this.keyPrefix + name] = value;
904 wakaba 1.22 }, // set
905    
906     getNames: function () {
907     var names = new JSTE.List;
908     for (var n in this) {
909 wakaba 1.24 if (n.substring (0, 6 + this.keyPrefix.length) == 'value-' + this.keyPrefix) {
910     names.push (n.substring (6 + this.keyPrefix.length));
911 wakaba 1.22 }
912     }
913     return names;
914 wakaba 1.24 }, // getNames
915    
916     setPrefix: function (newPrefix) {
917     this.keyPrefix = newPrefix;
918     } // setPrefix
919 wakaba 1.23 }); // 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 wakaba 1.32 del: function (name) {
957 wakaba 1.23 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 wakaba 1.32 }, // del
965 wakaba 1.23
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 wakaba 1.24 }, // getNames
974    
975     setPrefix: function (newPrefix) {
976     this.keyPrefix = newPrefix;
977     } // setPrefix
978 wakaba 1.23 }); // 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 wakaba 1.24 self.setPrefix = function (newPrefix) {
985     this.keyPrefix = 'localStorage-' + newPrefix;
986     }; // setPrefix
987 wakaba 1.23 return self;
988     }); // Local
989 wakaba 1.22
990 wakaba 1.25 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 wakaba 1.9
1012 wakaba 1.1 /* Events: load, close, shown, hidden */
1013 wakaba 1.18 JSTE.Message = new JSTE.Class (function (doc, template, commandTarget, availCommands) {
1014 wakaba 1.1 if (!doc) return;
1015     this._targetDocument = doc;
1016     this._template = template || doc.createDocumentFragment ();
1017 wakaba 1.8
1018 wakaba 1.1 this._commandTarget = commandTarget;
1019 wakaba 1.18 this._availCommands = availCommands || new JSTE.List;
1020 wakaba 1.8
1021 wakaba 1.1 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 wakaba 1.20
1034     if (!this._availCommands.list.length) {
1035 wakaba 1.8 this._availCommands.push ({name: 'back'});
1036     this._availCommands.push ({name: 'next'});
1037 wakaba 1.7 }
1038 wakaba 1.20
1039 wakaba 1.30 this._availCommands = this._availCommands.grep (function (item) {
1040 wakaba 1.20 return self._commandTarget.canExecuteCommand (item.name, item.args);
1041     });
1042 wakaba 1.1
1043 wakaba 1.8 this._outermostElement = this._render (msgContainer);
1044 wakaba 1.1
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 wakaba 1.8
1054     var buttonContainer = this.createCommandButtons ();
1055 wakaba 1.1 container.appendChild (buttonContainer);
1056 wakaba 1.8
1057 wakaba 1.1 doc.documentElement.appendChild (container);
1058    
1059     return container;
1060     }, // _render
1061 wakaba 1.8 createCommandButtons: function () {
1062     var self = this;
1063 wakaba 1.18 var doc = this._targetDocument;
1064     var buttonContainer = doc.createElement ('menu');
1065 wakaba 1.8 this._availCommands.forEach (function (cmd) {
1066 wakaba 1.18 var label = cmd.name;
1067     if (cmd.labelTemplate) {
1068     label = JSTE.Element.createTemplate (doc, cmd.labelTemplate);
1069     }
1070    
1071 wakaba 1.8 var button = new JSTE.Message.Button
1072 wakaba 1.26 (label, self._commandTarget, cmd.name, cmd.args, cmd.actions);
1073 wakaba 1.8 buttonContainer.appendChild (button.element);
1074 wakaba 1.27
1075     if (cmd.name == 'url') {
1076     JSTE.Prefetch.URL (cmd.args.url);
1077     }
1078 wakaba 1.8 });
1079     return buttonContainer;
1080     }, // createCommandButtons
1081    
1082 wakaba 1.1 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 wakaba 1.7 /* TODO: button label text should refer message catalog */
1129    
1130 wakaba 1.1 JSTE.Message.Button =
1131 wakaba 1.26 new JSTE.Class (function (label, commandTarget, commandName, commandArgs, commandActions) {
1132 wakaba 1.18 this._label = label != null ? label : "";
1133 wakaba 1.6
1134 wakaba 1.1 if (commandTarget && commandTarget instanceof Function) {
1135     this._command = commandTarget;
1136 wakaba 1.6 this._classNames = new JSTE.List;
1137 wakaba 1.1 } else if (commandTarget) {
1138     this._command = function () {
1139     return commandTarget.executeCommand.apply
1140 wakaba 1.26 (commandTarget, [commandName, commandArgs, commandActions]);
1141 wakaba 1.1 };
1142 wakaba 1.6 this._classNames = new JSTE.List (['jste-command-' + commandName]);
1143 wakaba 1.1 } else {
1144     this._command = function () { };
1145 wakaba 1.6 this._classNames = new JSTE.List;
1146 wakaba 1.1 }
1147    
1148 wakaba 1.6 this._createElement ();
1149 wakaba 1.1 }, {
1150 wakaba 1.6 _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 wakaba 1.18 if (this._label.nodeType) {
1158     this.element.appendChild (this._label);
1159     } else {
1160     JSTE.Element.appendText (this.element, this._label);
1161     }
1162 wakaba 1.6 this.element.className = this._classNames.list.join (' ');
1163 wakaba 1.1
1164 wakaba 1.6 var self = this;
1165     new JSTE.Observer ("click", this.element, function (e) {
1166     self._command (e);
1167     });
1168     } // _createElement
1169 wakaba 1.1 }); // Button
1170    
1171     JSTE.Course = new JSTE.Class (function (doc) {
1172     this._targetDocument = doc;
1173    
1174 wakaba 1.29 this._entryPoints = new JSTE.List;
1175     this._entryPoints.push
1176     ({conditions: new JSTE.List ([{type: 'state', value: 'done'}]),
1177     stepUid: 'special-none'});
1178 wakaba 1.1
1179     this._stepsState = new JSTE.List ([new JSTE.Hash]);
1180     this._steps = new JSTE.Hash;
1181    
1182     var nullState = new JSTE.Step;
1183 wakaba 1.29 nullState.uid = "special-none";
1184 wakaba 1.1 this._steps.setNamedItem (nullState.uid, nullState);
1185     this._initialStepUid = nullState.uid;
1186     }, {
1187 wakaba 1.10 _processStepsContent: function (el, parentSteps) {
1188 wakaba 1.1 var self = this;
1189     new JSTE.List (el.childNodes).switchByElementType (
1190     new JSTE.List.SwitchByLocalName (JSTE.WATNS, {
1191 wakaba 1.10 steps: function (n) { self._processStepsElement (n, parentSteps) },
1192     step: function (n) { self._processStepElement (n, parentSteps) },
1193     jump: function (n) { self._processJumpElement (n, parentSteps) },
1194 wakaba 1.29 'entry-point': function (n) { self._processEntryPointElement (n, parentSteps) }
1195 wakaba 1.1 })
1196     );
1197     }, // _processStepsContent
1198 wakaba 1.10 _processStepsElement: function (e, parentSteps) {
1199     var steps = new JSTE.Steps ();
1200     steps.parentSteps = parentSteps;
1201 wakaba 1.1 this._stepsState.pushCloneOfLast ();
1202     this._stepsState.getLast ().prevStep = null;
1203 wakaba 1.29
1204     this._addConditionsFromElement (e, steps.conditions);
1205 wakaba 1.10 this._processStepsContent (e, steps);
1206 wakaba 1.29
1207 wakaba 1.1 this._stepsState.pop ();
1208     }, // _processStepsElement
1209    
1210 wakaba 1.14 _processEntryPointElement: function (e, parentSteps) {
1211 wakaba 1.29 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 wakaba 1.14 }, // _processEntryPointElement
1218 wakaba 1.22
1219 wakaba 1.29 _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 wakaba 1.22 }
1227 wakaba 1.29
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 wakaba 1.1 }
1235 wakaba 1.29
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 wakaba 1.1 });
1241 wakaba 1.29 }
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 wakaba 1.1 });
1248     }
1249 wakaba 1.29
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 wakaba 1.1 });
1255 wakaba 1.29 }
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 wakaba 1.1 });
1262     }
1263 wakaba 1.29 }, // _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 wakaba 1.32 matched = JSTE.URL.eq (cond.value, docURL);
1281 wakaba 1.29 } 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 wakaba 1.1 }, // findEntryPoint
1300    
1301 wakaba 1.10 _processStepElement: function (e, parentSteps) {
1302 wakaba 1.18 var self = this;
1303    
1304 wakaba 1.1 var step = new JSTE.Step (e.getAttribute ('id'));
1305 wakaba 1.10 step.parentSteps = parentSteps;
1306 wakaba 1.1 step.setPreviousStep (this._stepsState.getLast ().prevStep);
1307     step.select = e.getAttribute ('select') || "";
1308     step.nextEvents.append
1309 wakaba 1.10 (JSTE.List.spaceSeparated (e.getAttribute ('next-event')));
1310 wakaba 1.17
1311 wakaba 1.28 step.noHistory = JSTE.Element.hasAttribute (e, 'nohistory');
1312    
1313 wakaba 1.17 var cs = JSTE.Element.getChildrenClassifiedByType (e);
1314    
1315     var msgEl = cs.get (JSTE.WATNS, 'message').list[0];
1316 wakaba 1.1 if (msgEl) {
1317     var msg = JSTE.Element.createTemplate (this._targetDocument, msgEl);
1318     step.setMessageTemplate (msg);
1319     }
1320 wakaba 1.17
1321     var nextEls = cs.get (JSTE.WATNS, 'next-step');
1322 wakaba 1.16 if (nextEls.list.length) {
1323 wakaba 1.1 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 wakaba 1.17
1332 wakaba 1.19 cs.get (JSTE.WATNS, 'command').forEach (function (bEl) {
1333 wakaba 1.18 var cmd = {
1334 wakaba 1.20 name: bEl.getAttribute ('type') || 'gotoStep'
1335 wakaba 1.18 };
1336     if (cmd.name == 'gotoStep') {
1337 wakaba 1.25 cmd.args = {stepUid: 'id-' + bEl.getAttribute ('step')};
1338 wakaba 1.24 } else if (cmd.name == 'url') {
1339 wakaba 1.27 // TODO: relative URL
1340 wakaba 1.26 cmd.args = {url: bEl.getAttribute ('href')};
1341 wakaba 1.18 }
1342 wakaba 1.26 cmd.actions = {
1343 wakaba 1.28 saveStateNames: JSTE.List.spaceSeparated (bEl.getAttribute ('save-state')),
1344 wakaba 1.26 clearStateNames: JSTE.List.spaceSeparated (bEl.getAttribute ('clear-state'))
1345     };
1346 wakaba 1.18 if (!JSTE.Element.isEmpty (bEl)) {
1347     cmd.labelTemplate = JSTE.Element.createTemplate (self._targetDocument, bEl);
1348     }
1349     step.availCommands.push (cmd);
1350 wakaba 1.22 }); // wat:command
1351 wakaba 1.18
1352 wakaba 1.22 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 wakaba 1.13
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 wakaba 1.1
1364     this._steps.setNamedItem (step.uid, step);
1365 wakaba 1.29 /*if (!this._initialStepUid) {
1366 wakaba 1.1 this._initialStepUid = step.uid;
1367 wakaba 1.29 }*/
1368 wakaba 1.1 }, // _processStepElement
1369    
1370 wakaba 1.10 _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 wakaba 1.1 }, // _processJumpElement
1378    
1379     getStep: function (uid) {
1380     return this._steps.getNamedItem (uid);
1381     } // getStep
1382     }); // Course
1383    
1384 wakaba 1.9 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 wakaba 1.10 course._processStepsContent (docEl, null);
1391 wakaba 1.32 var name = docEl.getAttribute ('name');
1392     if (name != null) {
1393     course.name = name + '-';
1394     } else {
1395     course.name = '';
1396     }
1397 wakaba 1.9 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 wakaba 1.1
1408 wakaba 1.10 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 wakaba 1.25 commandTarget.gotoStep ({stepUid: self.stepUid});
1420 wakaba 1.10 };
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 wakaba 1.29 this.conditions = new JSTE.List;
1439 wakaba 1.10 }, {
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 wakaba 1.1 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 wakaba 1.18 this.availCommands = new JSTE.List;
1475 wakaba 1.22 this.saveStates = new JSTE.List;
1476 wakaba 1.1 this.select = "";
1477 wakaba 1.30 // this._messageTemplate
1478 wakaba 1.1 }, {
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 wakaba 1.18 msg = new msg (doc, clone, commandTarget, this.availCommands.clone ());
1490 wakaba 1.1 } else {
1491 wakaba 1.30 msg = new msg (doc, null, commandTarget, this.availCommands.clone ());
1492 wakaba 1.1 }
1493     msg.select = this.select;
1494     return msg;
1495     }, // createMessage
1496    
1497     addNextStep: function (condition, stepId) {
1498 wakaba 1.16 if (stepId != null) this._nextSteps.push ([condition, stepId]);
1499 wakaba 1.1 }, // 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 wakaba 1.10 }, // 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 wakaba 1.1 }); // Step
1534    
1535 wakaba 1.22 JSTE.SaveState = new JSTE.Class (function (name, value) {
1536     this.name = name || '';
1537     this.value = value || '';
1538     }, {
1539 wakaba 1.25 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 wakaba 1.22 } // save
1545     }); // SaveState
1546    
1547     /* Events: load, error, cssomready, close */
1548 wakaba 1.9 JSTE.Tutorial = new JSTE.Class (function (course, doc, args) {
1549 wakaba 1.1 this._course = course;
1550     this._targetDocument = doc;
1551     this._messageClass = JSTE.Message;
1552     if (args) {
1553     if (args.messageClass) this._messageClass = args.messageClass;
1554 wakaba 1.22 if (args.states) this._states = args.states;
1555 wakaba 1.1 }
1556 wakaba 1.22 if (!this._states) this._states = new JSTE.Storage.PageLocal;
1557 wakaba 1.24 this._states.setPrefix (course.name);
1558 wakaba 1.1
1559     this._currentMessages = new JSTE.List;
1560     this._currentObservers = new JSTE.List;
1561 wakaba 1.25 this._currentStepsObjects = new JSTE.List;
1562    
1563 wakaba 1.1 this._prevStepUids = new JSTE.List;
1564 wakaba 1.25 this._loadBackState ();
1565    
1566     var stepUid;
1567 wakaba 1.29 if (this._states.flushGet ('is-back') && this._prevStepUids.list.length) {
1568 wakaba 1.25 stepUid = this._prevStepUids.pop ();
1569     } else {
1570     stepUid = this._course.findEntryPoint (document, this._states);
1571     }
1572    
1573 wakaba 1.1 this._currentStep = this._getStepOrError (stepUid);
1574     if (this._currentStep) {
1575     var e = new JSTE.Event ('load');
1576     this.dispatchEvent (e);
1577 wakaba 1.25
1578     this._saveBackState ();
1579    
1580 wakaba 1.3 var self = this;
1581     new JSTE.Observer ('cssomready', this, function () {
1582     self._renderCurrentStep ();
1583     });
1584     this._dispatchCSSOMReadyEvent ();
1585 wakaba 1.1 return this;
1586     } else {
1587     return {};
1588     }
1589     }, {
1590     _getStepOrError: function (stepUid) {
1591 wakaba 1.29 if (stepUid == 'special-none') {
1592     return null;
1593     }
1594    
1595 wakaba 1.1 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 wakaba 1.22
1611 wakaba 1.25 step.saveStates.forEach (function (ss) { ss.save (self) });
1612 wakaba 1.1
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 wakaba 1.10
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 wakaba 1.1 }, // _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 wakaba 1.10 clearStepsHandlers: function () {
1659     this._currentStepsObjects.forEach (function (item) {
1660     item.uninstallJumps ();
1661     });
1662     this._currentStepsObjects.clear ();
1663     }, // clearStepsHandlers
1664 wakaba 1.1
1665 wakaba 1.26 executeCommand: function (commandName, commandArgs, commandActions) {
1666 wakaba 1.1 if (this[commandName]) {
1667 wakaba 1.26 // Common actions
1668 wakaba 1.28 if (commandActions) {
1669 wakaba 1.26 var self = this;
1670 wakaba 1.28 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 wakaba 1.29 if (stateName == 'back-state') {
1678     self._prevStateUids = new JSTE.List;
1679     self._prevPages = new JSTE.List;
1680     }
1681 wakaba 1.32 self._states.del (stateName);
1682 wakaba 1.28 });
1683     }
1684 wakaba 1.26 }
1685    
1686 wakaba 1.25 return this[commandName].apply (this, [commandArgs || {}]);
1687 wakaba 1.1 } 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 wakaba 1.7 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 wakaba 1.1
1708     back: function () {
1709 wakaba 1.25 while (this._prevStepUids.list.length == 0 &&
1710     this._prevPages.list.length > 0) {
1711     var prevPage = this._prevPages.pop ();
1712 wakaba 1.32 if (!JSTE.URL.eq (prevPage.url, location.href)) { // TODO: fragment?
1713 wakaba 1.25 this._saveBackState (true);
1714 wakaba 1.29 this._states.flushSet ('is-back', true);
1715 wakaba 1.32 if (JSTE.URL.eq (document.referrer, prevPage.url)) { // TODO: fragment?
1716 wakaba 1.26 history.back ();
1717     } else {
1718     location.href = prevPage.url;
1719     }
1720 wakaba 1.25 // 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 wakaba 1.1 var prevStepUid = this._prevStepUids.pop ();
1727     var prevStep = this._getStepOrError (prevStepUid);
1728     if (prevStep) {
1729     this.clearMessages ();
1730 wakaba 1.25 this._saveBackState ();
1731 wakaba 1.1 this._currentStep = prevStep;
1732     this._renderCurrentStep ();
1733     }
1734     }, // back
1735 wakaba 1.7 canBack: function () {
1736 wakaba 1.25 return this._prevStepUids.list.length > 0 || this._prevPages.list.length > 0;
1737 wakaba 1.7 }, // canBack
1738 wakaba 1.1 next: function () {
1739     var nextStepUid = this._currentStep.getNextStepUid (this._targetDocument);
1740     var nextStep = this._getStepOrError (nextStepUid);
1741     if (nextStep) {
1742 wakaba 1.28 if (!this._currentStep.noHistory) {
1743     this._prevStepUids.push (this._currentStep.uid);
1744     }
1745 wakaba 1.1 this.clearMessages ();
1746 wakaba 1.25 this._saveBackState ();
1747 wakaba 1.1 this._currentStep = nextStep;
1748     this._renderCurrentStep ();
1749     }
1750 wakaba 1.3 }, // next
1751 wakaba 1.7 canNext: function () {
1752     return this._currentStep.getNextStepUid (this._targetDocument) != null;
1753     }, // canNext
1754 wakaba 1.25 gotoStep: function (args) {
1755     var nextStep = this._getStepOrError (args.stepUid);
1756 wakaba 1.10 if (nextStep) {
1757 wakaba 1.28 if (!this._currentStep.noHistory) {
1758     this._prevStepUids.push (this._currentStep.uid);
1759     }
1760 wakaba 1.25 this._saveBackState ();
1761 wakaba 1.10 this.clearMessages ();
1762     this._currentStep = nextStep;
1763     this._renderCurrentStep ();
1764     }
1765     }, // gotoStep
1766 wakaba 1.24
1767 wakaba 1.25 url: function (args) {
1768     location.href = args.url;
1769 wakaba 1.24 }, // url
1770 wakaba 1.20
1771     close: function () {
1772     this.clearMessages ();
1773 wakaba 1.22 var e = new JSTE.Event ('closed');
1774     this.dispatchEvent (e);
1775 wakaba 1.20 }, // close
1776 wakaba 1.25
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 wakaba 1.32 if (JSTE.URL.eq ((this._prevPages.getLast () || {}).url, location.href)) { // TODO: fragment?
1787 wakaba 1.25 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 wakaba 1.28 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 wakaba 1.25 }
1804     this._states.setJSON ('back-state', bs);
1805     }, // _saveBackState
1806 wakaba 1.3
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 wakaba 1.1 }); // Tutorial
1823 wakaba 1.9
1824     JSTE.Class.addClassMethods (JSTE.Tutorial, {
1825 wakaba 1.12 createFromURL: function (url, doc, args, onload) {
1826 wakaba 1.9 JSTE.Course.createFromURL (url, doc, function (course) {
1827 wakaba 1.12 var tutorial = new JSTE.Tutorial (course, doc, args);
1828     if (onload) onload (tutorial);
1829 wakaba 1.9 });
1830     } // createFromURL
1831     }); // Tutorial class methods
1832    
1833    
1834 wakaba 1.5
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 wakaba 1.2
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