From 07ff2d0f44d07bcca830f020e72ae2389a909f4f Mon Sep 17 00:00:00 2001
From: Adam Novak
Date: Tue, 5 May 2020 14:27:58 -0700
Subject: Add JS at front end for lists, and date support on backend

---
 bh20simplewebuploader/main.py             |  3 ++
 bh20simplewebuploader/static/main.css     | 15 +++++--
 bh20simplewebuploader/static/main.js      | 75 +++++++++++++++++++++++++++++++
 bh20simplewebuploader/templates/form.html | 56 +++++++++++++----------
 4 files changed, 121 insertions(+), 28 deletions(-)

(limited to 'bh20simplewebuploader')

diff --git a/bh20simplewebuploader/main.py b/bh20simplewebuploader/main.py
index 7b6e6e1..1a441f0 100644
--- a/bh20simplewebuploader/main.py
+++ b/bh20simplewebuploader/main.py
@@ -268,6 +268,9 @@ def parse_input(input_string, html_type, number_step=None):
             return int(input_string)
         else:
             return float(input_string)
+    elif html_type == 'date':
+        # Don't do our own date validation; pass it on as a string
+        return input_string
     else:
         raise NotImplementedError('Unimplemented input type: {}'.format(html_type))
 
diff --git a/bh20simplewebuploader/static/main.css b/bh20simplewebuploader/static/main.css
index 57e29ef..80ee6b7 100644
--- a/bh20simplewebuploader/static/main.css
+++ b/bh20simplewebuploader/static/main.css
@@ -167,16 +167,19 @@ pre code {
     border: solid 1px black;
 }
 
