Serials: a wizard for the pattern code field of the caption_and_pattern object
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Fri, 3 Sep 2010 16:30:47 +0000 (16:30 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Fri, 3 Sep 2010 16:30:47 +0000 (16:30 +0000)
This field holds the same data you'd find in the contents of an 853, 854, or
855 tag.  This wizard aims to make it a little easier for mere mortals to
compose this information, since the correctness of this field is fairly
central to reasonably accurate serials issuance prediction.

Find a button to launch this wizard from the Caption/Pattern interface of the
Serial Control View.

Big thanks to Galen Charlton, Dan Wells and Mike Rylander for all the help in
getting this started and understanding the MARC.

The wizard does still have a way to go before it's everything it can be. It
still needs (in no particular order):
    - scrollbars on that dialog window
    - support for subfield $y and probably $z, possibly others
    - i18n fixes, accessibility improvements
    - more control over subfield $8
    - more input validation and user guidance; sane defaults for some fields?
    - reconsideration of the order of the parts of the wizard
    - finding out if it makes sense to allow $m without $j,$k and $l present

git-svn-id: svn://svn.open-ils.org/ILS/trunk@17472 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xul/staff_client/server/locale/en-US/serial.properties
Open-ILS/xul/staff_client/server/serial/batch_receive.js
Open-ILS/xul/staff_client/server/serial/batch_receive_overlay.xul
Open-ILS/xul/staff_client/server/serial/common.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/pattern_wizard.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/pattern_wizard.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/pattern_wizard_overlay.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/scap_editor.js
Open-ILS/xul/staff_client/server/serial/scap_editor.xul
Open-ILS/xul/staff_client/server/skin/serial.css

index fd1cbf7..0f3a07f 100644 (file)
 <!ENTITY staff.serial.mfhd_menu.add.label "Add MFHD Record">
 <!ENTITY staff.serial.mfhd_menu.edit.label "Edit MFHD Record">
 <!ENTITY staff.serial.mfhd_menu.delete.label "Delete MFHD Record">
+<!ENTITY staff.serial.scap_editor.pattern_wizard "Pattern Code Wizard">
+<!ENTITY staff.serial.scap_editor.pattern_wizard.accesskey "Z">
+<!ENTITY staff.serial.scap_editor.modify.accesskey "M">
 <!ENTITY staff.serial.scap_editor.modify "Modify Caption and Pattern(s)">
 <!ENTITY staff.serial.scap_editor.modify.accesskey "M">
 <!ENTITY staff.serial.scap_editor.create "Create Caption and Pattern(s)">
index 79f3dcb..84b0669 100644 (file)
@@ -67,3 +67,18 @@ batch_receive.receive_time_note=Receive-time Note
 batch_receive.cn_for_lib=Do you want to use this call number at %1$s?\nIt doesn't exist there, and it will have to be created.
 batch_receive.missing_units=You have not provided barcodes and call numbers for all of the selected items.  Choose OK to receive those items anyway, or choose Cancel to supply the missing information.
 batch_receive.missing_cn=You cannot assign a barcode without selecting a call number. Please correct the non-conforming units.
+pattern_wizard.enumeration.a=First level
+pattern_wizard.enumeration.b=Second level
+pattern_wizard.enumeration.c=Third level
+pattern_wizard.enumeration.d=Fourth level
+pattern_wizard.enumeration.e=Fifth level
+pattern_wizard.enumeration.f=Sixth level
+pattern_wizard.enumeration.g=First alternate
+pattern_wizard.enumeration.h=Second alternate
+pattern_wizard.chronology.i=First level
+pattern_wizard.chronology.j=Second level
+pattern_wizard.chronology.k=Third level
+pattern_wizard.chronology.l=Fourth level
+pattern_wizard.chronology.m=Alternative numbering scheme
+pattern_wizard.not_removable_row=You cannot remove this row because it's not at the end of the sequence.  Remove later rows first.
+pattern_wizard.bad_date_value=That is not a valid day for that month.
index 3e7ffd3..f43995d 100644 (file)
@@ -1,7 +1,6 @@
+/* The code in this file relies on common.js */
+
 dojo.require("dojo.cookie");
-dojo.require("dojo.date.locale");
-dojo.require("dojo.date.stamp");
-dojo.require("dojo.string");
 dojo.require("openils.Util");
 dojo.require("openils.User");
 dojo.require("openils.CGI");
@@ -9,61 +8,6 @@ dojo.require("openils.PermaCrud");
 
 var batch_receiver;
 
-String.prototype.trim = function() {return this.replace(/^\s*(.+)\s*$/,"$1");}
-
-/**
- * hard_empty() is needed because dojo.empty() doesn't seem to work on
- * XUL nodes. This also means that dojo.place() with a position argument of
- * "only" doesn't do what it should, but calling hard_empty() on the refnode
- * first will do the trick.
- */
-function hard_empty(node) {
-    if (typeof(node) == "string")
-        node = dojo.byId(node);
-
-    if (node && node.childNodes.length > 0) {
-        dojo.forEach(
-            node.childNodes,
-            function(c) {
-                if (c) {
-                    if (c.childNodes.length > 0)
-                        dojo.forEach(c.childNodes, hard_empty);
-                    dojo.destroy(c);
-                }
-            }
-        );
-    }
-}
-
-function hide(e) {
-    if (typeof(e) == "string") e = dojo.byId(e);
-    openils.Util.addCSSClass(e, "hideme");
-}
-
-function show(e) {
-    if (typeof(e) == "string") e = dojo.byId(e);
-    openils.Util.removeCSSClass(e, "hideme");
-}
-
-function hide_table_cell(e) {
-    if (typeof(e) == "string") e = dojo.byId(e);
-
-    e.style.display = "none";
-    e.style.visibility = "hidden";
-}
-
-function show_table_cell(e) {
-    if (typeof(e) == "string") e = dojo.byId(e);
-    e.style.display = "table-cell";
-    e.style.visibility = "visible";
-}
-
-function busy(on) {
-    if (typeof(busy._window) == "undefined")
-        busy._window = dojo.query("window")[0];
-    busy._window.style.cursor = on ? "wait" : "auto";
-}
-
 function S(k) {
     return dojo.byId("serialStrings").getString("batch_receive." + k).
         replace("\\n", "\n");
@@ -74,15 +18,6 @@ function F(k, args) {
         getFormattedString("batch_receive." + k, args).replace("\\n", "\n");
 }
 
-function T(s) { return document.createTextNode(s); }
-function D(s) {return s ? openils.Util.timeStamp(s,{"selector":"date"}) : "";}
-function node_by_name(s, ctx) {return dojo.query("[name='"+ s +"']",ctx)[0];}
-
-function num_sort(a, b) {
-    [a, b] = [Number(a), Number(b)];
-    return a > b ? 1 : (a < b ? -1 : 0);
-}
-
 function BatchReceiver() {
     var self = this;
 
index fe77020..adedc30 100644 (file)
@@ -6,10 +6,11 @@
     xmlns:h="http://www.w3.org/1999/xhtml"
     xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
+    <script type="text/javascript" src="/xul/server/serial/common.js" />
     <script type="text/javascript" src="/xul/server/serial/batch_receive.js" />
 
     <box id="batch_receive_main" flex="1" orient="vertical" class="my_overflow">
-        <caption label="&staff.serial.batch_receive;" />
+        <caption class="top" label="&staff.serial.batch_receive;" />
 
         <vbox flex="1" id="batch_receve_main_action">
             <vbox id="batch_receive_bib" class="hideme">
diff --git a/Open-ILS/xul/staff_client/server/serial/common.js b/Open-ILS/xul/staff_client/server/serial/common.js
new file mode 100644 (file)
index 0000000..674cd05
--- /dev/null
@@ -0,0 +1,62 @@
+String.prototype.trim = function() {return this.replace(/^\s*(.+)\s*$/,"$1");}
+
+/**
+ * hard_empty() is needed because dojo.empty() doesn't seem to work on
+ * XUL nodes. This also means that dojo.place() with a position argument of
+ * "only" doesn't do what it should, but calling hard_empty() on the refnode
+ * first will do the trick.
+ */
+function hard_empty(node) {
+    if (typeof(node) == "string")
+        node = dojo.byId(node);
+    if (node)
+        dojo.forEach(node.childNodes, dojo.destroy);
+}
+
+function hide(e) {
+    if (typeof(e) == "string") e = dojo.byId(e);
+    openils.Util.addCSSClass(e, "hideme");
+}
+
+function show(e) {
+    if (typeof(e) == "string") e = dojo.byId(e);
+    openils.Util.removeCSSClass(e, "hideme");
+}
+
+function hide_table_cell(e) {
+    if (typeof(e) == "string") e = dojo.byId(e);
+
+    e.style.display = "none";
+    e.style.visibility = "hidden";
+}
+
+function show_table_cell(e) {
+    if (typeof(e) == "string") e = dojo.byId(e);
+    e.style.display = "table-cell";
+    e.style.visibility = "visible";
+}
+
+function soft_hide(e) { /* doesn't disrupt XUL grid alignment */
+    if (typeof(e) == "string") e = dojo.byId(e);
+    e.style.visibility = "hidden";
+}
+
+function soft_show(e) {
+    if (typeof(e) == "string") e = dojo.byId(e);
+    e.style.visibility = "visible";
+}
+
+function busy(on) {
+    if (typeof(busy._window) == "undefined")
+        busy._window = dojo.query("window")[0];
+    busy._window.style.cursor = on ? "wait" : "auto";
+}
+
+function T(s) { return document.createTextNode(s); }
+function D(s) {return s ? openils.Util.timeStamp(s, {"selector":"date"}) : "";}
+function node_by_name(s, ctx) {return dojo.query("[name='" + s + "']", ctx)[0];}
+
+function num_sort(a, b) {
+    [a, b] = [Number(a), Number(b)];
+    return a > b ? 1 : (a < b ? -1 : 0);
+}
diff --git a/Open-ILS/xul/staff_client/server/serial/pattern_wizard.js b/Open-ILS/xul/staff_client/server/serial/pattern_wizard.js
new file mode 100644 (file)
index 0000000..95cbdec
--- /dev/null
@@ -0,0 +1,609 @@
+/* The code in this file relies on common.js */
+
+dojo.require("openils.Util");
+
+var wizard;
+
+function S(k) {
+    return dojo.byId("serialStrings").getString("pattern_wizard." + k).
+        replace("\\n", "\n");
+}
+
+function _month_menuitems() {
+    /* XXX i18n, and also this is just pathetic in general, but a datepicker
+     * seemed wrong since we don't want a year. */
+    return [
+        ["01", "January"],
+        ["02", "February"],
+        ["03", "March"],
+        ["04", "April"],
+        ["05", "May"],
+        ["06", "June"],
+        ["07", "July"],
+        ["08", "August"],
+        ["09", "September"],
+        ["10", "October"],
+        ["11", "November"],
+        ["12", "December"]
+    ].map(
+        function(t) {
+            return dojo.create("menuitem", {"value": t[0], "label": t[1]});
+        }
+    );
+}
+
+function _date_validate(date_val, month_val) {
+    /* general purpose date validation irrespective of year */
+    date_val = date_val.trim();
+
+    if (!date_val.match(/^[0123]?\d$/))
+        return false;
+
+    date_val = Number(date_val); /* do NOT use parseInt */
+    month_val = Number(month_val);
+
+    if (date_val < 1) {
+        return false;
+    } else if (month_val == 2) {
+        return date_val <= 29;
+    } else if ([1,3,5,7,8,10,12].indexOf(month_val) != -1) {
+        return date_val <= 31;
+    } else {
+        return date_val <= 30;
+    }
+}
+
+function CalendarChangeRow() {
+    var self = this;
+
+    this._init = function(template, id, manager) {
+        this.element = dojo.clone(template);
+
+        this.point_widgets = ["month", "season", "date"].map(
+            function(type) { return node_by_name(type, self.element); }
+        );
+
+        dojo.attr(
+            node_by_name("type", this.element), "oncommand", function(ev) {
+                self.point_widgets.forEach(
+                    function(w) {
+                        var active = (dojo.attr(w, "name") == ev.target.value);
+                        (active ? show : hide)(w);
+                    }
+                );
+            }
+        );
+
+        var date_month_selector = node_by_name("date_month", this.element);
+
+        dojo.attr(
+            node_by_name("date_day", this.element), "onchange", function(ev) {
+                if (_date_validate(ev.target.value,date_month_selector.value)){
+                    return true;
+                } else {
+                    alert(S("bad_date_value"));
+                    ev.target.focus();
+                    return false;
+                }
+            }
+        );
+
+        this.remover = node_by_name("remover", this.element);
+        dojo.attr(
+            this.remover, "onclick", function() { manager.remove_row(id); }
+        );
+    };
+
+    this._compile_month = function() {
+        return node_by_name("month", this.element).value;
+    };
+
+    this._compile_season = function() {
+        return node_by_name("season", this.element).value;
+    };
+
+    this._compile_date = function() {
+        var n = Number(node_by_name("date_day", this.element).value);
+        if (n < 10)
+            n = "0" + String(n);
+        else
+            n = String(n);
+
+        return node_by_name("date_month", this.element).value + n;
+    };
+
+    this.compile = function() {
+        var type = node_by_name("type", this.element).value;
+
+        return this["_compile_" + type]();
+    };
+
+    this._init.apply(this, arguments);
+};
+
+function CalendarChangeEditor() {
+    var self = this;
+
+    this._init = function() {
+        this.rows = {};
+        this.row_count = 0;
+
+        this._get_template();
+        this.add_row();
+    };
+
+    this._get_template = function() {
+        var temp_template = dojo.byId("calendar_row_template");
+        this.grid = temp_template.parentNode;
+        this.template = this.grid.removeChild(temp_template);
+        this.template.removeAttribute("id");
+
+        [
+            dojo.query("[name='month'] menupopup", this.template)[0],
+            dojo.query("[name='date_month'] menupopup", this.template)[0]
+        ].forEach(
+            function(menupopup) {
+                _month_menuitems().forEach(
+                    function(menuitem) {
+                        dojo.place(menuitem, menupopup, "last");
+                    }
+                );
+            }
+        );
+    };
+
+    this.remove_row = function(id) {
+        hard_empty(this.rows[id].element);
+        dojo.destroy(this.rows[id].element);
+        delete this.rows[id];
+
+        dojo.byId("calendar_change_add_row").disabled = false;
+    };
+
+    this.add_row = function() {
+        var id = this.row_count++;
+
+        this.rows[id] =
+            new CalendarChangeRow(dojo.clone(this.template), id, this);
+        dojo.place(this.rows[id].element, this.grid, "last");
+    };
+
+    this.toggle = function(ev) {
+        (ev.target.checked ? show : hide)("calendar_change_editor_here");
+    };
+
+    this.compile = function() {
+        return [
+            "x",
+            openils.Util.objectProperties(this.rows).sort(num_sort).map(
+                function(key) { return self.rows[key].compile(); }
+            ).join(",")
+        ];
+    };
+
+    this._init.apply(this, arguments);
+}
+
+function ChronRow() {
+    var self = this;
+
+    this._init = function(template, subfield, manager) {
+        this.subfield = subfield;
+        this.element = dojo.clone(template);
+
+        dojo.attr(
+            node_by_name("caption_label", this.element),
+            "value", S("chronology." + subfield) + ":"
+        );
+
+        this.fields = {};
+        ["caption", "display_in_holding"].forEach(
+            function(o) { self.fields[o] = node_by_name(o, self.element); }
+        );
+
+        this.remover = node_by_name("remover", this.element);
+        dojo.attr(
+            this.remover, "onclick", function(){ manager.remove_row(subfield); }
+        );
+    };
+
+    this._init.apply(this, arguments);
+};
+
+function ChronEditor() {
+    /* TODO make this enforce unique caption values for each row? */
+    var self = this;
+
+    this._init = function() {
+        this.rows = {};
+
+        this.subfields = ["i", "j", "k", "l", "m"];
+
+        this._get_template();
+        this.add_row();
+    };
+
+    this._get_template = function() {
+        var temp_template = dojo.byId("chron_row_template");
+        this.grid = temp_template.parentNode;
+        this.template = this.grid.removeChild(temp_template);
+        this.template.removeAttribute("id");
+    };
+
+    this._test_removability = function(subfield) {
+        var start = this.subfields.indexOf(subfield);
+
+        if (start < 0) {
+            /* no such field, not OK to remove */
+            return false;
+        } else if (!this.subfields[start]) {
+            /* field row not present, not OK to remove */
+            return false;
+        }
+
+        var next = this.subfields[start + 1];
+        if (typeof(next) == "undefined") { /* last in set, ok to remove */
+            return true;
+        } else {
+            if (this.rows[next]) { /* NOT last in set, not ok to remove */
+                return false;
+            } else { /* last in set actually present, ok to remove */
+                return true;
+            }
+        }
+    };
+
+    this.remove_row = function(subfield) {
+        if (this._test_removability(subfield)) {
+            hard_empty(this.rows[subfield].element);
+            dojo.destroy(this.rows[subfield].element);
+            delete this.rows[subfield];
+
+            dojo.byId("chron_add_row").disabled = false;
+        } else {
+            alert(S("not_removable_row"));
+        }
+    };
+
+    this.add_row = function() {
+        var available = this.subfields.filter(
+            function(subfield) { return !Boolean(self.rows[subfield]); }
+        );
+
+        if (available.length) {
+            var subfield = available.shift();
+            if (!available.length)
+                dojo.byId("chron_add_row").disabled = true;
+        } else {
+            /* We shouldn't really be able to get here. */
+            return;
+        }
+
+        this.rows[subfield] =
+            new ChronRow(dojo.clone(this.template), subfield, this);
+
+        dojo.place(this.rows[subfield].element, this.grid, "last");
+    };
+
+    this.toggle = function(ev) {
+        (ev.target.checked ? show : hide)("chron_editor_here");
+    };
+
+    this.compile = function() {
+        return this.subfields.filter(
+            function(subfield) { return Boolean(self.rows[subfield]); }
+        ).reduce(
+            function(result, subfield) {
+                var caption = self.rows[subfield].fields.caption.value;
+                if (!self.rows[subfield].fields.display_in_holding.checked)
+                    caption = "(" + caption + ")";
+                return result.concat([subfield, caption]);
+            }, []
+        );
+    };
+
+    this._init.apply(this, arguments);
+}
+
+function EnumRow() {
+    var self = this;
+
+    this._init = function(template, subfield, manager) {
+        this.subfield = subfield;
+        this.element = dojo.clone(template);
+
+        this.fields = {};
+        ["caption","units_per","units_per_number","continuity","remover"].
+            forEach(
+                function(o) { self.fields[o] = node_by_name(o, self.element); }
+            );
+
+        if (subfield == "a" || subfield == "g") {
+            ["units_per", "continuity"].forEach(
+                function(o) { soft_hide(node_by_name(o, self.element)); }
+            );
+        }
+
+        var caption_id = "enum_caption_" + subfield;
+        var caption_label = node_by_name("caption_label", this.element);
+        dojo.attr(this.fields.caption, "id", caption_id);
+        dojo.attr(caption_label, "control", caption_id);
+        dojo.attr(caption_label, "value", S("enumeration." + subfield) + ":");
+
+        this.remover = this.fields.remover;
+        dojo.attr(
+            this.remover, "onclick", function(){manager.remove_row(subfield);}
+        );
+    };
+
+    this._init.apply(this, arguments);
+};
+
+function EnumEditor() {
+    var self = this;
+
+    this._init = function() {
+        this.normal_rows = {};
+        this.alt_rows = {};
+
+        this.normal_subfields = ["a","b","c","d","e","f"];
+        this.alt_subfields = ["g","h"];
+
+        this._get_template();
+        this.add_normal_row();
+    };
+
+    this._get_template = function() {
+        var temp_template = dojo.byId("enum_row_template");
+        this.grid = temp_template.parentNode;
+        this.template = this.grid.removeChild(temp_template);
+        this.template.removeAttribute("id");
+    };
+
+    this.remove_row = function(subfield) {
+        if (this._test_removability(subfield)) {
+            var add_button = "enum_add_normal_row";
+            var set = this.normal_rows;
+            if (!set[subfield]) {
+                set = this.alt_rows;
+                add_button = "enum_add_alt_row";
+            }
+
+            hard_empty(set[subfield].element);
+            dojo.destroy(set[subfield].element);
+            delete set[subfield];
+            dojo.byId(add_button).disabled = false;
+        } else {
+            alert(S("not_removable_row"));
+        }
+    };
+
+    this._test_removability = function(id) {
+        var set = this.normal_subfields;
+        var rows = this.normal_rows;
+        var start = set.indexOf(id);
+
+        if (start == -1) {
+            set = this.alt_subfields;
+            rows = this.alt_rows;
+            start = set.indexOf(id);
+        }
+
+        if (start < 0) {
+            /* no such field, not OK to remove */
+            return false;
+        } else if (!set[start]) {
+            /* field row not present, not OK to remove */
+            return false;
+        }
+
+        var next = set[start + 1];
+        if (typeof(next) == "undefined") { /* last in set, ok to remove */
+            return true;
+        } else {
+            if (rows[next]) { /* NOT last in set, not ok to remove */
+                return false;
+            } else { /* last in set actually present, ok to remove */
+                return true;
+            }
+        }
+    };
+
+    this.add_normal_row = function() {
+        var available = this.normal_subfields.filter(
+            function(subfield) { return !Boolean(self.normal_rows[subfield]); }
+        );
+        if (available.length) {
+            var subfield = available.shift();
+            if (!available.length) {
+                /* If that was the last available normal row, disable the
+                 * add rows button. */
+                dojo.byId("enum_add_normal_row").disabled = true;
+            }
+        } else {
+            /* We shouldn't really be able to get here. */
+            return;
+        }
+
+        this.normal_rows[subfield] =
+            new EnumRow(dojo.clone(this.template), subfield, this);
+
+        dojo.place(this.normal_rows[subfield].element, this.grid, "last");
+    };
+
+    this.add_alt_row = function() {
+        var available = this.alt_subfields.filter(
+            function(subfield) { return !Boolean(self.alt_rows[subfield]); }
+        );
+        if (available.length) {
+            var subfield = available.shift();
+            if (!available.length) {
+                /* If that was the last available normal row, disable the
+                 * add rows button. */
+                dojo.byId("enum_add_alt_row").disabled = true;
+            }
+        } else {
+            /* We shouldn't really be able to get here. */
+            return;
+        }
+
+        this.alt_rows[subfield] =
+            new EnumRow(dojo.clone(this.template), subfield, this);
+
+        dojo.place(this.alt_rows[subfield].element, this.grid, "last");
+    };
+
+    this.toggle = function(ev) {
+        var func;
+        var use_calendar_change = dojo.byId("use_calendar_change");
+
+        if (ev.target.checked) {
+            func = show;
+            use_calendar_change.disabled = false;
+        } else {
+            use_calendar_change.checked = false;
+            use_calendar_change.doCommand();
+            use_calendar_change.disabled = true;
+            func = hide;
+        }
+
+        func("enum_editor_here");
+    };
+
+    this.compile = function() {
+        var rows = dojo.mixin({}, this.normal_rows, this.alt_rows);
+        var subfields = [].concat(this.normal_subfields, this.alt_subfields);
+
+        return subfields.filter(
+            function(subfield) { return Boolean(rows[subfield]); }
+        ).reduce(
+            function(result, subfield) {
+                var fields = rows[subfield].fields;
+                var pairs = [subfield, fields.caption.value];
+
+                if (subfield != "a" && subfield != "g") {
+                    if (fields.units_per.value == "number") {
+                        if (fields.units_per_number.value) {
+                            pairs = pairs.concat([
+                                "u", fields.units_per_number.value,
+                                "v", fields.continuity.value
+                            ]);
+                        }
+                    } else {
+                        pairs = pairs.concat([
+                            "u", fields.units_per.value,
+                            "v", fields.continuity.value
+                        ]);
+                    }
+                }
+
+                return result.concat(pairs);
+            }, []
+        );
+    };
+
+    this._init.apply(this, arguments);
+}
+
+function Wizard() {
+    var self = this;
+
+    var _step_prefix = "wizard_step_";
+    var _step_regex = new RegExp("^" + _step_prefix + "(.+)$");
+
+    this._init = function(onsubmit) {
+        this._onsubmit = onsubmit;
+
+        this.load();
+        this.reset();
+    };
+
+    this.load = function() {
+        /* The Wizard object will handle simpler parts of the wizard (those
+         * parts with more-or-less static controls) itself, and will
+         * instantiate more specific objects to deal with more dynamic
+         * super-widgets (like the enum and chron editors).
+         */
+        this.steps = dojo.query("[id^='" + _step_prefix + "']").map(
+            function(o) {
+                return dojo.attr(o, "id").match(_step_regex)[1];
+            }
+        );
+
+        this.enum_editor = new EnumEditor();
+        this.chron_editor = new ChronEditor();
+        this.calendar_change_editor = new CalendarChangeEditor();
+
+        this.field_w = dojo.byId("hard_w");
+    };
+
+    this.reset = function() {
+        this.step = 0;
+        this.show_only_step(this.steps[0]);
+        this.step_bounds_check();
+    };
+
+    this.show_step = function(step) { show(_step_prefix + step); }
+    this.hide_step = function(step) { hide(_step_prefix + step); }
+
+    this.step_bounds_check = function() {
+        dojo.byId("wizard_previous_step").disabled = this.step < 1;
+        dojo.byId("wizard_next_step").disabled =
+            this.step >= this.steps.length -1;
+    };
+
+    this.show_only_step = function(to_keep) {
+        this.steps.forEach(
+            function(step) { if (step != to_keep) self.hide_step(step); }
+        );
+        this.show_step(to_keep);
+    };
+
+    this.previous_step = function() {
+        this.show_only_step(this.steps[--(this.step)]);
+        this.step_bounds_check();
+    };
+
+    /* Figure out the what step we're in, and proceed to the next */
+    this.next_step = function() {
+        this.show_only_step(this.steps[++(this.step)]);
+        this.step_bounds_check();
+    };
+
+    this.frequency_type_toggle = function(which) {
+        var other = which == "soft_w" ? "hard_w" : "soft_w";
+
+        dojo.byId(other).disabled = true;
+        dojo.byId(which).disabled = false;
+        dojo.byId(which).focus();
+
+        this.field_w = dojo.byId(which);
+    };
+
+    this.compile = function() {
+        var code = [
+            dojo.byId("ind1").value, dojo.byId("ind2").value,
+            "8", "1" /* TODO find out how to best deal with $8 */
+        ];
+
+        code = code.concat(this.enum_editor.compile());
+        code = code.concat(this.chron_editor.compile());
+
+        code = code.concat("w", this.field_w.value);
+
+        code = code.concat(this.calendar_change_editor.compile());
+
+        return code;
+    };
+
+    this.submit = function() {
+        this._onsubmit(js2JSON(this.compile()));
+        window.close();
+    };
+
+    this._init.apply(this, arguments);
+}
+
+function my_init() {
+    wizard = new Wizard(window.arguments[0]);
+}
diff --git a/Open-ILS/xul/staff_client/server/serial/pattern_wizard.xul b/Open-ILS/xul/staff_client/server/serial/pattern_wizard.xul
new file mode 100644 (file)
index 0000000..4dcf3f9
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/serial.css" type="text/css"?>
+<!DOCTYPE window PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+<?xul-overlay href="/xul/server/serial/pattern_wizard_overlay.xul"?>
+
+<window id="pattern_wizard_win"
+    onload="try{my_init();font_helper();persist_helper();}catch(E){alert(E);}"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <script type="text/javascript">
+        var myPackageDir = "open_ils_staff_client";
+        var IAMXUL = true;
+        var g = {};
+    </script>
+
+    <scripts id="openils_util_scripts" />
+
+    <!-- JSAN is still needed for font_helper stuff, but I'm going to try
+        not to use it otherwise.  -->
+    <script type="text/javascript" src="/xul/server/main/JSAN.js" />
+
+    <messagecatalog id="serialStrings"
+        src="/xul/server/locale/<!--#echo var='locale'-->/serial.properties" />
+
+    <commandset />
+    <box id="pattern_wizard_main" />
+</window>
diff --git a/Open-ILS/xul/staff_client/server/serial/pattern_wizard_overlay.xul b/Open-ILS/xul/staff_client/server/serial/pattern_wizard_overlay.xul
new file mode 100644 (file)
index 0000000..47a8605
--- /dev/null
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+<overlay id="pattern_wizard_overlay"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <script type="text/javascript" src="/xul/server/serial/common.js" />
+    <script type="text/javascript" src="/xul/server/serial/pattern_wizard.js" />
+
+    <box orient="vertical" id="pattern_wizard_main" flex="1">
+        <caption class="top" label="Pattern Code Wizard" />
+        <vbox flex="1">
+            <hbox align="center" class="padded_bottom">
+                <button id="wizard_previous_step" disabled="true"
+                    icon="go-back" label="Previous"
+                    oncommand="wizard.previous_step();" />
+                <spacer flex="1" />
+                <button id="wizard_next_step" disabled="true"
+                    icon="go-forward" label="Next"
+                    oncommand="wizard.next_step();" />
+            </hbox>
+            <vbox id="wizard_step_captions" class="hideme">
+                <checkbox id="use_enum"
+                    oncommand="wizard.enum_editor.toggle(event);"
+                    label="Use enumerations?" />
+                <vbox id="enum_editor_here" class="hideme">
+                    <description class="step">
+                        "v." and "no." are common first and second level
+                        enumeration captions.
+                    </description>
+                    <grid flex="1">
+                        <columns>
+                            <column />
+                            <column />
+                            <column />
+                            <column />
+                        </columns>
+                        <rows>
+                            <row id="enum_row_headings">
+                                <label value="Enumeration Caption" />
+                                <label value="Units Per Higher Level" />
+                                <label value="Numbering Continuity" />
+                                <label />
+                            </row>
+                            <row id="enum_row_template" align="top">
+                                <vbox>
+                                    <label name="caption_label" />
+                                    <hbox>
+                                        <spacer flex="1" />
+                                        <textbox size="6" name="caption" />
+                                    </hbox>
+                                </vbox>
+                                <radiogroup name="units_per">
+                                    <hbox align="center">
+                                        <radio label="Number" value="number" />
+                                        <textbox name="units_per_number"
+                                            size="6" />
+                                    </hbox>
+                                    <radio label="Varies" value="var" />
+                                    <radio label="Undetermined" value="und" />
+                                </radiogroup>
+                                <menulist name="continuity">
+                                    <menupopup>
+                                        <menuitem value="c" label="Increments continuously" />
+                                        <menuitem value="r" label="Restarts at unit completion" />
+                                    </menupopup>
+                                </menulist>
+                                <button icon="remove" name="remover" label="Remove" />
+                            </row>
+                        </rows>
+                    </grid>
+                    <hbox>
+                        <spacer flex="1" />
+                        <button id="enum_add_normal_row"
+                            icon="add"
+                            label="Add Enumeration" accesskey="E"
+                            oncommand="wizard.enum_editor.add_normal_row();"
+                            />
+                        <spacer flex="1" />
+                        <button id="enum_add_alt_row"
+                            icon="add"
+                            label="Add Alternate Enumeration" accesskey="A"
+                            oncommand="wizard.enum_editor.add_alt_row();"
+                            />
+                        <spacer flex="1" />
+                    </hbox>
+                </vbox>
+            </vbox>
+            <vbox id="wizard_step_calendar_change" class="hideme">
+                <checkbox id="use_calendar_change"
+                    disabled="true"
+                    oncommand="wizard.calendar_change_editor.toggle(event);"
+                    label="Use calendar changes?" />
+                <vbox id="calendar_change_editor_here" class="hideme">
+                    <description class="step">
+                        Identify any points during the year at which the
+                        highest level enumeration caption changes.
+                    </description>
+                    <grid>
+                        <columns>
+                            <column />
+                            <column flex="1" />
+                            <column />
+                        </columns>
+                        <rows>
+                            <row id="calendar_change_row_headings">
+                                <label value="Type" />
+                                <label value="Point" />
+                                <label />
+                            </row>
+                            <row id="calendar_row_template">
+                                <menulist name="type">
+                                    <menupopup>
+                                        <menuitem value="month" label="At start of a month" />
+                                        <menuitem value="season" label="At start of a season" />
+                                        <menuitem value="date" label="On a date" />
+                                    </menupopup>
+                                </menulist>
+                                <hbox align="center">
+                                    <menulist name="month">
+                                        <menupopup>
+                                        </menupopup>
+                                    </menulist>
+                                    <menulist name="season" class="hideme">
+                                        <menupopup>
+                                            <menuitem value="21" label="Spring" />
+                                            <menuitem value="22" label="Summer" />
+                                            <menuitem value="23" label="Autumn" />
+                                            <menuitem value="24" label="Winter" />
+                                        </menupopup>
+                                    </menulist>
+                                    <hbox name="date" class="hideme">
+                                        <menulist name="date_month">
+                                            <menupopup>
+                                            </menupopup>
+                                        </menulist>
+                                        <textbox name="date_day" size="3" />
+                                    </hbox>
+                                </hbox>
+                                <button icon="remove" name="remover" label="Remove" />
+                            </row>
+                        </rows>
+                    </grid>
+                    <hbox pack="center">
+                        <button
+                            id="calendar_change_add_row"
+                            label="Add Calendar Change"
+                            accesskey="C"
+                            oncommand="wizard.calendar_change_editor.add_row();" />
+                    </hbox>
+                </vbox>
+            </vbox>
+            <vbox id="wizard_step_chronology" class="hideme">
+                <checkbox id="use_chron"
+                    oncommand="wizard.chron_editor.toggle(event);"
+                    label="Use chronology captions?" />
+                <vbox id="chron_editor_here" class="hideme">
+                    <description class="step">
+                        Generally, each caption should be a smaller unit of
+                        time than the preceding caption.
+                    </description>
+                    <grid>
+                        <columns>
+                            <column />
+                            <column />
+                            <column flex="1" />
+                            <column />
+                        </columns>
+                        <rows>
+                            <row id="chron_row_headings">
+                                <label />
+                                <label value="Caption" />
+                                <label value="Display in holding field?" />
+                                <label />
+                            </row>
+                            <row id="chron_row_template">
+                                <label name="caption_label" />
+                                <menulist name="caption">
+                                    <menupopup>
+                                        <menuitem label="Year" value="year" />
+                                        <menuitem label="Season" value="season" />
+                                        <menuitem label="Month" value="month" />
+                                        <menuitem label="Week" value="week" />
+                                        <menuitem label="Day" value="day" />
+                                        <menuitem label="Hour" value="hour" />
+                                    </menupopup>
+                                </menulist>
+                                <checkbox name="display_in_holding" />
+                                <button icon="remove" name="remover" label="Remove" />
+                            </row>
+                        </rows>
+                    </grid>
+                    <hbox pack="center">
+                        <button
+                            id="chron_add_row"
+                            label="Add Chronology Caption"
+                            accesskey="C"
+                            oncommand="wizard.chron_editor.add_row();" />
+                    </hbox>
+                </vbox>
+            </vbox>
+            <vbox id="wizard_step_basics" class="hideme">
+                <grid class="padded_bottom">
+                    <columns>
+                        <column />
+                        <column />
+                    </columns>
+                    <rows><!-- TODO hide these inputs if we're doing an 855 -->
+                        <row align="center">
+                            <label align="right" value="Compressibility and Expandability:" />
+                            <menulist id="ind1">
+                                <menupopup>
+                                    <menuitem value="0" label="Cannot compress or expand" />
+                                    <menuitem value="1" label="Can compress but not expand" />
+                                    <menuitem value="2" label="Can compress or expand" />
+                                    <menuitem value="3" label="Unknown" />
+                                </menupopup>
+                            </menulist>
+                        </row>
+                        <row align="center">
+                            <label align="right" value="Caption Evaluation:" />
+                            <menulist id="ind2">
+                                <menupopup>
+                                    <menuitem value="0" label="Captions verified; all levels present" />
+                                    <menuitem value="1" label="Captions verified; all levels may not be present" />
+                                    <menuitem value="2" label="Captions unverified; all levels present" />
+                                    <menuitem value="3" label="Captions unverified; all levels may not be present" />
+                                </menupopup>
+                            </menulist>
+                        </row>
+                    </rows>
+                </grid>
+                <radiogroup
+                    oncommand="wizard.frequency_type_toggle(this.value);">
+                    <grid>
+                        <columns><column /><column /></columns>
+                        <rows>
+                            <row>
+                                <radio
+                                    label="Select fundamental periodicity:"
+                                    accesskey="F" selected="true"
+                                    value="hard_w" />
+                                <menulist id="hard_w">
+                                    <menupopup>
+                                        <menuitem label="Annual" value="a" />
+                                        <menuitem label="Bimonthly" value="b" />
+                                        <menuitem label="Semiweekly" value="c" />
+                                        <menuitem label="Daily" value="d" />
+                                        <menuitem label="Biweekly" value="e" />
+                                        <menuitem label="Semiannual" value="f" />
+                                        <menuitem label="Biennial" value="g" />
+                                        <menuitem label="Triennial" value="h" />
+                                        <menuitem label="Three times a week" value="i" />
+                                        <menuitem label="Three times a month" value="j" />
+                                        <menuitem label="Continuously updated" value="k" />
+                                        <menuitem label="Monthly" value="m" />
+                                        <menuitem label="Quarterly" value="q" />
+                                        <menuitem label="Semimonthly" value="s" />
+                                        <menuitem label="Three times a year" value="t" />
+                                        <menuitem label="Weekly" value="w" />
+                                        <menuitem label="Completely irregular" value="x" />
+                                    </menupopup>
+                                </menulist>
+                            </row>
+                            <row>
+                                <radio
+                                    label="Use number of issues per year:"
+                                    value="soft_w"
+                                    selected="false" accesskey="I" />
+                                <textbox id="soft_w" disabled="true" />
+                            </row>
+                        </rows>
+                    </grid>
+                </radiogroup>
+            </vbox>
+            <vbox id="wizard_step_submit" class="hideme">
+                <description class="step">
+                    Are you ready to create a pattern code from your
+                    selections in this wizard?
+                </description>
+                <hbox pack="center">
+                    <button oncommand="wizard.submit();" icon="accept"
+                        accesskey="P" label="Create Pattern Code" />
+                </hbox>
+            </vbox>
+        </vbox>
+    </box>
+</overlay>
index f76f5f4..1a54084 100644 (file)
@@ -2,6 +2,7 @@ dump('entering serial/scap_editor.js\n');
 // vim:noet:sw=4:ts=4:
 
 JSAN.use('serial.editor_base');
+var pattern_code_key = 'Pattern Code (temporary)';
 
 if (typeof serial == 'undefined') serial = {};
 serial.scap_editor = function (params) {
@@ -98,7 +99,7 @@ serial.scap_editor.prototype = {
                 }
             ],
             [
-                'Pattern Code (temporary)',
+                pattern_code_key,
                 { 
                     render: 'fm.pattern_code() == null ? "" : fm.pattern_code();',
                     input: 'c = function(v){ obj.apply("pattern_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.pattern_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
@@ -132,6 +133,24 @@ serial.scap_editor.prototype = {
         obj.editor_base_save('open-ils.serial.caption_and_pattern.batch.update');
     },
 
+    /* Pattern/caption wizard */
+    "pattern_wizard": function() {
+        var obj = this;
+
+        var onsubmit = function(value) {
+            obj.apply("pattern_code", value);
+            obj.summarize(obj.scaps);
+            obj.render();
+        };
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        window.openDialog(
+            xulG.url_prefix("/xul/server/serial/pattern_wizard.xul"),
+            "pattern_wizard",
+            "scrollbars=yes", /* XXX FIXME: scrollbars aren't working. what to do? */
+            onsubmit
+        );
+    },
+
     /******************************************************************************************************/
     'save_attributes' : serial.editor_base.editor_base_save_attributes
 };
index 9c272f7..2dd4b28 100644 (file)
@@ -22,6 +22,7 @@
 
                <hbox id="scap_editor_nav">
                        <spacer flex="1"/>
+                       <button id="scap_pattern_wizard" label="&staff.serial.scap_editor.pattern_wizard;" accesskey="&staff.serial.scap_editor.pattern_wizard.accesskey;" oncommand="g.manage_subs.scap_editor.pattern_wizard();" />
                        <button id="scap_save" label="&staff.serial.scap_editor.modify;" hidden="true" accesskey="&staff.serial.scap_editor.modify.accesskey;" oncommand="g.manage_subs.scap_editor.save()" />
                        <!--<button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="window.close();"/>-->
                </hbox>
index c790a1a..1c6bfa6 100644 (file)
@@ -1,5 +1,5 @@
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-caption {
+caption.top {
     background-color: #00246b;
     color: #ff6308;
     font-size: 200%;
@@ -10,3 +10,13 @@ label.receiving { width: 20em; text-align: right; }
 #batch_receive_entry { padding-top: 10px; }
 #entry_submitter { padding: 20px 0; }
 menulist.cn { width: 12em; }
+button[icon="remove"] { min-width: 2.5em; }
+#enum_row_headings label { font-weight: bold; }
+#chron_row_headings label { font-weight: bold; }
+#calendar_change_row_headings label { font-weight: bold; }
+description.step { font-style: italic; font-size: 110%; }
+checkbox:focus:not([label]) .checkbox-label-box {
+    -moz-appearance: none;
+    border: none;
+}
+.padded_bottom { padding-bottom: 10px; }