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/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/QueryBuilder/GeneralFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js similarity index 59% rename from app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js rename to app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js index 7a250199..f1b04e74 100644 --- a/app/static/js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js +++ b/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js @@ -1,4 +1,4 @@ -class GeneralFunctionsQueryBuilder { +class GeneralQueryBuilderFunctions { constructor(elements) { this.elements = elements; } @@ -82,9 +82,10 @@ class GeneralFunctionsQueryBuilder { 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); + 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); } @@ -103,7 +104,8 @@ class GeneralFunctionsQueryBuilder { } }); let chipActionButtons = queryChipElement.querySelectorAll('.chip-action-button'); - chipActionButtons.forEach(button => { + // chipActionButtons.forEach(button => { + for (let button of chipActionButtons) { button.addEventListener('click', (event) => { if (event.target.dataset.chipAction === 'delete') { this.deleteChipElement(queryChipElement); @@ -113,62 +115,22 @@ class GeneralFunctionsQueryBuilder { 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(//g, ''); - let isEnglishEntType = this.elements.englishEntTypeSelection.querySelector(`option[value=${entType}]`) !== null; - let selection = isEnglishEntType ? this.elements.englishEntTypeSelection : this.elements.germanEntTypeSelection; - this.resetMaterializeSelection([selection], entType); + this.editStartEntityChipElement(queryChipElement); 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; + this.editTextAnnotationChipElement(queryChipElement); 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}); - } + let queryElementsContent = this.prepareQueryElementsContent(queryChipElement); this.editTokenChipElement(queryElementsContent); break; default: @@ -176,6 +138,59 @@ class GeneralFunctionsQueryBuilder { } } + editStartEntityChipElement(queryChipElement) { + this.elements.structuralAttrModal.open(); + this.toggleClass(['entity-builder'], 'hide', 'remove'); + this.toggleEditingAreaStructuralAttrModal('add'); + let entType = queryChipElement.dataset.query.replace(//g, ''); + let isEnglishEntType = this.elements.englishEntTypeSelection.querySelector(`option[value=${entType}]`) !== null; + let selection = isEnglishEntType ? this.elements.englishEntTypeSelection : this.elements.germanEntTypeSelection; + this.resetMaterializeSelection([selection], entType); + } + + editTextAnnotationChipElement(queryChipElement) { + this.elements.structuralAttrModal.open(); + this.toggleClass(['text-annotation-builder'], 'hide', 'remove'); + this.structuralAttributeBuilderFunctions.toggleEditingAreaStructuralAttrModal('add'); + let [textAnnotationSelection, textAnnotationContent] = queryChipElement.dataset.query + .replace(/:: ?match\.text_|"|"/g, '') + .split('='); + this.resetMaterializeSelection([this.elements.textAnnotationSelection], textAnnotationSelection); + this.elements.textAnnotationInput.value = textAnnotationContent; + } + + prepareQueryElementsContent(queryChipElement) { + //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}); + } + return queryElementsContent; + } + editTokenChipElement(queryElementsContent) { this.elements.positionalAttrModal.open(); queryElementsContent.forEach((queryElement) => { @@ -207,7 +222,6 @@ class GeneralFunctionsQueryBuilder { } }); - } lockClosingChipElement(queryChipElement) { @@ -216,11 +230,6 @@ class GeneralFunctionsQueryBuilder { 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) { @@ -233,9 +242,8 @@ class GeneralFunctionsQueryBuilder { 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') { + if (nextElement !== undefined && nextElement.dataset.type === 'token-incidence-modifier') { this.deleteChipElement(nextElement); } default: @@ -264,16 +272,22 @@ class GeneralFunctionsQueryBuilder { 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 = nopaque.Utils.HTMLToElement('Drop here'); 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); + 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); @@ -371,135 +385,111 @@ class GeneralFunctionsQueryBuilder { this.tokenIncidenceModifierHandler(input, pretty_input); } - switchToExpertModeParser() { - let expertModeInputField = document.querySelector('#corpus-analysis-concordance-form-query'); - expertModeInputField.value = ''; - let queryBuilderInputFieldValue = nopaque.Utils.unescape(document.querySelector('#corpus-analysis-concordance-query-preview').innerHTML.trim()); - if (queryBuilderInputFieldValue !== "" && queryBuilderInputFieldValue !== ";") { - expertModeInputField.value = queryBuilderInputFieldValue; + //#region Functions from other classes + + //TODO: Move these functions back to their og classes and make it work. + + 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); + } + + 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'); } } - 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']); + 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'); } } - parseTextToChip(query) { - const parsingElementDict = { - '': { - pretty: 'Sentence Start', - type: 'start-sentence' - }, - '<\/s>': { - pretty: 'Sentence End', - type: 'end-sentence' - }, - '': { - pretty: 'Entity Start', - type: 'start-empty-entity' - }, - '': { - 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' - }, - '(? { + 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}`; } - - 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 '': - prettyText = `Entity Type=${stringElement.replace(//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 '(? { + this.elements.tokenSubmitButton.classList.remove('red'); + }, 500); + setTimeout(() => { + this.elements.noValueMessage.classList.add('hide'); + }, 3000); + } + + //#endregion Functions from other classes } 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..5050c09f --- /dev/null +++ b/app/static/js/CorpusAnalysis/query-builder/index.js @@ -0,0 +1,192 @@ +class ConcordanceQueryBuilder { + + constructor() { + this.elements = new ElementReferencesQueryBuilder(); + this.generalFunctions = new GeneralQueryBuilderFunctions(this.elements); + this.tokenAttributeBuilderFunctions = new TokenAttributeBuilderFunctions(this.elements); + this.structuralAttributeBuilderFunctions = new StructuralAttributeBuilderFunctions(this.elements); + + this.incidenceModifierEventListeners(); + this.nAndMInputSubmitEventListeners(); + + let queryBuilderDisplay = document.querySelector("#corpus-analysis-concordance-query-builder-display"); + let expertModeDisplay = document.querySelector("#corpus-analysis-concordance-expert-mode-display"); + let expertModeSwitch = document.querySelector("#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(); + } + }); + + } + + incidenceModifierEventListeners() { + // 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)); + } + }); + } + + nAndMInputSubmitEventListeners() { + // 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)); + } + }); + } + + 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.generalFunctions.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.generalFunctions.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable); + } + } + + parseTextToChip(query) { + const parsingElementDict = { + '': { + pretty: 'Sentence Start', + type: 'start-sentence' + }, + '<\/s>': { + pretty: 'Sentence End', + type: 'end-sentence' + }, + '': { + pretty: 'Entity Start', + type: 'start-empty-entity' + }, + '': { + 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' + }, + '(? `(${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 '': + prettyText = `Entity Type=${stringElement.replace(//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 '(? this.textAnnotationSubmitHandler()); + + this.elements.structuralAttrModal = M.Modal.init( + document.querySelector('#corpus-analysis-concordance-structural-attr-modal'), + { + onCloseStart: () => { + this.resetStructuralAttrModal(); + } + } + ); + } + + structuralAttrModalEventlisteners() { document.querySelectorAll('[data-structural-attr-modal-action-button]').forEach(button => { button.addEventListener('click', () => { this.actionButtonInStrucAttrModalHandler(button.dataset.structuralAttrModalActionButton); @@ -35,16 +49,11 @@ class StructuralAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQu this.elements.textAnnotationInput.value = ''; this.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add'); - this.toggleEditingAreaStructureAttrModal('remove'); + this.toggleEditingAreaStructuralAttrModal('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': diff --git a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js b/app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js similarity index 59% rename from app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js rename to app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js index bebb5d83..d3dbe55c 100644 --- a/app/static/js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js +++ b/app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js @@ -1,7 +1,6 @@ -class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBuilder { +class TokenAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { constructor(elements) { super(elements); - this.elements = elements; this.elements.positionalAttrSelection.addEventListener('change', () => { this.preparePositionalAttrModal(); @@ -13,6 +12,18 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu }); this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();}); + + this.elements.positionalAttrModal = M.Modal.init( + document.querySelector('#corpus-analysis-concordance-positional-attr-modal'), + { + onOpenStart: () => { + this.preparePositionalAttrModal(); + }, + onCloseStart: () => { + this.resetPositionalAttrModal(); + } + } + ); } resetPositionalAttrModal() { @@ -35,100 +46,6 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu 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); @@ -173,7 +90,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu tokenInput.value += '{' + input + '}'; } - conditionHandler(conditionText, editMode = false) { + conditionHandler(conditionText) { 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}"])`); @@ -261,4 +178,5 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu this.resetMaterializeSelection([this.elements.positionalAttrSelection], selection); this.preparePositionalAttrModal(); } + } diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index b4f362a4..152409d3 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -95,13 +95,13 @@ {%- 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/query-builder/general-query-builder-functions.js', + 'js/CorpusAnalysis/query-builder/structural-attribute-builder-functions.js', + 'js/CorpusAnalysis/query-builder/token-attribute-builder-functions.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 @@ {% endmacro %}