From 1ff9c8bfe3f8d454974e7474ffccb2aea53c174b Mon Sep 17 00:00:00 2001
From: Inga Kirschnick <inga.kirschnick@uni-bielefeld.de>
Date: Tue, 7 Nov 2023 13:24:01 +0100
Subject: [PATCH] Query Builder in one class

---
 app/static/js/CorpusAnalysis/QueryBuilder.js  |  52 -
 .../GeneralFunctionsQueryBuilder.js           | 505 ----------
 ...alAttributeBuilderFunctionsQueryBuilder.js |  89 --
 ...enAttributeBuilderFunctionsQueryBuilder.js | 264 -----
 .../element-references.js}                    |   0
 .../js/CorpusAnalysis/query-builder/index.js  | 937 ++++++++++++++++++
 app/templates/_scripts.html.j2                |   7 +-
 .../corpora/_analysis/concordance.html.j2     |  16 -
 8 files changed, 939 insertions(+), 931 deletions(-)
 delete mode 100644 app/static/js/CorpusAnalysis/QueryBuilder.js
 delete mode 100644 app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js
 delete mode 100644 app/static/js/CorpusAnalysis/QueryBuilder/StructuralAttributeBuilderFunctionsQueryBuilder.js
 delete mode 100644 app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js
 rename app/static/js/CorpusAnalysis/{QueryBuilder/ElementReferencesQueryBuilder.js => query-builder/element-references.js} (100%)
 create mode 100644 app/static/js/CorpusAnalysis/query-builder/index.js

