From f78c7066ff18add3b7273f7dbb448030b7145828 Mon Sep 17 00:00:00 2001
From: "Mark M. Florida" <markflorida@wustl.edu>
Date: Thu, 25 Aug 2016 15:54:21 -0500
Subject: [PATCH] XNAT-4483, XNAT-4400: refactored widget spawning functions
 and updated site-admin-elements.yaml

---
 .../xnat/spawner/site-admin-elements.yaml     | 98 ++++++++-----------
 src/main/webapp/scripts/globals.js            | 10 +-
 src/main/webapp/scripts/lib/spawn/spawn.js    | 26 ++---
 .../webapp/scripts/xnat/admin/passwords.js    |  8 +-
 src/main/webapp/scripts/xnat/ui/input.js      |  6 +-
 src/main/webapp/scripts/xnat/ui/panel.js      | 19 +++-
 src/main/webapp/scripts/xnat/ui/templates.js  | 32 ++++--
 7 files changed, 105 insertions(+), 94 deletions(-)

diff --git a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml
index 3005662d..aba9d999 100644
--- a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml
+++ b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml
@@ -31,7 +31,7 @@ siteDescriptionPage:
         <p class="description">Specify a velocity template file to display on the login page</p>
 
 siteDescriptionText:
-    tag: textarea|data-code-editor;data-code-language=html
+    tag: textarea|data-code-editor=html|data-code-language=html
     element:
         name: siteDescriptionText
         rows: 8
@@ -39,23 +39,19 @@ siteDescriptionText:
         <p class="description">Specify a simple text description of this site.</p>
 
 passwordExpirationInterval:
-    tag: input
-    element:
-        type: text
-        id: passwordExpirationInterval
-        name: passwordExpirationInterval
-        label: Password Expiration (Interval)
+    kind: input.text
+#    id: passwordExpirationInterval
+    name: passwordExpirationInterval
+    label: Password Expiration (Interval)
     after: >
       <p class="description">Interval of time after which unchanged passwords expire and users have to change them.
       Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a></p>
 
 passwordExpirationDate:
-    tag: input
-    element:
-        type: text
-        id: passwordExpirationDate
-        name: passwordExpirationDate
-        label: Password Expiration (Date)
+    kind: input.text
+#    id: passwordExpirationDate
+    name: passwordExpirationDate
+    label: Password Expiration (Date)
     after:
         datePicker:
             tag: span#datePicker
@@ -78,21 +74,19 @@ siteDescriptionType:
                 style: "margin: 3px 0 8px 0;"
             content: Select a site description option below
         siteDescriptionTypePage:
-            tag: input|data-value=Page
-            element:
-                type: radio
-                name: siteDescriptionType
-                value: Page
+            kind: input.radio
+            name: siteDescriptionType
+            id: siteDescriptionTypePage
+            value: Page
             after:
                 label:
                     tag: label.pad5h|for=siteDescriptionTypePage
                     content: Page
         siteDescriptionTypeText:
-            tag: input|data-value=Text
-            element:
-                type: radio
-                name: siteDescriptionType
-                value: Text
+            kind: input.radio
+            name: siteDescriptionType
+            id: siteDescriptionTypeText
+            value: Text
             after:
                 label:
                     tag: label.pad5h|for=siteDescriptionTypeText
@@ -106,9 +100,7 @@ siteDescriptionType:
             contents:
                 ${siteDescriptionText}
         siteInfoJs:
-            tag: script
-            element:
-                src: ~/scripts/xnat/admin/siteInfo.js
+            tag: script|src=/scripts/xnat/admin/siteInfo.js
 
 siteLoginLanding:
     kind: panel.input.text
@@ -410,34 +402,28 @@ passwords:
                         style: "margin: 3px 0 8px 0;"
                     content: Select a password expiration type below
                 passwordExpirationTypeDisabled:
-                    tag: input|data-value=Disabled
-                    element:
-                        type: radio
-                        id: passwordExpirationTypeDisabled
-                        name: passwordExpirationType
-                        value: Disabled
+                    kind: input.radio
+                    id: passwordExpirationTypeDisabled
+                    name: passwordExpirationType
+                    value: Disabled
                     after:
                         label:
                             tag: label.pad5h|for=passwordExpirationTypeDisabled
                             content: Disabled
                 passwordExpirationTypeInterval:
