From 0565f309f891e7da7467f5b752fe671b7e503294 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick Date: Wed, 8 Nov 2023 15:46:53 +0100 Subject: [PATCH] Split QB back to mult. classes, as far as possible --- .../general-query-builder-functions.js | 495 +++++++++++ .../js/CorpusAnalysis/query-builder/index.js | 819 +----------------- .../structural-attribute-builder-functions.js | 98 +++ .../token-attribute-builder-functions.js | 182 ++++ app/templates/_scripts.html.j2 | 3 + 5 files changed, 815 insertions(+), 782 deletions(-) create mode 100644 app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js create mode 100644 app/static/js/CorpusAnalysis/query-builder/structural-attribute-builder-functions.js create mode 100644 app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js diff --git a/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js b/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js new file mode 100644 index 00000000..d58704c8 --- /dev/null +++ b/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js @@ -0,0 +1,495 @@ +class GeneralQueryBuilderFunctions { + 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('Click on a button to add a query component'); + 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( + ` + + ${prettyQueryText}${isEditable ? 'edit': ''} + ${isClosingTag ? 'lock_open' : 'close'} + + ` + ); + 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); + } + }); + // }); + } + } + + editChipElement(queryChipElement) { + this.elements.editingModusOn = true; + this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement); + switch (queryChipElement.dataset.type) { + case 'start-entity': + this.editStartEntityChipElement(queryChipElement); + break; + case 'text-annotation': + this.editTextAnnotationChipElement(queryChipElement); + break; + case 'token': + let queryElementsContent = this.prepareQueryElementsContent(queryChipElement); + this.editTokenChipElement(queryElementsContent); + break; + default: + break; + } + } + + 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) => { + 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('Drop here'); + 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); + } + + //#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'); + } + } + + 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'); + } + } + + 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; + } + + 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); + } + + //#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 index 82a01a7c..5050c09f 100644 --- a/app/static/js/CorpusAnalysis/query-builder/index.js +++ b/app/static/js/CorpusAnalysis/query-builder/index.js @@ -1,55 +1,18 @@ 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); - //#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)); - } - }); + 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"); - // 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) { @@ -62,50 +25,33 @@ class ConcordanceQueryBuilder { 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', ''); - this.submitQueryChipElement('end-entity', 'Entity End', '', 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}`, ``, null, false, true); - if (!this.elements.editingModusOn) { - this.submitQueryChipElement('end-entity', 'Entity End', '', 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}`, ``, null, false, true); - if (!this.elements.editingModusOn) { - this.submitQueryChipElement('end-entity', 'Entity End', '', 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 + + } + + 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)); + } + }); } - //#region QB Functions switchToExpertModeParser() { let expertModeInputField = document.querySelector('#corpus-analysis-concordance-form-query'); expertModeInputField.value = ''; @@ -116,7 +62,7 @@ class ConcordanceQueryBuilder { } switchToQueryBuilderParser() { - this.resetQueryInputField(); + this.generalFunctions.resetQueryInputField(); let expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value; let chipElements = this.parseTextToChip(expertModeInputFieldValue); let closingTagElements = ['end-sentence', 'end-entity']; @@ -127,7 +73,7 @@ class ConcordanceQueryBuilder { if (chipElement['query'] === '[]'){ isEditable = false; } - this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable); + this.generalFunctions.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable); } } @@ -242,696 +188,5 @@ class ConcordanceQueryBuilder { 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('Click on a button to add a query component'); - 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( - ` - - ${prettyQueryText}${isEditable ? 'edit': ''} - ${isClosingTag ? 'lock_open' : 'close'} - - ` - ); - 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(//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('Drop here'); - 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', ''); - this.submitQueryChipElement('end-sentence', 'Sentence End', '', 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 = - ` - - - - - - - `; - 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 = - ` - - - - - - `; - 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/static/js/CorpusAnalysis/query-builder/structural-attribute-builder-functions.js b/app/static/js/CorpusAnalysis/query-builder/structural-attribute-builder-functions.js new file mode 100644 index 00000000..88026866 --- /dev/null +++ b/app/static/js/CorpusAnalysis/query-builder/structural-attribute-builder-functions.js @@ -0,0 +1,98 @@ +class StructuralAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { + constructor(elements) { + super(elements); + + this.structuralAttrModalEventlisteners(); + + document.querySelector('#corpus-analysis-concordance-text-annotation-submit').addEventListener('click', () => 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); + }); + }); + document.querySelector('.ent-type-selection-action[data-ent-type="any"]').addEventListener('click', () => { + this.submitQueryChipElement('start-empty-entity', 'Entity Start', ''); + this.submitQueryChipElement('end-entity', 'Entity End', '', 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}`, ``, null, false, true); + if (!this.elements.editingModusOn) { + this.submitQueryChipElement('end-entity', 'Entity End', '', 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}`, ``, null, false, true); + if (!this.elements.editingModusOn) { + this.submitQueryChipElement('end-entity', 'Entity End', '', 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.toggleEditingAreaStructuralAttrModal('remove'); + this.elements.editingModusOn = false; + this.elements.editedQueryChipElementIndex = undefined; + } + + actionButtonInStrucAttrModalHandler(action) { + switch (action) { + case 'sentence': + this.submitQueryChipElement('start-sentence', 'Sentence Start', ''); + this.submitQueryChipElement('end-sentence', 'Sentence End', '', 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/query-builder/token-attribute-builder-functions.js b/app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js new file mode 100644 index 00000000..d3dbe55c --- /dev/null +++ b/app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js @@ -0,0 +1,182 @@ +class TokenAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { + constructor(elements) { + super(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();}); + + this.elements.positionalAttrModal = M.Modal.init( + document.querySelector('#corpus-analysis-concordance-positional-attr-modal'), + { + onOpenStart: () => { + this.preparePositionalAttrModal(); + }, + onCloseStart: () => { + this.resetPositionalAttrModal(); + } + } + ); + } + + resetPositionalAttrModal() { + let originalSelectionList = + ` + + + + + + + `; + 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; + } + + 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) { + 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 = + ` + + + + + + `; + 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/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 6b3c49fe..63da9c5d 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -105,6 +105,9 @@ output='gen/CorpusAnalysis.%(version)s.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/CorpusAnalysisReader.js',