diff --git a/app/static/js/CorpusAnalysis/QueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder.js
deleted file mode 100644
index b37cdf14..00000000
--- a/app/static/js/CorpusAnalysis/QueryBuilder.js
+++ /dev/null
@@ -1,52 +0,0 @@
-class ConcordanceQueryBuilder {
-
-  constructor() {
-    
-    this.elements = new ElementReferencesQueryBuilder();
-    this.generalFunctions = new GeneralFunctionsQueryBuilder(this.elements);
-    this.tokenAttributeBuilderFunctions = new TokenAttributeBuilderFunctionsQueryBuilder(this.elements);
-    this.structuralAttributeBuilderFunctions = new StructuralAttributeBuilderFunctionsQueryBuilder(this.elements);
-
-    // Eventlisteners for the incidence modifiers. There are two different types of incidence modifiers: token and character incidence modifiers.
-    document.querySelectorAll('.incidence-modifier-selection').forEach(button => {
-      let dropdownId = button.parentNode.parentNode.id;
-      if (dropdownId === 'corpus-analysis-concordance-token-incidence-modifiers-dropdown') {
-        button.addEventListener('click', () => this.generalFunctions.tokenIncidenceModifierHandler(button.dataset.token, button.innerHTML));
-      } else if (dropdownId === 'corpus-analysis-concordance-character-incidence-modifiers-dropdown') {
-        button.addEventListener('click', () => this.tokenAttributeBuilderFunctions.characterIncidenceModifierHandler(button));
-      }
-    });
-    
-    // Eventlisteners for the submit of n- and m-values of the incidence modifier modal for "exactly n" or "between n and m".
-    document.querySelectorAll('.n-m-submit-button').forEach(button => {
-      let modalId = button.dataset.modalId;
-      if (modalId === 'corpus-analysis-concordance-exactly-n-token-modal' || modalId === 'corpus-analysis-concordance-between-nm-token-modal') {
-        button.addEventListener('click', () => this.generalFunctions.tokenNMSubmitHandler(modalId));
-      } else if (modalId === 'corpus-analysis-concordance-exactly-n-character-modal' || modalId === 'corpus-analysis-concordance-between-nm-character-modal') {
-        button.addEventListener('click', () => this.tokenAttributeBuilderFunctions.characterNMSubmitHandler(modalId));
-      }
-    });
-
-    document.querySelector('#corpus-analysis-concordance-text-annotation-submit').addEventListener('click', () => this.structuralAttributeBuilderFunctions.textAnnotationSubmitHandler());
-
-    this.elements.positionalAttrModal = M.Modal.init(
-      document.querySelector('#corpus-analysis-concordance-positional-attr-modal'), 
-      {
-        onOpenStart: () => {
-          this.tokenAttributeBuilderFunctions.preparePositionalAttrModal();
-        },
-        onCloseStart: () => {
-          this.tokenAttributeBuilderFunctions.resetPositionalAttrModal();
-        }
-      }
-    );
-    this.elements.structuralAttrModal = M.Modal.init(
-      document.querySelector('#corpus-analysis-concordance-structural-attr-modal'), 
-      {
-        onCloseStart: () => {
-          this.structuralAttributeBuilderFunctions.resetStructuralAttrModal();
-        }
-      }
-    );
-  }
-}
diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js
deleted file mode 100644
index 481e9b6d..00000000
--- a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js
+++ /dev/null
@@ -1,505 +0,0 @@
-class GeneralFunctionsQueryBuilder {
-  constructor(elements) {
-    this.elements = elements;
-  }
-
-  toggleClass(elements, className, action){
-    elements.forEach(element => {
-      document.querySelector(`[data-toggle-area="${element}"]`).classList[action](className);
-    });
-  }
-
-  resetQueryInputField() {
-    this.elements.queryInputField.innerHTML = '';
-    this.addPlaceholder();
-    this.updateChipList();
-    this.queryPreviewBuilder();
-  }
-
-  updateChipList() {
-    this.elements.queryChipElements = this.elements.queryInputField.querySelectorAll('.query-component');
-  }
-
-  removePlaceholder() {
-    let placeholder = this.elements.queryInputField.querySelector('#corpus-analysis-concordance-query-builder-input-field-placeholder');  
-    if (placeholder && this.elements.queryInputField !== undefined) {
-      this.elements.queryInputField.innerHTML = '';
-    }
-  }
-
-  addPlaceholder() {
-    let placeholder = Utils.HTMLToElement('<span id="corpus-analysis-concordance-query-builder-input-field-placeholder">Click on a button to add a query component</span>');
-    this.elements.queryInputField.appendChild(placeholder);
-  }
-  
-  resetMaterializeSelection(selectionElements, value = "default") {
-    selectionElements.forEach(selectionElement => {
-      if (selectionElement.querySelector(`option[value=${value}]`) !== null) {
-        selectionElement.querySelector(`option[value=${value}]`).selected = true;
-      }
-      let instance = M.FormSelect.getInstance(selectionElement);
-      instance.destroy();
-      M.FormSelect.init(selectionElement);
-    })
-  }
-
-  submitQueryChipElement(dataType = undefined, prettyQueryText = undefined, queryText = undefined, index = null, isClosingTag = false, isEditable = false) {
-    if (this.elements.editingModusOn) {
-      let editedQueryChipElement = this.elements.queryChipElements[this.elements.editedQueryChipElementIndex];
-      editedQueryChipElement.dataset.type = dataType;
-      editedQueryChipElement.dataset.query = queryText;
-      editedQueryChipElement.firstChild.textContent = prettyQueryText;
-      this.updateChipList();
-      this.queryPreviewBuilder();
-    } else {
-      this.queryChipFactory(dataType, prettyQueryText, queryText, index, isClosingTag, isEditable);
-    }
-  }
-
-  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);
-    prettyQueryText = Utils.escape(prettyQueryText);
-    let queryChipElement = Utils.HTMLToElement(
-      `
-        <span class="chip query-component" data-type="${dataType}" data-query="${queryText}" draggable="true" data-closing-tag="${isClosingTag}">
-          ${prettyQueryText}${isEditable ? '<i class="material-icons chip-action-button" data-chip-action="edit" style="padding-left:5px; font-size:18px; cursor:pointer;">edit</i>': ''}
-          ${isClosingTag ? '<i class="material-icons chip-action-button" data-chip-action="lock" style="padding-top:5px; font-size:20px; cursor:pointer;">lock_open</i>' : '<i class="material-icons close chip-action-button" data-chip-action="delete">close</i>'}
-        </span>
-      `
-    );
-    this.actionListeners(queryChipElement);
-    queryChipElement.addEventListener('dragstart', this.handleDragStart.bind(this, queryChipElement));
-    queryChipElement.addEventListener('dragend', this.handleDragEnd);
-
-    // Ensures that metadata is always at the end of the query and if an index is given, inserts the query chip at the given index and if there is a closing tag, inserts the query chip before the closing tag.
-    this.removePlaceholder();
-    let lastChild = this.elements.queryInputField.lastChild;
-    let isLastChildTextAnnotation = lastChild && lastChild.dataset.type === 'text-annotation';
-    if (!index) {
-      let closingTagElement = this.elements.queryInputField.querySelector('[data-closing-tag="true"]');
-      if (closingTagElement) {
-        index = Array.from(this.elements.queryInputField.children).indexOf(closingTagElement);
-      }
-    }
-    if (index || isLastChildTextAnnotation) {
-      let insertingElement = isLastChildTextAnnotation ? lastChild : this.elements.queryChipElements[index];
-      this.elements.queryInputField.insertBefore(queryChipElement, insertingElement);
-    } else {
-      this.elements.queryInputField.appendChild(queryChipElement);
-    }
-
-    this.updateChipList();
-    this.queryPreviewBuilder();
-  }
-
-  actionListeners(queryChipElement) {
-    let notQuantifiableDataTypes = ['start-sentence', 'end-sentence', 'start-entity', 'start-empty-entity', 'end-entity', 'token-incidence-modifier'];
-    queryChipElement.addEventListener('click', (event) => {
-      if (event.target.classList.contains('chip')) {
-        if (!notQuantifiableDataTypes.includes(queryChipElement.dataset.type)) {
-          this.selectChipElement(queryChipElement);
-        }
-      }
-    });
-    let chipActionButtons = queryChipElement.querySelectorAll('.chip-action-button');
-    chipActionButtons.forEach(button => {
-      button.addEventListener('click', (event) => {
-      if (event.target.dataset.chipAction === 'delete') {
-        this.deleteChipElement(queryChipElement);
-      } else if (event.target.dataset.chipAction === 'edit') {
-        this.editChipElement(queryChipElement);
-      } else if (event.target.dataset.chipAction === 'lock') {
-        this.lockClosingChipElement(queryChipElement);
-      }
-      });
-    });
-  }
-
-  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) {
-      case 'start-entity':
-        this.elements.structuralAttrModal.open();
-        this.toggleClass(['entity-builder'], 'hide', 'remove');
-        this.toggleEditingAreaStructureAttrModal('add');
-        let entType = queryChipElement.dataset.query.replace(/<ent_type="|">/g, '');
-        let isEnglishEntType = this.elements.englishEntTypeSelection.querySelector(`option[value=${entType}]`) !== null;
-        let selection = isEnglishEntType ? this.elements.englishEntTypeSelection : this.elements.germanEntTypeSelection;
-        this.resetMaterializeSelection([selection], entType);
-        break;
-      case 'text-annotation':
-        this.elements.structuralAttrModal.open();
-        this.toggleClass(['text-annotation-builder'], 'hide', 'remove');
-        this.toggleEditingAreaStructureAttrModal('add');
-        let [textAnnotationSelection, textAnnotationContent] = queryChipElement.dataset.query
-          .replace(/:: ?match\.text_|"|"/g, '')
-          .split('=');
-        this.resetMaterializeSelection([this.elements.textAnnotationSelection], textAnnotationSelection);
-        this.elements.textAnnotationInput.value = textAnnotationContent;
-        break;
-      case 'token':
-        //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(queryElementsContent);
-        break;
-      default:
-        break;
-    }
-  }
-
-  editTokenChipElement(queryElementsContent) {
-    this.elements.positionalAttrModal.open();
-    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) {
-    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) {
-    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) {
-      this.addPlaceholder();
-    }
-    this.updateChipList();
-    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');
-    setTimeout(() => {
-      let targetChipElement = Utils.HTMLToElement('<span class="chip drop-target">Drop here</span>');
-      for (let element of queryChips) {
-        if (element === queryChipElement.nextSibling) {continue;}
-        let targetChipClone = targetChipElement.cloneNode(true);
-        if (element === queryChipElement && queryChips[queryChips.length - 1] !== element) {
-          queryChips[queryChips.length - 1].insertAdjacentElement('afterend', targetChipClone);
-        } else {
-          element.insertAdjacentElement('beforebegin', targetChipClone);
-        }
-        this.addDragDropListeners(targetChipClone, queryChipElement);
-      }
-    }, 0);
-  }
-
-  handleDragEnd(event) {
-    document.querySelectorAll('.drop-target').forEach(target => target.remove());
-  }
-
-  addDragDropListeners(targetChipClone, queryChipElement) {
-    targetChipClone.addEventListener('dragover', (event) => {
-      event.preventDefault();
-    });
-    targetChipClone.addEventListener('dragenter', (event) => {
-      event.preventDefault();
-      event.target.style.borderStyle = 'solid dotted';
-    });
-    targetChipClone.addEventListener('dragleave', (event) => {
-      event.preventDefault();
-      event.target.style.borderStyle = 'hidden';
-    });
-    targetChipClone.addEventListener('drop', (event) => {
-      let dropzone = event.target;
-      dropzone.parentElement.replaceChild(queryChipElement, dropzone);
-      this.updateChipList();
-      this.queryPreviewBuilder();
-    });
-  }
-
-  queryPreviewBuilder() {
-    // Builds the query preview in the form of pure CQL and displays it in the query preview field.
-    let queryPreview = document.querySelector('#corpus-analysis-concordance-query-preview');
-    let queryInputFieldContent = [];
-    this.elements.queryChipElements.forEach(element => {
-      let queryElement = element.dataset.query;
-      if (queryElement !== undefined) {
-        queryElement = Utils.escape(queryElement);
-      }
-      queryInputFieldContent.push(queryElement);
-    });
-  
-    let queryString = queryInputFieldContent.join(' ');
-    let replacements = {
-      ' +': '+',
-      ' *': '*',
-      ' ?': '?',
-      ' {': '{'
-    };
-
-    for (let key in replacements) {
-      queryString = queryString.replace(key, replacements[key]);
-    }
-    queryString += ';';
-  
-    queryPreview.innerHTML = queryString;
-    queryPreview.parentNode.classList.toggle('hide', queryString === ';');
-  }
-
-  selectChipElement(attr) {
-    document.querySelectorAll('.chip.teal').forEach(element => {
-        if (element !== attr) {
-          element.classList.remove('teal', 'lighten-2');
-          this.toggleClass(['token-incidence-modifiers'], 'disabled', 'add');
-        }
-    });
-
-    this.toggleClass(['token-incidence-modifiers'], 'disabled', 'toggle');
-    attr.classList.toggle('teal');
-    attr.classList.toggle('lighten-5');
-  }
-
-  tokenIncidenceModifierHandler(incidenceModifier, incidenceModifierPretty) {
-    // Adds a token incidence modifier to the query input field.
-    let selectedChip = this.elements.queryInputField.querySelector('.chip.teal');
-    let selectedChipIndex = Array.from(this.elements.queryInputField.children).indexOf(selectedChip);
-    this.submitQueryChipElement('token-incidence-modifier', incidenceModifierPretty, incidenceModifier, selectedChipIndex+1);
-    this.selectChipElement(selectedChip);
-  }
-
-  tokenNMSubmitHandler(modalId) {
-    // Adds a token incidence modifier (exactly n or between n and m) to the query input field.
-    let modal = document.querySelector(`#${modalId}`);
-    let input_n = modal.querySelector('.n-m-input[data-value-type="n"]').value;
-    let input_m = modal.querySelector('.n-m-input[data-value-type="m"]') || undefined;
-    input_m = input_m !== undefined ? input_m.value : '';
-    let input = `{${input_n}${input_m !== '' ? ',' : ''}${input_m}}`;
-    let pretty_input = `between ${input_n} and ${input_m} (${input})`;
-    if (input_m === '') {
-     pretty_input = `exactly ${input_n} (${input})`;
-    }
-
-    let instance = M.Modal.getInstance(modal);
-    instance.close();
-
-    this.tokenIncidenceModifierHandler(input, pretty_input);
-  }
-
-  switchToExpertModeParser() {
-    let expertModeInputField = document.querySelector('#corpus-analysis-concordance-form-query');
-    expertModeInputField.value = '';
-    let queryBuilderInputFieldValue = Utils.unescape(document.querySelector('#corpus-analysis-concordance-query-preview').innerHTML.trim());
-    if (queryBuilderInputFieldValue !== "" && queryBuilderInputFieldValue !== ";") {
-      expertModeInputField.value = queryBuilderInputFieldValue;
-    }
-  }
-
-  switchToQueryBuilderParser() {
-    this.resetQueryInputField();
-    let expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value;
-    let chipElements = this.parseTextToChip(expertModeInputFieldValue);
-    for (let chipElement of chipElements) {
-      this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query']);
-    }
-  }
-
-  parseTextToChip(query) {
-    const parsingElementDict = {
-      '<s>': {
-        pretty: 'Sentence Start',
-        type: 'start-sentence'
-      },
-      '<\/s>': {
-        pretty: 'Sentence End',
-        type: 'end-sentence'
-      },
-      '<ent>': {
-        pretty: 'Entity Start',
-        type: 'start-empty-entity'
-      },
-      '<ent_type="([A-Z]+)">': {
-        pretty: '',
-        type: 'start-entity'
-      },
-      '<\\\/ent(_type)?>': {
-        pretty: 'Entity End',
-        type: 'end-entity'
-      },
-      ':: ?match\\.text_[A-Za-z]+="[^"]+"': {
-        pretty: '',
-        type: 'text-annotation'
-      },
-      '\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]': {
-        pretty: '',
-        type: 'token'
-      },
-      '\\[\\]': {
-        pretty: 'Empty Token',
-        type: 'token'
-      },
-      '(?<!\\[) ?\\+ ?(?![^\\]]\\])': {
-        pretty: ' one or more (+)',
-        type: 'token-incidence-modifier'
-      },
-      '(?<!\\[) ?\\* ?(?![^\\]]\\])': {
-        pretty: 'zero or more (*)',
-        type: 'token-incidence-modifier'
-      },
-      '(?<!\\[) ?\\? ?(?![^\\]]\\])': {
-        pretty: 'zero or one (?)',
-        type: 'token-incidence-modifier'
-      },
-      '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])': {
-        pretty: '',
-        type: 'token-incidence-modifier'
-      },
-      '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])': {
-        pretty: '',
-        type: 'token-incidence-modifier'
-      }
-    }
-  
-    let chipElements = [];
-    let regexPattern = Object.keys(parsingElementDict).map(pattern => `(${pattern})`).join('|');
-    const regex = new RegExp(regexPattern, 'gi');
-    let match;
-  
-    while ((match = regex.exec(query)) !== null) {
-      // This is necessary to avoid infinite loops with zero-width matches
-      if (match.index === regex.lastIndex) {
-        regex.lastIndex++;
-      }
-      let stringElement = match[0];
-      for (let [pattern, chipElement] of Object.entries(parsingElementDict)) {
-        const parsingRegex = new RegExp(pattern, 'gi');
-        if (parsingRegex.exec(stringElement)) {
-          // Creating the pretty text for the chip element
-          let prettyText;
-          switch (pattern) {
-            case '<ent_type="([A-Z]+)">':
-              prettyText = `Entity Type=${stringElement.replace(/<ent_type="|">/g, '')}`;
-              break;
-            case ':: ?match\\.text_[A-Za-z]+="[^"]+"':
-              prettyText = stringElement.replace(/:: ?match\.text_|"|"/g, '');
-              break;
-            case '\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]':
-              let doubleQuotes = /(word|lemma|pos|simple_pos)="[^"]+"/gi;
-              let singleQuotes = /(word|lemma|pos|simple_pos)='[^']+'/gi;
-              if (doubleQuotes.exec(stringElement)) {
-                prettyText = stringElement.replace(/^\[|\]$|"/g, '');
-              } else if (singleQuotes.exec(stringElement)) {
-                prettyText = stringElement.replace(/^\[|\]$|'/g, '');
-              }
-              prettyText = prettyText.replace(/\&/g, ' and ').replace(/\|/g, ' or ');
-              break;
-            case '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])':
-              prettyText = `exactly ${stringElement.replace(/{|}/g, '')} (${stringElement})`;
-              break;
-            case '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])':
-              prettyText = `between${stringElement.replace(/{|}/g, ' ').replace(',', ' and ')}(${stringElement})`;
-              break;
-            default:
-              prettyText = chipElement.pretty;
-              break;
-          }
-          chipElements.push({
-            type: chipElement.type,
-            pretty: prettyText,
-            query: stringElement
-          });
-          break;
-        }
-      }
-    }
-  
-    return chipElements;
-  }
-
-}
-
diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/StructuralAttributeBuilderFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/StructuralAttributeBuilderFunctionsQueryBuilder.js
deleted file mode 100644
index d8f9fbe9..00000000
--- a/app/static/js/CorpusAnalysis/QueryBuilder/StructuralAttributeBuilderFunctionsQueryBuilder.js
+++ /dev/null
@@ -1,89 +0,0 @@
-class StructuralAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBuilder {
-  constructor(elements) {
-    super(elements);
-    this.elements = elements;
-
-    document.querySelectorAll('[data-structural-attr-modal-action-button]').forEach(button => {
-      button.addEventListener('click', () => {
-        this.actionButtonInStrucAttrModalHandler(button.dataset.structuralAttrModalActionButton);
-      });
-    });
-    document.querySelector('.ent-type-selection-action[data-ent-type="any"]').addEventListener('click', () => {
-      this.submitQueryChipElement('start-empty-entity', 'Entity Start', '<ent>');
-      this.submitQueryChipElement('end-entity', 'Entity End', '</ent>', null, true);
-      this.elements.structuralAttrModal.close();
-    });
-    document.querySelector('.ent-type-selection-action[data-ent-type="english"]').addEventListener('change', (event) => {
-      this.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, `<ent_type="${event.target.value}">`, null, false, true);
-      if (!this.elements.editingModusOn) {
-        this.submitQueryChipElement('end-entity', 'Entity End', '</ent_type>', null, true);
-      }
-      this.elements.structuralAttrModal.close();
-    });
-    document.querySelector('.ent-type-selection-action[data-ent-type="german"]').addEventListener('change', (event) => {
-      this.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, `<ent_type="${event.target.value}">`, null, false, true); 
-      if (!this.elements.editingModusOn) {
-        this.submitQueryChipElement('end-entity', 'Entity End', '</ent_type>', null, true);
-      }
-      this.elements.structuralAttrModal.close();
-    });
-  }
-
-  resetStructuralAttrModal() {
-    this.resetMaterializeSelection([this.elements.englishEntTypeSelection, this.elements.germanEntTypeSelection]);
-    this.resetMaterializeSelection([this.elements.textAnnotationSelection], 'address');
-    this.elements.textAnnotationInput.value = '';
-
-    this.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add');
-    this.toggleEditingAreaStructureAttrModal('remove');
-    this.elements.editingModusOn = false;
-    this.elements.editedQueryChipElementIndex = undefined;
-  }
-
-  toggleEditingAreaStructureAttrModal(action) {
-    // If the user edits a query chip element, the corresponding editing area is displayed and the other areas are hidden or disabled.
-    this.toggleClass(['sentence-button', 'entity-button', 'text-annotation-button', 'any-type-entity-button'], 'disabled', action);
-  }
-
-  actionButtonInStrucAttrModalHandler(action) {
-    switch (action) {
-      case 'sentence':
-        this.submitQueryChipElement('start-sentence', 'Sentence Start', '<s>');
-        this.submitQueryChipElement('end-sentence', 'Sentence End', '</s>', null, true);
-        this.elements.structuralAttrModal.close();
-        break;
-      case 'entity':
-        this.toggleClass(['entity-builder'], 'hide', 'toggle');
-        this.toggleClass(['text-annotation-builder'], 'hide', 'add');
-        break;
-      case 'meta-data':
-        this.toggleClass(['text-annotation-builder'], 'hide', 'toggle');
-        this.toggleClass(['entity-builder'], 'hide', 'add');
-        break;
-      default:
-        break;
-    }
-  }
-
-  textAnnotationSubmitHandler() {
-    let noValueMetadataMessage = document.querySelector('#corpus-analysis-concordance-no-value-metadata-message');
-    let textAnnotationSubmit = document.querySelector('#corpus-analysis-concordance-text-annotation-submit');
-    let textAnnotationInput = document.querySelector('#corpus-analysis-concordance-text-annotation-input');
-    let textAnnotationOptions = document.querySelector('#corpus-analysis-concordance-text-annotation-options');
-
-    if (textAnnotationInput.value === '') {
-      textAnnotationSubmit.classList.add('red');
-      noValueMetadataMessage.classList.remove('hide');
-      setTimeout(() => {
-        textAnnotationSubmit.classList.remove('red');
-      }, 500);
-      setTimeout(() => {
-        noValueMetadataMessage.classList.add('hide');
-      }, 3000);
-    } else {
-      let queryText = `:: match.text_${textAnnotationOptions.value}="${textAnnotationInput.value}"`;
-      this.submitQueryChipElement('text-annotation', `${textAnnotationOptions.value}=${textAnnotationInput.value}`, queryText, null, false, true);
-      this.elements.structuralAttrModal.close();
-    }
-  }
-}
diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js
deleted file mode 100644
index bebb5d83..00000000
--- a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js
+++ /dev/null
@@ -1,264 +0,0 @@
-class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBuilder {
-  constructor(elements) {
-    super(elements);
-    this.elements = elements;
-
-    this.elements.positionalAttrSelection.addEventListener('change', () => {
-      this.preparePositionalAttrModal();
-    });
-
-    // Options for positional attribute selection
-    document.querySelectorAll('.positional-attr-options-action-button[data-options-action]').forEach(button => {
-      button.addEventListener('click', () => {this.actionButtonInOptionSectionHandler(button.dataset.optionsAction);});
-    });
-
-    this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();});
-  }
-
-  resetPositionalAttrModal() {
-    let originalSelectionList = 
-      `
-        <option value="word" selected>word</option>
-        <option value="lemma" >lemma</option>
-        <option value="english-pos">english pos</option>
-        <option value="german-pos">german pos</option>
-        <option value="simple_pos">simple_pos</option>
-        <option value="empty-token">empty token</option>
-      `;
-    this.elements.positionalAttrSelection.innerHTML = originalSelectionList;
-    this.elements.tokenQuery.innerHTML = '';
-    this.elements.tokenBuilderContent.innerHTML = '';
-    this.toggleClass(['input-field-options'], 'hide', 'remove');
-    this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
-    this.resetMaterializeSelection([this.elements.positionalAttrSelection], "word");
-    this.elements.ignoreCaseCheckbox.checked = false;
-    this.elements.editingModusOn = false;
-    this.elements.editedQueryChipElementIndex = undefined;
-  }
-  
-  preparePositionalAttrModal() {
-    let selection = this.elements.positionalAttrSelection.value;
-    if (selection !== 'empty-token') {
-      let selectionTemplate = document.querySelector(`.token-builder-section[data-token-builder-section="${selection}"]`);
-      let selectionTemplateClone = selectionTemplate.content.cloneNode(true);
-    
-      this.elements.tokenBuilderContent.innerHTML = '';
-      this.elements.tokenBuilderContent.appendChild(selectionTemplateClone);
-      if (this.elements.tokenBuilderContent.querySelector('select') !== null) {
-        let selectElement = this.elements.tokenBuilderContent.querySelector('select');
-        M.FormSelect.init(selectElement);
-        selectElement.addEventListener('change', () => {this.optionToggleHandler();});
-      } else {
-        this.elements.tokenBuilderContent.querySelector('input').addEventListener('input', () => {this.optionToggleHandler();});
-      }
-    }
-    this.optionToggleHandler();
-
-    if (selection === 'word' || selection === 'lemma') {
-      this.toggleClass(['input-field-options'], 'hide', 'remove');
-    } else if (selection === 'empty-token'){
-      this.addTokenToQuery();
-    } else {
-      this.toggleClass(['input-field-options'], 'hide', 'add');
-    }
-  }
-
-  tokenInputCheck(elem) {
-    return elem.querySelector('select') !== null ? elem.querySelector('select') : elem.querySelector('input');
-  }
-
-  optionToggleHandler() {
-    let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
-    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');
-    } else {
-      this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'remove');
-    }
-  }
-
-  disableTokenSubmit() {
-    this.elements.tokenSubmitButton.classList.add('red');
-    this.elements.noValueMessage.classList.remove('hide');
-    setTimeout(() => {
-      this.elements.tokenSubmitButton.classList.remove('red');
-    }, 500);
-    setTimeout(() => {
-      this.elements.noValueMessage.classList.add('hide');
-    }, 3000);
-  }
-
-  addTokenToQuery() {
-    let tokenQueryPrettyText = '';
-    let tokenQueryCQLText = '';
-    let input;
-    let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value);
-    
-    // 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 => {
-      let ignoreCaseCheckbox = row.querySelector('input[type="checkbox"]');
-      let c = ignoreCaseCheckbox !== null && ignoreCaseCheckbox.checked ? ' %c' : '';
-      let tokenQueryRowInput = this.tokenInputCheck(row.querySelector('.token-query-template-content'));
-      let tokenQueryKindOfToken = this.kindOfTokenCheck(tokenQueryRowInput.closest('.input-field').dataset.kindOfToken);
-      let tokenConditionPrettyText = row.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
-      let tokenConditionCQLText = row.querySelector('[data-condition-cql-text]').dataset.conditionCqlText;
-      tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `;
-      tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`;
-    });
-    if (kindOfToken === 'empty-token') {
-      tokenQueryPrettyText += 'empty token';
-    } else {
-      let c = this.elements.ignoreCaseCheckbox.checked ? ' %c' : '';
-      input = this.tokenInputCheck(this.elements.tokenBuilderContent);
-      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 (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') {
-      this.disableTokenSubmit();
-    } else {
-      tokenQueryCQLText = `[${tokenQueryCQLText}]`;
-      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);
-    switch (elem) {
-      case 'option-group':
-        input.value  += '(option1|option2)';
-        let firstIndex = input.value.indexOf('option1');
-        let lastIndex = firstIndex + 'option1'.length;
-        input.focus();
-        input.setSelectionRange(firstIndex, lastIndex);
-        break;
-      case 'wildcard-char':
-        input.value += '.';
-        break;
-      case 'and':
-        this.conditionHandler('and');
-        break;
-      case 'or':
-        this.conditionHandler('or');
-        break;
-      default:
-        break;
-    }
-    this.optionToggleHandler();
-  }
-
-  characterIncidenceModifierHandler(elem) {
-    let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
-    input.value += elem.dataset.token;
-  }
-
-  characterNMSubmitHandler(modalId) {
-    let modal = document.querySelector(`#${modalId}`);
-    let input_n = modal.querySelector('.n-m-input[data-value-type="n"]').value;
-    let input_m = modal.querySelector('.n-m-input[data-value-type="m"]') || undefined;
-    input_m = input_m !== undefined ? ',' + input_m.value : '';
-    let input = `${input_n}${input_m}`;
-
-    let instance = M.Modal.getInstance(modal);
-    instance.close();
-    let tokenInput = this.tokenInputCheck(this.elements.tokenBuilderContent);
-    tokenInput.value += '{' + input + '}';
-  }
-
-  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}"])`);
-    let deleteButton = tokenQueryTemplateClone.querySelector(`[data-token-query-content-action="delete"]`);
-    deleteButton.addEventListener('click', (event) => {
-      this.deleteTokenQueryRow(event.target);
-    });
-    notSelectedButton.parentNode.removeChild(notSelectedButton);
-    this.elements.tokenQuery.appendChild(tokenQueryTemplateClone);
-
-    // Deleting the options which do not make sense in the context of the condition like "word" AND "word". Also sets selection default.
-    let selectionDefault = "word";
-    let optionDeleteList = ['empty-token'];
-    if (conditionText === 'and') {
-      switch (this.elements.positionalAttrSelection.value) {
-        case 'english-pos' || 'german-pos':
-          optionDeleteList.push('english-pos', 'german-pos');
-          break;
-        default:
-          optionDeleteList.push(this.elements.positionalAttrSelection.value);
-          break;
-      }
-    } else {
-      let originalSelectionList = 
-      `
-        <option value="word" selected>word</option>
-        <option value="lemma" >lemma</option>
-        <option value="english-pos">english pos</option>
-        <option value="german-pos">german pos</option>
-        <option value="simple_pos">simple_pos</option>
-      `;
-      this.elements.positionalAttrSelection.innerHTML = originalSelectionList;
-      M.FormSelect.init(this.elements.positionalAttrSelection);
-    }
-    let lastTokenQueryRow = this.elements.tokenQuery.lastElementChild;
-    if(lastTokenQueryRow.querySelector('[data-kind-of-token="word"]') || lastTokenQueryRow.querySelector('[data-kind-of-token="lemma"]')) {
-      this.appendIgnoreCaseCheckbox(lastTokenQueryRow.querySelector('.token-query-template-content'), this.elements.ignoreCaseCheckbox.checked);
-    }
-    this.elements.ignoreCaseCheckbox.checked = false;
-    this.setTokenSelection(selectionDefault, optionDeleteList);
-  }
-
-  deleteTokenQueryRow(deleteButton) {
-    let deletedRow = deleteButton.closest('.row');
-    let condition = deletedRow.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
-    if (condition === 'and') {
-      let kindOfToken = deletedRow.querySelector('[data-kind-of-token]').dataset.kindOfToken;
-      switch (kindOfToken) {
-        case 'english-pos' || 'german-pos':
-          this.createOptionElementForPosAttrSelection('english-pos');
-          this.createOptionElementForPosAttrSelection('german-pos');
-          break;
-        default:
-          this.createOptionElementForPosAttrSelection(kindOfToken);
-          break;
-      }
-      M.FormSelect.init(this.elements.positionalAttrSelection);
-    }
-    deletedRow.remove();
-  }
-
-  createOptionElementForPosAttrSelection(kindOfToken) {
-    let option = document.createElement('option');
-    option.value = kindOfToken;
-    option.text = kindOfToken;
-    this.elements.positionalAttrSelection.appendChild(option);
-  }
-
-  appendIgnoreCaseCheckbox(parentElement, checked = false) {
-    let ignoreCaseCheckboxClone = document.querySelector('#ignore-case-checkbox-template').content.cloneNode(true);
-    parentElement.appendChild(ignoreCaseCheckboxClone);
-    M.Tooltip.init(parentElement.querySelectorAll('.tooltipped'));
-    if (checked) {
-      parentElement.querySelector('input[type="checkbox"]').checked = true;
-    }
-  }
-
-  setTokenSelection(selection, optionDeleteList) {
-    optionDeleteList.forEach(option => {
-      if (this.elements.positionalAttrSelection.querySelector(`option[value=${option}]`) !== null) {
-        this.elements.positionalAttrSelection.querySelector(`option[value=${option}]`).remove();
-      }
-    });
-
-    this.resetMaterializeSelection([this.elements.positionalAttrSelection], selection);
-    this.preparePositionalAttrModal();
-  }
-}
diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js b/app/static/js/CorpusAnalysis/query-builder/element-references.js
similarity index 100%
rename from app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js
rename to app/static/js/CorpusAnalysis/query-builder/element-references.js
diff --git a/app/static/js/CorpusAnalysis/query-builder/index.js b/app/static/js/CorpusAnalysis/query-builder/index.js
new file mode 100644
index 00000000..82a01a7c
--- /dev/null
+++ b/app/static/js/CorpusAnalysis/query-builder/index.js
@@ -0,0 +1,937 @@
+class ConcordanceQueryBuilder {
+  
+  constructor() {
+    this.elements = new ElementReferencesQueryBuilder();
+
+    //#region QB Constructor
+    // Eventlisteners for the incidence modifiers. There are two different types of incidence modifiers: token and character incidence modifiers.
+    document.querySelectorAll('.incidence-modifier-selection').forEach(button => {
+      let dropdownId = button.parentNode.parentNode.id;
+      if (dropdownId === 'corpus-analysis-concordance-token-incidence-modifiers-dropdown') {
+        button.addEventListener('click', () => this.tokenIncidenceModifierHandler(button.dataset.token, button.innerHTML));
+      } else if (dropdownId === 'corpus-analysis-concordance-character-incidence-modifiers-dropdown') {
+        button.addEventListener('click', () => this.characterIncidenceModifierHandler(button));
+      }
+    });
+    
+    // Eventlisteners for the submit of n- and m-values of the incidence modifier modal for "exactly n" or "between n and m".
+    document.querySelectorAll('.n-m-submit-button').forEach(button => {
+      let modalId = button.dataset.modalId;
+      if (modalId === 'corpus-analysis-concordance-exactly-n-token-modal' || modalId === 'corpus-analysis-concordance-between-nm-token-modal') {
+        button.addEventListener('click', () => this.tokenNMSubmitHandler(modalId));
+      } else if (modalId === 'corpus-analysis-concordance-exactly-n-character-modal' || modalId === 'corpus-analysis-concordance-between-nm-character-modal') {
+        button.addEventListener('click', () => this.characterNMSubmitHandler(modalId));
+      }
+    });
+
+    document.querySelector('#corpus-analysis-concordance-text-annotation-submit').addEventListener('click', () => this.textAnnotationSubmitHandler());
+
+    this.elements.positionalAttrModal = M.Modal.init(
+      document.querySelector('#corpus-analysis-concordance-positional-attr-modal'), 
+      {
+        onOpenStart: () => {
+          this.preparePositionalAttrModal();
+        },
+        onCloseStart: () => {
+          this.resetPositionalAttrModal();
+        }
+      }
+    );
+    this.elements.structuralAttrModal = M.Modal.init(
+      document.querySelector('#corpus-analysis-concordance-structural-attr-modal'), 
+      {
+        onCloseStart: () => {
+          this.resetStructuralAttrModal();
+        }
+      }
+    );
+
+    let queryBuilderDisplay = document.getElementById("corpus-analysis-concordance-query-builder-display");
+    let expertModeDisplay = document.getElementById("corpus-analysis-concordance-expert-mode-display");
+    let expertModeSwitch = document.getElementById("corpus-analysis-concordance-expert-mode-switch");
+  
+    expertModeSwitch.addEventListener("change", () => {
+      const isChecked = expertModeSwitch.checked;
+      if (isChecked) {
+        queryBuilderDisplay.classList.add("hide");
+        expertModeDisplay.classList.remove("hide");
+        this.switchToExpertModeParser();
+      } else {
+        queryBuilderDisplay.classList.remove("hide");
+        expertModeDisplay.classList.add("hide");
+        this.switchToQueryBuilderParser();
+      }
+    });
+    //#endregion QB Constructor
+
+    //#region Structural Attribute Builder Constructor
+    document.querySelectorAll('[data-structural-attr-modal-action-button]').forEach(button => {
+      button.addEventListener('click', () => {
+        this.actionButtonInStrucAttrModalHandler(button.dataset.structuralAttrModalActionButton);
+      });
+    });
+    document.querySelector('.ent-type-selection-action[data-ent-type="any"]').addEventListener('click', () => {
+      this.submitQueryChipElement('start-empty-entity', 'Entity Start', '<ent>');
+      this.submitQueryChipElement('end-entity', 'Entity End', '</ent>', null, true);
+      this.elements.structuralAttrModal.close();
+    });
+    document.querySelector('.ent-type-selection-action[data-ent-type="english"]').addEventListener('change', (event) => {
+      this.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, `<ent_type="${event.target.value}">`, null, false, true);
+      if (!this.elements.editingModusOn) {
+        this.submitQueryChipElement('end-entity', 'Entity End', '</ent_type>', null, true);
+      }
+      this.elements.structuralAttrModal.close();
+    });
+    document.querySelector('.ent-type-selection-action[data-ent-type="german"]').addEventListener('change', (event) => {
+      this.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, `<ent_type="${event.target.value}">`, null, false, true); 
+      if (!this.elements.editingModusOn) {
+        this.submitQueryChipElement('end-entity', 'Entity End', '</ent_type>', null, true);
+      }
+      this.elements.structuralAttrModal.close();
+    });
+    //#endregion Structural Attribute Builder Constructor
+
+    //#region Token Attribute Builder Constructor
+    this.elements.positionalAttrSelection.addEventListener('change', () => {
+      this.preparePositionalAttrModal();
+    });
+
+    // Options for positional attribute selection
+    document.querySelectorAll('.positional-attr-options-action-button[data-options-action]').forEach(button => {
+      button.addEventListener('click', () => {this.actionButtonInOptionSectionHandler(button.dataset.optionsAction);});
+    });
+
+    this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();});
+    //#endregion Token Attribute Builder Constructor
+  }
+
+  //#region QB Functions
+  switchToExpertModeParser() {
+    let expertModeInputField = document.querySelector('#corpus-analysis-concordance-form-query');
+    expertModeInputField.value = '';
+    let queryBuilderInputFieldValue = Utils.unescape(document.querySelector('#corpus-analysis-concordance-query-preview').innerHTML.trim());
+    if (queryBuilderInputFieldValue !== "" && queryBuilderInputFieldValue !== ";") {
+      expertModeInputField.value = queryBuilderInputFieldValue;
+    }
+  }
+
+  switchToQueryBuilderParser() {
+    this.resetQueryInputField();
+    let expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value;
+    let chipElements = this.parseTextToChip(expertModeInputFieldValue);
+    let closingTagElements = ['end-sentence', 'end-entity'];
+    let editableElements = ['start-entity', 'text-annotation', 'token'];
+    for (let chipElement of chipElements) {
+      let isClosingTag = closingTagElements.includes(chipElement['type']);
+      let isEditable = editableElements.includes(chipElement['type']);
+      if (chipElement['query'] === '[]'){
+        isEditable = false;
+      }
+      this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable);
+    }
+  }
+
+  parseTextToChip(query) {
+    const parsingElementDict = {
+      '<s>': {
+        pretty: 'Sentence Start',
+        type: 'start-sentence'
+      },
+      '<\/s>': {
+        pretty: 'Sentence End',
+        type: 'end-sentence'
+      },
+      '<ent>': {
+        pretty: 'Entity Start',
+        type: 'start-empty-entity'
+      },
+      '<ent_type="([A-Z]+)">': {
+        pretty: '',
+        type: 'start-entity'
+      },
+      '<\\\/ent(_type)?>': {
+        pretty: 'Entity End',
+        type: 'end-entity'
+      },
+      ':: ?match\\.text_[A-Za-z]+="[^"]+"': {
+        pretty: '',
+        type: 'text-annotation'
+      },
+      '\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]': {
+        pretty: '',
+        type: 'token'
+      },
+      '\\[\\]': {
+        pretty: 'Empty Token',
+        type: 'token'
+      },
+      '(?<!\\[) ?\\+ ?(?![^\\]]\\])': {
+        pretty: ' one or more (+)',
+        type: 'token-incidence-modifier'
+      },
+      '(?<!\\[) ?\\* ?(?![^\\]]\\])': {
+        pretty: 'zero or more (*)',
+        type: 'token-incidence-modifier'
+      },
+      '(?<!\\[) ?\\? ?(?![^\\]]\\])': {
+        pretty: 'zero or one (?)',
+        type: 'token-incidence-modifier'
+      },
+      '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])': {
+        pretty: '',
+        type: 'token-incidence-modifier'
+      },
+      '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])': {
+        pretty: '',
+        type: 'token-incidence-modifier'
+      }
+    }
+  
+    let chipElements = [];
+    let regexPattern = Object.keys(parsingElementDict).map(pattern => `(${pattern})`).join('|');
+    const regex = new RegExp(regexPattern, 'gi');
+    let match;
+  
+    while ((match = regex.exec(query)) !== null) {
+      // this is necessary to avoid infinite loops with zero-width matches
+      if (match.index === regex.lastIndex) {
+        regex.lastIndex++;
+      }
+      let stringElement = match[0];
+      for (let [pattern, chipElement] of Object.entries(parsingElementDict)) {
+        const parsingRegex = new RegExp(pattern, 'gi');
+        if (parsingRegex.exec(stringElement)) {
+          // Creating the pretty text for the chip element
+          let prettyText;
+          switch (pattern) {
+            case '<ent_type="([A-Z]+)">':
+              prettyText = `Entity Type=${stringElement.replace(/<ent_type="|">/g, '')}`;
+              break;
+            case ':: ?match\\.text_[A-Za-z]+="[^"]+"':
+              prettyText = stringElement.replace(/:: ?match\.text_|"|"/g, '');
+              break;
+            case '\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]':
+              let doubleQuotes = /(word|lemma|pos|simple_pos)="[^"]+"/gi;
+              let singleQuotes = /(word|lemma|pos|simple_pos)='[^']+'/gi;
+              if (doubleQuotes.exec(stringElement)) {
+                prettyText = stringElement.replace(/^\[|\]$|"/g, '');
+              } else if (singleQuotes.exec(stringElement)) {
+                prettyText = stringElement.replace(/^\[|\]$|'/g, '');
+              }
+              prettyText = prettyText.replace(/\&/g, ' and ').replace(/\|/g, ' or ');
+              break;
+            case '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])':
+              prettyText = `exactly ${stringElement.replace(/{|}/g, '')} (${stringElement})`;
+              break;
+            case '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])':
+              prettyText = `between${stringElement.replace(/{|}/g, ' ').replace(',', ' and ')}(${stringElement})`;
+              break;
+            default:
+              prettyText = chipElement.pretty;
+              break;
+          }
+          chipElements.push({
+            type: chipElement.type,
+            pretty: prettyText,
+            query: stringElement
+          });
+          break;
+        }
+      }
+    }
+  
+    return chipElements;
+  }
+  //#endregion QB Functions
+
+  //#region General Functions
+  toggleClass(elements, className, action){
+    elements.forEach(element => {
+      document.querySelector(`[data-toggle-area="${element}"]`).classList[action](className);
+    });
+  }
+
+  resetQueryInputField() {
+    console.log("resetQueryInputField");
+    this.elements.queryInputField.innerHTML = '';
+    this.addPlaceholder();
+    this.updateChipList();
+    this.queryPreviewBuilder();
+  }
+
+  updateChipList() {
+    this.elements.queryChipElements = this.elements.queryInputField.querySelectorAll('.query-component');
+  }
+
+  removePlaceholder() {
+    let placeholder = this.elements.queryInputField.querySelector('#corpus-analysis-concordance-query-builder-input-field-placeholder');  
+    if (placeholder && this.elements.queryInputField !== undefined) {
+      this.elements.queryInputField.innerHTML = '';
+    }
+  }
+
+  addPlaceholder() {
+    let placeholder = Utils.HTMLToElement('<span id="corpus-analysis-concordance-query-builder-input-field-placeholder">Click on a button to add a query component</span>');
+    this.elements.queryInputField.appendChild(placeholder);
+  }
+  
+  resetMaterializeSelection(selectionElements, value = "default") {
+    selectionElements.forEach(selectionElement => {
+      if (selectionElement.querySelector(`option[value=${value}]`) !== null) {
+        selectionElement.querySelector(`option[value=${value}]`).selected = true;
+      }
+      let instance = M.FormSelect.getInstance(selectionElement);
+      instance.destroy();
+      M.FormSelect.init(selectionElement);
+    })
+  }
+
+  submitQueryChipElement(dataType = undefined, prettyQueryText = undefined, queryText = undefined, index = null, isClosingTag = false, isEditable = false) {
+    if (this.elements.editingModusOn) {
+      let editedQueryChipElement = this.elements.queryChipElements[this.elements.editedQueryChipElementIndex];
+      editedQueryChipElement.dataset.type = dataType;
+      editedQueryChipElement.dataset.query = queryText;
+      editedQueryChipElement.firstChild.textContent = prettyQueryText;
+      this.updateChipList();
+      this.queryPreviewBuilder();
+    } else {
+      this.queryChipFactory(dataType, prettyQueryText, queryText, index, isClosingTag, isEditable);
+    }
+  }
+
+  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);
+    prettyQueryText = Utils.escape(prettyQueryText);
+    let queryChipElement = Utils.HTMLToElement(
+      `
+        <span class="chip query-component" data-type="${dataType}" data-query="${queryText}" draggable="true" data-closing-tag="${isClosingTag}">
+          ${prettyQueryText}${isEditable ? '<i class="material-icons chip-action-button" data-chip-action="edit" style="padding-left:5px; font-size:18px; cursor:pointer;">edit</i>': ''}
+          ${isClosingTag ? '<i class="material-icons chip-action-button" data-chip-action="lock" style="padding-top:5px; font-size:20px; cursor:pointer;">lock_open</i>' : '<i class="material-icons close chip-action-button" data-chip-action="delete">close</i>'}
+        </span>
+      `
+    );
+    this.actionListeners(queryChipElement);
+    queryChipElement.addEventListener('dragstart', this.handleDragStart.bind(this, queryChipElement));
+    queryChipElement.addEventListener('dragend', this.handleDragEnd);
+
+    // Ensures that metadata is always at the end of the query and if an index is given, inserts the query chip at the given index and if there is a closing tag, inserts the query chip before the closing tag.
+    this.removePlaceholder();
+    let lastChild = this.elements.queryInputField.lastChild;
+    let isLastChildTextAnnotation = lastChild && lastChild.dataset.type === 'text-annotation';
+    if (!index) {
+      let closingTagElement = this.elements.queryInputField.querySelector('[data-closing-tag="true"]');
+      if (closingTagElement) {
+        index = Array.from(this.elements.queryInputField.children).indexOf(closingTagElement);
+      }
+    }
+    if (dataType !== 'text-annotation' && index) {
+      this.elements.queryInputField.insertBefore(queryChipElement, this.elements.queryChipElements[index]);
+    } else if (dataType !== 'text-annotation' && isLastChildTextAnnotation) {
+      this.elements.queryInputField.insertBefore(queryChipElement, lastChild);
+    } else {
+      this.elements.queryInputField.appendChild(queryChipElement);
+    }
+
+    this.updateChipList();
+    this.queryPreviewBuilder();
+  }
+
+  actionListeners(queryChipElement) {
+    let notQuantifiableDataTypes = ['start-sentence', 'end-sentence', 'start-entity', 'start-empty-entity', 'end-entity', 'token-incidence-modifier'];
+    queryChipElement.addEventListener('click', (event) => {
+      if (event.target.classList.contains('chip')) {
+        if (!notQuantifiableDataTypes.includes(queryChipElement.dataset.type)) {
+          this.selectChipElement(queryChipElement);
+        }
+      }
+    });
+    let chipActionButtons = queryChipElement.querySelectorAll('.chip-action-button');
+    // chipActionButtons.forEach(button => {
+    for (let button of chipActionButtons) {
+      button.addEventListener('click', (event) => {
+      if (event.target.dataset.chipAction === 'delete') {
+        this.deleteChipElement(queryChipElement);
+      } else if (event.target.dataset.chipAction === 'edit') {
+        this.editChipElement(queryChipElement);
+      } else if (event.target.dataset.chipAction === 'lock') {
+        this.lockClosingChipElement(queryChipElement);
+      }
+      });
+    // });
+    }
+  }
+
+  //hier wird this.toggleEditingAreaStructuralAttrModal('add'); aufgerufen aus StructuralAttributeBuilderFunctionsQueryBuilder.js
+  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) {
+      case 'start-entity':
+        this.elements.structuralAttrModal.open();
+        this.toggleClass(['entity-builder'], 'hide', 'remove');
+        this.toggleEditingAreaStructuralAttrModal('add');
+        let entType = queryChipElement.dataset.query.replace(/<ent_type="|">/g, '');
+        let isEnglishEntType = this.elements.englishEntTypeSelection.querySelector(`option[value=${entType}]`) !== null;
+        let selection = isEnglishEntType ? this.elements.englishEntTypeSelection : this.elements.germanEntTypeSelection;
+        this.resetMaterializeSelection([selection], entType);
+        break;
+      case 'text-annotation':
+        this.elements.structuralAttrModal.open();
+        this.toggleClass(['text-annotation-builder'], 'hide', 'remove');
+        this.toggleEditingAreaStructuralAttrModal('add');
+        let [textAnnotationSelection, textAnnotationContent] = queryChipElement.dataset.query
+          .replace(/:: ?match\.text_|"|"/g, '')
+          .split('=');
+        this.resetMaterializeSelection([this.elements.textAnnotationSelection], textAnnotationSelection);
+        this.elements.textAnnotationInput.value = textAnnotationContent;
+        break;
+      case 'token':
+        //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(queryElementsContent);
+        break;
+      default:
+        break;
+    }
+  }
+
+  //hier wird this.preparePositionalAttrModal(); und this.conditionHandler(); aufgerufen aus TokenAttributeBuilderFunctionsQueryBuilder.js
+  editTokenChipElement(queryElementsContent) {
+    this.elements.positionalAttrModal.open();
+    for (let queryElement of queryElementsContent) {
+    // 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) {
+    queryChipElement.dataset.closingTag = 'false';
+    let lockIcon = queryChipElement.querySelector('[data-chip-action="lock"]');
+    lockIcon.textContent = 'lock';
+    //TODO: Write unlock-Function?
+    lockIcon.dataset.chipAction = 'unlock';
+  }
+  
+  deleteChipElement(attr) {
+    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':
+        let nextElement = Array.from(this.elements.queryInputField.children)[elementIndex+1];
+        if (nextElement !== undefined && nextElement.dataset.type === 'token-incidence-modifier') {
+          this.deleteChipElement(nextElement);
+        }
+      default:
+        break;
+    }
+    this.elements.queryInputField.removeChild(attr);
+    if (this.elements.queryInputField.children.length === 0) {
+      this.addPlaceholder();
+    }
+    this.updateChipList();
+    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');
+    if (queryChipElement.dataset.type === 'token-incidence-modifier') {
+      queryChips = this.elements.queryInputField.querySelectorAll('.query-component[data-type="token"]');
+    }
+    setTimeout(() => {
+      let targetChipElement = Utils.HTMLToElement('<span class="chip drop-target">Drop here</span>');
+      for (let element of queryChips) {
+        if (element === this.elements.queryInputField.querySelectorAll('.query-component')[0]) {
+          let secondTargetChipClone = targetChipElement.cloneNode(true);
+          element.insertAdjacentElement('beforebegin', secondTargetChipClone);
+          this.addDragDropListeners(secondTargetChipClone, queryChipElement);
+        }
+        if (element === queryChipElement || element.nextSibling === queryChipElement) {continue;}
+
+        let targetChipClone = targetChipElement.cloneNode(true);
+        element.insertAdjacentElement('afterend', targetChipClone);
+
+        this.addDragDropListeners(targetChipClone, queryChipElement);
+      }
+    }, 0);
+  }
+
+  handleDragEnd(event) {
+    document.querySelectorAll('.drop-target').forEach(target => target.remove());
+  }
+
+  addDragDropListeners(targetChipClone, queryChipElement) {
+    targetChipClone.addEventListener('dragover', (event) => {
+      event.preventDefault();
+    });
+    targetChipClone.addEventListener('dragenter', (event) => {
+      event.preventDefault();
+      event.target.style.borderStyle = 'solid dotted';
+    });
+    targetChipClone.addEventListener('dragleave', (event) => {
+      event.preventDefault();
+      event.target.style.borderStyle = 'hidden';
+    });
+    targetChipClone.addEventListener('drop', (event) => {
+      let dropzone = event.target;
+      dropzone.parentElement.replaceChild(queryChipElement, dropzone);
+      this.updateChipList();
+      this.queryPreviewBuilder();
+    });
+  }
+
+  queryPreviewBuilder() {
+    // Builds the query preview in the form of pure CQL and displays it in the query preview field.
+    let queryPreview = document.querySelector('#corpus-analysis-concordance-query-preview');
+    let queryInputFieldContent = [];
+    this.elements.queryChipElements.forEach(element => {
+      let queryElement = element.dataset.query;
+      if (queryElement !== undefined) {
+        queryElement = Utils.escape(queryElement);
+      }
+      queryInputFieldContent.push(queryElement);
+    });
+  
+    let queryString = queryInputFieldContent.join(' ');
+    let replacements = {
+      ' +': '+',
+      ' *': '*',
+      ' ?': '?',
+      ' {': '{'
+    };
+
+    for (let key in replacements) {
+      queryString = queryString.replace(key, replacements[key]);
+    }
+    queryString += ';';
+  
+    queryPreview.innerHTML = queryString;
+    queryPreview.parentNode.classList.toggle('hide', queryString === ';');
+  }
+
+  selectChipElement(attr) {
+    document.querySelectorAll('.chip.teal').forEach(element => {
+        if (element !== attr) {
+          element.classList.remove('teal', 'lighten-2');
+          this.toggleClass(['token-incidence-modifiers'], 'disabled', 'add');
+        }
+    });
+
+    this.toggleClass(['token-incidence-modifiers'], 'disabled', 'toggle');
+    attr.classList.toggle('teal');
+    attr.classList.toggle('lighten-5');
+  }
+
+  tokenIncidenceModifierHandler(incidenceModifier, incidenceModifierPretty) {
+    // Adds a token incidence modifier to the query input field.
+    let selectedChip = this.elements.queryInputField.querySelector('.chip.teal');
+    let selectedChipIndex = Array.from(this.elements.queryInputField.children).indexOf(selectedChip);
+    this.submitQueryChipElement('token-incidence-modifier', incidenceModifierPretty, incidenceModifier, selectedChipIndex+1);
+    this.selectChipElement(selectedChip);
+  }
+
+  tokenNMSubmitHandler(modalId) {
+    // Adds a token incidence modifier (exactly n or between n and m) to the query input field.
+    let modal = document.querySelector(`#${modalId}`);
+    let input_n = modal.querySelector('.n-m-input[data-value-type="n"]').value;
+    let input_m = modal.querySelector('.n-m-input[data-value-type="m"]') || undefined;
+    input_m = input_m !== undefined ? input_m.value : '';
+    let input = `{${input_n}${input_m !== '' ? ',' : ''}${input_m}}`;
+    let pretty_input = `between ${input_n} and ${input_m} (${input})`;
+    if (input_m === '') {
+     pretty_input = `exactly ${input_n} (${input})`;
+    }
+
+    let instance = M.Modal.getInstance(modal);
+    instance.close();
+
+    this.tokenIncidenceModifierHandler(input, pretty_input);
+  }
+  //#endregion General Functions
+
+  //#region Structural Attribute Builder Functions
+  // Hier wird resetMaterializeSelection() und toggleClass() aufgerufen, das in GeneralFunctionsQueryBuilder definiert ist.
+  resetStructuralAttrModal() {
+    this.resetMaterializeSelection([this.elements.englishEntTypeSelection, this.elements.germanEntTypeSelection]);
+    this.resetMaterializeSelection([this.elements.textAnnotationSelection], 'address');
+    this.elements.textAnnotationInput.value = '';
+
+    this.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add');
+    this.toggleEditingAreaStructuralAttrModal('remove');
+    this.elements.editingModusOn = false;
+    this.elements.editedQueryChipElementIndex = undefined;
+  }
+
+  // Hier wird toggleClass() aufgerufen, das in GeneralFunctionsQueryBuilder definiert ist.
+  toggleEditingAreaStructuralAttrModal(action) {
+    // If the user edits a query chip element, the corresponding editing area is displayed and the other areas are hidden or disabled.
+    this.toggleClass(['sentence-button', 'entity-button', 'text-annotation-button', 'any-type-entity-button'], 'disabled', action);
+  }
+
+  // Hier wird toggleClass() und submitQueryChipElement() aufgerufen, das in GeneralFunctionsQueryBuilder definiert ist.
+  actionButtonInStrucAttrModalHandler(action) {
+    switch (action) {
+      case 'sentence':
+        this.submitQueryChipElement('start-sentence', 'Sentence Start', '<s>');
+        this.submitQueryChipElement('end-sentence', 'Sentence End', '</s>', null, true);
+        this.elements.structuralAttrModal.close();
+        break;
+      case 'entity':
+        this.toggleClass(['entity-builder'], 'hide', 'toggle');
+        this.toggleClass(['text-annotation-builder'], 'hide', 'add');
+        break;
+      case 'meta-data':
+        this.toggleClass(['text-annotation-builder'], 'hide', 'toggle');
+        this.toggleClass(['entity-builder'], 'hide', 'add');
+        break;
+      default:
+        break;
+    }
+  }
+
+  // Hier wird submitQueryChipElement() aufgerufen, das in GeneralFunctionsQueryBuilder definiert ist.
+  textAnnotationSubmitHandler() {
+    let noValueMetadataMessage = document.querySelector('#corpus-analysis-concordance-no-value-metadata-message');
+    let textAnnotationSubmit = document.querySelector('#corpus-analysis-concordance-text-annotation-submit');
+    let textAnnotationInput = document.querySelector('#corpus-analysis-concordance-text-annotation-input');
+    let textAnnotationOptions = document.querySelector('#corpus-analysis-concordance-text-annotation-options');
+
+    if (textAnnotationInput.value === '') {
+      textAnnotationSubmit.classList.add('red');
+      noValueMetadataMessage.classList.remove('hide');
+      setTimeout(() => {
+        textAnnotationSubmit.classList.remove('red');
+      }, 500);
+      setTimeout(() => {
+        noValueMetadataMessage.classList.add('hide');
+      }, 3000);
+    } else {
+      let queryText = `:: match.text_${textAnnotationOptions.value}="${textAnnotationInput.value}"`;
+      this.submitQueryChipElement('text-annotation', `${textAnnotationOptions.value}=${textAnnotationInput.value}`, queryText, null, false, true);
+      this.elements.structuralAttrModal.close();
+    }
+  }
+  //#endregion Structural Attribute Builder Functions
+
+  //#region Token Attribute Builder Functions
+  resetPositionalAttrModal() {
+    let originalSelectionList = 
+      `
+        <option value="word" selected>word</option>
+        <option value="lemma" >lemma</option>
+        <option value="english-pos">english pos</option>
+        <option value="german-pos">german pos</option>
+        <option value="simple_pos">simple_pos</option>
+        <option value="empty-token">empty token</option>
+      `;
+    this.elements.positionalAttrSelection.innerHTML = originalSelectionList;
+    this.elements.tokenQuery.innerHTML = '';
+    this.elements.tokenBuilderContent.innerHTML = '';
+    this.toggleClass(['input-field-options'], 'hide', 'remove');
+    this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
+    this.resetMaterializeSelection([this.elements.positionalAttrSelection], "word");
+    this.elements.ignoreCaseCheckbox.checked = false;
+    this.elements.editingModusOn = false;
+    this.elements.editedQueryChipElementIndex = undefined;
+  }
+  
+  preparePositionalAttrModal() {
+    let selection = this.elements.positionalAttrSelection.value;
+    if (selection !== 'empty-token') {
+      let selectionTemplate = document.querySelector(`.token-builder-section[data-token-builder-section="${selection}"]`);
+      let selectionTemplateClone = selectionTemplate.content.cloneNode(true);
+    
+      this.elements.tokenBuilderContent.innerHTML = '';
+      this.elements.tokenBuilderContent.appendChild(selectionTemplateClone);
+      if (this.elements.tokenBuilderContent.querySelector('select') !== null) {
+        let selectElement = this.elements.tokenBuilderContent.querySelector('select');
+        M.FormSelect.init(selectElement);
+        selectElement.addEventListener('change', () => {this.optionToggleHandler();});
+      } else {
+        this.elements.tokenBuilderContent.querySelector('input').addEventListener('input', () => {this.optionToggleHandler();});
+      }
+    }
+    this.optionToggleHandler();
+
+    if (selection === 'word' || selection === 'lemma') {
+      this.toggleClass(['input-field-options'], 'hide', 'remove');
+    } else if (selection === 'empty-token'){
+      this.addTokenToQuery();
+    } else {
+      this.toggleClass(['input-field-options'], 'hide', 'add');
+    }
+  }
+
+  tokenInputCheck(elem) {
+    return elem.querySelector('select') !== null ? elem.querySelector('select') : elem.querySelector('input');
+  }
+
+  optionToggleHandler() {
+    let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
+    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');
+      this.toggleClass(['or'], 'disabled', 'remove');
+    } else {
+      this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'remove');
+    }
+  }
+
+  disableTokenSubmit() {
+    this.elements.tokenSubmitButton.classList.add('red');
+    this.elements.noValueMessage.classList.remove('hide');
+    setTimeout(() => {
+      this.elements.tokenSubmitButton.classList.remove('red');
+    }, 500);
+    setTimeout(() => {
+      this.elements.noValueMessage.classList.add('hide');
+    }, 3000);
+  }
+
+  addTokenToQuery() {
+    let tokenQueryPrettyText = '';
+    let tokenQueryCQLText = '';
+    let input;
+    let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value);
+    
+    // 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 => {
+      let ignoreCaseCheckbox = row.querySelector('input[type="checkbox"]');
+      let c = ignoreCaseCheckbox !== null && ignoreCaseCheckbox.checked ? ' %c' : '';
+      let tokenQueryRowInput = this.tokenInputCheck(row.querySelector('.token-query-template-content'));
+      let tokenQueryKindOfToken = this.kindOfTokenCheck(tokenQueryRowInput.closest('.input-field').dataset.kindOfToken);
+      let tokenConditionPrettyText = row.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
+      let tokenConditionCQLText = row.querySelector('[data-condition-cql-text]').dataset.conditionCqlText;
+      tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `;
+      tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`;
+    });
+    if (kindOfToken === 'empty-token') {
+      tokenQueryPrettyText += 'empty token';
+    } else {
+      let c = this.elements.ignoreCaseCheckbox.checked ? ' %c' : '';
+      input = this.tokenInputCheck(this.elements.tokenBuilderContent);
+      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 (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') {
+      this.disableTokenSubmit();
+    } else {
+      tokenQueryCQLText = `[${tokenQueryCQLText}]`;
+      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);
+    switch (elem) {
+      case 'option-group':
+        input.value  += '(option1|option2)';
+        let firstIndex = input.value.indexOf('option1');
+        let lastIndex = firstIndex + 'option1'.length;
+        input.focus();
+        input.setSelectionRange(firstIndex, lastIndex);
+        break;
+      case 'wildcard-char':
+        input.value += '.';
+        break;
+      case 'and':
+        this.conditionHandler('and');
+        break;
+      case 'or':
+        this.conditionHandler('or');
+        break;
+      default:
+        break;
+    }
+    this.optionToggleHandler();
+  }
+
+  characterIncidenceModifierHandler(elem) {
+    let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
+    input.value += elem.dataset.token;
+  }
+
+  characterNMSubmitHandler(modalId) {
+    let modal = document.querySelector(`#${modalId}`);
+    let input_n = modal.querySelector('.n-m-input[data-value-type="n"]').value;
+    let input_m = modal.querySelector('.n-m-input[data-value-type="m"]') || undefined;
+    input_m = input_m !== undefined ? ',' + input_m.value : '';
+    let input = `${input_n}${input_m}`;
+
+    let instance = M.Modal.getInstance(modal);
+    instance.close();
+    let tokenInput = this.tokenInputCheck(this.elements.tokenBuilderContent);
+    tokenInput.value += '{' + input + '}';
+  }
+
+  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}"])`);
+    let deleteButton = tokenQueryTemplateClone.querySelector(`[data-token-query-content-action="delete"]`);
+    deleteButton.addEventListener('click', (event) => {
+      this.deleteTokenQueryRow(event.target);
+    });
+    notSelectedButton.parentNode.removeChild(notSelectedButton);
+    this.elements.tokenQuery.appendChild(tokenQueryTemplateClone);
+
+    // Deleting the options which do not make sense in the context of the condition like "word" AND "word". Also sets selection default.
+    let selectionDefault = "word";
+    let optionDeleteList = ['empty-token'];
+    if (conditionText === 'and') {
+      switch (this.elements.positionalAttrSelection.value) {
+        case 'english-pos' || 'german-pos':
+          optionDeleteList.push('english-pos', 'german-pos');
+          break;
+        default:
+          optionDeleteList.push(this.elements.positionalAttrSelection.value);
+          break;
+      }
+    } else {
+      let originalSelectionList = 
+      `
+        <option value="word" selected>word</option>
+        <option value="lemma" >lemma</option>
+        <option value="english-pos">english pos</option>
+        <option value="german-pos">german pos</option>
+        <option value="simple_pos">simple_pos</option>
+      `;
+      this.elements.positionalAttrSelection.innerHTML = originalSelectionList;
+      M.FormSelect.init(this.elements.positionalAttrSelection);
+    }
+    let lastTokenQueryRow = this.elements.tokenQuery.lastElementChild;
+    if(lastTokenQueryRow.querySelector('[data-kind-of-token="word"]') || lastTokenQueryRow.querySelector('[data-kind-of-token="lemma"]')) {
+      this.appendIgnoreCaseCheckbox(lastTokenQueryRow.querySelector('.token-query-template-content'), this.elements.ignoreCaseCheckbox.checked);
+    }
+    this.elements.ignoreCaseCheckbox.checked = false;
+    this.setTokenSelection(selectionDefault, optionDeleteList);
+  }
+
+  deleteTokenQueryRow(deleteButton) {
+    let deletedRow = deleteButton.closest('.row');
+    let condition = deletedRow.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
+    if (condition === 'and') {
+      let kindOfToken = deletedRow.querySelector('[data-kind-of-token]').dataset.kindOfToken;
+      switch (kindOfToken) {
+        case 'english-pos' || 'german-pos':
+          this.createOptionElementForPosAttrSelection('english-pos');
+          this.createOptionElementForPosAttrSelection('german-pos');
+          break;
+        default:
+          this.createOptionElementForPosAttrSelection(kindOfToken);
+          break;
+      }
+      M.FormSelect.init(this.elements.positionalAttrSelection);
+    }
+    deletedRow.remove();
+  }
+
+  createOptionElementForPosAttrSelection(kindOfToken) {
+    let option = document.createElement('option');
+    option.value = kindOfToken;
+    option.text = kindOfToken;
+    this.elements.positionalAttrSelection.appendChild(option);
+  }
+
+  appendIgnoreCaseCheckbox(parentElement, checked = false) {
+    let ignoreCaseCheckboxClone = document.querySelector('#ignore-case-checkbox-template').content.cloneNode(true);
+    parentElement.appendChild(ignoreCaseCheckboxClone);
+    M.Tooltip.init(parentElement.querySelectorAll('.tooltipped'));
+    if (checked) {
+      parentElement.querySelector('input[type="checkbox"]').checked = true;
+    }
+  }
+
+  setTokenSelection(selection, optionDeleteList) {
+    optionDeleteList.forEach(option => {
+      if (this.elements.positionalAttrSelection.querySelector(`option[value=${option}]`) !== null) {
+        this.elements.positionalAttrSelection.querySelector(`option[value=${option}]`).remove();
+      }
+    });
+
+    this.resetMaterializeSelection([this.elements.positionalAttrSelection], selection);
+    this.preparePositionalAttrModal();
+  }
+  //#endregion Token Attribute Builder Functions
+}
diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2
index a1897b56..53b2b763 100644
--- a/app/templates/_scripts.html.j2
+++ b/app/templates/_scripts.html.j2
@@ -103,13 +103,10 @@
 {%- assets
   filters='rjsmin',
   output='gen/CorpusAnalysis.%(version)s.js',
-  'js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js',
-  'js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js',
-  'js/CorpusAnalysis/QueryBuilder/StructuralAttributeBuilderFunctionsQueryBuilder.js',
-  'js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js',
+  'js/CorpusAnalysis/query-builder/index.js',
+  'js/CorpusAnalysis/query-builder/element-references.js',
   'js/CorpusAnalysis/CorpusAnalysisApp.js',
   'js/CorpusAnalysis/CorpusAnalysisConcordance.js',
-  'js/CorpusAnalysis/QueryBuilder.js',
   'js/CorpusAnalysis/CorpusAnalysisReader.js',
   'js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js'
 %}
