]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/PhysCharWizard.js
daae2b1d4cc4b9c803f91ce6ad8d23ab656e56bf
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / PhysCharWizard.js
1 if (!dojo._hasResource["openils.widget.PhysCharWizard"]) {
2     dojo._hasResource["openils.widget.PhysCharWizard"] = true;
3
4     dojo.provide("openils.widget.PhysCharWizard");
5     dojo.require("dojo.string");
6     dojo.require("openils.User");
7     dojo.require("openils.Util");
8     dojo.require("openils.PermaCrud");
9     dojo.requireLocalization("openils.widget", "PhysCharWizard");
10
11     (function() {   /* Namespace protection so we can still make little helpers
12                     within our own bubble */
13
14         var _xhtml_ns = "http://www.w3.org/1999/xhtml";
15
16         function _show_button(n, yes) { /* yet another hide/reveal thing */
17             /* This is a re-invented wheel, but I was having trouble
18              * getting my <button>s to react to the disabled property, and
19              * decided to hide/reveal them instead of disable/enable then.
20              * Then I had this need to do it in a consistent way. */
21             n.setAttribute("style", "visibility: " + (yes? "visible":"hidden"));
22         }
23
24         function _get_xul_combobox_value(menulist) {
25             /* XUL comboboxes (<menulist editable="true">) are funny. */
26
27             return menulist.selectedItem ?
28                 menulist.selectedItem.value :
29                 menulist.label;  /* sic! Even .getAttribute('label') is
30                                     wrong, and anything to do with 'value'
31                                     is also wrong. */
32         }
33
34         /* Within the openils.widget.PhysCharWizard class, methods and
35          * properties that start "_" should be considered private, and others
36          * public.
37          *
38          * The methods whose names end in "_XUL" could be replaced with HTML
39          * versions to make this wizard work as an HTML thing instead of as
40          * a XUL thing.
41          */
42         dojo.declare(
43             "openils.widget.PhysCharWizard", [], {
44                 "active": true,
45                 "constructor": function(args) {
46                     this._ = openils.widget.PhysCharWizard.localeStrings;
47                     this._cache = openils.widget.PhysCharWizard.cache;
48
49                     /* Reserve a little of the window namespace that we'll need
50                      * (under XUL anyway) */
51                     window._owPCWinstances = window._owPCWinstances || 0;
52                     window._owPCW = window._owPCW || {};
53                     this.instance_num = window._owPCWinstances++;
54                     window._owPCW[this.instance_num] = this;
55                     this.outside_ref =
56                         "window._owPCW[" + this.instance_num + "]";
57
58                     /* Initialize and save misc values, and call build() to
59                      * make and place widgets. */
60                     this.onapply = args.onapply;
61
62                     this.step = 'a';
63                     this.more_back = false;
64                     this.more_forward = true;
65                     this.value = this.original_value = args.node.value;
66
67                     this.pcrud = new openils.PermaCrud(
68                         {"authtoken": ses ? ses() : openils.User.authtoken}
69                     );
70
71                     this.build(args.node);
72
73                     this._load_all_types(   /* and then: */
74                         dojo.hitch(this, function() { this.move(0); })
75                     );
76                 },
77                 "build": function(where) {
78                     this.original_node = where;
79                     var p = this.container_node = where.parentNode;
80                     p.removeChild(where);
81
82                     this._build_XUL();
83                 },
84                 "update_question": function(label, values) {
85                     this._update_question_XUL(label, values);
86                 },
87                 "update_value_label": function() {
88                     this._update_value_label_XUL(
89                         this._get_step_slot(), this.value
90                     );
91                 },
92                 "update_pagers": function() {
93                     _show_button(this.back_button, this.more_back);
94                     _show_button(this.forward_button, this.more_forward);
95                 },
96                 "apply": function(callback) {
97                     this.active = false;
98
99                     this.move(
100                         0, dojo.hitch(this, function() {
101                             this.onapply(this.value);
102                             if (typeof callback == "function")
103                                 callback();
104                         })
105                     );
106                 },
107                 "cancel": function() {
108                     this.active = false;
109                     this.container_node.removeChild(this.wizard_root_node);
110                     this.container_node.appendChild(this.original_node);
111                 },
112                 "_default_after_00": function() {
113                     /* This method assumes that the things it looks for in
114                      * the cache are freshly put there. */
115                     var working_ptype = this.value.substr(0, 1);
116                     var sf_list = this._cache.subfields[working_ptype];
117                     if (!sf_list)
118                         throw new Error(this._.BAD_WORKING_PTYPE);
119
120                     this.value = working_ptype;
121                     for (var i = 0; i < sf_list.length; i++) {
122                         var s = sf_list[i];
123                         var gap = s.start_pos() - this.value.length;
124                         if (gap > 0) {
125                             for (var j = 0; j < gap; j++)
126                                 this.value += " ";  /* XXX or '#' ? */
127                         } else if (gap < 0) {
128                             throw new Error(
129                                 dojo.string.substitute(
130                                     this._.BACKWARDS_SUBFIELD_PROGRESSION,
131                                     [working_ptype]
132                                 )
133                             );
134                         }
135
136                         for (var j = 0; j < s.length(); j++)
137                             this.value += "|";
138                     }
139                 },
140                 "move": function(offset, callback) {
141                     /* When we move the wizard, we need to accomplish five
142                      * things:
143                      *  1) Disable both pager buttons - sic
144                      *  2) Update the appopriate _slot of the working _value_
145                      *  with the value from the user input control.
146                      *  ---- sync above here ^ --------- async below here v ----
147                      *  3) Determine what the next _step_ will be and set it
148                      *  4) Replace the question and the dropdown with appro-
149                      *  priate data from the new _step_
150                      *  5) Reenable appropriate pager buttons
151                      *  6) (optional) fire any callback
152                      */
153
154                     /* Step 1 */
155                     _show_button(this.back_button, false);
156                     _show_button(this.forward_button, false);
157
158                     /* Step 2. No sweat so far. Skip if there is no
159                      * user control yet (initializing whole wizard still). */
160                     var a_changed = false;
161                     if (this.step_user_control) {
162                         a_changed = this.update_value_slot(
163                                 this._get_step_slot(),
164                                 this.get_step_value_from_control()
165                             ) && this.step == 'a';
166                     }
167
168                     /* Step 3 depends on knowing a) our working_ptype, which
169                      * may have just changed if step was 'a' and b) all the
170                      * subfields for that ptype, which we may have to
171                      * retrieve asynchronously. */
172                     this._get_subfields_for_type(
173                         this.value.substr(0, 1), /* working_ptype */
174                         /* and then: */ dojo.hitch(this, function() {
175
176                             /* Step 2.9 (small) */
177                             if (a_changed) this._default_after_00();
178
179                             /* Step 3 proper: */
180                             this._move_step(offset);
181
182                             /* Step 4: For the call to update_question, we had
183                              * better have values loaded for our current step.
184                              */
185                             this._get_values_for_step(
186                                 this.step,
187                                 /* and then: */ dojo.hitch(this, function(l, v){
188                                     /* Step 4 proper: */
189                                     this.update_value_label();
190                                     this.update_question(l, v);
191
192                                     /* Step 5 */
193                                     this.update_pagers();
194
195                                     if (typeof callback == "function") {
196                                         callback();
197                                     }
198                                 })
199                             );
200                         })
201                     );
202                 },
203                 "get_step_value_from_control": function() {
204                     return _get_xul_combobox_value(this.step_user_control);
205                 },
206                 "get_step_value": function() {
207                     return String.prototype.substr.apply(
208                         this.value, this._get_step_slot()
209                     );
210                 },
211                 "update_value_slot": function(slot, value) {
212                     /* Return true if this.value changes */
213
214                     if (!value.length) {
215                         /* Prevent erasing positions when backing up. */
216                         for (var i = 0; i < slot[1]; i++)
217                             value += '|';
218                     }
219
220                     var old_value = this.value;
221                     var before = this.value.substr(0, slot[0]);
222                     var after = this.value.substr(slot[0] + slot[1]);
223
224                     this.value = before + value.substr(0, slot[1]) + after;
225                     return (this.value != old_value);
226                 },
227                 "_load_all_types": function(callback) {
228                     /* It's easiest to have these always ready, and it's not
229                      * a large dataset. */
230
231                     if (this._cache.types.length)  /* maybe we already do */
232                         callback();
233
234                     this.pcrud.retrieveAll(
235                         "cmpctm", {
236                             "oncomplete": dojo.hitch(this, function(r) {
237                                 if (r = openils.Util.readResponse(r)) {
238                                     this._cache.types = r.map(
239                                         function(o) {
240                                             return [o.ptype_key(), o.label()];
241                                         }
242                                     );
243                                     callback();
244                                 } else {
245                                     throw new Error(this._.DATA_ERROR_007);
246                                 }
247                             })
248                         }
249                     );
250                 },
251                 "_get_subfields_for_type": function(working_ptype, callback) {
252                     if (this._cache.subfields[working_ptype]) {
253                         callback(this._cache.subfields[working_ptype]);
254                     } else {
255                         this.pcrud.search(
256                             "cmpcsm", {"ptype_key": working_ptype}, {
257                                 "order_by": {"cmpcsm": "subfield"},
258                                 "oncomplete": dojo.hitch(this, function(r) {
259                                     if (r = openils.Util.readResponse(r)) {
260                                         this._cache.subfields[working_ptype]= r;
261                                         callback(r);
262                                     } else {
263                                         throw new Error(this._.DATA_ERROR_007);
264                                     }
265                                 })
266                             }
267                         );
268                     }
269                 },
270                 "_get_values_for_step": function(step, callback) {
271                     /* Values are cached by subfield ID, so we find the
272                      * current subfield ID using the step and the
273                      * working_ptype. */
274
275                     if (this.step == 'a') {
276                         callback(this._.A_LABEL, this._cache.types);
277                         return;
278                     }
279
280                     var step = this.step;   /* for use w/in closure */
281                     var working_ptype = this.value.substr(0, 1);
282                     var subfields =
283                         this._cache.subfields[working_ptype].filter(
284                             function(s) { return s.subfield() == step; }
285                         );
286
287                     if (subfields.length != 1) {
288                         throw new Error(this._.BAD_SUBFIELD_DATA);
289                         return;
290                     }
291
292                     var subfield = subfields[0];
293                     if (this._cache.values[subfield.id()]) {
294                         callback(
295                             subfield.label(),
296                             this._cache.values[subfield.id()]
297                         );
298                     } else {
299                         this.pcrud.search(
300                             "cmpcvm", {"ptype_subfield": subfield.id()}, {
301                                 "order_by": {"cmpcvm": "value"},
302                                 "onresponse": dojo.hitch(this, function(r) {
303                                     if (r = openils.Util.readResponse(r)) {
304                                         this._cache.values[subfield.id()] =
305                                             r = r.map(
306                                                 function(v) {
307                                                     return [v.value(),v.label()]
308                                                 }
309                                             );
310                                         callback(subfield.label(), r);
311                                     } else {
312                                         throw new Error(this._.DATA_ERROR_007);
313                                     }
314                                 })
315                             }
316                         );
317                     }
318                 },
319                 "_get_step_slot": function() {
320                     /* We should never need to know the slot for our step
321                      * until *after* we have the subfields for that step
322                      * loaded. That allows us to keep this function sync
323                      * (i.e., it returns instead of using a callback).  */
324
325                     if (this.step == 'a') {
326                         return [0, 1];
327                     } else {
328                         var step = this.step;   /* to use w/in closure */
329                         var working_ptype = this.value.substr(0, 1);
330                         var matches =
331                             this._cache.subfields[working_ptype].filter(
332                                 function(s) { return s.subfield() == step; }
333                             );
334
335                         if (matches.length == 1)
336                             return [matches[0].start_pos(),matches[0].length()];
337                         else
338                             throw new Error(this._.BAD_SUBFIELD_DATA);
339                     }
340                 },
341                 "_move_step": function(offset) {
342                     /* This method is/should only be called when we know we
343                      * have the list of subfields for our working_ptype cached.
344                      *
345                      * We have two jobs in this method:
346                      *  1) Set this.step to something new.
347                      *  2) Update this.more_forward and this.more_back (bools)
348                      */
349                     var working_ptype = this.value.substr(0, 1);
350                     var found = -1;
351                     var sf_list = this._cache.subfields[working_ptype];
352
353                     for (var i = 0; i < sf_list.length; i++) {
354                         if (sf_list[i].subfield() == this.step) {
355                             found = i;
356                             break;
357                         }
358                     }
359
360                     var idx = found + offset;
361                     if (sf_list[idx]) {
362                         this.step = sf_list[idx].subfield();
363                         this.more_forward = Boolean(sf_list[idx + 1]);
364                         this.more_back = Boolean(idx >= 0);
365                     } else if (idx == -1) { /* 'a' */
366                         this.step = 'a';
367                         this.more_back = false;
368                         this.more_forward = true; /* or something's broke */
369                     } else {
370                         throw new Error(this._.FELL_OFF_STEPS);
371                     }
372                 },
373                 "_update_question_XUL": function(step_label, value_list) {
374                     var qh = this.question_holder;
375
376                     while (qh.firstChild) qh.removeChild(qh.firstChild);
377
378                     /* Add question label */
379                     var label = document.createElement("label");
380                     label.setAttribute("value", step_label + "?");
381                     label.setAttribute("style", "min-width: 16em;");
382                     qh.appendChild(label);
383
384                     /* Create combobox (in XUL this a <menulist editable="true">
385                      * with <menupopup> underneath and several <menuitem>s under
386                      * that). */
387                     var ml = this.step_user_control =
388                         document.createElement("menulist");
389                     ml.setAttribute("editable", "true");
390                     var mp = document.createElement("menupopup");
391                     ml.appendChild(mp);
392
393                     var starting_value = this.get_step_value();
394                     var found_starting_value = false;
395
396                     value_list.forEach(
397                         function(v) {
398                             var mi = document.createElement("menuitem");
399                             mi.setAttribute("label", v[0] + ": " + v[1]);
400                             mi.setAttribute("value", v[0]);
401
402                             if (v[0] == starting_value) {
403                                 mi.setAttribute("selected", "true");
404                                 found_starting_value = true;
405                             }
406
407                             mp.appendChild(mi);
408                         }
409                     );
410
411                     if (!found_starting_value) {
412                         /* Starting value wasn't one of the menuitems, but
413                          * we can force it: */
414                         ml.setAttribute("label", starting_value);
415                     }
416                     qh.appendChild(ml);
417                 },
418                 "_update_value_label_XUL": function(step_win, value) {
419                     var before = value.substr(0, step_win[0]);
420                     var within = value.substr(step_win[0], step_win[1]);
421                     var after = value.substr(step_win[0] + step_win[1]);
422
423                     var div = this.value_label;
424                     while (div.firstChild)
425                         div.removeChild(div.firstChild);
426
427                     div.appendChild(document.createTextNode(before));
428
429                     var el = document.createElementNS(_xhtml_ns,"xhtml:strong");
430                     el.appendChild(document.createTextNode(within));
431                     div.appendChild(el);
432
433                     div.appendChild(document.createTextNode(after));
434                 },
435                 "_gen_XUL_oncommand": function(methstr) {
436                     return "try { " + this.outside_ref +
437                         "." + methstr + " } catch (E) { alert('" +
438                         this.outside_ref + ": ' + E) }";
439                 },
440                 "_build_XUL": function() {
441                     var vbox = this.container_node.appendChild(
442                         document.createElement("vbox")
443                     );
444
445                     var top_hbox =
446                         vbox.appendChild(document.createElement("hbox"));
447
448                     this.question_holder =
449                         vbox.appendChild(document.createElement("hbox"));
450                     this.question_holder.setAttribute("align", "center");
451
452                     var bottom_hbox =
453                         vbox.appendChild(document.createElement("hbox"));
454
455                     this.value_label = top_hbox.appendChild(
456                         document.createElementNS(_xhtml_ns, "xhtml:div")
457                     );
458
459                     /* These em's must be measured in terms of the body
460                      * font-size, not the font-size local to these elements?
461                      * Or is that how em's always work? */
462                     this.value_label.setAttribute(
463                         "style", "min-width: 16em; white-space: pre;"
464                     );
465
466                     /* From here to the end of the method we're just building
467                      * and placing the wizard's four buttons. */
468                     var button;
469
470                     button = document.createElement("button");
471                     button.setAttribute("label", this._.OK);
472                     button.setAttribute("icon", "apply");
473                     button.setAttribute(
474                         "oncommand", this._gen_XUL_oncommand("apply()")
475                     );
476                     top_hbox.appendChild(button);
477
478                     button = document.createElement("button");
479                     button.setAttribute("label", this._.CANCEL);
480                     button.setAttribute("icon", "cancel");
481                     button.setAttribute(
482                         "oncommand", this._gen_XUL_oncommand("cancel()")
483                     );
484                     top_hbox.appendChild(button);
485
486                     this.back_button = button =
487                         document.createElement("button");
488                     button.setAttribute("label", this._.BACK);
489                     button.setAttribute("icon", "go-back");
490                     button.setAttribute(
491                         "oncommand", this._gen_XUL_oncommand("move(-1)")
492                     );
493                     button.disabled = true;
494                     bottom_hbox.appendChild(button);
495
496                     this.forward_button = button =
497                         document.createElement("button");
498                     button.setAttribute("label", this._.FORWARD);
499                     button.setAttribute("icon", "go-forward");
500                     button.setAttribute(
501                         "oncommand", this._gen_XUL_oncommand("move(1)")
502                     );
503                     button.disabled = true;
504                     bottom_hbox.appendChild(button);
505
506                     /* Save reference to root node of wizard for easy
507                      * removal when finished. */
508                     this.wizard_root_node = vbox;
509                 }
510             }
511         );
512     })();
513
514     /* Class-wide cache; all instance objects share this */
515     openils.widget.PhysCharWizard.cache = {
516         "subfields": {},    /* by type */
517         "values": {},       /* by subfield ID */
518         "types": []
519     };
520
521     openils.widget.PhysCharWizard.localeStrings =
522         dojo.i18n.getLocalization("openils.widget", "PhysCharWizard");
523 }