From 765bf4ef1107a16ddbf84dbccecde762dd3df4f6 Mon Sep 17 00:00:00 2001
From: "Mark M. Florida" <mflorida@gmail.com>
Date: Tue, 17 May 2016 16:48:21 -0500
Subject: [PATCH] =?UTF-8?q?XNAT-4210:=20Site=20Setup=20page=20works=20as?=
 =?UTF-8?q?=20requested=20(initialized:true)=20is=20set=20before=20showing?=
 =?UTF-8?q?=20the=20confirmation=20dialog,=20also=20moved=20site=20setup?=
 =?UTF-8?q?=20functions=20to=20/scripts/xnat/app/siteSetup.js;=20XNAT-4192?=
 =?UTF-8?q?:=20session=20timer=20works=20on=20all=20pages=20and=20DOESN'T?=
 =?UTF-8?q?=20RELY=20ON=20YUI=20ANYMORE;=20more=20reliable=20lookup=20of?=
 =?UTF-8?q?=20Spawner=E2=84=A2=20widget=20methods;=20added=20hooks=20in=20?=
 =?UTF-8?q?form=20input=20widgets=20to=20lookup=20a=20value=20via=20AJAX?=
 =?UTF-8?q?=20using=20syntax:=20'value:=20=3F$=20/path/to/data=20|=20prop.?=
 =?UTF-8?q?name';?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/main/webapp/WEB-INF/tags/page/xnat.tag    |   3 +-
 src/main/webapp/scripts/globals.js            |  23 ++
 .../lib/jquery-plugins/jquery.hasClasses.js   |  76 +++++
 src/main/webapp/scripts/timeLeft.js           | 278 +++++++++++-------
 src/main/webapp/scripts/xmodal-v1/xmodal.js   |  12 +-
 src/main/webapp/scripts/xnat/app/siteSetup.js | 171 +++++++++++
 src/main/webapp/scripts/xnat/spawner.js       |  24 +-
 src/main/webapp/scripts/xnat/ui/input.js      |  20 --
 src/main/webapp/scripts/xnat/ui/panel.js      |  54 ++--
 src/main/webapp/scripts/xnat/ui/templates.js  | 137 +++++----
 src/main/webapp/scripts/xnat/xhr.js           |  21 +-
 src/main/webapp/setup/index.jsp               |   9 +-
 12 files changed, 600 insertions(+), 228 deletions(-)
 create mode 100644 src/main/webapp/scripts/lib/jquery-plugins/jquery.hasClasses.js
 create mode 100644 src/main/webapp/scripts/xnat/app/siteSetup.js

diff --git a/src/main/webapp/WEB-INF/tags/page/xnat.tag b/src/main/webapp/WEB-INF/tags/page/xnat.tag
index 368cdd1e..924ed878 100644
--- a/src/main/webapp/WEB-INF/tags/page/xnat.tag
+++ b/src/main/webapp/WEB-INF/tags/page/xnat.tag
@@ -67,6 +67,7 @@
     <link rel="stylesheet" type="text/css" href="${SITE_ROOT}/scripts/lib/jquery-plugins/chosen/chosen.min.css?${versionString}">
     <script src="${SITE_ROOT}/scripts/lib/jquery-plugins/chosen/chosen.jquery.min.js"></script>
     <script src="${SITE_ROOT}/scripts/lib/jquery-plugins/jquery.maskedinput.min.js"></script>
+    <script src="${SITE_ROOT}/scripts/lib/jquery-plugins/jquery.hasClasses.js"></script>
     <script src="${SITE_ROOT}/scripts/lib/jquery-plugins/jquery.dataAttr.js"></script>
     <script src="${SITE_ROOT}/scripts/lib/jquery-plugins/jquery.form.js"></script>
 
@@ -209,7 +210,7 @@
 
     <script src="${SITE_ROOT}/scripts/xnat/spawner.js"></script>
 
-    <%--<script src="${SITE_ROOT}/scripts/timeLeft.js"></script>--%>
+    <script src="${SITE_ROOT}/scripts/timeLeft.js"></script>
 
     ${headBottom}
 
diff --git a/src/main/webapp/scripts/globals.js b/src/main/webapp/scripts/globals.js
index 032b9005..be8df8d6 100644
--- a/src/main/webapp/scripts/globals.js
+++ b/src/main/webapp/scripts/globals.js
@@ -305,6 +305,29 @@ function setExtendedObject(obj, str, val){
     return newObj;
 }
 