diff --git a/app/templates/corpora/_analysis/concordance.html.j2 b/app/templates/corpora/_analysis/concordance.html.j2
index b160c2fb..f50578bd 100644
--- a/app/templates/corpora/_analysis/concordance.html.j2
+++ b/app/templates/corpora/_analysis/concordance.html.j2
@@ -130,21 +130,5 @@
 <script>
   const corpusAnalysisConcordance = new CorpusAnalysisConcordance(corpusAnalysisApp);
   const concordanceQueryBuilder = new ConcordanceQueryBuilder();
-
-  let queryBuilderDisplay = document.getElementById("corpus-analysis-concordance-query-builder-display");
-  let expertModeDisplay = document.getElementById("corpus-analysis-concordance-expert-mode-display");
-  let expertModeSwitch = document.getElementById("corpus-analysis-concordance-expert-mode-switch");
-
-  expertModeSwitch.addEventListener("change", function() {
-    if (this.checked) {
-      queryBuilderDisplay.classList.add("hide");
-      expertModeDisplay.classList.remove("hide");
-      concordanceQueryBuilder.generalFunctions.switchToExpertModeParser();
-    } else {
-      queryBuilderDisplay.classList.remove("hide");
-      expertModeDisplay.classList.add("hide");
-      concordanceQueryBuilder.generalFunctions.switchToQueryBuilderParser();
-    }
-  });
 </script>
 {% endmacro %}
-- 
GitLab