From 985e9b406fce051f63a7362a665e9991dec93375 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick Date: Thu, 26 Oct 2023 15:18:03 +0200 Subject: [PATCH] Editing nested token queries and bug fixes --- .../ElementReferencesQueryBuilder.js | 3 - .../GeneralFunctionsQueryBuilder.js | 134 +++++++++++++----- ...enAttributeBuilderFunctionsQueryBuilder.js | 15 +- .../query_builder/_query_builder.html.j2 | 12 +- 4 files changed, 110 insertions(+), 54 deletions(-) diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js index 157ca2a7..518975f0 100644 --- a/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js +++ b/app/static/js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js @@ -8,9 +8,6 @@ class ElementReferencesQueryBuilder { // Structural Attribute Builder Elements this.structuralAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-structural-attr-modal')); - this.sentenceElement = document.querySelector('[data-structural-attr-modal-action-button="sentence"]'); - this.entityElement = document.querySelector('[data-structural-attr-modal-action-button="entity"]'); - this.textAnnotationElement = document.querySelector('[data-structural-attr-modal-action-button="text-annotation"]'); this.englishEntTypeSelection = document.querySelector('#corpus-analysis-concordance-english-ent-type-selection'); this.germanEntTypeSelection = document.querySelector('#corpus-analysis-concordance-german-ent-type-selection'); this.textAnnotationSelection = document.querySelector('#corpus-analysis-concordance-text-annotation-options'); diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js index f6c0004e..481e9b6d 100644 --- a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js +++ b/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js @@ -56,7 +56,6 @@ class GeneralFunctionsQueryBuilder { } } - queryChipFactory(dataType, prettyQueryText, queryText, index = null, isClosingTag = false, isEditable = false) { // Creates a new query chip element, adds Eventlisteners for selection, deletion and drag and drop and appends it to the query input field. queryText = Utils.escape(queryText); @@ -118,6 +117,7 @@ class GeneralFunctionsQueryBuilder { } editChipElement(queryChipElement) { + //TODO: Split this function into smaller functionss this.elements.editingModusOn = true; this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement); switch (queryChipElement.dataset.type) { @@ -141,54 +141,105 @@ class GeneralFunctionsQueryBuilder { this.elements.textAnnotationInput.value = textAnnotationContent; break; case 'token': - let [tokenAttr, tokenValue] = queryChipElement.dataset.query.replace(/\[|\]|"/g, '').split('='); - if (tokenAttr === 'pos') { - tokenAttr = this.elements.englishPosSelection.querySelector(`option[value=${tokenValue}]`) ? 'english-pos' : 'german-pos'; + //This regex searches for word or lemma or pos or simple_pos="any string within single or double quotes" followed by one or no ignore case markers, followed by one or no condition characters. + let regex = new RegExp('(word|lemma|pos|simple_pos)=(("[^"]+")|(\\\\u0027[^\\\\u0027]+\\\\u0027)) ?(%c)? ?(\\&|\\|)?', 'gm'); + let m; + let queryElementsContent = []; + while ((m = regex.exec(queryChipElement.dataset.query)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + let tokenAttr = m[1]; + // Passes english-pos by default so that the template is added. In editTokenChipElement it is then checked whether it is english-pos or german-pos. + if (tokenAttr === 'pos') { + tokenAttr = 'english-pos'; + } + let tokenValue = m[2].replace(/"|'/g, ''); + let ignoreCase = false; + let condition = undefined; + m.forEach((match) => { + if (match === "%c") { + ignoreCase = true; + } else if (match === "&") { + condition = "and"; + } else if (match === "|") { + condition = "or"; + } + }); + queryElementsContent.push({tokenAttr: tokenAttr, tokenValue: tokenValue, ignoreCase: ignoreCase, condition: condition}); } - this.editTokenChipElement(tokenAttr, tokenValue); + this.editTokenChipElement(queryElementsContent); break; default: break; } } - editTokenChipElement(tokenAttr, tokenValue) { - this.resetMaterializeSelection([this.elements.positionalAttrSelection], tokenAttr); + editTokenChipElement(queryElementsContent) { this.elements.positionalAttrModal.open(); - switch (tokenAttr) { - case 'word': - this.elements.wordInput.value = tokenValue; - break; - case 'lemma': - this.elements.lemmaInput.value = tokenValue; - break; - case 'english-pos': - this.resetMaterializeSelection([this.elements.englishPosSelection], tokenValue); - break; - case 'german-pos': - this.resetMaterializeSelection([this.elements.germanPosSelection], tokenValue); - break; - case 'simple-pos': - this.resetMaterializeSelection([this.elements.simplePosSelection], tokenValue); - break; - default: - break; - } + queryElementsContent.forEach((queryElement) => { + this.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr); + this.preparePositionalAttrModal(); + switch (queryElement.tokenAttr) { + case 'word': + case 'lemma': + this.elements.tokenBuilderContent.querySelector('input').value = queryElement.tokenValue; + break; + case 'english-pos': + // English-pos is selected by default. Then it is checked whether the passed token value occurs in the english-pos selection. If not, the selection is reseted and changed to german-pos. + let selection = this.elements.tokenBuilderContent.querySelector('select'); + queryElement.tokenAttr = selection.querySelector(`option[value=${queryElement.tokenValue}]`) ? 'english-pos' : 'german-pos'; + this.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr); + this.preparePositionalAttrModal(); + this.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue); + break; + case 'simple_pos': + this.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue); + default: + break; + } + if (queryElement.ignoreCase) { + this.elements.ignoreCaseCheckbox.checked = true; + } + if (queryElement.condition !== undefined) { + this.conditionHandler(queryElement.condition, true); + } + + }); + } lockClosingChipElement(queryChipElement) { - let chipIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement); - this.submitQueryChipElement(queryChipElement.dataset.type, queryChipElement.firstChild.textContent, queryChipElement.dataset.query, chipIndex+1); - this.deleteChipElement(queryChipElement); - this.updateChipList(); - } + queryChipElement.dataset.closingTag = 'false'; + let lockIcon = queryChipElement.querySelector('[data-chip-action="lock"]'); + lockIcon.textContent = 'lock'; + //TODO: Write unlock-Function? + lockIcon.dataset.chipAction = 'unlock'; + // let chipIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement); + // this.submitQueryChipElement(queryChipElement.dataset.type, queryChipElement.firstChild.textContent, queryChipElement.dataset.query, chipIndex+1); + // this.deleteChipElement(queryChipElement); + // this.updateChipList(); + } deleteChipElement(attr) { - if (attr.dataset.type === "start-sentence") { - this.elements.sentenceElement.innerHTML = 'Sentence'; - } else if (attr.dataset.type === "start-entity" || attr.dataset.type === "start-empty-entity") { - this.elements.entityElement.innerHTML = 'Entity'; + let elementIndex = Array.from(this.elements.queryInputField.children).indexOf(attr); + switch (attr.dataset.type) { + case 'start-sentence': + this.deletingClosingTagHandler(elementIndex, 'end-sentence'); + break; + case 'start-entity': + this.deletingClosingTagHandler(elementIndex, 'end-entity'); + break; + case 'token': + console.log(Array.from(this.elements.queryInputField.children)[elementIndex+1]); + let nextElement = Array.from(this.elements.queryInputField.children)[elementIndex+1]; + if (nextElement.dataset.type === 'token-incidence-modifier') { + this.deleteChipElement(nextElement); + } + default: + break; } this.elements.queryInputField.removeChild(attr); if (this.elements.queryInputField.children.length === 0) { @@ -198,6 +249,18 @@ class GeneralFunctionsQueryBuilder { this.queryPreviewBuilder(); } + deletingClosingTagHandler(elementIndex, closingTagType) { + let closingTags = this.elements.queryInputField.querySelectorAll(`[data-type="${closingTagType}"]`); + for (let i = 0; i < closingTags.length; i++) { + let closingTag = closingTags[i]; + + if (Array.from(this.elements.queryInputField.children).indexOf(closingTag) > elementIndex) { + this.deleteChipElement(closingTag); + break; + } + } + } + handleDragStart(queryChipElement, event) { // is called when a query chip is dragged. It creates a dropzone (in form of a chip) for the dragged chip and adds it to the query input field. let queryChips = this.elements.queryInputField.querySelectorAll('.query-component'); @@ -325,7 +388,6 @@ class GeneralFunctionsQueryBuilder { this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query']); } } - parseTextToChip(query) { const parsingElementDict = { diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js index 74d604eb..bebb5d83 100644 --- a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js +++ b/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js @@ -69,7 +69,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu optionToggleHandler() { let input = this.tokenInputCheck(this.elements.tokenBuilderContent); - if ((input.value === '' || input.value === 'default') && this.elements.editingModusOn === false) { + if (input.value === '' && this.elements.editingModusOn === false) { this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add'); } else if (this.elements.positionalAttrSelection.querySelectorAll('option').length === 1) { this.toggleClass(['and'], 'disabled', 'add'); @@ -95,7 +95,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu let input; let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value); - // Takes all rows of the token query (if there is a query concatenation). + // Takes all rows of the token query (if there is a query concatenation). // Adds their contents to tokenQueryPrettyText and tokenQueryCQLText, which will later be expanded with the current input field. let tokenQueryRows = this.elements.tokenQuery.querySelectorAll('.row'); tokenQueryRows.forEach(row => { @@ -108,7 +108,6 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `; tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`; }); - if (kindOfToken === 'empty-token') { tokenQueryPrettyText += 'empty token'; } else { @@ -117,21 +116,19 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu tokenQueryPrettyText += `${kindOfToken}=${input.value}${c}`; tokenQueryCQLText += `${kindOfToken}="${input.value}"${c}`; } - // isTokenQueryInvalid looks if a valid value is passed. If the input fields/dropdowns are empty (isTokenQueryInvalid === true), no token is added. - if ((input.value === '' || input.value === 'default') && this.elements.positionalAttrSelection.value !== 'empty-token') { + if (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') { this.disableTokenSubmit(); } else { tokenQueryCQLText = `[${tokenQueryCQLText}]`; - this.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, true); + this.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, kindOfToken === 'empty-token' ? false : true); this.elements.positionalAttrModal.close(); } } kindOfTokenCheck(kindOfToken) { return kindOfToken === 'english-pos' || kindOfToken === 'german-pos' ? 'pos' : kindOfToken; - } - + } actionButtonInOptionSectionHandler(elem) { let input = this.tokenInputCheck(this.elements.tokenBuilderContent); @@ -176,7 +173,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu tokenInput.value += '{' + input + '}'; } - conditionHandler(conditionText) { + conditionHandler(conditionText, editMode = false) { let tokenQueryTemplateClone = this.elements.tokenQueryTemplate.content.cloneNode(true); tokenQueryTemplateClone.querySelector('.token-query-template-content').appendChild(this.elements.tokenBuilderContent.firstElementChild); let notSelectedButton = tokenQueryTemplateClone.querySelector(`[data-condition-pretty-text]:not([data-condition-pretty-text="${conditionText}"])`); diff --git a/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 b/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 index ff8e9151..3a3bf2fe 100644 --- a/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 +++ b/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 @@ -173,8 +173,8 @@
- or - and + or + and delete
@@ -208,7 +208,7 @@