Skip to content
Snippets Groups Projects
GeneralFunctionsQueryBuilder.js 14.17 KiB
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.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 => {
      selectionElement.querySelector(`option[value=${value}]`).selected = true;
      let instance = M.FormSelect.getInstance(selectionElement);
      instance.destroy();
      M.FormSelect.init(selectionElement);
    })
  }


  queryChipFactory(dataType, prettyQueryText, queryText, index = null, isClosingTag = 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}
          ${isClosingTag ? '<i class="material-icons" style="padding-top:5px; font-size:20px; cursor:pointer;">lock_open</i>' : '<i class="material-icons close">close</i>'}
        </span>
      `
    );
    this.actionListeners(queryChipElement, isClosingTag);
    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, isClosingTag) {
    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);
        }
      }
    });
    queryChipElement.querySelector('i').addEventListener('click', () => {
      if (isClosingTag) {
        this.lockClosingChipElement(queryChipElement);
      } else {
        this.deleteChipElement(queryChipElement);
      }
    });
  }

  lockClosingChipElement(queryChipElement) {
    let chipIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
    this.queryChipFactory(queryChipElement.dataset.type, queryChipElement.firstChild.textContent, queryChipElement.dataset.query, chipIndex+1);
    this.deleteChipElement(queryChipElement);
    this.updateChipList();
  }

  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 === ';');
  }

  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';
    }
    this.elements.queryInputField.removeChild(attr);
    if (this.elements.queryInputField.children.length === 0) {
      this.addPlaceholder();
    }
    this.updateChipList();
    this.queryPreviewBuilder();
  }

  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.queryChipFactory('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 !== "") {
      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.queryChipFactory(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 = [];
  
    const regex = new RegExp(`<s>|<\/s>|<ent>|<ent_type="([A-Z]+)">|<\\\/ent(_type)?>|\\[(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?((\\&|\\|) ?(word|lemma|pos|simple_pos)=(("[^"]+")|(\\u0027[^\\u0027]+\\u0027)) ?(%c)? ?)*\\]|:: ?match\\.text_[A-Za-z]+="[^"]+"|(?<!\\[) ?(\\+|\\?|\\*|{[0-9]+(,[0-9]+)?}) ?(?![^\\]]\\])`, '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)) {
          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;
  }

}