/[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.28 - (hide annotations) (download) (as text)
Tue Feb 3 07:03:36 2009 UTC (16 years, 6 months ago) by wakaba
Branch: MAIN
Changes since 1.27: +26 -9 lines
File MIME type: application/javascript
Implemented @save-state and @nohistory

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

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24