-                    tag: input|data-value=Interval
-                    element:
-                        type: radio
-                        id: passwordExpirationTypeInterval
-                        name: passwordExpirationType
-                        value: Interval
+                    kind: input.radio
+                    id: passwordExpirationTypeInterval
+                    name: passwordExpirationType
+                    value: Interval
                     after:
                         label:
                             tag: label.pad5h|for=passwordExpirationTypeInterval
                             content: Interval
                 passwordExpirationTypeDate:
-                    tag: input|data-value=Date
-                    element:
-                        type: radio
-                        id: passwordExpirationTypeDate
-                        name: passwordExpirationType
-                        value: Date
+                    kind: input.radio
+                    id: passwordExpirationTypeDate
+                    name: passwordExpirationType
+                    value: Date
                     after:
                         label:
                             tag: label.pad5h|for=passwordExpirationTypeDate
@@ -451,9 +437,7 @@ passwords:
                     contents:
                         ${passwordExpirationDate}
                 pwExpTypeJs:
-                    tag: script
-                    element:
-                        src: ~/scripts/xnat/admin/passwords.js
+                    tag: script|src=/scripts/xnat/admin/passwords.js
         passwordReuseRestriction:
             kind: panel.select.single
             id: passwordReuseRestriction
@@ -713,13 +697,10 @@ themeManagement:
     action: /xapi/theme
     contents:
         themeScript:
-            tag: script
-            element:
-                src: ~/scripts/xnat/admin/themeManagement.js
+            tag: script|src=/scripts/xnat/admin/themeManagement.js
         themeStyle:
             tag: style
-            element:
-                html: ".themeUploader{width:270px;display:inline !important;}"
+            contents: ".themeUploader{width:270px;display:inline !important;}"
         currentTheme:
             kind: panel.display
             id: currentTheme
@@ -737,14 +718,11 @@ themeManagement:
             description: Selected a new global theme from those available on the system.
             value: ""
             options:
-                default:
-                    label: None
-                    value: None
-            after:
-                - "<span style=\"position: relative; top: -78px;left: 270px;\"> <!-- &nbsp;<button id=\"submitThemeButton\" onclick=\"setTheme();\">Set Theme</button>&nbsp;&nbsp; --> <button id=\"removeThemeButton\" onclick=\"removeTheme();\">Remove Theme</button></span>"
+                None: None
             element:
-                style:
-                    min-width: 250px
+                style: "width:220px;margin-right:39px;"
+            afterElement: >
+                <button class="btn btn-sm" id="removeThemeButton" onclick="removeTheme()">Remove Theme</button>
         uploadTheme:
             kind: panel.input.upload
             id: themeFileUpload
@@ -939,6 +917,7 @@ sessionBuilder:
             name: sessionXmlRebuilderRepeat
             label: Session Idle Check Interval
             placeholder: Interval in milliseconds
+            afterElement: milliseconds
             description: >
                 This controls how often the system checks to see if any incoming DICOM sessions in the prearchive have
                 been idle for longer than the configured session idle time. This value should be specified in
@@ -948,6 +927,7 @@ sessionBuilder:
             name: sessionXmlRebuilderInterval
             label: Session Idle Time
             placeholder: Time in minutes
+            afterElement: minutes
             description: >
                 This tells the system how long a DICOM session should sit idle—that is, with no new data added to the
                 session—before attempting to build a session document from the DICOM data. This value is specified in
diff --git a/src/main/webapp/scripts/globals.js b/src/main/webapp/scripts/globals.js
index 74784137..dbaef7f0 100644
--- a/src/main/webapp/scripts/globals.js
+++ b/src/main/webapp/scripts/globals.js
@@ -320,7 +320,8 @@ function lookupObjectValue(root, objStr, prop){
         delim = '.',
         brackets = /[\]\[]/,
         hasBrackets = false,
-        parts = [];
+        parts = [],
+        undefined;
     
     if (!objStr) {
         objStr = root+'';
@@ -362,7 +363,7 @@ function lookupObjectValue(root, objStr, prop){
             val = root[part] || '';
         }
         else {
-            if (!val) return false;
+            if (val === undefined) return false;
             val = val[part];
         }
     });
