diff --git a/app/static/js/corpus-analysis/query-builder/element-references.js b/app/static/js/corpus-analysis/query-builder/element-references.js index 2946511258c3b0aa961d6602cedd62ece3bca93b..1b9f384e62880a1a27dae706e8a129076e0cc652 100644 --- a/app/static/js/corpus-analysis/query-builder/element-references.js +++ b/app/static/js/corpus-analysis/query-builder/element-references.js @@ -3,8 +3,10 @@ nopaque.corpus_analysis.query_builder.ElementReferences = class ElementReference // General Elements this.queryInputField = document.querySelector('#corpus-analysis-concordance-query-builder-input-field'); this.queryChipElements = []; + this.queryElementTarget = document.querySelector('.query-element-target') this.editingModusOn = false; this.editedQueryChipElementIndex = undefined; + this.deleteQueryButton = document.querySelector('#corpus-analysis-concordance-delete-query-button'); // Structural Attribute Builder Elements this.structuralAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-structural-attr-modal')); diff --git a/app/static/js/corpus-analysis/query-builder/query-builder.js b/app/static/js/corpus-analysis/query-builder/query-builder.js index 5098b6847785b7d825f5419f539cb41ef4f2b65f..702037f7c931450e81cb6e2dfa98574b7516d590 100644 --- a/app/static/js/corpus-analysis/query-builder/query-builder.js +++ b/app/static/js/corpus-analysis/query-builder/query-builder.js @@ -2,25 +2,13 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { constructor() { this.elements = new nopaque.corpus_analysis.query_builder.ElementReferences(); - this.incidenceModifierEventListeners(); - this.nAndMInputSubmitEventListeners(); + this.addEventListenersToQueryElementTarget(); + this.addEventListenersToIncidenceModifier(); + this.addEventListenersToNAndMInputSubmit(); - 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(); - } - }); + this.elements.deleteQueryButton.addEventListener('click', () => {this.resetQueryInputField()}); + + this.expertModeQueryBuilderSwitchHandler(); this.extensions = { structuralAttributeBuilderFunctions: new nopaque.corpus_analysis.query_builder.StructuralAttributeBuilderFunctions(this), @@ -28,6 +16,38 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { }; } + addEventListenersToQueryElementTarget() { + this.elements.queryElementTarget.addEventListener('click', () => { + this.elements.positionalAttrModal.open(); + }); + this.elements.queryElementTarget.addEventListener('dragstart', this.handleDragStart.bind(this, this.elements.queryElementTarget)); + this.elements.queryElementTarget.addEventListener('dragend', this.handleDragEnd); + } + + addEventListenersToIncidenceModifier() { + // 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.extensions.tokenAttributeBuilderFunctions.characterIncidenceModifierHandler(button)); + } + }); + } + + addEventListenersToNAndMInputSubmit() { + // 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.extensions.tokenAttributeBuilderFunctions.characterNMSubmitHandler(modalId)); + } + }); + } + toggleClass(elements, className, action) { elements.forEach(element => { document.querySelector(`[data-toggle-area="${element}"]`).classList[action](className); @@ -36,25 +56,26 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { resetQueryInputField() { this.elements.queryInputField.innerHTML = ''; - this.addPlaceholder(); + this.addQueryElementTarget(); 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 = ''; - } + addQueryElementTarget() { + let queryElementTarget = nopaque.Utils.HTMLToElement( + ` + <a class="query-element-target btn-floating btn-small blue-grey lighten-4 waves-effect waves-light tooltipped" style="margin-bottom:10px; margin-right:5px;" draggable="true" data-position="bottom" data-tooltip="Add an Element to your query"> + <i class="material-icons">add</i> + </a> + ` + ); + this.elements.queryInputField.appendChild(queryElementTarget); + this.elements.queryElementTarget = queryElementTarget; + this.addEventListenersToQueryElementTarget(); } - addPlaceholder() { - let placeholder = nopaque.Utils.HTMLToElement('<span id="corpus-analysis-concordance-query-builder-input-field-placeholder">Click on a button to add a query component</span>'); - this.elements.queryInputField.appendChild(placeholder); + updateChipList() { + this.elements.queryChipElements = this.elements.queryInputField.querySelectorAll('.query-component'); } resetMaterializeSelection(selectionElements, value = "default") { @@ -89,32 +110,32 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { ` <span class="chip query-component" data-type="${dataType}" data-query="${queryText}" draggable="true" data-closing-tag="${isClosingTag}"> ${prettyQueryText}${isEditable ? '<i class="material-icons chip-action-button" data-chip-action="edit" style="padding-left:5px; font-size:18px; cursor:pointer;">edit</i>': ''} - ${isClosingTag ? '<i class="material-icons chip-action-button" data-chip-action="lock" style="padding-top:5px; font-size:20px; cursor:pointer;">lock_open</i>' : '<i class="material-icons close chip-action-button" data-chip-action="delete">close</i>'} + ${isClosingTag ? '' : '<i class="material-icons close chip-action-button" data-chip-action="delete">close</i>'} </span> ` ); this.addActionListeners(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(); - if (!index) { - let closingTagElement = this.elements.queryInputField.querySelector('[data-closing-tag="true"]'); - if (closingTagElement) { - index = Array.from(this.elements.queryInputField.children).indexOf(closingTagElement); - } - } - if (index) { - this.elements.queryInputField.insertBefore(queryChipElement, this.elements.queryChipElements[index]); + // If an index is given, inserts the query chip after the given index (only relevant for Incidence Modifier) and if there is a closing tag, inserts the query chip before the closing tag. + if (index !== null) { + this.updateChipList(); + this.elements.queryChipElements[index].after(queryChipElement); } else { - this.elements.queryInputField.appendChild(queryChipElement); + this.elements.queryInputField.insertBefore(queryChipElement, this.elements.queryElementTarget); + } + if (isClosingTag) { + this.moveQueryElementTarget(queryChipElement); } this.updateChipList(); this.queryPreviewBuilder(); } + moveQueryElementTarget(element) { + this.elements.queryInputField.insertBefore(this.elements.queryElementTarget, element); + } + addActionListeners(queryChipElement) { let notQuantifiableDataTypes = ['start-sentence', 'end-sentence', 'start-entity', 'start-empty-entity', 'end-entity', 'token-incidence-modifier']; queryChipElement.addEventListener('click', (event) => { @@ -154,20 +175,13 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { } } - 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.deleteClosingTagHandler(elementIndex, 'end-sentence'); break; + case 'start-empty-entity': case 'start-entity': this.deleteClosingTagHandler(elementIndex, 'end-entity'); break; @@ -180,9 +194,6 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { break; } this.elements.queryInputField.removeChild(attr); - if (this.elements.queryInputField.children.length === 0) { - this.addPlaceholder(); - } this.updateChipList(); this.queryPreviewBuilder(); } @@ -217,13 +228,17 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { let targetChipClone = targetChipElement.cloneNode(true); element.insertAdjacentElement('afterend', targetChipClone); - + //TODO: Change to two different functions for drag and drop this.addDragDropListeners(targetChipClone, queryChipElement); } }, 0); } - handleDragEnd() { + handleDragEnd(event) { + // is called when a query chip is dropped. It removes the dropzones and initializes the tooltips if the dragged element is the query element target. + if (event.target.classList.contains('query-element-target')) { + M.Tooltip.init(event.target); + } document.querySelectorAll('.drop-target').forEach(target => target.remove()); } @@ -292,8 +307,8 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { 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); + let selectedChipIndex = Array.from(this.elements.queryChipElements).indexOf(selectedChip); + this.submitQueryChipElement('token-incidence-modifier', incidenceModifierPretty, incidenceModifier, selectedChipIndex); this.selectChipElement(selectedChip); } @@ -315,26 +330,27 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { this.tokenIncidenceModifierHandler(input, pretty_input); } - 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.extensions.tokenAttributeBuilderFunctions.characterIncidenceModifierHandler(button)); - } + expertModeQueryBuilderSwitchHandler() { + 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"); + let submitModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-switch-to-query-builder-submit-modal')); + + let confirmSwitchToQueryBuilderButton = document.querySelector('.switch-action[data-switch-action="confirm"]'); + confirmSwitchToQueryBuilderButton.addEventListener("click", () => { + queryBuilderDisplay.classList.remove("hide"); + expertModeDisplay.classList.add("hide"); + this.switchToQueryBuilderParser(); }); - } - 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.extensions.tokenAttributeBuilderFunctions.characterNMSubmitHandler(modalId)); + expertModeSwitch.addEventListener("change", () => { + const isChecked = expertModeSwitch.checked; + if (isChecked) { + queryBuilderDisplay.classList.add("hide"); + expertModeDisplay.classList.remove("hide"); + this.switchToExpertModeParser(); + } else { + submitModal.open(); } }); } @@ -353,7 +369,7 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { 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', 'token']; + let editableElements = ['start-entity', 'token']; for (let chipElement of chipElements) { let isClosingTag = closingTagElements.includes(chipElement['type']); let isEditable = editableElements.includes(chipElement['type']); @@ -361,9 +377,6 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder { isEditable = false; } this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable); - if (isClosingTag) { - this.lockClosingChipElement(this.elements.queryChipElements[this.elements.queryChipElements.length-1]); - } } } diff --git a/app/templates/corpora/_analysis/query_builder/_expert_mode.html.j2 b/app/templates/corpora/_analysis/query_builder/_expert_mode.html.j2 index 7ebf4643df3d91c6c2d7cd9d190acf6b455f65e6..9926b3673ac801b9b9e1340db576fdce0c772355 100644 --- a/app/templates/corpora/_analysis/query_builder/_expert_mode.html.j2 +++ b/app/templates/corpora/_analysis/query_builder/_expert_mode.html.j2 @@ -23,4 +23,15 @@ </div> </form> </div> + +<div id="corpus-analysis-concordance-switch-to-query-builder-submit-modal" class="modal"> + <div class="modal-content"> + <h4>Switch to Query Builder</h4> + <p>Switching back to the query builder can cause elements the query builder does not recognize to become lost. Continue?</p> + </div> + <div class="modal-footer"> + <a class="btn modal-close waves-effect waves-light">Cancel</a> + <a class="btn modal-close red waves-effect waves-light switch-action" data-switch-action="confirm">Switch to Query Builder</a> + </div> +</div> {% endmacro %} diff --git a/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 b/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 index 4953e41c9ffe6d014fcedfacada9416ad4132298..6baecddaa1016cd13f86528fbfcf8f2eee1d4fc5 100644 --- a/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 +++ b/app/templates/corpora/_analysis/query_builder/_query_builder.html.j2 @@ -1,11 +1,18 @@ {% macro card_content(id_prefix) %} <form id="corpus-analysis-concordance-query-builder-form"> <div class="row"> - <div class="col s9" id="corpus-analysis-concordance-query-builder-input-field-container"> + <div class="col s8" id="corpus-analysis-concordance-query-builder-input-field-container"> <div id="corpus-analysis-concordance-query-builder-input-field"> - <p id="corpus-analysis-concordance-query-builder-input-field-placeholder">Click on the buttons below to build your query.</p> + <a class="query-element-target btn-floating btn-small blue-grey lighten-4 waves-effect waves-light tooltipped" style="margin-bottom:10px; margin-right:5px;" draggable="true" data-position="bottom" data-tooltip="Add a token to your query"> + <i class="material-icons">add</i> + </a> </div> </div> + <div class="col s1 center-align"> + <a class="btn-floating btn waves-effect waves-light red" id="corpus-analysis-concordance-delete-query-button" style="margin-top:18px;"> + <i class="material-icons">delete</i> + </a> + </div> <div class="input-field col s3"> <i class="material-icons prefix">arrow_forward</i> <input class="validate corpus-analysis-action" id="corpus-analysis-concordance-form-subcorpus-name" name="subcorpus-name" type="text" required pattern="^[A-Z][a-z0-9\-]*" value="Last"></input> @@ -30,9 +37,9 @@ <div class="row"> <div class="col s12"> <p></p> - <a class="btn waves-effect waves-light tooltipped modal-trigger" href="#corpus-analysis-concordance-positional-attr-modal" data-position="bottom" data-tooltip="Search for any token, for example a word, a lemma or a part-of-speech tag">Add new token to your query</a> - <a class="btn waves-effect waves-light tooltipped modal-trigger" href="#corpus-analysis-concordance-structural-attr-modal" data-position="bottom" data-tooltip="Structure your query with structural attributes, for example sentences, entities or annotate the text">Add structural attributes to your query</a> - <a class="btn waves-effect waves-light tooltipped dropdown-trigger disabled" data-target="corpus-analysis-concordance-token-incidence-modifiers-dropdown" data-toggle-area="token-incidence-modifiers" data-position="top" data-tooltip="Incidence Modifiers are special characters or patterns, <br>which determine how often a character represented previously should occur.">incidence modifiers</a> + <a class="btn-small waves-effect waves-light tooltipped modal-trigger" href="#corpus-analysis-concordance-positional-attr-modal" data-position="bottom" data-tooltip="Search for any token, for example a word, a lemma or a part-of-speech tag">Add new token to your query</a> + <a class="btn-small waves-effect waves-light tooltipped modal-trigger" href="#corpus-analysis-concordance-structural-attr-modal" data-position="bottom" data-tooltip="Structure your query with structural attributes, for example sentences, entities or annotate the text">Add structural attributes to your query</a> + <a class="btn-small waves-effect waves-light tooltipped dropdown-trigger disabled" data-target="corpus-analysis-concordance-token-incidence-modifiers-dropdown" data-toggle-area="token-incidence-modifiers" data-position="top" data-tooltip="Incidence Modifiers are special characters or patterns, <br>which determine how often a character represented previously should occur.">incidence modifiers</a> </div> </div> <div class="row">