From 985e9b406fce051f63a7362a665e9991dec93375 Mon Sep 17 00:00:00 2001
From: Inga Kirschnick <inga.kirschnick@uni-bielefeld.de>
Date: Thu, 26 Oct 2023 15:18:03 +0200
Subject: [PATCH] Editing nested token queries and bug fixes

---
 .../ElementReferencesQueryBuilder.js          |   3 -
 .../GeneralFunctionsQueryBuilder.js           | 134 +++++++++++++-----
 ...enAttributeBuilderFunctionsQueryBuilder.js |  15 +-
 .../query_builder/_query_builder.html.j2      |  12 +-
 4 files changed, 110 insertions(+), 54 deletions(-)

diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js
index 157ca2a7..518975f0 100644
--- a/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js
+++ b/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js
@@ -8,9 +8,6 @@ class ElementReferencesQueryBuilder {
 
     // Structural Attribute Builder Elements
     this.structuralAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-structural-attr-modal'));
-    this.sentenceElement = document.querySelector('[data-structural-attr-modal-action-button="sentence"]');
-    this.entityElement = document.querySelector('[data-structural-attr-modal-action-button="entity"]');
-    this.textAnnotationElement = document.querySelector('[data-structural-attr-modal-action-button="text-annotation"]');
     this.englishEntTypeSelection = document.querySelector('#corpus-analysis-concordance-english-ent-type-selection');
     this.germanEntTypeSelection = document.querySelector('#corpus-analysis-concordance-german-ent-type-selection');
     this.textAnnotationSelection = document.querySelector('#corpus-analysis-concordance-text-annotation-options');
diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js
index f6c0004e..481e9b6d 100644
--- a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js
+++ b/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js
@@ -56,7 +56,6 @@ class GeneralFunctionsQueryBuilder {
     }
   }
 