@@ -666,6 +667,11 @@ function toDashed(str){
 //hyphenate = toDashed;
 //dashify   = toDashed;
 
+// like toDashed() but with underscores instead of hyphens
+function toUnderscore(str){
+    return toDashed(str).replace(/-+/g, '_');
+}
+
 // set 'forceLower' === true (or omit argument)
 // to ensure *only* 'cameled' letters are uppercase
 function toCamelCase(str) {
diff --git a/src/main/webapp/scripts/lib/spawn/spawn.js b/src/main/webapp/scripts/lib/spawn/spawn.js
index 9f828299..4f847fd5 100644
--- a/src/main/webapp/scripts/lib/spawn/spawn.js
+++ b/src/main/webapp/scripts/lib/spawn/spawn.js
@@ -2,8 +2,8 @@
  * DOM element spawner with *optional* jQuery functionality
  *
  * EXAMPLES:
- * var p1 = spawn('p|id:p1', 'Text for paragraph 1.');
- * var div2 = spawn('div|class=div2', ['Text for div2.', p1]) // inserts text and puts p1 inside div2
+ * var p1 = spawn('p#p1', 'Text for paragraph 1.');
+ * var div2 = spawn('div.div2', ['Text for div2.', p1]) // inserts text and puts p1 inside div2
  * var ul1 = spawn('ul', [['li', 'Content for <li> 1.'], ['li', 'Content for the next <li>.']]);
  * div2.appendChild(ul1); // add ul1 to div2
  */
@@ -84,12 +84,16 @@
 
 
     function parseAttrs(el, attrs){
-        // allow ';' or ',' for attribute delimeter
-        (attrs.split(/;|,/) || []).forEach(function(att, i){
+        // allow 'attrs' to be a string or array
+        if (typeof attrs == 'string'){
+            // use '|' for attribute delimiter
+            attrs = attrs.split('|');
+        }
+        attrs.forEach(function(att, i){
             if (!att) return;
-            // allow ':' or '=' for key/value separator
-            var sep = /:|=/;
-            // tolerate quotes around values
+            // use '=' for key/value separator
+            var sep = '=';
+            // remove quotes around values
             var quotes = /^['"]+|['"]+$/g;
             var key = att.split(sep)[0].trim();
             var val = (att.split(sep)[1]||'').trim().replace(quotes, '') || key;
@@ -252,10 +256,10 @@
 
         if (parts.length){
             // pass element attributes in 'tag' string, like:
-            // spawn('a|id="foo-link";href="foo";class="bar"');
-            // or (colons for separators, commas for delimeters, no quotes),:
-            // spawn('input|type:checkbox,id:foo-ckbx');
-            parseAttrs(el, parts[0]||'');
+            // spawn('a|id="foo-link"|href="foo"|class="bar"');
+            // or (without quotes),:
+            // spawn('input|type=checkbox|id=foo-ckbx');
+            parseAttrs(el, parts);
         }
 
         if (!opts && !children){
diff --git a/src/main/webapp/scripts/xnat/admin/passwords.js b/src/main/webapp/scripts/xnat/admin/passwords.js
index fc31a06b..9fa3adfa 100644
--- a/src/main/webapp/scripts/xnat/admin/passwords.js
+++ b/src/main/webapp/scripts/xnat/admin/passwords.js
@@ -11,13 +11,13 @@ console.log('passwordExpirationType.js');
     (function(){
 
         var fieldInterval$ =
-                container$.find('#passwordExpirationInterval')
+                container$.find('[name="passwordExpirationInterval"]')
                     .css({ marginTop: '10px' });
 
         var oldInterval = fieldInterval$.val();
 
         var fieldDate$ =
-                container$.find('#passwordExpirationDate')
+                container$.find('[name="passwordExpirationDate"]')
                     .attr({
                         size: 10,
                         placeholder: 'MM/DD/YYYY'
@@ -40,7 +40,7 @@ console.log('passwordExpirationType.js');
                         fieldDate$.datetimepicker('show');
                     });
 
-        container$.find('input[name="passwordExpirationType"]').on('change', function(){
+        container$.find('[name="passwordExpirationType"]').on('change', function(){
 
             // Does the interval need to be set to "-1" to disable expiration?
 
@@ -69,7 +69,7 @@ console.log('passwordExpirationType.js');
     (function(){
 
         var durationContainer$ = $('div[data-name="passwordHistoryDuration"]');
-        var durationInput$ = durationContainer$.find('input#passwordHistoryDuration');
+        var durationInput$ = durationContainer$.find('[name="passwordHistoryDuration"]');
 
         $('#passwordReuseRestriction').on('change', function(){
             changePasswordReuseType(this.value);
diff --git a/src/main/webapp/scripts/xnat/ui/input.js b/src/main/webapp/scripts/xnat/ui/input.js
index 66875c5a..ab081a3d 100644
--- a/src/main/webapp/scripts/xnat/ui/input.js
+++ b/src/main/webapp/scripts/xnat/ui/input.js
@@ -55,16 +55,16 @@ var XNAT = getObject(XNAT);
             config = type;
             type = null; // it MUST contain a 'type' property
         }
-        config = getObject(config);
+        config = cloneObject(config);
         config.type = type || config.type || 'text';
         // lookup a value if it starts with '??'
         var doLookup = '??';
-        if (config.value && config.value.toString().indexOf(doLookup) === 0) {
+        if (config.value && (config.value+'').indexOf(doLookup) === 0) {
             config.value = lookupValue(config.value.split(doLookup)[1])
         }
         // lookup a value from a namespaced object
         // if no value is given
-        if (!config.value && config.data && config.data.lookup) {
+        if (config.value === undefined && config.data && config.data.lookup) {
             config.value = lookupValue(config.data.lookup)
         }
         var spawned = spawn('input', config);
diff --git a/src/main/webapp/scripts/xnat/ui/panel.js b/src/main/webapp/scripts/xnat/ui/panel.js
index 01ffa152..6a5a9dfb 100644
--- a/src/main/webapp/scripts/xnat/ui/panel.js
+++ b/src/main/webapp/scripts/xnat/ui/panel.js
@@ -230,8 +230,6 @@ var XNAT = getObject(XNAT || {});
                 var $this = $(this);
                 var val = lookupObjectValue(dataObj, this.name||this.title);
 
-                //if (!val) return;
-
                 if (Array.isArray(val)) {
                     val = val.join(', ');
                     $this.addClass('array-list')
@@ -240,7 +238,15 @@ var XNAT = getObject(XNAT || {});
                     val = stringable(val) ? val : JSON.stringify(val);
                 }
 
-                $this.not(':checkbox, :radio').changeVal(val);
+                // used on hidden inputs to reset values
+                if ($this.hasClasses('proxy && dirty')) {
+                    this.value = $this.dataAttr('value');
+                }
+
+                //if (val === "") return;
+
+                // $this.not(':checkbox, :radio').changeVal(val);
+                $this.not(':radio').changeVal(val);
 
                 if (/checkbox/i.test(this.type)) {
                     this.checked = realValue(val);
@@ -253,6 +259,8 @@ var XNAT = getObject(XNAT || {});
                     }
                 }
 
+                $this.removeClass('dirty').dataAttr('value', val);
+
             });
 
             loadingDialog().closeAll();
@@ -492,7 +500,7 @@ var XNAT = getObject(XNAT || {});
                     var obj = {};
                     // actually, NEVER use returned data...
                     // ALWAYS reload from the server
-                    obj.refresh = opts.refresh || opts.reload || opts.url || opts.load;
+                    obj.load = opts.refresh || opts.reload || opts.url || opts.load;
                     if (!silent){
                         XNAT.ui.banner.top(2000, 'Data saved successfully.', 'success');
                         loadData($form, obj);
@@ -509,6 +517,7 @@ var XNAT = getObject(XNAT || {});
                 ajaxConfig.processData = false;
                 ajaxConfig.contentType = 'application/json';
                 $.ajax(ajaxConfig);
+                // XNAT.xhr.form($form, ajaxConfig);
             }
             else {
                 $(this).ajaxSubmit(ajaxConfig);
@@ -846,7 +855,7 @@ var XNAT = getObject(XNAT || {});
                 multiple: true,
                 className: addClassName(opts, 'file-upload-input')
             }],
-            ['button', {
+            ['button.btn.btn-sm', {
                 type: 'submit',
                 id: opts.id +'-button',
                 html: 'Upload'
diff --git a/src/main/webapp/scripts/xnat/ui/templates.js b/src/main/webapp/scripts/xnat/ui/templates.js
index 44c568fe..2207c18a 100644
--- a/src/main/webapp/scripts/xnat/ui/templates.js
+++ b/src/main/webapp/scripts/xnat/ui/templates.js
@@ -188,7 +188,7 @@ var XNAT = getObject(XNAT);
             addClassName(opts.element, opts.data.validate);
         }
 
-        addDataObjects(opts.element, opts.data||{});
+        addDataObjects(opts.element, opts.data);
         
         if (opts.placeholder) {
             opts.element.placeholder = opts.placeholder;
@@ -202,16 +202,17 @@ var XNAT = getObject(XNAT);
         var $element = $(element);
 
         // set the value of individual form elements
+        var hasValue = isDefined(opts.value);
         
         // look up a namespaced object value if the value starts with '??'
         var doLookup = '??';
-        if (opts.value && opts.value.toString().indexOf(doLookup) === 0) {
+        if (hasValue && opts.value.toString().indexOf(doLookup) === 0) {
             // element.value = lookupValue(opts.value.split(doLookup)[1].trim());
-            $element.val(lookupObjectValue(opts.value.split(doLookup)[1].trim())).change();
+            $element.val(lookupObjectValue(opts.value.split(doLookup)[1].trim()));
         }
 
         var doEval = '!?';
-        if (opts.value && opts.value.toString().indexOf(doEval) === 0) {
+        if (hasValue && opts.value.toString().indexOf(doEval) === 0) {
             opts.value = (opts.value.split(doEval)[1]||'').trim();
             try {
                 $element.val(eval(opts.value)).change();
@@ -226,7 +227,7 @@ var XNAT = getObject(XNAT);
         var ajaxPrefix = '$?';
         var ajaxUrl = '';
         var ajaxProp = '';
-        if (opts.value && opts.value.toString().indexOf(ajaxPrefix) === 0) {
+        if (hasValue && 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());
@@ -256,23 +257,34 @@ var XNAT = getObject(XNAT);
             inner.push(spawn('span.after', opts.afterElement));
         }
 
-        var hiddenInput;
+        var $hiddenInput, hiddenInput;
 
         // check buttons if value is true
         if (/checkbox/i.test(element.type||'')) {
 
-            element.checked = /true|checked/i.test((opts.checked||element.value||'').toString());
+            element.checked = /true|checked/i.test(opts.checked||element.value||'');
 
             // add a hidden input to capture the checkbox/radio value
-            hiddenInput = spawn('input', {
+            $hiddenInput = $.spawn('input.proxy', {
                 type: 'hidden',
                 name: element.name,
-                value: element.checked ? element.value || opts.value || element.checked : false
+                value: element.checked ? (element.value || opts.value || element.checked || 'true') : 'false'
             });
 
+            hiddenInput = $hiddenInput[0];
+
+            // add [data-value] attribute
+            $hiddenInput.dataAttr('value', hiddenInput.value);
+
             // change the value of the hidden input onclick
             element.onclick = function(){
-                hiddenInput.value = this.checked ? this.value || this.checked.toString() : false;
+                // if the checkbox value is boolean,
+                // match the value to the 'checked' state
+                if (/true|false/i.test(this.value)) {
+                    this.value = this.checked;
+                }
+                hiddenInput.value = this.checked ? (this.value || this.checked || 'true') : 'false';
+                $hiddenInput.toggleClass('dirty');
             };
             
             // copy name to title
-- 
GitLab