-.record {
+.record, .record .field-group, .record .field-group .field {
     display: flex;
     flex-direction: column;
+    -webkit-column-break-inside: avoid; /* Chrome, Safari, Opera */
+    page-break-inside: avoid; /* Firefox */
+    break-inside: avoid;
+}
+
+.record {
     border: solid 1px #808080;
     padding: 1em;
     background: #F8F8F8;
     margin-bottom: 1em;
-    -webkit-column-break-inside: avoid; /* Chrome, Safari, Opera */
-    page-break-inside: avoid; /* Firefox */
-    break-inside: avoid;
 }
 
 .record label {
@@ -184,6 +187,10 @@ pre code {
     margin-top: 10px;
 }
 
+.hidden {
+    display: none;
+}
+
 .search-section {
     display: flex;
     justify-content: space-between;
diff --git a/bh20simplewebuploader/static/main.js b/bh20simplewebuploader/static/main.js
index 96199a0..a67d3df 100644
--- a/bh20simplewebuploader/static/main.js
+++ b/bh20simplewebuploader/static/main.js
@@ -45,3 +45,78 @@ function displayForm() {
   }
   fillFormSpot.classList.add("invisible");
 }
+
+/**
+ * Add another form field to the group this button is part of.
+ */
+function addField(e) {
+  // Find our parent field-group div
+  let fieldGroup = this.parentElement
+  
+  // Get its keypath
+  let keypath = fieldGroup.dataset.keypath
+  
+  // Find its last field child
+  let existingFields = fieldGroup.getElementsByClassName('field')
+  let templateField = existingFields[existingFields.length - 1]
+  
+  // Get its number
+  let fieldNumber = templateField.dataset.number
+  
+  // Duplicate it
+  let newField = templateField.cloneNode(true)
+  
+  // Increment the number and use the keypath and number to set IDs and cross
+  // references.
+  // TODO: Heavily dependent on the form field HTML. Maybe we want custom
+  // elements for the labeled controlsd that know how to be list items?
+  fieldNumber++
+  newField.dataset.number = fieldNumber
+  let newID = keypath + '[' + fieldNumber + ']'
+  let newControl = newField.getElementsByClassName('control')[0]
+  newControl.id = newID
+  newControl.setAttribute('name', newID)
+  let newLabel = newField.getElementsByTagName('label')[0]
+  newLabel.setAttribute('for', newID)
+  
+  // Find the minus button
+  let minusButton = fieldGroup.getElementsByClassName('remove-field')[0]
+  
+  // Put new field as a child before the minus button
+  fieldGroup.insertBefore(newField, minusButton)
+  
+  // Enable the minus button
+  minusButton.classList.remove('hidden')
+}
+
+/**
+ * Remove the last form field from the group button is part of.
+ */
+function removeField(e) {
+  // Find our parent field-group div
+  let fieldGroup = this.parentElement
+  
+  // Find its field children
+  let existingFields = fieldGroup.getElementsByClassName('field')
+  
+  if (existingFields.length > 1) {
+    // There is a last field we can safely remove.
+    let lastField = existingFields[existingFields.length - 1]
+    fieldGroup.removeChild(lastField)
+  }
+  
+  if (existingFields.length <= 1) {
+    // Collection auto-updates. Now there's only one element. Don't let the
+    // user remove it. If they don't want it, they can leave it empty.
+    this.classList.add('hidden')
+  }
+}
+
+// Find all the add and remove field buttons and hook up the listeners.
+for (let button of document.getElementsByClassName('add-field')) {
+  button.addEventListener('click', addField)
+}
+for (let button of document.getElementsByClassName('remove-field')) {
+  button.addEventListener('click', removeField)
+}
+
diff --git a/bh20simplewebuploader/templates/form.html b/bh20simplewebuploader/templates/form.html
index ffd4158..cea444c 100644
--- a/bh20simplewebuploader/templates/form.html
+++ b/bh20simplewebuploader/templates/form.html
@@ -32,10 +32,10 @@
           <div class="search">
               <input id="search-input" id="global-search" type="search" placeholder="FASTA uri" required>
               <button class="button search-button" type="submit" onclick="search()">
-		  <span class="icon ion-search">
-		      <span class="sr-only">Search</span>
-		  </span>
-	      </button>
+                  <span class="icon ion-search">
+                      <span class="sr-only">Search</span>
+                  </span>
+              </button>
           </div>
       </section>
 
@@ -116,26 +116,34 @@
                     <div class="record">
                         <h4>{{ record['heading'] }}</h4>
                         {% else %}
-                        <label for="{{ record['id'] }}" title="{{ record.get('docstring', '') }}">
-                            {{ record['label'] }}
-                            {{ "*" if record['required'] else "" }}
-                            {% if 'docstring' in record %}
-                            <a href='javascript:alert({{ record['docstring'] | tojson }})'>❓</a>
+                        <div class="field-group" data-keypath="{{ record['id'] }}">
+                            <div class="field" data-number="0">
+                                <label for="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" title="{{ record.get('docstring', '') }}">
+                                    {{ record['label'] }}
+                                    {{ "*" if record['required'] else "" }}
+                                    {% if 'docstring' in record %}
+                                    <a href='javascript:alert({{ record['docstring'] | tojson }})'>❓</a>
+                                    {% endif %}
+                                    {% if 'ref_iri' in record %}
+                                    <a href="{{ record['ref_iri'] }}" target="_blank" title="Ontology Link">🔗</a>
+                                    {% endif %}
+                                </label>
+                                {% if record['type'] == 'select' %}
+                                <select class="control" id="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" name="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" {{ "required" if record['required'] else "" }}>
+                                    <option value="" selected>Choose one...</option>
+                                    {% for option in record['options'] %}
+                                    <option value="{{ option[1] }}">{{ option[0] }}</option>
+                                    {% endfor %}
+                                </select>
+                                {% else %}
+                                <input class="control" type="{{ record['type'] }}" id="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" name="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" {{ "required" if record['required'] else "" }} {{ ("step=" + record['step']) if 'step' in record else ""}}>
+                                {% endif %}
+                            </div>
+                            {% if record['list'] %}
+                            <button type="button" title="Remove field" class="remove-field hidden">➖</button>
+                            <button type="button" title="Add field" class="add-field">➕</button>
                             {% endif %}
-                            {% if 'ref_iri' in record %}
-                            <a href="{{ record['ref_iri'] }}" target="_blank" title="Ontology Link">🔗</a>
-                            {% endif %}
-                        </label>
-                        {% if record['type'] == 'select' %}
-                        <select id="{{ record['id'] }}" name="{{ record['id'] }}" {{ "required" if record['required'] else "" }}>
-                            <option value="" selected>Choose one...</option>
-                            {% for option in record['options'] %}
-                            <option value="{{ option[1] }}">{{ option[0] }}</option>
-                            {% endfor %}
-                        </select>
-                        {% else %}
-                        <input type="{{ record['type'] }}" id="{{ record['id'] }}" name="{{ record['id'] }}" {{ "required" if record['required'] else "" }} {{ ("step=" + record['step']) if 'step' in record else ""}}>
-                        {% endif %}
+                        </div>
                         {% endif %}
                         {% if loop.index == loop.length %}
                     </div>
@@ -190,7 +198,7 @@
 </div>
 
 <script type="text/javascript">
- let scriptRoot = {{ request.script_root|tojson|safe }};
+    let scriptRoot = {{ request.script_root|tojson|safe }};
 </script>
 
 <script type="text/javascript" src="/static/main.js"></script>
-- 
cgit v1.2.3