-
   queryChipFactory(dataType, prettyQueryText, queryText, index = null, isClosingTag = false, isEditable = false) {
     // Creates a new query chip element, adds Eventlisteners for selection, deletion and drag and drop and appends it to the query input field.
     queryText = Utils.escape(queryText);
@@ -118,6 +117,7 @@ class GeneralFunctionsQueryBuilder {
   }
 
   editChipElement(queryChipElement) {
+    //TODO: Split this function into smaller functionss
     this.elements.editingModusOn = true;
     this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
     switch (queryChipElement.dataset.type) {
@@ -141,54 +141,105 @@ class GeneralFunctionsQueryBuilder {
         this.elements.textAnnotationInput.value = textAnnotationContent;
         break;
       case 'token':
-        let [tokenAttr, tokenValue] = queryChipElement.dataset.query.replace(/\[|\]|"/g, '').split('=');
-        if (tokenAttr === 'pos') {
-          tokenAttr = this.elements.englishPosSelection.querySelector(`option[value=${tokenValue}]`) ? 'english-pos' : 'german-pos';
+        //This regex searches for word or lemma or pos or simple_pos="any string within single or double quotes" followed by one or no ignore case markers, followed by one or no condition characters.
+      	let regex = new RegExp('(word|lemma|pos|simple_pos)=(("[^"]+")|(\\\\u0027[^\\\\u0027]+\\\\u0027)) ?(%c)? ?(\\&|\\|)?', 'gm');
+        let m;
+        let queryElementsContent = [];
+        while ((m = regex.exec(queryChipElement.dataset.query)) !== null) {
+          // This is necessary to avoid infinite loops with zero-width matches
+          if (m.index === regex.lastIndex) {
+              regex.lastIndex++;
+          }
+          let tokenAttr = m[1];
+          // Passes english-pos by default so that the template is added. In editTokenChipElement it is then checked whether it is english-pos or german-pos.
+          if (tokenAttr === 'pos') {
+            tokenAttr = 'english-pos';
+          }
+          let tokenValue = m[2].replace(/"|'/g, '');
+          let ignoreCase = false;
+          let condition = undefined;
+          m.forEach((match) => {
+            if (match === "%c") {
+              ignoreCase = true;
+            } else if (match === "&") {
+              condition = "and";
+            } else if (match === "|") {
+              condition = "or";
+            }
+          });
+          queryElementsContent.push({tokenAttr: tokenAttr, tokenValue: tokenValue, ignoreCase: ignoreCase, condition: condition});
         }
-        this.editTokenChipElement(tokenAttr, tokenValue);
+        this.editTokenChipElement(queryElementsContent);
         break;
       default:
         break;
     }
   }
 
-  editTokenChipElement(tokenAttr, tokenValue) {
-    this.resetMaterializeSelection([this.elements.positionalAttrSelection], tokenAttr);
+  editTokenChipElement(queryElementsContent) {
     this.elements.positionalAttrModal.open();
-    switch (tokenAttr) {
-      case 'word':
-        this.elements.wordInput.value = tokenValue;
-        break;
-      case 'lemma':
-        this.elements.lemmaInput.value = tokenValue;
-        break;
-      case 'english-pos':
-        this.resetMaterializeSelection([this.elements.englishPosSelection], tokenValue);
-        break;
-      case 'german-pos':
-        this.resetMaterializeSelection([this.elements.germanPosSelection], tokenValue);
-        break;
-      case 'simple-pos':
-        this.resetMaterializeSelection([this.elements.simplePosSelection], tokenValue);
-        break;
-      default:
-        break;
-    }
+    queryElementsContent.forEach((queryElement) => {
+      this.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr);
+      this.preparePositionalAttrModal();
+      switch (queryElement.tokenAttr) {
+        case 'word':
+        case 'lemma':
+          this.elements.tokenBuilderContent.querySelector('input').value = queryElement.tokenValue;
+          break;
+        case 'english-pos':
+          // English-pos is selected by default. Then it is checked whether the passed token value occurs in the english-pos selection. If not, the selection is reseted and changed to german-pos.
+          let selection = this.elements.tokenBuilderContent.querySelector('select');
+          queryElement.tokenAttr = selection.querySelector(`option[value=${queryElement.tokenValue}]`) ? 'english-pos' : 'german-pos';
+          this.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr);
+          this.preparePositionalAttrModal();
+          this.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue);
+          break;
+        case 'simple_pos':
+          this.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue);
+        default:
+          break;
+      }
+      if (queryElement.ignoreCase) {
+        this.elements.ignoreCaseCheckbox.checked = true;
+      }
+      if (queryElement.condition !== undefined) {
+        this.conditionHandler(queryElement.condition, true);
+      }
+
+    });
+
   }
 
   lockClosingChipElement(queryChipElement) {
-    let chipIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
-    this.submitQueryChipElement(queryChipElement.dataset.type, queryChipElement.firstChild.textContent, queryChipElement.dataset.query, chipIndex+1);
-    this.deleteChipElement(queryChipElement);
-    this.updateChipList();
-  }
+    queryChipElement.dataset.closingTag = 'false';
+    let lockIcon = queryChipElement.querySelector('[data-chip-action="lock"]');
+    lockIcon.textContent = 'lock';
+    //TODO: Write unlock-Function?
+    lockIcon.dataset.chipAction = 'unlock';
 
+    // let chipIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
+    // this.submitQueryChipElement(queryChipElement.dataset.type, queryChipElement.firstChild.textContent, queryChipElement.dataset.query, chipIndex+1);
+    // this.deleteChipElement(queryChipElement);
+    // this.updateChipList();
+  }
   
   deleteChipElement(attr) {
-    if (attr.dataset.type === "start-sentence") {
-      this.elements.sentenceElement.innerHTML = 'Sentence';
-    } else if (attr.dataset.type === "start-entity" || attr.dataset.type === "start-empty-entity") {
-      this.elements.entityElement.innerHTML = 'Entity';
+    let elementIndex = Array.from(this.elements.queryInputField.children).indexOf(attr);
+    switch (attr.dataset.type) {
+      case 'start-sentence':
+        this.deletingClosingTagHandler(elementIndex, 'end-sentence');
+        break;
+      case 'start-entity':
+        this.deletingClosingTagHandler(elementIndex, 'end-entity');
+        break;
+      case 'token':
+        console.log(Array.from(this.elements.queryInputField.children)[elementIndex+1]);
+        let nextElement = Array.from(this.elements.queryInputField.children)[elementIndex+1];
+        if (nextElement.dataset.type === 'token-incidence-modifier') {
+          this.deleteChipElement(nextElement);
+        }
+      default:
+        break;
     }
     this.elements.queryInputField.removeChild(attr);
     if (this.elements.queryInputField.children.length === 0) {
@@ -198,6 +249,18 @@ class GeneralFunctionsQueryBuilder {
     this.queryPreviewBuilder();
   }
 
+  deletingClosingTagHandler(elementIndex, closingTagType) {
+    let closingTags = this.elements.queryInputField.querySelectorAll(`[data-type="${closingTagType}"]`);
+    for (let i = 0; i < closingTags.length; i++) {
+      let closingTag = closingTags[i];
+    
+      if (Array.from(this.elements.queryInputField.children).indexOf(closingTag) > elementIndex) {
+        this.deleteChipElement(closingTag);
+        break;
+      }
+    }
+  }
+
   handleDragStart(queryChipElement, event) {
     // is called when a query chip is dragged. It creates a dropzone (in form of a chip) for the dragged chip and adds it to the query input field.
     let queryChips = this.elements.queryInputField.querySelectorAll('.query-component');
@@ -325,7 +388,6 @@ class GeneralFunctionsQueryBuilder {
       this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query']);
     }
   }
-  
 
   parseTextToChip(query) {
     const parsingElementDict = {
diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js
index 74d604eb..bebb5d83 100644
--- a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js
+++ b/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js
@@ -69,7 +69,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
 
   optionToggleHandler() {
     let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
-    if ((input.value === '' || input.value === 'default') && this.elements.editingModusOn === false) {
+    if (input.value === '' && this.elements.editingModusOn === false) {
       this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
     } else if (this.elements.positionalAttrSelection.querySelectorAll('option').length === 1) {
       this.toggleClass(['and'], 'disabled', 'add');
@@ -95,7 +95,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
     let input;
     let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value);
     
-    // Takes all rows of the token query (if there is a query concatenation). 
+    // Takes all rows of the token query (if there is a query concatenation).
     // Adds their contents to tokenQueryPrettyText and tokenQueryCQLText, which will later be expanded with the current input field.
     let tokenQueryRows = this.elements.tokenQuery.querySelectorAll('.row');
     tokenQueryRows.forEach(row => {
@@ -108,7 +108,6 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
       tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `;
       tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`;
     });
-
     if (kindOfToken === 'empty-token') {
       tokenQueryPrettyText += 'empty token';
     } else {
@@ -117,21 +116,19 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
       tokenQueryPrettyText += `${kindOfToken}=${input.value}${c}`;
       tokenQueryCQLText += `${kindOfToken}="${input.value}"${c}`;
     }
-
     // isTokenQueryInvalid looks if a valid value is passed. If the input fields/dropdowns are empty (isTokenQueryInvalid === true), no token is added.
-    if ((input.value === '' || input.value === 'default') && this.elements.positionalAttrSelection.value !== 'empty-token') {
+    if (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') {
       this.disableTokenSubmit();
     } else {
       tokenQueryCQLText = `[${tokenQueryCQLText}]`;
-      this.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, true);
+      this.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, kindOfToken === 'empty-token' ? false : true);
       this.elements.positionalAttrModal.close();
     }
   }
 
   kindOfTokenCheck(kindOfToken) {
     return kindOfToken === 'english-pos' || kindOfToken === 'german-pos' ? 'pos' : kindOfToken;
-  } 
-
+  }
 
   actionButtonInOptionSectionHandler(elem) {
     let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
@@ -176,7 +173,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
     tokenInput.value += '{' + input + '}';
   }
 
-  conditionHandler(conditionText) {
+  conditionHandler(conditionText, editMode = false) {
     let tokenQueryTemplateClone = this.elements.tokenQueryTemplate.content.cloneNode(true);
     tokenQueryTemplateClone.querySelector('.token-query-template-content').appendChild(this.elements.tokenBuilderContent.firstElementChild);
     let notSelectedButton = tokenQueryTemplateClone.querySelector(`[data-condition-pretty-text]:not([data-condition-pretty-text="${conditionText}"])`);
diff --git a/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 b/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2
index ff8e9151..3a3bf2fe 100644
--- a/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2
+++ b/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2
@@ -173,8 +173,8 @@
       <div class="row">
         <div class="token-query-template-content"></div>
         <div class="col s4" style="margin-top:15px;">
-          <a class="btn-small waves-effect waves-light disabled" data-condition-pretty-text="or" data-condition-cql-text=" | ">or</a>
-          <a class="btn-small waves-effect waves-light disabled" data-condition-pretty-text="and" data-condition-cql-text=" & ">and</a>
+          <a class="btn-small waves-effect waves-light disabled" data-condition-pretty-text="or" data-condition-cql-text="| ">or</a>
+          <a class="btn-small waves-effect waves-light disabled" data-condition-pretty-text="and" data-condition-cql-text="& ">and</a>
           <a class="btn-floating waves-effect waves-light red" data-token-query-content-action="delete" style="margin-left:8px;"><i class="material-icons right">delete</i></a>
         </div>
       </div>
@@ -208,7 +208,7 @@
       <template class="token-builder-section" data-token-builder-section="english-pos">
         <div class= "input-field col s4" data-kind-of-token="english-pos">
           <select name="englishpos">
-            <option value="default" disabled selected>English pos tagset</option>
+            <option value="" disabled selected>English pos tagset</option>
             <option value="ADD">email</option>
             <option value="AFX">affix</option>
             <option value="CC">conjunction, coordinating</option>
@@ -264,7 +264,7 @@
       <template class="token-builder-section" data-token-builder-section="german-pos">
         <div class= "input-field col s4" data-kind-of-token="german-pos">
           <select name="germanpos">
-            <option value="default" disabled selected>German pos tagset</option>
+            <option value="" disabled selected>German pos tagset</option>
             <option value="ADJA">adjective, attributive</option>
             <option value="ADJD">adjective, adverbial or predicative</option>
             <option value="ADV">adverb</option>
@@ -327,7 +327,7 @@
       <template class="token-builder-section" data-token-builder-section="simple_pos">
         <div class= "input-field col s4" data-kind-of-token="simple_pos">
           <select name="simplepos">
-            <option value="default" disabled selected>simple_pos tagset</option>
+            <option value="" disabled selected>simple_pos tagset</option>
             <option value="ADJ">adjective</option>
             <option value="ADP">adposition</option>
             <option value="ADV">adverb</option>
@@ -367,7 +367,7 @@
       </div>
       <p></p>
       <div class="row">
-        <div class="col s8" >
+        <div class="col s12" >
           <a class="btn-small waves-effect waves-light tooltipped positional-attr-options-action-button" data-options-action="wildcard-char" data-position="top" data-tooltip="Look for a variable character (also called wildcard character)">Wildcard character</a>
           <a class="btn-small waves-effect waves-light tooltipped positional-attr-options-action-button" data-options-action="option-group" data-position="top" data-tooltip="Find character sequences from a list of options">Option Group</a>
           <a class="dropdown-trigger btn-small waves-effect waves-light disabled" href="#" data-target="corpus-analysis-concordance-character-incidence-modifiers-dropdown" data-toggle-area="incidence-modifiers" data-position="top" data-tooltip="Incidence Modifiers are special characters or patterns, <br>which determine how often a character represented previously should occur.">incidence modifiers</a>
-- 
GitLab