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 index d58704c8..003bb01a 100644 --- a/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js +++ b/app/static/js/CorpusAnalysis/query-builder/general-query-builder-functions.js @@ -1,6 +1,29 @@ class GeneralQueryBuilderFunctions { - constructor(elements) { + name = 'General Query Builder Functions'; + + constructor(app, elements) { + this.app = app; this.elements = 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(); + } + }); } toggleClass(elements, className, action){ @@ -124,106 +147,20 @@ class GeneralQueryBuilderFunctions { this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement); switch (queryChipElement.dataset.type) { case 'start-entity': - this.editStartEntityChipElement(queryChipElement); + this.app.extensions.structuralAttributeBuilderFunctions.editStartEntityChipElement(queryChipElement); break; case 'text-annotation': - this.editTextAnnotationChipElement(queryChipElement); + this.app.extensions.structuralAttributeBuilderFunctions.editTextAnnotationChipElement(queryChipElement); break; case 'token': - let queryElementsContent = this.prepareQueryElementsContent(queryChipElement); - this.editTokenChipElement(queryElementsContent); + let queryElementsContent = this.app.extensions.tokenAttributeBuilderFunctions.prepareTokenQueryElementsContent(queryChipElement); + this.app.extensions.tokenAttributeBuilderFunctions.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"]'); @@ -385,111 +322,165 @@ class GeneralQueryBuilderFunctions { 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); + 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.tokenIncidenceModifierHandler(button.dataset.token, button.innerHTML)); + } else if (dropdownId === 'corpus-analysis-concordance-character-incidence-modifiers-dropdown') { + button.addEventListener('click', () => this.characterIncidenceModifierHandler(button)); + } + }); } - 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();}); + 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.tokenNMSubmitHandler(modalId)); + } else if (modalId === 'corpus-analysis-concordance-exactly-n-character-modal' || modalId === 'corpus-analysis-concordance-between-nm-character-modal') { + button.addEventListener('click', () => this.app.extensions.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.resetQueryInputField(); + let expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value; + let chipElements = this.parseTextToChip(expertModeInputFieldValue); + let closingTagElements = ['end-sentence', 'end-entity']; + let editableElements = ['start-entity', 'text-annotation', 'token']; + for (let chipElement of chipElements) { + let isClosingTag = closingTagElements.includes(chipElement['type']); + let isEditable = editableElements.includes(chipElement['type']); + if (chipElement['query'] === '[]'){ + isEditable = false; + } + this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable); + } + } + + parseTextToChip(query) { + const parsingElementDict = { + '': { + 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}`; - } - // 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; + 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 index 5050c09f..9d85b853 100644 --- a/app/static/js/CorpusAnalysis/query-builder/index.js +++ b/app/static/js/CorpusAnalysis/query-builder/index.js @@ -1,192 +1,15 @@ 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"); + this.extensions = { + generalFunctions: new GeneralQueryBuilderFunctions(this, this.elements), + structuralAttributeBuilderFunctions: new StructuralAttributeBuilderFunctions(this, this.elements), + tokenAttributeBuilderFunctions: new TokenAttributeBuilderFunctions(this, this.elements), + }; - 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()); @@ -23,32 +26,32 @@ class StructuralAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { }); }); 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.app.extensions.generalFunctions.submitQueryChipElement('start-empty-entity', 'Entity Start', ''); + this.app.extensions.generalFunctions.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); + this.app.extensions.generalFunctions.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, ``, null, false, true); if (!this.elements.editingModusOn) { - this.submitQueryChipElement('end-entity', 'Entity End', '', null, true); + this.app.extensions.generalFunctions.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); + this.app.extensions.generalFunctions.submitQueryChipElement('start-entity', `Entity Type=${event.target.value}`, ``, null, false, true); if (!this.elements.editingModusOn) { - this.submitQueryChipElement('end-entity', 'Entity End', '', null, true); + this.app.extensions.generalFunctions.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.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.englishEntTypeSelection, this.elements.germanEntTypeSelection]); + this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.textAnnotationSelection], 'address'); this.elements.textAnnotationInput.value = ''; - this.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add'); + this.app.extensions.generalFunctions.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add'); this.toggleEditingAreaStructuralAttrModal('remove'); this.elements.editingModusOn = false; this.elements.editedQueryChipElementIndex = undefined; @@ -57,23 +60,28 @@ class StructuralAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { actionButtonInStrucAttrModalHandler(action) { switch (action) { case 'sentence': - this.submitQueryChipElement('start-sentence', 'Sentence Start', ''); - this.submitQueryChipElement('end-sentence', 'Sentence End', '', null, true); + this.app.extensions.generalFunctions.submitQueryChipElement('start-sentence', 'Sentence Start', ''); + this.app.extensions.generalFunctions.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'); + this.app.extensions.generalFunctions.toggleClass(['entity-builder'], 'hide', 'toggle'); + this.app.extensions.generalFunctions.toggleClass(['text-annotation-builder'], 'hide', 'add'); break; case 'meta-data': - this.toggleClass(['text-annotation-builder'], 'hide', 'toggle'); - this.toggleClass(['entity-builder'], 'hide', 'add'); + this.app.extensions.generalFunctions.toggleClass(['text-annotation-builder'], 'hide', 'toggle'); + this.app.extensions.generalFunctions.toggleClass(['entity-builder'], 'hide', 'add'); break; default: break; } } + 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.app.extensions.generalFunctions.toggleClass(['sentence-button', 'entity-button', 'text-annotation-button', 'any-type-entity-button'], 'disabled', action); + } + textAnnotationSubmitHandler() { let noValueMetadataMessage = document.querySelector('#corpus-analysis-concordance-no-value-metadata-message'); let textAnnotationSubmit = document.querySelector('#corpus-analysis-concordance-text-annotation-submit'); @@ -91,8 +99,29 @@ class StructuralAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { }, 3000); } else { let queryText = `:: match.text_${textAnnotationOptions.value}="${textAnnotationInput.value}"`; - this.submitQueryChipElement('text-annotation', `${textAnnotationOptions.value}=${textAnnotationInput.value}`, queryText, null, false, true); + this.app.extensions.generalFunctions.submitQueryChipElement('text-annotation', `${textAnnotationOptions.value}=${textAnnotationInput.value}`, queryText, null, false, true); this.elements.structuralAttrModal.close(); } } + + editStartEntityChipElement(queryChipElement) { + this.elements.structuralAttrModal.open(); + this.app.extensions.generalFunctions.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.app.extensions.generalFunctions.resetMaterializeSelection([selection], entType); + } + + editTextAnnotationChipElement(queryChipElement) { + this.elements.structuralAttrModal.open(); + this.app.extensions.generalFunctions.toggleClass(['text-annotation-builder'], 'hide', 'remove'); + this.toggleEditingAreaStructuralAttrModal('add'); + let [textAnnotationSelection, textAnnotationContent] = queryChipElement.dataset.query + .replace(/:: ?match\.text_|"|"/g, '') + .split('='); + this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.textAnnotationSelection], textAnnotationSelection); + this.elements.textAnnotationInput.value = textAnnotationContent; + } } 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 index d3dbe55c..5f897fbe 100644 --- a/app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js +++ b/app/static/js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js @@ -1,6 +1,9 @@ -class TokenAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { - constructor(elements) { - super(elements); +class TokenAttributeBuilderFunctions { + name = 'Token Attribute Builder Functions'; + + constructor(app, elements) { + this.app = app; + this.elements = elements; this.elements.positionalAttrSelection.addEventListener('change', () => { this.preparePositionalAttrModal(); @@ -39,9 +42,9 @@ class TokenAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { 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.app.extensions.generalFunctions.toggleClass(['input-field-options'], 'hide', 'remove'); + this.app.extensions.generalFunctions.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add'); + this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], "word"); this.elements.ignoreCaseCheckbox.checked = false; this.elements.editingModusOn = false; this.elements.editedQueryChipElementIndex = undefined; @@ -175,8 +178,168 @@ class TokenAttributeBuilderFunctions extends GeneralQueryBuilderFunctions { } }); - this.resetMaterializeSelection([this.elements.positionalAttrSelection], selection); + this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], selection); this.preparePositionalAttrModal(); } + 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.app.extensions.generalFunctions.toggleClass(['input-field-options'], 'hide', 'remove'); + } else if (selection === 'empty-token'){ + this.addTokenToQuery(); + } else { + this.app.extensions.generalFunctions.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.app.extensions.generalFunctions.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add'); + } else if (this.elements.positionalAttrSelection.querySelectorAll('option').length === 1) { + this.app.extensions.generalFunctions.toggleClass(['and'], 'disabled', 'add'); + this.app.extensions.generalFunctions.toggleClass(['or'], 'disabled', 'remove'); + } else { + this.app.extensions.generalFunctions.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.app.extensions.generalFunctions.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); + } + + editTokenChipElement(queryElementsContent) { + this.elements.positionalAttrModal.open(); + queryElementsContent.forEach((queryElement) => { + this.app.extensions.generalFunctions.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.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.positionalAttrSelection], queryElement.tokenAttr); + this.preparePositionalAttrModal(); + this.app.extensions.generalFunctions.resetMaterializeSelection([this.elements.tokenBuilderContent.querySelector('select')], queryElement.tokenValue); + break; + case 'simple_pos': + this.app.extensions.generalFunctions.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); + } + + }); + } + + prepareTokenQueryElementsContent(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; + } + }