Compare commits

...

2 Commits

Author SHA1 Message Date
Inga Kirschnick
c5aea0be94 Merge branch 'query-builder' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into query-builder 2023-11-22 12:50:18 +01:00
Inga Kirschnick
afcb890ccf Element Target drag&drop + small improvements 2023-11-22 12:50:08 +01:00
4 changed files with 119 additions and 86 deletions

View File

@ -3,8 +3,10 @@ nopaque.corpus_analysis.query_builder.ElementReferences = class ElementReference
// General Elements // General Elements
this.queryInputField = document.querySelector('#corpus-analysis-concordance-query-builder-input-field'); this.queryInputField = document.querySelector('#corpus-analysis-concordance-query-builder-input-field');
this.queryChipElements = []; this.queryChipElements = [];
this.queryElementTarget = document.querySelector('.query-element-target')
this.editingModusOn = false; this.editingModusOn = false;
this.editedQueryChipElementIndex = undefined; this.editedQueryChipElementIndex = undefined;
this.deleteQueryButton = document.querySelector('#corpus-analysis-concordance-delete-query-button');
// Structural Attribute Builder Elements // Structural Attribute Builder Elements
this.structuralAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-structural-attr-modal')); this.structuralAttrModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-structural-attr-modal'));

View File

@ -2,25 +2,13 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
constructor() { constructor() {
this.elements = new nopaque.corpus_analysis.query_builder.ElementReferences(); this.elements = new nopaque.corpus_analysis.query_builder.ElementReferences();
this.incidenceModifierEventListeners(); this.addEventListenersToQueryElementTarget();
this.nAndMInputSubmitEventListeners(); this.addEventListenersToIncidenceModifier();
this.addEventListenersToNAndMInputSubmit();
let queryBuilderDisplay = document.querySelector("#corpus-analysis-concordance-query-builder-display"); this.elements.deleteQueryButton.addEventListener('click', () => {this.resetQueryInputField()});
let expertModeDisplay = document.querySelector("#corpus-analysis-concordance-expert-mode-display");
let expertModeSwitch = document.querySelector("#corpus-analysis-concordance-expert-mode-switch"); this.expertModeQueryBuilderSwitchHandler();
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.extensions = { this.extensions = {
structuralAttributeBuilderFunctions: new nopaque.corpus_analysis.query_builder.StructuralAttributeBuilderFunctions(this), 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) { toggleClass(elements, className, action) {
elements.forEach(element => { elements.forEach(element => {
document.querySelector(`[data-toggle-area="${element}"]`).classList[action](className); document.querySelector(`[data-toggle-area="${element}"]`).classList[action](className);
@ -36,26 +56,27 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
resetQueryInputField() { resetQueryInputField() {
this.elements.queryInputField.innerHTML = ''; this.elements.queryInputField.innerHTML = '';
this.addPlaceholder(); this.addQueryElementTarget();
this.updateChipList(); this.updateChipList();
this.queryPreviewBuilder(); this.queryPreviewBuilder();
} }
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();
}
updateChipList() { updateChipList() {
this.elements.queryChipElements = this.elements.queryInputField.querySelectorAll('.query-component'); 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 = 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);
}
resetMaterializeSelection(selectionElements, value = "default") { resetMaterializeSelection(selectionElements, value = "default") {
selectionElements.forEach(selectionElement => { selectionElements.forEach(selectionElement => {
@ -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}"> <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>': ''} ${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> </span>
` `
); );
this.addActionListeners(queryChipElement); this.addActionListeners(queryChipElement);
queryChipElement.addEventListener('dragstart', this.handleDragStart.bind(this, queryChipElement)); queryChipElement.addEventListener('dragstart', this.handleDragStart.bind(this, queryChipElement));
queryChipElement.addEventListener('dragend', this.handleDragEnd); queryChipElement.addEventListener('dragend', this.handleDragEnd);
// 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.
// 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. if (index !== null) {
this.removePlaceholder(); this.updateChipList();
if (!index) { this.elements.queryChipElements[index].after(queryChipElement);
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]);
} else { } else {
this.elements.queryInputField.appendChild(queryChipElement); this.elements.queryInputField.insertBefore(queryChipElement, this.elements.queryElementTarget);
}
if (isClosingTag) {
this.moveQueryElementTarget(queryChipElement);
} }
this.updateChipList(); this.updateChipList();
this.queryPreviewBuilder(); this.queryPreviewBuilder();
} }
moveQueryElementTarget(element) {
this.elements.queryInputField.insertBefore(this.elements.queryElementTarget, element);
}
addActionListeners(queryChipElement) { addActionListeners(queryChipElement) {
let notQuantifiableDataTypes = ['start-sentence', 'end-sentence', 'start-entity', 'start-empty-entity', 'end-entity', 'token-incidence-modifier']; let notQuantifiableDataTypes = ['start-sentence', 'end-sentence', 'start-entity', 'start-empty-entity', 'end-entity', 'token-incidence-modifier'];
queryChipElement.addEventListener('click', (event) => { 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) { deleteChipElement(attr) {
let elementIndex = Array.from(this.elements.queryInputField.children).indexOf(attr); let elementIndex = Array.from(this.elements.queryInputField.children).indexOf(attr);
switch (attr.dataset.type) { switch (attr.dataset.type) {
case 'start-sentence': case 'start-sentence':
this.deleteClosingTagHandler(elementIndex, 'end-sentence'); this.deleteClosingTagHandler(elementIndex, 'end-sentence');
break; break;
case 'start-empty-entity':
case 'start-entity': case 'start-entity':
this.deleteClosingTagHandler(elementIndex, 'end-entity'); this.deleteClosingTagHandler(elementIndex, 'end-entity');
break; break;
@ -180,9 +194,6 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
break; break;
} }
this.elements.queryInputField.removeChild(attr); this.elements.queryInputField.removeChild(attr);
if (this.elements.queryInputField.children.length === 0) {
this.addPlaceholder();
}
this.updateChipList(); this.updateChipList();
this.queryPreviewBuilder(); this.queryPreviewBuilder();
} }
@ -217,13 +228,17 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
let targetChipClone = targetChipElement.cloneNode(true); let targetChipClone = targetChipElement.cloneNode(true);
element.insertAdjacentElement('afterend', targetChipClone); element.insertAdjacentElement('afterend', targetChipClone);
//TODO: Change to two different functions for drag and drop
this.addDragDropListeners(targetChipClone, queryChipElement); this.addDragDropListeners(targetChipClone, queryChipElement);
} }
}, 0); }, 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()); document.querySelectorAll('.drop-target').forEach(target => target.remove());
} }
@ -292,8 +307,8 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
tokenIncidenceModifierHandler(incidenceModifier, incidenceModifierPretty) { tokenIncidenceModifierHandler(incidenceModifier, incidenceModifierPretty) {
// Adds a token incidence modifier to the query input field. // Adds a token incidence modifier to the query input field.
let selectedChip = this.elements.queryInputField.querySelector('.chip.teal'); let selectedChip = this.elements.queryInputField.querySelector('.chip.teal');
let selectedChipIndex = Array.from(this.elements.queryInputField.children).indexOf(selectedChip); let selectedChipIndex = Array.from(this.elements.queryChipElements).indexOf(selectedChip);
this.submitQueryChipElement('token-incidence-modifier', incidenceModifierPretty, incidenceModifier, selectedChipIndex+1); this.submitQueryChipElement('token-incidence-modifier', incidenceModifierPretty, incidenceModifier, selectedChipIndex);
this.selectChipElement(selectedChip); this.selectChipElement(selectedChip);
} }
@ -315,26 +330,27 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
this.tokenIncidenceModifierHandler(input, pretty_input); this.tokenIncidenceModifierHandler(input, pretty_input);
} }
incidenceModifierEventListeners() { expertModeQueryBuilderSwitchHandler() {
// Eventlisteners for the incidence modifiers. There are two different types of incidence modifiers: token and character incidence modifiers. let queryBuilderDisplay = document.querySelector("#corpus-analysis-concordance-query-builder-display");
document.querySelectorAll('.incidence-modifier-selection').forEach(button => { let expertModeDisplay = document.querySelector("#corpus-analysis-concordance-expert-mode-display");
let dropdownId = button.parentNode.parentNode.id; let expertModeSwitch = document.querySelector("#corpus-analysis-concordance-expert-mode-switch");
if (dropdownId === 'corpus-analysis-concordance-token-incidence-modifiers-dropdown') { let submitModal = M.Modal.getInstance(document.querySelector('#corpus-analysis-concordance-switch-to-query-builder-submit-modal'));
button.addEventListener('click', () => this.tokenIncidenceModifierHandler(button.dataset.token, button.innerHTML));
} else if (dropdownId === 'corpus-analysis-concordance-character-incidence-modifiers-dropdown') { let confirmSwitchToQueryBuilderButton = document.querySelector('.switch-action[data-switch-action="confirm"]');
button.addEventListener('click', () => this.extensions.tokenAttributeBuilderFunctions.characterIncidenceModifierHandler(button)); confirmSwitchToQueryBuilderButton.addEventListener("click", () => {
} queryBuilderDisplay.classList.remove("hide");
expertModeDisplay.classList.add("hide");
this.switchToQueryBuilderParser();
}); });
}
nAndMInputSubmitEventListeners() { expertModeSwitch.addEventListener("change", () => {
// Eventlisteners for the submit of n- and m-values of the incidence modifier modal for "exactly n" or "between n and m". const isChecked = expertModeSwitch.checked;
document.querySelectorAll('.n-m-submit-button').forEach(button => { if (isChecked) {
let modalId = button.dataset.modalId; queryBuilderDisplay.classList.add("hide");
if (modalId === 'corpus-analysis-concordance-exactly-n-token-modal' || modalId === 'corpus-analysis-concordance-between-nm-token-modal') { expertModeDisplay.classList.remove("hide");
button.addEventListener('click', () => this.tokenNMSubmitHandler(modalId)); this.switchToExpertModeParser();
} else if (modalId === 'corpus-analysis-concordance-exactly-n-character-modal' || modalId === 'corpus-analysis-concordance-between-nm-character-modal') { } else {
button.addEventListener('click', () => this.extensions.tokenAttributeBuilderFunctions.characterNMSubmitHandler(modalId)); 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 expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value;
let chipElements = this.parseTextToChip(expertModeInputFieldValue); let chipElements = this.parseTextToChip(expertModeInputFieldValue);
let closingTagElements = ['end-sentence', 'end-entity']; let closingTagElements = ['end-sentence', 'end-entity'];
let editableElements = ['start-entity', 'token']; let editableElements = ['start-entity', 'token'];
for (let chipElement of chipElements) { for (let chipElement of chipElements) {
let isClosingTag = closingTagElements.includes(chipElement['type']); let isClosingTag = closingTagElements.includes(chipElement['type']);
let isEditable = editableElements.includes(chipElement['type']); let isEditable = editableElements.includes(chipElement['type']);
@ -361,9 +377,6 @@ nopaque.corpus_analysis.query_builder.QueryBuilder = class QueryBuilder {
isEditable = false; isEditable = false;
} }
this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable); this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query'], null, isClosingTag, isEditable);
if (isClosingTag) {
this.lockClosingChipElement(this.elements.queryChipElements[this.elements.queryChipElements.length-1]);
}
} }
} }

View File

@ -23,4 +23,15 @@
</div> </div>
</form> </form>
</div> </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 %} {% endmacro %}

View File

@ -1,11 +1,18 @@
{% macro card_content(id_prefix) %} {% macro card_content(id_prefix) %}
<form id="corpus-analysis-concordance-query-builder-form"> <form id="corpus-analysis-concordance-query-builder-form">
<div class="row"> <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"> <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> </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"> <div class="input-field col s3">
<i class="material-icons prefix">arrow_forward</i> <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> <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="row">
<div class="col s12"> <div class="col s12">
<p></p> <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-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 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 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 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> </div>
<div class="row"> <div class="row">