From f4a378d9d36bc7a22041c6c105825fddad2762c8 Mon Sep 17 00:00:00 2001 From: Inga Kirschnick Date: Thu, 12 Jan 2023 15:40:28 +0100 Subject: [PATCH] Query Builder fixes --- app/static/js/CorpusAnalysis/QueryBuilder.js | 220 +++++++++---------- app/static/js/Utils.js | 17 ++ 2 files changed, 117 insertions(+), 120 deletions(-) diff --git a/app/static/js/CorpusAnalysis/QueryBuilder.js b/app/static/js/CorpusAnalysis/QueryBuilder.js index 325e9982..4e34f04c 100644 --- a/app/static/js/CorpusAnalysis/QueryBuilder.js +++ b/app/static/js/CorpusAnalysis/QueryBuilder.js @@ -144,7 +144,7 @@ class ConcordanceQueryBuilder { this.elements.generalOptionsQueryBuilderTutorialInfoIcon.addEventListener('click', () => {this.tutorialIconHandler('#general-options-query-builder');}); this.elements.positionalAttr.addEventListener('change', () => {this.tokenTypeSelector();}); - this.elements.tokenSubmitButton.addEventListener('click', () => {this.addToken();}); + this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();}); this.elements.ignoreCase.addEventListener('change', () => {this.inputOptionHandler(this.elements.ignoreCase);}); @@ -205,27 +205,69 @@ class ConcordanceQueryBuilder { this.elements.structuralAttrArea.classList.remove('hide'); } - buttonfactory(dataType, prettyText, queryText) { + queryChipFactory(dataType, prettyQueryText, queryText) { window.location.href = '#query-container'; - this.elements.counter += 1; - queryText = encodeURI(queryText); - let buttonElement = Utils.HTMLToElement( + queryText = Utils.escape(queryText); + prettyQueryText = Utils.escape(prettyQueryText); + let queryChipElement = Utils.HTMLToElement( ` -
- ${prettyText} + + ${prettyQueryText} close -
+ ` ); - buttonElement.addEventListener('click', () => {this.deleteAttr(buttonElement);}); - buttonElement.addEventListener('dragstart', (event) => {this.dragStartHandler(event);}); - buttonElement.addEventListener('dragend', (event) => {this.dragEndHandler(event);}); + queryChipElement.addEventListener('click', () => {this.deleteAttr(queryChipElement);}); + queryChipElement.addEventListener('dragstart', (event) => { + // selects all nodes without target class + let queryChips = this.elements.yourQuery.querySelectorAll('.query-component'); + + // Adds a target chip in front of all draggable childnodes + setTimeout(() => { + let targetChipElement = Utils.HTMLToElement('Drop here'); + for (let element of queryChips) { + if (element === queryChipElement.nextSibling) {continue;} + let targetChipClone = targetChipElement.cloneNode(true); + if (element === queryChipElement) { + // If the dragged element is not at the very end, a target chip is also inserted at the end + if (queryChips[queryChips.length - 1] !== element) { + queryChips[queryChips.length - 1].insertAdjacentElement('afterend', targetChipClone); + } + } else { + element.insertAdjacentElement('beforebegin', targetChipClone); + } + 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.queryPreviewBuilder(); + }); + } + }, 0); + }); + + queryChipElement.addEventListener('dragend', (event) => { + let targets = document.querySelectorAll('.drop-target'); + for (let target of targets) { + target.remove(); + } + }); // Ensures that metadata is always at the end of the query: if (this.elements.yourQuery.lastChild === null || this.elements.yourQuery.lastChild.dataset.type !== 'text-annotation') { - this.elements.yourQuery.appendChild(buttonElement); + this.elements.yourQuery.appendChild(queryChipElement); } else if (this.elements.yourQuery.lastChild.dataset.type === 'text-annotation') { - this.elements.yourQuery.insertBefore(buttonElement, this.elements.yourQuery.lastChild); + this.elements.yourQuery.insertBefore(queryChipElement, this.elements.yourQuery.lastChild); } this.elements.queryContainer.classList.remove('hide'); this.queryPreviewBuilder(); @@ -236,73 +278,11 @@ class ConcordanceQueryBuilder { } } - //#region Drag&Drop Events - dragStartHandler(event) { - // Creates element with the class 'target' and all necessary drop functions, in which drop content can be released - this.elements.dropButton = event.target; - let targetChipElement = Utils.HTMLToElement('
Drop here
'); - targetChipElement.addEventListener('dragover', (event) => {this.dragOverHandler(event);}); - targetChipElement.addEventListener('dragenter', (event) => {this.dragEnterHandler(event);}); - targetChipElement.addEventListener('dragleave', (event) => {this.dragLeaveHandler(event);}); - targetChipElement.addEventListener('drop', (event) => {this.dropHandler(event);}); - // selects all nodes without target class - let childNodes = this.elements.yourQuery.querySelectorAll('div:not(.target)'); - - // Adds a target chip in front of all draggable childnodes - setTimeout(() => { - for (let element of childNodes) { - if (element === this.elements.dropButton) { - // If the dragged element is not at the very end, a target chip is also inserted at the end - if (childNodes[childNodes.length - 1] !== element) { - childNodes[childNodes.length - 1].insertAdjacentElement('afterend', targetChipElement); - } - } else if (element === this.elements.dropButton.nextSibling) { - continue; - } else { - element.insertAdjacentElement('beforebegin', targetChipElement) - } - } - }, 0); - } - - dragOverHandler(event) { - event.preventDefault(); - } - - dragEnterHandler(event) { - event.preventDefault(); - event.target.style.borderStyle = 'solid dotted'; - } - - dragLeaveHandler(event) { - event.preventDefault(); - event.target.style.borderStyle = 'hidden'; - } - - dragEndHandler(event) { - let targets = document.querySelectorAll('.target'); - for (let target of targets) { - target.remove(); - } - } - - dropHandler(event) { - let dropzone = event.target; - dropzone.parentElement.replaceChild(this.elements.dropButton, dropzone); - this.queryPreviewBuilder(); - } - //#endregion Drag&Drop Events - queryPreviewBuilder() { this.elements.yourQueryContent = []; for (let element of this.elements.yourQuery.childNodes) { let queryElement = decodeURI(element.dataset.query); - if (queryElement.includes('<')) { - queryElement = queryElement.replace('<', '<'); - } - if (queryElement.includes('>')) { - queryElement = queryElement.replace('>', '>'); - } + queryElement = Utils.escape(queryElement); if (queryElement !== 'undefined') { this.elements.yourQueryContent.push(queryElement); } @@ -469,19 +449,19 @@ class ConcordanceQueryBuilder { } - tokenButtonfactory(prettyText, tokenText) { + tokenChipFactory(prettyQueryText, tokenText) { tokenText = encodeURI(tokenText); let builderElement; - let buttonElement; + let queryChipElement; builderElement = document.createElement('div'); builderElement.innerHTML = `
- ${prettyText} + ${prettyQueryText} close
`; - buttonElement = builderElement.firstElementChild; - buttonElement.addEventListener('click', () => {this.deleteTokenAttr(buttonElement);}); - this.elements.tokenQuery.appendChild(buttonElement); + queryChipElement = builderElement.firstElementChild; + queryChipElement.addEventListener('click', () => {this.deleteTokenAttr(queryChipElement);}); + this.elements.tokenQuery.appendChild(queryChipElement); } deleteTokenAttr(attr) { @@ -494,12 +474,12 @@ class ConcordanceQueryBuilder { } - addToken() { + addTokenToQuery() { let c; - let tokenQueryContent = ''; //for ButtonFactory(prettyText) + let tokenQueryContent = ''; //for ButtonFactory(prettyQueryText) let tokenQueryText = ''; //for ButtonFactory(queryText) this.elements.cancelBool = false; - let emptyTokenCheck = false; + let tokenIsEmpty = true; if (this.elements.ignoreCase.checked) { c = ' %c'; @@ -511,8 +491,8 @@ class ConcordanceQueryBuilder { for (let element of this.elements.tokenQuery.childNodes) { tokenQueryContent += ' ' + element.firstChild.data + ' '; tokenQueryText += decodeURI(element.dataset.tokentext); - if (element.innerText.indexOf('empty token') !== -1) { - emptyTokenCheck = true; + if (element.innerTe8888xt.indexOf('empty token') !== -1) { + emptyTokenCheck = false; } } @@ -572,10 +552,10 @@ class ConcordanceQueryBuilder { // cancelBool looks in disableTokenSubmit() whether a value is passed. If the input fields/dropdowns are empty (cancelBool === true), no token is added. if (this.elements.cancelBool === false) { // Square brackets are added only if it is not an empty token (where they are already present). - if (emptyTokenCheck === false) { + if (tokenIsEmpty === false) { tokenQueryText = '[' + tokenQueryText + ']'; } - this.buttonfactory('token', tokenQueryContent, tokenQueryText); + this.queryChipFactory('token', tokenQueryContent, tokenQueryText); this.hideEverything(); this.elements.positionalAttrArea.classList.add('hide'); this.elements.tokenQuery.innerHTML = ''; @@ -661,7 +641,7 @@ class ConcordanceQueryBuilder { } emptyTokenHandler() { - this.tokenButtonfactory('empty token', '[]'); + this.tokenChipFactory('empty token', '[]'); this.elements.tokenQueryFilled = true; this.hideEverything(); this.elements.incidenceModifiersButton.classList.remove('hide'); @@ -703,27 +683,27 @@ class ConcordanceQueryBuilder { break; case 'english-pos': this.elements.tokenQueryFilled = true; - this.tokenButtonfactory(`pos=${this.elements.englishPos.value}`, `pos="${this.elements.englishPos.value}"`); - this.tokenButtonfactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); + this.tokenChipFactory(`pos=${this.elements.englishPos.value}`, `pos="${this.elements.englishPos.value}"`); + this.tokenChipFactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); this.elements.englishPosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); break; case 'german-pos': this.elements.tokenQueryFilled = true; - this.tokenButtonfactory(`pos=${this.elements.germanPos.value}`, `pos="${this.elements.germanPos.value}"`); - this.tokenButtonfactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); + this.tokenChipFactory(`pos=${this.elements.germanPos.value}`, `pos="${this.elements.germanPos.value}"`); + this.tokenChipFactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); this.elements.germanPosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); break; case 'simple-pos-button': this.elements.tokenQueryFilled = true; - this.tokenButtonfactory(`simple_pos=${this.elements.simplePos.value}`, `simple_pos="${this.elements.simplePos.value}"`); - this.tokenButtonfactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); + this.tokenChipFactory(`simple_pos=${this.elements.simplePos.value}`, `simple_pos="${this.elements.simplePos.value}"`); + this.tokenChipFactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); this.elements.simplePosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); break; case 'empty-token': - this.tokenButtonfactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); + this.tokenChipFactory('{' + this.elements.nInput.value + '}', '{' + this.elements.nInput.value + '}'); break; default: break; @@ -744,27 +724,27 @@ class ConcordanceQueryBuilder { break; case 'english-pos': this.elements.tokenQueryFilled = true; - this.tokenButtonfactory(`pos=${this.elements.englishPos.value}`, `pos="${this.elements.englishPos.value}"`); - this.tokenButtonfactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); + this.tokenChipFactory(`pos=${this.elements.englishPos.value}`, `pos="${this.elements.englishPos.value}"`); + this.tokenChipFactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); this.elements.englishPosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); break; case 'german-pos': this.elements.tokenQueryFilled = true; - this.tokenButtonfactory(`pos=${this.elements.germanPos.value}`, `pos="${this.elements.germanPos.value}"`); - this.tokenButtonfactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); + this.tokenChipFactory(`pos=${this.elements.germanPos.value}`, `pos="${this.elements.germanPos.value}"`); + this.tokenChipFactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); this.elements.germanPosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); break; case 'simple-pos-button': this.elements.tokenQueryFilled = true; - this.tokenButtonfactory(`simple_pos=${this.elements.simplePos.value}`, `simple_pos="${this.elements.simplePos.value}"`); - this.tokenButtonfactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); + this.tokenChipFactory(`simple_pos=${this.elements.simplePos.value}`, `simple_pos="${this.elements.simplePos.value}"`); + this.tokenChipFactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); this.elements.simplePosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); break; case 'empty-token': - this.tokenButtonfactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); + this.tokenChipFactory(`{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`, `{${this.elements.nmInput.value}, ${this.elements.mInput.value}}`); break; default: break; @@ -774,22 +754,22 @@ class ConcordanceQueryBuilder { incidenceModifiersHandler(elem) { // For word and lemma, the incidence modifiers are inserted in the input field. For the others, one or two chips are created which contain the respective value of the token and the incidence modifier. if (this.elements.positionalAttr.value === 'empty-token') { - this.tokenButtonfactory(elem.innerText, elem.dataset.token); + this.tokenChipFactory(elem.innerText, elem.dataset.token); } else if (this.elements.positionalAttr.value === 'english-pos') { - this.tokenButtonfactory(`pos=${this.elements.englishPos.value}`, `pos="${this.elements.englishPos.value}"`); - this.tokenButtonfactory(elem.innerText, elem.dataset.token); + this.tokenChipFactory(`pos=${this.elements.englishPos.value}`, `pos="${this.elements.englishPos.value}"`); + this.tokenChipFactory(elem.innerText, elem.dataset.token); this.elements.englishPosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); this.elements.tokenQueryFilled = true; } else if (this.elements.positionalAttr.value === 'german-pos') { - this.tokenButtonfactory(`pos=${this.elements.germanPos.value}`, `pos="${this.elements.germanPos.value}"`); - this.tokenButtonfactory(elem.innerText, elem.dataset.token); + this.tokenChipFactory(`pos=${this.elements.germanPos.value}`, `pos="${this.elements.germanPos.value}"`); + this.tokenChipFactory(elem.innerText, elem.dataset.token); this.elements.germanPosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); this.elements.tokenQueryFilled = true; } else if (this.elements.positionalAttr.value === 'simple-pos-button') { - this.tokenButtonfactory(`simple_pos=${this.elements.simplePos.value}`, `simple_pos="${this.elements.simplePos.value}"`); - this.tokenButtonfactory(elem.innerText, elem.dataset.token); + this.tokenChipFactory(`simple_pos=${this.elements.simplePos.value}`, `simple_pos="${this.elements.simplePos.value}"`); + this.tokenChipFactory(elem.innerText, elem.dataset.token); this.elements.simplePosBuilder.classList.add('hide'); this.elements.incidenceModifiersButton.classList.add('hide'); this.elements.tokenQueryFilled = true; @@ -858,8 +838,8 @@ class ConcordanceQueryBuilder { break; } - this.tokenButtonfactory(tokenQueryContent, tokenQueryText); - this.tokenButtonfactory(conditionText, conditionQueryContent); + this.tokenChipFactory(tokenQueryContent, tokenQueryText); + this.tokenChipFactory(conditionText, conditionQueryContent); this.wordBuilder(); } @@ -876,10 +856,10 @@ class ConcordanceQueryBuilder { addSentence() { this.hideEverything(); if (this.elements.sentence.text === 'End Sentence') { - this.buttonfactory('end-sentence', 'Sentence End', ''); + this.queryChipFactory('end-sentence', 'Sentence End', ''); this.elements.sentence.innerHTML = 'Sentence'; } else { - this.buttonfactory('start-sentence', 'Sentence Start', ''); + this.queryChipFactory('start-sentence', 'Sentence Start', ''); this.elements.queryContent.push('sentence'); this.elements.sentence.innerHTML = 'End Sentence'; } @@ -893,7 +873,7 @@ class ConcordanceQueryBuilder { } else { queryText = ''; } - this.buttonfactory('end-entity', 'Entity End', queryText); + this.queryChipFactory('end-entity', 'Entity End', queryText); this.elements.entity.innerHTML = 'Entity'; } else { this.hideEverything(); @@ -903,7 +883,7 @@ class ConcordanceQueryBuilder { } englishEntTypeHandler() { - this.buttonfactory('start-entity', 'Entity Type=' + this.elements.englishEntType.value, ''); + this.queryChipFactory('start-entity', 'Entity Type=' + this.elements.englishEntType.value, ''); this.elements.entity.innerHTML = 'End Entity'; this.hideEverything(); this.elements.entityAnyType = false; @@ -915,7 +895,7 @@ class ConcordanceQueryBuilder { } germanEntTypeHandler() { - this.buttonfactory('start-entity', 'Entity Type=' + this.elements.germanEntType.value, ''); + this.queryChipFactory('start-entity', 'Entity Type=' + this.elements.germanEntType.value, ''); this.elements.entity.innerHTML = 'End Entity'; this.hideEverything(); this.elements.entityAnyType = false; @@ -927,7 +907,7 @@ class ConcordanceQueryBuilder { } emptyEntityButton() { - this.buttonfactory('start-empty-entity', 'Entity Start', ''); + this.queryChipFactory('start-empty-entity', 'Entity Start', ''); this.elements.entity.innerHTML = 'End Entity'; this.hideEverything(); this.elements.entityAnyType = true; @@ -957,7 +937,7 @@ class ConcordanceQueryBuilder { }, 3000); } else { let queryText = `:: match.text_${this.elements.textAnnotationOptions.value}="${this.elements.textAnnotationInput.value}"`; - this.buttonfactory('text-annotation', `${this.elements.textAnnotationOptions.value}=${this.elements.textAnnotationInput.value}`, queryText); + this.queryChipFactory('text-annotation', `${this.elements.textAnnotationOptions.value}=${this.elements.textAnnotationInput.value}`, queryText); this.hideEverything(); } } diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js index 2e847477..d63be025 100644 --- a/app/static/js/Utils.js +++ b/app/static/js/Utils.js @@ -1,4 +1,21 @@ class Utils { + static escape(text) { + // https://codereview.stackexchange.com/a/126722 + var table = { + '<': 'lt', + '>': 'gt', + '"': 'quot', + '\'': 'apos', + '&': 'amp', + '\r': '#10', + '\n': '#13' + }; + + return text.toString().replace(/[<>"'\r\n&]/g, (chr) => { + return '&' + table[chr] + ';'; + }); + }; + static HTMLToElement(HTMLString) { let templateElement = document.createElement('template'); templateElement.innerHTML = HTMLString.trim();