+// loops over object string using dot notation:
+// var myVal = lookupObjectValue(XNAT, 'data.siteConfig.siteId');
+// --> myVal == 'myXnatSiteId'
+function lookupObjectValue(root, objStr){
+    var val = '';
+    if (!objStr) {
+        objStr = root;
+        root = window;
+    }
+    root = root || window;
+    objStr.toString().trim().split('.').forEach(function(part, i){
+        part = part.trim();
+        // start at the root object
+        if (i === 0) {
+            val = root[part] || '';
+        }
+        else {
+            if (!val) return false;
+            val = val[part];
+        }
+    });
+    return val;
+}
 
 // return the last item in an array-like object
 function getLast(arr){
diff --git a/src/main/webapp/scripts/lib/jquery-plugins/jquery.hasClasses.js b/src/main/webapp/scripts/lib/jquery-plugins/jquery.hasClasses.js
new file mode 100644
index 00000000..d40c5408
--- /dev/null
+++ b/src/main/webapp/scripts/lib/jquery-plugins/jquery.hasClasses.js
@@ -0,0 +1,76 @@
+/*!
+ * jQuery.hasClasses plugin
+ *
+ * Check if an element has ALL classes
+ * or ANY of a number of classes
+ *
+ * usage:
+ * $('#el-id').hasClasses('foo || bar')   <- has EITHER 'foo' or 'bar'
+ * $('#el-id').hasClasses('|| foo bar')   <- has EITHER 'foo' or 'bar'
+ * $('#el-id').hasClasses('any: foo bar') <- has EITHER 'foo' or 'bar'
+ *
+ * $('#el-id').hasClasses('foo && bar')   <- must have BOTH 'foo' AND 'bar'
+ * $('#el-id').hasClasses('&& foo bar')   <- must have BOTH 'foo' AND 'bar'
+ * $('#el-id').hasClasses('all: foo bar') <- must have BOTH 'foo' AND 'bar'
+ *
+ * or call hasAnyClass() or hasAllClasses directly
+ * $('#el-id').hasAnyClass('foo bar') <- has EITHER 'foo' or 'bar'
+ * $('#el-id').hasAllClasses('foo bar') <- must have BOTH 'foo' AND 'bar'
+ *
+ */
+
+(function($){
+
+    // wrap jQuery's .hasClass in a local function
+    function hasClassName($element, className) {
+        return $element.hasClass(className);
+    }
+
+    // remove 'all:', 'any:', delimeters '||' and '&&'
+    // and any bogus commas or periods
+    function splitClassNames(classNames){
+        return classNames.replace(/\.|,|any:|all:|\|\||&&/g,' ').trim().split(/\s+/);
+    }
+
+    $.fn.hasAnyClass = function(classNames) {
+        var i = -1,
+            classes = splitClassNames(classNames),
+            len = classes.length;
+        while (++i < len) {
+            if (hasClassName(this, classes[i])) {
+                return true;
+            }
+        }
+        return false;
+    };
+
+    $.fn.hasAllClasses = function(classNames) {
+        var i = -1,
+            classes = splitClassNames(classNames),
+            len = classes.length,
+            matches = 0;
+        while (++i < len) {
+            if (hasClassName(this, classes[i])) {
+                matches++;
+            }
+        }
+        return matches === len;
+    };
+
+    $.fn.hasClasses = function( classNames ){
+        var any = false;
+        // forgive stupidity if || is included in an 'all:' check
+        if (classNames.indexOf('all:') === 0){
+            any = false;
+        }
+        else if (classNames.indexOf('any:') === 0){
+            any = true;
+        }
+        else if (classNames.indexOf('||') > -1){
+            any = true;
+        }
+        // 'all' is default check if no 'any' indicators are present
+        return any ? this.hasAnyClass(classNames) : this.hasAllClasses(classNames);
+    };
+
+})(jQuery);
diff --git a/src/main/webapp/scripts/timeLeft.js b/src/main/webapp/scripts/timeLeft.js
index 0572060e..b32ce67f 100644
--- a/src/main/webapp/scripts/timeLeft.js
+++ b/src/main/webapp/scripts/timeLeft.js
@@ -5,7 +5,10 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
 
 // wrap it in an IIFE and pass the 'XNAT.app.timeout' object as the argument.
 // Is this the best way to do this? I don't know.
-(function(timeout){
+$(function(){
+
+    var timeout = XNAT.app.timeout;
+
     /*
      * The SESSION_EXPIRATION_TIME cookie returned from the server is double quoted for some reason
      * so unquote it before parsing it out.
@@ -22,6 +25,58 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
         return ret;
     };
 
+
+
+    timeout.warningDialog = (function() {
+
+        // var timeLeft = timeout.settings.expirationTime.timeLeft;
+
+        // dialog.find('.body.content > .inner').html("Your "+XNAT.app.siteId+" session will expire in:</br></br>&nbsp;&nbsp;&nbsp; " + timeLeft.hoursPart + " hours "
+        //     + timeout.zeroPad(timeLeft.minutesPart) + " minutes " + +timeout.zeroPad(timeLeft.secondsPart) + ' seconds.' +
+        //     '</br></br>Click "Renew" to reset session timer.');
+
+        var z = 99999;
+
+        var dialog = xmodal.open({
+            id: 'session-timeout-warning',
+            classes: 'keep static',
+            width: 320,
+            height: 200,
+            title: false,
+            content: 'Your ' + XNAT.app.siteId + ' session will expire in: <br><br>' +
+            '<span class="mono timeout-hours"></span> hours ' +
+            '<span class="mono timeout-minutes"></span> minutes ' +
+            '<span class="mono timeout-seconds"></span> seconds.' +
+            '</br></br>Click "Renew" to reset session timer.',
+            okLabel: 'Renew',
+            okAction: function(obj){
+                timeout.handleOk();
+                obj.$modal.hide();
+            },
+            okClose: false
+        });
+
+        dialog.$mask.hide().css('z-index', z-1);
+        dialog.$modal.hide().css('z-index', z);
+
+        dialog.hours   = dialog.$modal.find('span.timeout-hours');
+        dialog.minutes = dialog.$modal.find('span.timeout-minutes');
+        dialog.seconds = dialog.$modal.find('span.timeout-seconds');
+
+        return dialog;
+
+    })();
+
+    timeout.warningDialog.show = function(){
+        timeout.warningDialog.$mask.show();
+        timeout.warningDialog.$modal.show();
+    };
+
+    timeout.warningDialog.hide = function(){
+        timeout.warningDialog.$mask.hide();
+        timeout.warningDialog.$modal.hide();
+    };
+
     /**
      * These cookies are available across tabs and windows:
      * - expirationTime : Stores a tuple that is (0|1, maximum idle time)
@@ -142,19 +197,19 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
         timeout.synchronizingCookies.hasRedirected.clear();
     };
 
-    timeout.disableButtons = function(dialog) {
-        var buttons = dialog.getButtons();
-        for (var i = 0; i < buttons.length; i++) {
-            buttons[i].set('disabled', true);
-        }
-    };
-
-    timeout.enableButtons = function(dialog) {
-        var buttons = dialog.getButtons();
-        for (var i = 0; i < buttons.length; i++) {
-            buttons[i].set('disabled', false);
-        }
-    };
+    // timeout.disableButtons = function(dialog) {
+    //     var buttons = dialog.getButtons();
+    //     for (var i = 0; i < buttons.length; i++) {
+    //         buttons[i].set('disabled', true);
+    //     }
+    // };
+    //
+    // timeout.enableButtons = function(dialog) {
+    //     var buttons = dialog.getButtons();
+    //     for (var i = 0; i < buttons.length; i++) {
+    //         buttons[i].set('disabled', false);
+    //     }
+    // };
 
     /**
      * If a user double-clicks a button in YUI's SimpleDialog
@@ -162,23 +217,23 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
      * So we have to disable the buttons after each click and
      * enable them again when the dialog is shown.
      */
-    timeout.hideWarningDialog = function(dialog) {
-        timeout.disableButtons(dialog);
+    timeout.hideWarningDialog = function() {
+        // timeout.disableButtons(dialog);
         timeout.synchronizingCookies.dialogDisplay.set("false");
-        dialog.hide();
+        timeout.warningDialog.hide();
     };
 
-    timeout.showWarningDialog = function(dialog) {
-        timeout.enableButtons(dialog);
+    timeout.showWarningDialog = function() {
+        // timeout.enableButtons();
         timeout.synchronizingCookies.dialogDisplay.set("true");
-        dialog.show();
+        timeout.warningDialog.show();
     };
 
     /**
      * If the user wants to extend the session, hide the dialog and "touch" the server
      */
     timeout.handleOk = function () {
-        timeout.hideWarningDialog(timeout.warningDialog);
+        // timeout.hideWarningDialog();
         timeout.touchCallback.startTime = new Date().getTime();
         XNAT.xhr.get(XNAT.url.restUrl('/xapi/siteConfig/buildInfo'), timeout.touchCallback);
         $('applet').css('visibility', 'visible');
@@ -189,8 +244,8 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
      * to all tabs and ensure that they all close their dialogs.
      */
     timeout.handleCancel = function () {
-        timeout.hideWarningDialog(timeout.warningDialog);
-        timeout.settings.warningDisplayedOnce = true;
+        timeout.hideWarningDialog();
+        // timeout.settings.warningDisplayedOnce = true;
         // don't make it any more complicated than necessary - just show the thing
         $('applet').css('visibility', 'visible');
     };
@@ -206,7 +261,7 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
             }
             else {
                 timeout.refreshSynchronizingCookies();
-                timeout.settings.warningDisplayedOnce = false;
+                // timeout.settings.warningDisplayedOnce = false;
             }
         },
         failure: function () {
@@ -221,35 +276,35 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
         return (y < 10) ? '0'+y : ''+y ;
     };
 
-    /**
-     * The warning dialog
-     */
-    timeout.warningDialog = new YAHOO.widget.SimpleDialog("session_timeout_dialog", {
-        width: "300px",
-        close: true,
-        fixedcenter: true,
-        // z-index is manhandled in xnat.css
-        // but we need to set it here as a base z-index for the other YUI dialogs
-        zIndex: 5001,
-        constraintoviewport: true,
-        modal: true,
-        icon: YAHOO.widget.SimpleDialog.ICON_WARN,
-        visible: true,
-        draggable: true,
-        hideAfterSubmit: true,
-        buttons: [
-            { text: 'Renew', handler: timeout.handleOk, isDefault: true },
-            { text: 'Close', handler: timeout.handleCancel }
-        ]
-    });
+    // /**
+    //  * The warning dialog
+    //  */
+    // timeout.warningDialog = new YAHOO.widget.SimpleDialog("session_timeout_dialog", {
+    //     width: "300px",
+    //     close: true,
+    //     fixedcenter: true,
+    //     // z-index is manhandled in xnat.css
+    //     // but we need to set it here as a base z-index for the other YUI dialogs
+    //     zIndex: 5001,
+    //     constraintoviewport: true,
+    //     modal: true,
+    //     icon: YAHOO.widget.SimpleDialog.ICON_WARN,
+    //     visible: true,
+    //     draggable: true,
+    //     hideAfterSubmit: true,
+    //     buttons: [
+    //         { text: 'Renew', handler: timeout.handleOk, isDefault: true },
+    //         { text: 'Close', handler: timeout.handleCancel }
+    //     ]
+    // });
 
     timeout.initWarningDialog = function(dialog) {
-        dialog.manager = this;
-        dialog.render(document.body);
-        dialog.setHeader("Session Timeout Warning");
-        dialog.setBody("");
-        dialog.bringToTop();
-        dialog.hide();
+        // dialog.manager = this;
+        // dialog.render(document.body);
+        // dialog.setHeader("Session Timeout Warning");
+        // dialog.setBody("");
+        // dialog.bringToTop();
+        // dialog.hide();
     };
 
     /**
@@ -318,23 +373,23 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
      * 4. The dialog should not be displayed and is not displayed. Hide the dialog anyway
      * in case another tab as been opened while the popup was open in this one.
      */
-    timeout.updateMessageOrHide = function(dialog) {
-        if (timeout.synchronizingCookies.dialogDisplay.get() === "true" && timeout.settings.warningDisplayedOnce) {
-            var timeLeft = timeout.settings.expirationTime.timeLeft;
-            dialog.setBody("Your "+XNAT.app.siteId+" session will expire in:</br></br>&nbsp;&nbsp;&nbsp; " + timeLeft.hoursPart + " hours "
-                + timeout.zeroPad(timeLeft.minutesPart) + " minutes " + +timeout.zeroPad(timeLeft.secondsPart) + ' seconds.' +
-                '</br></br>Click "Renew" to reset session timer.');
-        }
-        else if (timeout.synchronizingCookies.dialogDisplay.get() === "true" && !timeout.settings.warningDisplayedOnce) {
-            timeout.settings.warningDisplayedOnce = true;
-            timeout.showWarningDialog(dialog);
-            timeout.updateMessageOrHide(dialog);
-        }
-        else if (timeout.synchronizingCookies.dialogDisplay.get() === "false" && timeout.settings.warningDisplayedOnce) {
-            timeout.hideWarningDialog(dialog);
+    timeout.updateMessageOrHide = function() {
+        if (timeout.synchronizingCookies.dialogDisplay.get() === "true") {
+            // var timeLeft = timeout.settings.expirationTime.timeLeft;
+            // dialog.hours = timeLeft.hours
+            timeout.showWarningDialog();
+            // timeout.updateMessageOrHide();
+        // }
+        // else if (timeout.synchronizingCookies.dialogDisplay.get() === "true" && !timeout.settings.warningDisplayedOnce) {
+            // timeout.settings.warningDisplayedOnce = true;
+            // timeout.showWarningDialog();
+            // timeout.updateMessageOrHide();
         }
-        else if (timeout.synchronizingCookies.dialogDisplay.get() === "false" && !timeout.settings.warningDisplayedOnce) {
-            timeout.hideWarningDialog(dialog);
+        // else if (timeout.synchronizingCookies.dialogDisplay.get() === "false" && timeout.settings.warningDisplayedOnce) {
+        //     timeout.hideWarningDialog();
+        // }
+        else if (timeout.synchronizingCookies.dialogDisplay.get() === "false") {
+            timeout.hideWarningDialog();
         }
     };
 
@@ -374,12 +429,23 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
      */
     timeout.sessionCountdown = function() {
 
+        var dialog = timeout.warningDialog;
         var timeLeft = timeout.settings.expirationTime.timeLeft;
         var $timeLeft = $('#timeLeft');
 
-        $timeLeft.text(timeLeft.hoursPart + ":" + timeout.zeroPad(timeLeft.minutesPart) + ":" + timeout.zeroPad(timeLeft.secondsPart));
+        var hours = timeLeft.hoursPart;
+        var mins  = timeout.zeroPad(timeLeft.minutesPart);
+        var secs  = timeout.zeroPad(timeLeft.secondsPart);
+
+        $timeLeft.text(hours + ":" + mins + ":" + secs);
+
+        // Update the text in the dialog too so it's always in synch
+        dialog.hours.text(hours);
+        dialog.minutes.text(mins);
+        dialog.seconds.text(secs);
 
-        if ((timeLeft.secondsLeft < timeout.settings.popupTime) && (!timeout.settings.warningDisplayedOnce)) {
+        if ((timeLeft.secondsLeft < timeout.settings.popupTime)) {
+            timeout.warningDialog.show();
             timeout.synchronizingCookies.dialogDisplay.set("true");
         }
 
@@ -395,40 +461,46 @@ if (typeof XNAT.app.timeout == 'undefined'){ XNAT.app.timeout={} }
         }
     };
 
-})(XNAT.app.timeout);
-
-/**
- * Initialize the synchronizing cookies and warning dialog and kick off the
- * counter.
- */
-XNAT.app.timeout.refreshSynchronizingCookies();
-XNAT.app.timeout.initWarningDialog(XNAT.app.timeout.warningDialog);
-// only run the timer if *not* a guest user (if an authenticated user)
-if ((!!Cookies.get('guest')) && (Cookies.get('guest') === 'false')) {
-    setInterval(
-        function(){
-            XNAT.app.timeout.syncSessionExpirationCookieWithLocal();
-            XNAT.app.timeout.updateMessageOrHide(XNAT.app.timeout.warningDialog);
-            XNAT.app.timeout.sessionCountdown();
-        },
-        XNAT.app.timeout.settings.timerInterval
-    );
-}
-
-(function(){
-
-    var hash = window.location.hash.toLowerCase();
-
-    // force debug mode to 'stick' if set explicitly 'on' or 'off'
-    var debugOn = /(debug=on|debug=true)/.test(hash.toLowerCase());
-    var debugOff = /(debug=off|debug=false)/.test(hash.toLowerCase());
-
-    if (debugOn) { Cookies.set('debug','on') }
-    else if (debugOff) { Cookies.remove('debug') }
 
-    // if debugging, reset the timer every minute
-    if (debugOn || window.debug || isFalse(getQueryStringValue('timeout')) || /(on|true)/.test(Cookies.get('debug'))) {
-        setInterval(XNAT.app.timeout.handleOk, 60*1000);
+    /**
+     * Initialize the synchronizing cookies and warning dialog and kick off the
+     * counter.
+     */
+    XNAT.app.timeout.refreshSynchronizingCookies();
+    // XNAT.app.timeout.initWarningDialog();
+    // only run the timer if *not* a guest user (if an authenticated user)
+    if ((!!Cookies.get('guest')) && (Cookies.get('guest') === 'false')) {
+        setInterval(
+            function(){
+                XNAT.app.timeout.syncSessionExpirationCookieWithLocal();
+                XNAT.app.timeout.updateMessageOrHide();
+                XNAT.app.timeout.sessionCountdown();
+            },
+            XNAT.app.timeout.settings.timerInterval
+        );
     }
 
-})();
+    // attach event handler to elements with 'renew-session' class
+    $('body').on('click', '#timeLeftRenew, .renew-session', function(){
+        timeout.handleOk();
+    });
+
+    // (function(){
+    //
+    //     var hash = window.location.hash.toLowerCase();
+    //
+    //     // force debug mode to 'stick' if set explicitly 'on' or 'off'
+    //     var debugOn = /(debug=on|debug=true)/.test(hash.toLowerCase());
+    //     var debugOff = /(debug=off|debug=false)/.test(hash.toLowerCase());
+    //
+    //     if (debugOn) { Cookies.set('debug','on') }
+    //     else if (debugOff) { Cookies.remove('debug') }
+    //
+    //     // if debugging, reset the timer every 2 minutes
+    //     if (debugOn || window.debug || isFalse(getQueryStringValue('timeout')) || /(on|true)/.test(Cookies.get('debug'))) {
+    //         setInterval(XNAT.app.timeout.handleOk, 120*1000);
+    //     }
+    //
+    // })();
+
+});
diff --git a/src/main/webapp/scripts/xmodal-v1/xmodal.js b/src/main/webapp/scripts/xmodal-v1/xmodal.js
index b1399e3c..11792284 100644
--- a/src/main/webapp/scripts/xmodal-v1/xmodal.js
+++ b/src/main/webapp/scripts/xmodal-v1/xmodal.js
@@ -1004,14 +1004,18 @@ if (typeof jQuery == 'undefined') {
 
             // fade out then remove the modal
             $modal.fadeOut(fade, function(){
-                $(this).remove();
+                
                 // then if there's a mask that goes with it
                 // get rid of that too
                 if ($mask.length){
-                    $mask.fadeOut(fade/3, function(){
-                        $(this).remove();
-                    });
+                    $mask.hide();
+                }
+                // don't remove 'static' modals we want to 'keep'
+                if (!$modal.hasClasses('keep || static')) {
+                    $modal.remove();
+                    $mask.remove();
                 }
+
                 if (!$(xmodal.dialog.open).length) {
                     $body.removeClass('open');
                     $html.removeClass('noscroll');
diff --git a/src/main/webapp/scripts/xnat/app/siteSetup.js b/src/main/webapp/scripts/xnat/app/siteSetup.js
new file mode 100644
index 00000000..eb0119d3
--- /dev/null
+++ b/src/main/webapp/scripts/xnat/app/siteSetup.js
@@ -0,0 +1,171 @@
+/*!
+ * Spawn form input elements
+ */
+
+var XNAT = getObject(XNAT);
+
+(function(factory){
+    if (typeof define === 'function' && define.amd) {
+        define(factory);
+    }
+    else if (typeof exports === 'object') {
+        module.exports = factory();
+    }
+    else {
+        return factory();
+    }
+}(function(){
+
+    var siteSetup, undefined;
+    
+    XNAT.app = getObject(XNAT.app || {});
+    
+    XNAT.app.siteSetup = siteSetup =
+        getObject(XNAT.app.siteSetup || {});
+
+    var multiform = {
+        count: 0,
+        errors: 0
+    };
+
+    siteSetup.multiform = multiform;
+    
+    // use app.siteSetup.form for Spawner 'kind'
+
+    // creates a panel that submits all forms contained within
+    siteSetup.form = function(opts, callback){
+
+        opts = cloneObject(opts);
+        opts.element = opts.element || opts.config || {};
+
+        var inner = spawn('div.panel-body', opts.element),
+            
+            submitBtn = spawn('button', {
+                type: 'submit',
+                classes: 'btn submit save pull-right',
+                html: 'Save All'
+            }),
+
+            resetBtn = spawn('button', {
+                type: 'button',
+                classes: 'btn btn-sm btn-default revert pull-right',
+                html: 'Discard Changes',
+                onclick: function(e){
+                    e.preventDefault();
+                    $(this).closest('form.multi-form').find('form').each(function(){
+                        $(this).triggerHandler('reload-data');
+                    });
+                    return false;
+                }
+            }),
+
+            defaults = spawn('button', {
+                type: 'button',
+                classes: 'btn btn-link defaults pull-left',
+                html: 'Default Settings'
+            }),
+
+            footer = [
+                submitBtn,
+                ['span.pull-right', '&nbsp;&nbsp;&nbsp;'],
+                resetBtn,
+                // defaults,
+                ['div.clear']
+            ],
+
+            multiForm = spawn('form', {
+                name: opts.name,
+                classes: 'xnat-form-panel multi-form panel panel-default',
+                method: opts.method || 'POST',
+                action: opts.action ? XNAT.url.rootUrl(opts.action) : '#!',
+                onsubmit: function(e){
+
+                    e.preventDefault();
+                    var $forms = $(this).find('form');
+
+                    var loader = xmodal.loading.open('#multi-save');
+
+                    // reset error count on new submission
+                    multiform.errors = 0;
+
+                    // how many child forms are there?
+                    multiform.count = $forms.length;
+
+                    // submit ALL enclosed forms
+                    $forms.each(function(){
+                        $(this).addClass('silent').trigger('submit');
+                    });
+
+                    multiform.errors = $forms.filter('.error').length;
+
+                    if (multiform.errors) {
+                        xmodal.closeAll();
+                        xmodal.message('Error', 'Please correct the highlighted errors and re-submit the form.');
+                        return false;
+                    }
+
+                    XNAT.xhr.postJSON({
+                        url: XNAT.url.rootUrl('/xapi/siteConfig/batch'),
+                        data: JSON.stringify({initialized:true}),
+                        success: function(){
+                            xmodal.message({
+                                title: false,
+                                esc: false,
+                                content: 'Your XNAT site is ready to use. Click "OK" to continue to the home page.',
+                                action: function(){
+                                    // window.location.href = XNAT.url.rootUrl('/setup?init=true');
+                                    window.location.href = XNAT.url.rootUrl('/');
+                                    //$forms.each.triggerHandler('reload-data');
+                                }
+                            });
+                        }
+                    }).fail(function(e, txt, jQxhr){
+                        xmodal.loading.close(loader.$modal);
+                        xmodal.message({
+                            title: 'Error',
+                            content: [
+                                'An error occurred during initialization',
+                                e,
+                                txt
+                            ].join(': <br>')
+                        })
+                    });
+
+                    xmodal.loading.close(loader.$modal);
+                    return false;
+
+                }
+            }, [
+                ['div.panel-heading', [
+                    ['h3.panel-title', opts.title || opts.label]
+                ]],
+
+                // 'inner' is where the NEXT spawned item will render
+                inner,
+
+                ['div.panel-footer', opts.footer || footer]
+
+            ]);
+
+        // add an id to the outer panel element if present
+        if (opts.id || opts.element.id) {
+            multiForm.id = opts.id || (opts.element.id + '-panel');
+        }
+
+        return {
+            target: inner,
+            element: multiForm,
+            spawned: multiForm,
+            get: function(){
+                return multiForm
+            }
+        }
+    };
+    siteSetup.form.init = siteSetup.form;
+
+    // this script has loaded
+    siteSetup.loaded = true;
+
+    return XNAT.app.siteSetup = siteSetup;
+
+}));
diff --git a/src/main/webapp/scripts/xnat/spawner.js b/src/main/webapp/scripts/xnat/spawner.js
index ccff298b..98a068fd 100644
--- a/src/main/webapp/scripts/xnat/spawner.js
+++ b/src/main/webapp/scripts/xnat/spawner.js
@@ -43,7 +43,7 @@ var XNAT = getObject(XNAT);
 
         forOwn(obj, function(item, prop){
 
-            var kind, method, spawnedElement, $spawnedElement;
+            var kind, methodName, method, spawnedElement, $spawnedElement;
             
             // save the config properties in a new object
             prop = getObject(prop);
@@ -78,26 +78,29 @@ var XNAT = getObject(XNAT);
                 }
             }
             else {
+
                 // check for a matching XNAT.ui method to call:
                 method =
-                    // XNAT.ui.kind.init()
-                    eval(NAMESPACE + '.' + kind + '.init') ||
-
-                    // XNAT.ui.kind()
-                    eval(NAMESPACE + '.' + kind) ||
 
                     // XNAT.kind.init()
-                    eval('XNAT.' + kind + '.init') ||
+                    lookupObjectValue(XNAT, kind + '.init') ||
 
                     // XNAT.kind()
-                    eval('XNAT.' + kind) ||
+                    lookupObjectValue(XNAT, kind) ||
+
+                    // XNAT.ui.kind.init()
+                    lookupObjectValue(NAMESPACE + '.' + kind + '.init') ||
+
+                    // XNAT.ui.kind()
+                    lookupObjectValue(NAMESPACE + '.' + kind) ||
 
                     // kind.init()
-                    eval(kind + '.init') ||
+                    lookupObjectValue(kind + '.init') ||
 
                     // kind()
-                    eval(kind);
+                    lookupObjectValue(kind) ||
 
+                    null;
 
                 // only spawn elements with defined methods
                 if (isFunction(method)) {
@@ -199,3 +202,4 @@ var XNAT = getObject(XNAT);
     return XNAT.spawner = spawner;
 
 }));
+
diff --git a/src/main/webapp/scripts/xnat/ui/input.js b/src/main/webapp/scripts/xnat/ui/input.js
index bd52cf5f..0b9476a7 100644
--- a/src/main/webapp/scripts/xnat/ui/input.js
+++ b/src/main/webapp/scripts/xnat/ui/input.js
@@ -45,26 +45,6 @@ var XNAT = getObject(XNAT);
         return val;
     }
 
-    function lookupObjectValue(root, objStr){
-        var val = '';
-        if (!objStr) {
-            objStr = root;
-            root = window;
-        }
-        root = root || window;
-        objStr.toString().trim().split('.').forEach(function(part, i){
-            part = part.trim();
-            // start at the root object
-            if (i === 0) {
-                val = root[part] || {};
-            }
-            else {
-                val = val[part];
-            }
-        });
-        return val;
-    }
-
 
     // ========================================
     // MAIN FUNCTION
diff --git a/src/main/webapp/scripts/xnat/ui/panel.js b/src/main/webapp/scripts/xnat/ui/panel.js
index 9c5eda3b..78eb3742 100644
--- a/src/main/webapp/scripts/xnat/ui/panel.js
+++ b/src/main/webapp/scripts/xnat/ui/panel.js
@@ -35,30 +35,7 @@ var XNAT = getObject(XNAT || {});
         });
         return obj.data;
     }
-
-    // another way to do this without using eval()
-    // is to loop over object string using dot notation:
-    // var myVal = lookupObjectValue(XNAT, 'data.siteConfig.siteId');
-    // --> myVal == 'myXnatSiteId'
-    function lookupObjectValue(root, objStr){
-        var val = '';
-        if (!objStr) {
-            objStr = root;
-            root = window;
-        }
-        root = root || window;
-        objStr.toString().trim().split('.').forEach(function(part, i){
-            // start at the root object
-            if (i === 0) {
-                val = root[part] || {};
-            }
-            else {
-                val = val[part];
-            }
-        });
-        return val;
-    }
-
+    
     // string that indicates to look for a namespaced object value
     var doLookupString = '??';
 
@@ -274,14 +251,25 @@ var XNAT = getObject(XNAT || {});
 
         opts.callback = opts.callback || callback || diddly;
 
+        // is this form part of a multiForm?
+        multiform.parent = $formPanel.closest('form.multi-form');
+
+        if (multiform.parent) {
+            multiform.count = $(multiform.parent).find('form').length
+        }
+
+        multiform.errors = 0;
+
         // intercept the form submit to do it via REST instead
         $formPanel.on('submit', function(e){
 
             e.preventDefault();
 
-            var $form = $(this),
+            var $form = $(this).removeClass('error'),
                 errors = 0,
-                valid = true;
+                valid = true,
+                silent = $form.hasClass('silent'),
+                multiform = {};
 
             $form.dataAttr('errors', 0);
 
@@ -298,7 +286,8 @@ var XNAT = getObject(XNAT || {});
             $form.dataAttr('errors', errors);
 
             if (!valid) {
-                if (!multiform.count) {
+                $form.addClass('error');
+                if (!silent) {
                     xmodal.message('Error','Please enter values for the required items and re-submit the form.');
                 }
                 multiform.errors++; // keep track of errors for multi-form submission
@@ -359,7 +348,7 @@ var XNAT = getObject(XNAT || {});
                     }
 
                     // don't mess with modals for multiforms
-                    if (!multiform.count){
+                    if (!silent){
                         xmodal.loading.close('#form-save');
                         xmodal.message('Data saved successfully.', {
                             action: function(){
@@ -399,6 +388,7 @@ var XNAT = getObject(XNAT || {});
             }
         }
     };
+    panel.form.init = panel.form;
 
     // creates a panel that submits all forms contained within
     panel.multiForm = function(opts, callback){
@@ -462,9 +452,7 @@ var XNAT = getObject(XNAT || {});
 
                     // submit ALL enclosed forms
                     $forms.each(function(){
-                        //if (!multiform.errors) {
-                            $(this).trigger('submit');
-                        //}
+                        $(this).addClass('silent').trigger('submit');
                     });
 
                     if (multiform.errors) {
@@ -561,7 +549,8 @@ var XNAT = getObject(XNAT || {});
         }
 
     };
-
+    panel.element.init = panel.element;
+    
     panel.subhead = function(opts){
         opts = cloneObject(opts);
         opts.html = opts.html || opts.text || opts.label;
@@ -736,6 +725,7 @@ var XNAT = getObject(XNAT || {});
     panel.select.multi = function panelSelectMulti(opts){
         return panel.select.menu(opts, true)
     };
+    panel.select.multi.init = panel.select.multi;
 
     panel.selectMenu = function panelSelectMenu(opts){
         opts = cloneObject(opts);
diff --git a/src/main/webapp/scripts/xnat/ui/templates.js b/src/main/webapp/scripts/xnat/ui/templates.js
index f54247df..490b0442 100644
--- a/src/main/webapp/scripts/xnat/ui/templates.js
+++ b/src/main/webapp/scripts/xnat/ui/templates.js
@@ -55,28 +55,21 @@ var XNAT = getObject(XNAT);
         el.value = val;
         return val;
     }
+    
 
-    // another way to do this without using eval()
-    // is to loop over object string using dot notation:
-    // var myVal = lookupObjectValue(XNAT, 'data.siteConfig.siteId');
-    // --> myVal == 'myXnatSiteId'
-    function lookupObjectValue(root, objStr){
-        var val = '';
-        if (!objStr) {
-            objStr = root;
-            root = window;
-        }
-        root = root || window;
-        objStr.toString().trim().split('.').forEach(function(part, i){
-            // start at the root object
-            if (i === 0) {
-                val = root[part] || {};
-            }
-            else {
-                val = val[part];
+    // retrieve value via REST and put it in the element
+    function ajaxValue(el, url, prop){
+        var opts = {
+            url: XNAT.url.rootUrl(url),
+            success: function(data){
+                if (prop && isPlainObject(data)) {
+                    data = lookupObjectValue(data, prop.trim());
+                }
+                el.value = data;
+                // $$(el).val(data);
             }
-        });
-        return val;
+        };
+        return $.get(opts);
     }
 
 
@@ -198,53 +191,97 @@ var XNAT = getObject(XNAT);
         // or spawn a new one
         element = element || spawn('input', opts.element);
 
+        // cache a jQuery object
+        var $element = $(element);
+
         // set the value of individual form elements
         
         // look up a namespaced object value if the value starts with '??'
         var doLookup = '??';
         if (opts.value && opts.value.toString().indexOf(doLookup) === 0) {
-            element.value = lookupValue(opts.value.split(doLookup)[1]);
+            element.value = lookupValue(opts.value.split(doLookup)[1].trim());
         }
         
-        if (opts.load) {
-            if (opts.load.lookup) {
-                lookupValue(element, opts.load.lookup);
-            }
-            else if (opts.load.url){
-                $.ajax({
-                    method: opts.load.method || 'GET',
-                    url: XNAT.url.restUrl(opts.load.url),
-                    success: function(data){
-                        // get value from specific object path
-                        if (opts.load.prop) {
-                            opts.load.prop.split('.').forEach(function(part){
-                                data = data[part] || {};
-                            });
-                            // data = lookupObjectValue(opts.load.prop);
-                        }
-                        $(element).changeVal(data).dataAttr('value', data);
-                    }
-                })
-            }
+        // get value via REST/ajax if value starts with ?:
+        // value: ?$ /path/to/data | obj.prop.name
+        var ajaxPrefix = '?$';
+        var ajaxUrl = '';
+        var ajaxProp = '';
+        if (opts.value && opts.value.toString().indexOf(ajaxPrefix) === 0) {
+            ajaxUrl = (opts.value.split(ajaxPrefix)[1]||'').split('|')[0];
+            ajaxProp = opts.value.split('|')[1] || '';
+            ajaxValue(element, ajaxUrl.trim(), ajaxProp.trim());
         }
 
+        // if (opts.load) {
+        //     if (opts.load.lookup) {
+        //         lookupValue(element, opts.load.lookup.trim());
+        //     }
+        //     else if (opts.load.url){
+        //         $.ajax({
+        //             method: opts.load.method || 'GET',
+        //             url: XNAT.url.restUrl(opts.load.url),
+        //             success: function(data){
+        //                 // get value from specific object path
+        //                 if (isPlainObject(data) && opts.load.prop) {
+        //                     data = lookupObjectValue(data, opts.load.prop);
+        //                     // opts.load.prop.split('.').forEach(function(part){
+        //                     //     data = data[part] || {};
+        //                     // });
+        //                     // data = lookupObjectValue(opts.load.prop);
+        //                 }
+        //                 $(element).changeVal(data);
+        //                 $(element).not('textarea').dataAttr('value', data);
+        //             }
+        //         })
+        //     }
+        // }
+
+        // trigger an 'onchange' event
+        $element.trigger('change');
+
+        // add value to [data-value] attribute
+        // (except for textareas - that could get ugly
+        $element.not('textarea').dataAttr('value', element.value);
+
+        var inner = [element];
+
+        var hiddenInput;
+
         // check buttons if value is true
         if (/checkbox|radio/i.test(opts.type||'')) {
+
+            // add a hidden input to capture the checkbox/radio value
+            hiddenInput = spawn('input', {
+                type: 'hidden',
+                name: element.name,
+                value: element.checked
+            });
+
             element.checked = /true|checked/i.test((opts.checked || element.value).toString());
+
+            // change the value of the hidden input onclick
             element.onclick = function(){
-                this.value = this.checked.toString();
-                console.log('clicked');
-            }
+                hiddenInput.value = this.checked.toString();
+            };
+            
+            // change name of checkbox/radio to avoid conflicts
+            element.name = element.name + '-controller';
+
+            // and add a class for easy selection
+            addClassName(element, 'controller');
+
+            // and add the hidden input
+            inner.push(hiddenInput);
+
         }
 
+        // add the description after the input
+        inner.push(['div.description', opts.description||opts.body||opts.html]);
+
         return template.panelElement(opts, [
             ['label.element-label|for='+element.id||opts.id, opts.label],
-            ['div.element-wrapper', [
-
-                element,
-
-                ['div.description', opts.description||opts.body||opts.html]
-            ]]
+            ['div.element-wrapper', inner]
         ]);
     };
     // ========================================
diff --git a/src/main/webapp/scripts/xnat/xhr.js b/src/main/webapp/scripts/xnat/xhr.js
index 9be48d60..b285ff42 100755
--- a/src/main/webapp/scripts/xnat/xhr.js
+++ b/src/main/webapp/scripts/xnat/xhr.js
@@ -371,6 +371,14 @@ var XNAT = getObject(XNAT||{}),
         };
     });
 
+    // only do JSON.stringify on Arrays or Objects
+    function safeStringify(val){
+        if ($.isArray(val) || $.isPlainObject(val)) {
+            return JSON.stringify(val);
+        }
+        return '';
+    }
+
     function processJSON(data, stringify){
         var output = {};
         $.each(data, function(prop, val){
@@ -384,7 +392,7 @@ var XNAT = getObject(XNAT||{}),
             }
         });
         if (stringify) {
-            return JSON.stringify(output);
+            return safeStringify(output);
         }
         return output;
     }
@@ -397,7 +405,7 @@ var XNAT = getObject(XNAT||{}),
     // XNAT.xhr.formToJSON(form, true)
     xhr.formToJSON = formToJSON;
 
-    $.fn.toJSON = function(stringify){
+    $.fn.formToJSON = $.fn.toJSON = function(stringify){
         return formToJSON(this, stringify);
     };
 
@@ -413,6 +421,11 @@ var XNAT = getObject(XNAT||{}),
         return $el;
     }
 
+    // can the value be reasonably used as a string?
+    function stringable(val){
+        return /string|number|boolean/.test(typeof val);
+    }
+
     // set form element values from an object map
     function setValues(form, dataObj){
         // cache and check if form exists
@@ -425,7 +438,7 @@ var XNAT = getObject(XNAT||{}),
                 val = dataObj.join(', ');
             }
             else {
-                val = /string|number/i.test(typeof dataObj) ? dataObj+'' : dataObj[this.name] || '';
+                val = stringable(dataObj) ? dataObj+'' : dataObj[this.name] || '';
             }
             changeValue(this, val);
         });
@@ -434,7 +447,7 @@ var XNAT = getObject(XNAT||{}),
             var $textarea = $(this);
             $textarea.innerText =  (function(){
                 var val = dataObj[this.name];
-                return /string|number/i.test(typeof val) ? val+'' : JSON.stringify(val);
+                return stringable(val) ? val+'' : safeStringify(val);
             })();
         });
         return $form;
diff --git a/src/main/webapp/setup/index.jsp b/src/main/webapp/setup/index.jsp
index 15e1d5f4..daff6dd0 100644
--- a/src/main/webapp/setup/index.jsp
+++ b/src/main/webapp/setup/index.jsp
@@ -83,13 +83,14 @@
                             </div>
                             <!-- /#site-setup-panels -->
 
+                            <script src="<c:url value="/scripts/xnat/app/siteSetup.js"/>"></script>
 
                             <script>
 
-                                XNAT.app.setupComplete = function(){
-                                    XNAT.xhr.form('#site-setup', {});
-                                };
-
+//                                XNAT.app.setupComplete = function(){
+//                                    XNAT.xhr.form('#site-setup', {});
+//                                };
+//
                                 XNAT.xhr.get({
                                     url: XNAT.url.rootUrl('/page/admin/data/config/site-setup.yaml'),
                                     //url: XNAT.url.rootUrl('/xapi/spawner/resolve/siteAdmin/siteSetup'),
-- 
GitLab