Merge branch 'query-builder' of gitlab.ub.uni-bielefeld.de:sfb1288inf/nopaque into query-builder

This commit is contained in:
Patrick Jentsch 2023-11-09 14:30:10 +01:00
commit e9ddb85f03
8 changed files with 403 additions and 362 deletions

View File

@ -1,52 +0,0 @@
class ConcordanceQueryBuilder {
constructor() {
this.elements = new ElementReferencesQueryBuilder();
this.generalFunctions = new GeneralFunctionsQueryBuilder(this.elements);
this.tokenAttributeBuilderFunctions = new TokenAttributeBuilderFunctionsQueryBuilder(this.elements);
this.structuralAttributeBuilderFunctions = new StructuralAttributeBuilderFunctionsQueryBuilder(this.elements);
// 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));
}
});
// 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));
}
});
document.querySelector('#corpus-analysis-concordance-text-annotation-submit').addEventListener('click', () => this.structuralAttributeBuilderFunctions.textAnnotationSubmitHandler());
this.elements.positionalAttrModal = M.Modal.init(
document.querySelector('#corpus-analysis-concordance-positional-attr-modal'),
{
onOpenStart: () => {
this.tokenAttributeBuilderFunctions.preparePositionalAttrModal();
},
onCloseStart: () => {
this.tokenAttributeBuilderFunctions.resetPositionalAttrModal();
}
}
);
this.elements.structuralAttrModal = M.Modal.init(
document.querySelector('#corpus-analysis-concordance-structural-attr-modal'),
{
onCloseStart: () => {
this.structuralAttributeBuilderFunctions.resetStructuralAttrModal();
}
}
);
}
}

View File

@ -1,4 +1,4 @@
class GeneralFunctionsQueryBuilder { class GeneralQueryBuilderFunctions {
constructor(elements) { constructor(elements) {
this.elements = elements; this.elements = elements;
} }
@ -82,9 +82,10 @@ class GeneralFunctionsQueryBuilder {
index = Array.from(this.elements.queryInputField.children).indexOf(closingTagElement); index = Array.from(this.elements.queryInputField.children).indexOf(closingTagElement);
} }
} }
if (index || isLastChildTextAnnotation) { if (dataType !== 'text-annotation' && index) {
let insertingElement = isLastChildTextAnnotation ? lastChild : this.elements.queryChipElements[index]; this.elements.queryInputField.insertBefore(queryChipElement, this.elements.queryChipElements[index]);
this.elements.queryInputField.insertBefore(queryChipElement, insertingElement); } else if (dataType !== 'text-annotation' && isLastChildTextAnnotation) {
this.elements.queryInputField.insertBefore(queryChipElement, lastChild);
} else { } else {
this.elements.queryInputField.appendChild(queryChipElement); this.elements.queryInputField.appendChild(queryChipElement);
} }
@ -103,7 +104,8 @@ class GeneralFunctionsQueryBuilder {
} }
}); });
let chipActionButtons = queryChipElement.querySelectorAll('.chip-action-button'); let chipActionButtons = queryChipElement.querySelectorAll('.chip-action-button');
chipActionButtons.forEach(button => { // chipActionButtons.forEach(button => {
for (let button of chipActionButtons) {
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
if (event.target.dataset.chipAction === 'delete') { if (event.target.dataset.chipAction === 'delete') {
this.deleteChipElement(queryChipElement); this.deleteChipElement(queryChipElement);
@ -113,62 +115,22 @@ class GeneralFunctionsQueryBuilder {
this.lockClosingChipElement(queryChipElement); this.lockClosingChipElement(queryChipElement);
} }
}); });
}); // });
}
} }
editChipElement(queryChipElement) { editChipElement(queryChipElement) {
//TODO: Split this function into smaller functionss
this.elements.editingModusOn = true; this.elements.editingModusOn = true;
this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement); this.elements.editedQueryChipElementIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
switch (queryChipElement.dataset.type) { switch (queryChipElement.dataset.type) {
case 'start-entity': case 'start-entity':
this.elements.structuralAttrModal.open(); this.editStartEntityChipElement(queryChipElement);
this.toggleClass(['entity-builder'], 'hide', 'remove');
this.toggleEditingAreaStructureAttrModal('add');
let entType = queryChipElement.dataset.query.replace(/<ent_type="|">/g, '');
let isEnglishEntType = this.elements.englishEntTypeSelection.querySelector(`option[value=${entType}]`) !== null;
let selection = isEnglishEntType ? this.elements.englishEntTypeSelection : this.elements.germanEntTypeSelection;
this.resetMaterializeSelection([selection], entType);
break; break;
case 'text-annotation': case 'text-annotation':
this.elements.structuralAttrModal.open(); this.editTextAnnotationChipElement(queryChipElement);
this.toggleClass(['text-annotation-builder'], 'hide', 'remove');
this.toggleEditingAreaStructureAttrModal('add');
let [textAnnotationSelection, textAnnotationContent] = queryChipElement.dataset.query
.replace(/:: ?match\.text_|"|"/g, '')
.split('=');
this.resetMaterializeSelection([this.elements.textAnnotationSelection], textAnnotationSelection);
this.elements.textAnnotationInput.value = textAnnotationContent;
break; break;
case 'token': case 'token':
//This regex searches for word or lemma or pos or simple_pos="any string within single or double quotes" followed by one or no ignore case markers, followed by one or no condition characters. let queryElementsContent = this.prepareQueryElementsContent(queryChipElement);
let regex = new RegExp('(word|lemma|pos|simple_pos)=(("[^"]+")|(\\\\u0027[^\\\\u0027]+\\\\u0027)) ?(%c)? ?(\\&|\\|)?', 'gm');
let m;
let queryElementsContent = [];
while ((m = regex.exec(queryChipElement.dataset.query)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
let tokenAttr = m[1];
// Passes english-pos by default so that the template is added. In editTokenChipElement it is then checked whether it is english-pos or german-pos.
if (tokenAttr === 'pos') {
tokenAttr = 'english-pos';
}
let tokenValue = m[2].replace(/"|'/g, '');
let ignoreCase = false;
let condition = undefined;
m.forEach((match) => {
if (match === "%c") {
ignoreCase = true;
} else if (match === "&") {
condition = "and";
} else if (match === "|") {
condition = "or";
}
});
queryElementsContent.push({tokenAttr: tokenAttr, tokenValue: tokenValue, ignoreCase: ignoreCase, condition: condition});
}
this.editTokenChipElement(queryElementsContent); this.editTokenChipElement(queryElementsContent);
break; break;
default: default:
@ -176,6 +138,59 @@ class GeneralFunctionsQueryBuilder {
} }
} }
editStartEntityChipElement(queryChipElement) {
this.elements.structuralAttrModal.open();
this.toggleClass(['entity-builder'], 'hide', 'remove');
this.toggleEditingAreaStructuralAttrModal('add');
let entType = queryChipElement.dataset.query.replace(/<ent_type="|">/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) { editTokenChipElement(queryElementsContent) {
this.elements.positionalAttrModal.open(); this.elements.positionalAttrModal.open();
queryElementsContent.forEach((queryElement) => { queryElementsContent.forEach((queryElement) => {
@ -207,7 +222,6 @@ class GeneralFunctionsQueryBuilder {
} }
}); });
} }
lockClosingChipElement(queryChipElement) { lockClosingChipElement(queryChipElement) {
@ -216,11 +230,6 @@ class GeneralFunctionsQueryBuilder {
lockIcon.textContent = 'lock'; lockIcon.textContent = 'lock';
//TODO: Write unlock-Function? //TODO: Write unlock-Function?
lockIcon.dataset.chipAction = 'unlock'; lockIcon.dataset.chipAction = 'unlock';
// let chipIndex = Array.from(this.elements.queryInputField.children).indexOf(queryChipElement);
// this.submitQueryChipElement(queryChipElement.dataset.type, queryChipElement.firstChild.textContent, queryChipElement.dataset.query, chipIndex+1);
// this.deleteChipElement(queryChipElement);
// this.updateChipList();
} }
deleteChipElement(attr) { deleteChipElement(attr) {
@ -233,9 +242,8 @@ class GeneralFunctionsQueryBuilder {
this.deletingClosingTagHandler(elementIndex, 'end-entity'); this.deletingClosingTagHandler(elementIndex, 'end-entity');
break; break;
case 'token': case 'token':
console.log(Array.from(this.elements.queryInputField.children)[elementIndex+1]);
let nextElement = Array.from(this.elements.queryInputField.children)[elementIndex+1]; let nextElement = Array.from(this.elements.queryInputField.children)[elementIndex+1];
if (nextElement.dataset.type === 'token-incidence-modifier') { if (nextElement !== undefined && nextElement.dataset.type === 'token-incidence-modifier') {
this.deleteChipElement(nextElement); this.deleteChipElement(nextElement);
} }
default: default:
@ -264,16 +272,22 @@ class GeneralFunctionsQueryBuilder {
handleDragStart(queryChipElement, event) { handleDragStart(queryChipElement, event) {
// is called when a query chip is dragged. It creates a dropzone (in form of a chip) for the dragged chip and adds it to the query input field. // is called when a query chip is dragged. It creates a dropzone (in form of a chip) for the dragged chip and adds it to the query input field.
let queryChips = this.elements.queryInputField.querySelectorAll('.query-component'); let queryChips = this.elements.queryInputField.querySelectorAll('.query-component');
if (queryChipElement.dataset.type === 'token-incidence-modifier') {
queryChips = this.elements.queryInputField.querySelectorAll('.query-component[data-type="token"]');
}
setTimeout(() => { setTimeout(() => {
let targetChipElement = nopaque.Utils.HTMLToElement('<span class="chip drop-target">Drop here</span>'); let targetChipElement = nopaque.Utils.HTMLToElement('<span class="chip drop-target">Drop here</span>');
for (let element of queryChips) { for (let element of queryChips) {
if (element === queryChipElement.nextSibling) {continue;} if (element === this.elements.queryInputField.querySelectorAll('.query-component')[0]) {
let targetChipClone = targetChipElement.cloneNode(true); let secondTargetChipClone = targetChipElement.cloneNode(true);
if (element === queryChipElement && queryChips[queryChips.length - 1] !== element) { element.insertAdjacentElement('beforebegin', secondTargetChipClone);
queryChips[queryChips.length - 1].insertAdjacentElement('afterend', targetChipClone); this.addDragDropListeners(secondTargetChipClone, queryChipElement);
} else {
element.insertAdjacentElement('beforebegin', targetChipClone);
} }
if (element === queryChipElement || element.nextSibling === queryChipElement) {continue;}
let targetChipClone = targetChipElement.cloneNode(true);
element.insertAdjacentElement('afterend', targetChipClone);
this.addDragDropListeners(targetChipClone, queryChipElement); this.addDragDropListeners(targetChipClone, queryChipElement);
} }
}, 0); }, 0);
@ -371,135 +385,111 @@ class GeneralFunctionsQueryBuilder {
this.tokenIncidenceModifierHandler(input, pretty_input); this.tokenIncidenceModifierHandler(input, pretty_input);
} }
switchToExpertModeParser() { //#region Functions from other classes
let expertModeInputField = document.querySelector('#corpus-analysis-concordance-form-query');
expertModeInputField.value = ''; //TODO: Move these functions back to their og classes and make it work.
let queryBuilderInputFieldValue = nopaque.Utils.unescape(document.querySelector('#corpus-analysis-concordance-query-preview').innerHTML.trim());
if (queryBuilderInputFieldValue !== "" && queryBuilderInputFieldValue !== ";") { toggleEditingAreaStructuralAttrModal(action) {
expertModeInputField.value = queryBuilderInputFieldValue; // If the user edits a query chip element, the corresponding editing area is displayed and the other areas are hidden or disabled.
this.toggleClass(['sentence-button', 'entity-button', 'text-annotation-button', 'any-type-entity-button'], 'disabled', action);
}
preparePositionalAttrModal() {
let selection = this.elements.positionalAttrSelection.value;
if (selection !== 'empty-token') {
let selectionTemplate = document.querySelector(`.token-builder-section[data-token-builder-section="${selection}"]`);
let selectionTemplateClone = selectionTemplate.content.cloneNode(true);
this.elements.tokenBuilderContent.innerHTML = '';
this.elements.tokenBuilderContent.appendChild(selectionTemplateClone);
if (this.elements.tokenBuilderContent.querySelector('select') !== null) {
let selectElement = this.elements.tokenBuilderContent.querySelector('select');
M.FormSelect.init(selectElement);
selectElement.addEventListener('change', () => {this.optionToggleHandler();});
} else {
this.elements.tokenBuilderContent.querySelector('input').addEventListener('input', () => {this.optionToggleHandler();});
}
}
this.optionToggleHandler();
if (selection === 'word' || selection === 'lemma') {
this.toggleClass(['input-field-options'], 'hide', 'remove');
} else if (selection === 'empty-token'){
this.addTokenToQuery();
} else {
this.toggleClass(['input-field-options'], 'hide', 'add');
} }
} }
switchToQueryBuilderParser() { tokenInputCheck(elem) {
this.resetQueryInputField(); return elem.querySelector('select') !== null ? elem.querySelector('select') : elem.querySelector('input');
let expertModeInputFieldValue = document.querySelector('#corpus-analysis-concordance-form-query').value; }
let chipElements = this.parseTextToChip(expertModeInputFieldValue);
for (let chipElement of chipElements) { optionToggleHandler() {
this.submitQueryChipElement(chipElement['type'], chipElement['pretty'], chipElement['query']); let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
if (input.value === '' && this.elements.editingModusOn === false) {
this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
} else if (this.elements.positionalAttrSelection.querySelectorAll('option').length === 1) {
this.toggleClass(['and'], 'disabled', 'add');
this.toggleClass(['or'], 'disabled', 'remove');
} else {
this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'remove');
} }
} }
parseTextToChip(query) { addTokenToQuery() {
const parsingElementDict = { let tokenQueryPrettyText = '';
'<s>': { let tokenQueryCQLText = '';
pretty: 'Sentence Start', let input;
type: 'start-sentence' let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value);
},
'<\/s>': { // Takes all rows of the token query (if there is a query concatenation).
pretty: 'Sentence End', // Adds their contents to tokenQueryPrettyText and tokenQueryCQLText, which will later be expanded with the current input field.
type: 'end-sentence' let tokenQueryRows = this.elements.tokenQuery.querySelectorAll('.row');
}, tokenQueryRows.forEach(row => {
'<ent>': { let ignoreCaseCheckbox = row.querySelector('input[type="checkbox"]');
pretty: 'Entity Start', let c = ignoreCaseCheckbox !== null && ignoreCaseCheckbox.checked ? ' %c' : '';
type: 'start-empty-entity' let tokenQueryRowInput = this.tokenInputCheck(row.querySelector('.token-query-template-content'));
}, let tokenQueryKindOfToken = this.kindOfTokenCheck(tokenQueryRowInput.closest('.input-field').dataset.kindOfToken);
'<ent_type="([A-Z]+)">': { let tokenConditionPrettyText = row.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
pretty: '', let tokenConditionCQLText = row.querySelector('[data-condition-cql-text]').dataset.conditionCqlText;
type: 'start-entity' tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `;
}, tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`;
'<\\\/ent(_type)?>': { });
pretty: 'Entity End', if (kindOfToken === 'empty-token') {
type: 'end-entity' tokenQueryPrettyText += 'empty token';
}, } else {
':: ?match\\.text_[A-Za-z]+="[^"]+"': { let c = this.elements.ignoreCaseCheckbox.checked ? ' %c' : '';
pretty: '', input = this.tokenInputCheck(this.elements.tokenBuilderContent);
type: 'text-annotation' tokenQueryPrettyText += `${kindOfToken}=${input.value}${c}`;
}, tokenQueryCQLText += `${kindOfToken}="${input.value}"${c}`;
'\\[(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'
},
'(?<!\\[) ?\\+ ?(?![^\\]]\\])': {
pretty: ' one or more (+)',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\* ?(?![^\\]]\\])': {
pretty: 'zero or more (*)',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\? ?(?![^\\]]\\])': {
pretty: 'zero or one (?)',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])': {
pretty: '',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])': {
pretty: '',
type: 'token-incidence-modifier'
}
} }
// isTokenQueryInvalid looks if a valid value is passed. If the input fields/dropdowns are empty (isTokenQueryInvalid === true), no token is added.
let chipElements = []; if (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') {
let regexPattern = Object.keys(parsingElementDict).map(pattern => `(${pattern})`).join('|'); this.disableTokenSubmit();
const regex = new RegExp(regexPattern, 'gi'); } else {
let match; tokenQueryCQLText = `[${tokenQueryCQLText}]`;
this.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, kindOfToken === 'empty-token' ? false : true);
while ((match = regex.exec(query)) !== null) { this.elements.positionalAttrModal.close();
// 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 '<ent_type="([A-Z]+)">':
prettyText = `Entity Type=${stringElement.replace(/<ent_type="|">/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 '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])':
prettyText = `exactly ${stringElement.replace(/{|}/g, '')} (${stringElement})`;
break;
case '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])':
prettyText = `between${stringElement.replace(/{|}/g, ' ').replace(',', ' and ')}(${stringElement})`;
break;
default:
prettyText = chipElement.pretty;
break;
}
chipElements.push({
type: chipElement.type,
pretty: prettyText,
query: stringElement
});
break;
}
}
} }
return chipElements;
} }
kindOfTokenCheck(kindOfToken) {
return kindOfToken === 'english-pos' || kindOfToken === 'german-pos' ? 'pos' : kindOfToken;
}
disableTokenSubmit() {
this.elements.tokenSubmitButton.classList.add('red');
this.elements.noValueMessage.classList.remove('hide');
setTimeout(() => {
this.elements.tokenSubmitButton.classList.remove('red');
}, 500);
setTimeout(() => {
this.elements.noValueMessage.classList.add('hide');
}, 3000);
}
//#endregion Functions from other classes
} }

View File

@ -0,0 +1,192 @@
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");
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 = {
'<s>': {
pretty: 'Sentence Start',
type: 'start-sentence'
},
'<\/s>': {
pretty: 'Sentence End',
type: 'end-sentence'
},
'<ent>': {
pretty: 'Entity Start',
type: 'start-empty-entity'
},
'<ent_type="([A-Z]+)">': {
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'
},
'(?<!\\[) ?\\+ ?(?![^\\]]\\])': {
pretty: ' one or more (+)',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\* ?(?![^\\]]\\])': {
pretty: 'zero or more (*)',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\? ?(?![^\\]]\\])': {
pretty: 'zero or one (?)',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])': {
pretty: '',
type: 'token-incidence-modifier'
},
'(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])': {
pretty: '',
type: 'token-incidence-modifier'
}
}
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 '<ent_type="([A-Z]+)">':
prettyText = `Entity Type=${stringElement.replace(/<ent_type="|">/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 '(?<!\\[) ?\\{[0-9]+} ?(?![^\\]]\\])':
prettyText = `exactly ${stringElement.replace(/{|}/g, '')} (${stringElement})`;
break;
case '(?<!\\[) ?\\{[0-9]+(,[0-9]+)?} ?(?![^\\]]\\])':
prettyText = `between${stringElement.replace(/{|}/g, ' ').replace(',', ' and ')}(${stringElement})`;
break;
default:
prettyText = chipElement.pretty;
break;
}
chipElements.push({
type: chipElement.type,
pretty: prettyText,
query: stringElement
});
break;
}
}
}
return chipElements;
}
}

View File

@ -1,8 +1,22 @@
class StructuralAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBuilder { class StructuralAttributeBuilderFunctions extends GeneralQueryBuilderFunctions {
constructor(elements) { constructor(elements) {
super(elements); super(elements);
this.elements = elements;
this.structuralAttrModalEventlisteners();
document.querySelector('#corpus-analysis-concordance-text-annotation-submit').addEventListener('click', () => this.textAnnotationSubmitHandler());
this.elements.structuralAttrModal = M.Modal.init(
document.querySelector('#corpus-analysis-concordance-structural-attr-modal'),
{
onCloseStart: () => {
this.resetStructuralAttrModal();
}
}
);
}
structuralAttrModalEventlisteners() {
document.querySelectorAll('[data-structural-attr-modal-action-button]').forEach(button => { document.querySelectorAll('[data-structural-attr-modal-action-button]').forEach(button => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
this.actionButtonInStrucAttrModalHandler(button.dataset.structuralAttrModalActionButton); this.actionButtonInStrucAttrModalHandler(button.dataset.structuralAttrModalActionButton);
@ -35,16 +49,11 @@ class StructuralAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQu
this.elements.textAnnotationInput.value = ''; this.elements.textAnnotationInput.value = '';
this.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add'); this.toggleClass(['entity-builder', 'text-annotation-builder'], 'hide', 'add');
this.toggleEditingAreaStructureAttrModal('remove'); this.toggleEditingAreaStructuralAttrModal('remove');
this.elements.editingModusOn = false; this.elements.editingModusOn = false;
this.elements.editedQueryChipElementIndex = undefined; this.elements.editedQueryChipElementIndex = undefined;
} }
toggleEditingAreaStructureAttrModal(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);
}
actionButtonInStrucAttrModalHandler(action) { actionButtonInStrucAttrModalHandler(action) {
switch (action) { switch (action) {
case 'sentence': case 'sentence':

View File

@ -1,7 +1,6 @@
class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBuilder { class TokenAttributeBuilderFunctions extends GeneralQueryBuilderFunctions {
constructor(elements) { constructor(elements) {
super(elements); super(elements);
this.elements = elements;
this.elements.positionalAttrSelection.addEventListener('change', () => { this.elements.positionalAttrSelection.addEventListener('change', () => {
this.preparePositionalAttrModal(); this.preparePositionalAttrModal();
@ -13,6 +12,18 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
}); });
this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();}); this.elements.tokenSubmitButton.addEventListener('click', () => {this.addTokenToQuery();});
this.elements.positionalAttrModal = M.Modal.init(
document.querySelector('#corpus-analysis-concordance-positional-attr-modal'),
{
onOpenStart: () => {
this.preparePositionalAttrModal();
},
onCloseStart: () => {
this.resetPositionalAttrModal();
}
}
);
} }
resetPositionalAttrModal() { resetPositionalAttrModal() {
@ -35,100 +46,6 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
this.elements.editingModusOn = false; this.elements.editingModusOn = false;
this.elements.editedQueryChipElementIndex = undefined; this.elements.editedQueryChipElementIndex = undefined;
} }
preparePositionalAttrModal() {
let selection = this.elements.positionalAttrSelection.value;
if (selection !== 'empty-token') {
let selectionTemplate = document.querySelector(`.token-builder-section[data-token-builder-section="${selection}"]`);
let selectionTemplateClone = selectionTemplate.content.cloneNode(true);
this.elements.tokenBuilderContent.innerHTML = '';
this.elements.tokenBuilderContent.appendChild(selectionTemplateClone);
if (this.elements.tokenBuilderContent.querySelector('select') !== null) {
let selectElement = this.elements.tokenBuilderContent.querySelector('select');
M.FormSelect.init(selectElement);
selectElement.addEventListener('change', () => {this.optionToggleHandler();});
} else {
this.elements.tokenBuilderContent.querySelector('input').addEventListener('input', () => {this.optionToggleHandler();});
}
}
this.optionToggleHandler();
if (selection === 'word' || selection === 'lemma') {
this.toggleClass(['input-field-options'], 'hide', 'remove');
} else if (selection === 'empty-token'){
this.addTokenToQuery();
} else {
this.toggleClass(['input-field-options'], 'hide', 'add');
}
}
tokenInputCheck(elem) {
return elem.querySelector('select') !== null ? elem.querySelector('select') : elem.querySelector('input');
}
optionToggleHandler() {
let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
if (input.value === '' && this.elements.editingModusOn === false) {
this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'add');
} else if (this.elements.positionalAttrSelection.querySelectorAll('option').length === 1) {
this.toggleClass(['and'], 'disabled', 'add');
} else {
this.toggleClass(['incidence-modifiers', 'or', 'and'], 'disabled', 'remove');
}
}
disableTokenSubmit() {
this.elements.tokenSubmitButton.classList.add('red');
this.elements.noValueMessage.classList.remove('hide');
setTimeout(() => {
this.elements.tokenSubmitButton.classList.remove('red');
}, 500);
setTimeout(() => {
this.elements.noValueMessage.classList.add('hide');
}, 3000);
}
addTokenToQuery() {
let tokenQueryPrettyText = '';
let tokenQueryCQLText = '';
let input;
let kindOfToken = this.kindOfTokenCheck(this.elements.positionalAttrSelection.value);
// Takes all rows of the token query (if there is a query concatenation).
// Adds their contents to tokenQueryPrettyText and tokenQueryCQLText, which will later be expanded with the current input field.
let tokenQueryRows = this.elements.tokenQuery.querySelectorAll('.row');
tokenQueryRows.forEach(row => {
let ignoreCaseCheckbox = row.querySelector('input[type="checkbox"]');
let c = ignoreCaseCheckbox !== null && ignoreCaseCheckbox.checked ? ' %c' : '';
let tokenQueryRowInput = this.tokenInputCheck(row.querySelector('.token-query-template-content'));
let tokenQueryKindOfToken = this.kindOfTokenCheck(tokenQueryRowInput.closest('.input-field').dataset.kindOfToken);
let tokenConditionPrettyText = row.querySelector('[data-condition-pretty-text]').dataset.conditionPrettyText;
let tokenConditionCQLText = row.querySelector('[data-condition-cql-text]').dataset.conditionCqlText;
tokenQueryPrettyText += `${tokenQueryKindOfToken}=${tokenQueryRowInput.value}${c} ${tokenConditionPrettyText} `;
tokenQueryCQLText += `${tokenQueryKindOfToken}="${tokenQueryRowInput.value}"${c} ${tokenConditionCQLText}`;
});
if (kindOfToken === 'empty-token') {
tokenQueryPrettyText += 'empty token';
} else {
let c = this.elements.ignoreCaseCheckbox.checked ? ' %c' : '';
input = this.tokenInputCheck(this.elements.tokenBuilderContent);
tokenQueryPrettyText += `${kindOfToken}=${input.value}${c}`;
tokenQueryCQLText += `${kindOfToken}="${input.value}"${c}`;
}
// isTokenQueryInvalid looks if a valid value is passed. If the input fields/dropdowns are empty (isTokenQueryInvalid === true), no token is added.
if (this.elements.positionalAttrSelection.value !== 'empty-token' && input.value === '') {
this.disableTokenSubmit();
} else {
tokenQueryCQLText = `[${tokenQueryCQLText}]`;
this.submitQueryChipElement('token', tokenQueryPrettyText, tokenQueryCQLText, null, false, kindOfToken === 'empty-token' ? false : true);
this.elements.positionalAttrModal.close();
}
}
kindOfTokenCheck(kindOfToken) {
return kindOfToken === 'english-pos' || kindOfToken === 'german-pos' ? 'pos' : kindOfToken;
}
actionButtonInOptionSectionHandler(elem) { actionButtonInOptionSectionHandler(elem) {
let input = this.tokenInputCheck(this.elements.tokenBuilderContent); let input = this.tokenInputCheck(this.elements.tokenBuilderContent);
@ -173,7 +90,7 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
tokenInput.value += '{' + input + '}'; tokenInput.value += '{' + input + '}';
} }
conditionHandler(conditionText, editMode = false) { conditionHandler(conditionText) {
let tokenQueryTemplateClone = this.elements.tokenQueryTemplate.content.cloneNode(true); let tokenQueryTemplateClone = this.elements.tokenQueryTemplate.content.cloneNode(true);
tokenQueryTemplateClone.querySelector('.token-query-template-content').appendChild(this.elements.tokenBuilderContent.firstElementChild); tokenQueryTemplateClone.querySelector('.token-query-template-content').appendChild(this.elements.tokenBuilderContent.firstElementChild);
let notSelectedButton = tokenQueryTemplateClone.querySelector(`[data-condition-pretty-text]:not([data-condition-pretty-text="${conditionText}"])`); let notSelectedButton = tokenQueryTemplateClone.querySelector(`[data-condition-pretty-text]:not([data-condition-pretty-text="${conditionText}"])`);
@ -261,4 +178,5 @@ class TokenAttributeBuilderFunctionsQueryBuilder extends GeneralFunctionsQueryBu
this.resetMaterializeSelection([this.elements.positionalAttrSelection], selection); this.resetMaterializeSelection([this.elements.positionalAttrSelection], selection);
this.preparePositionalAttrModal(); this.preparePositionalAttrModal();
} }
} }

View File

@ -95,13 +95,13 @@
{%- assets {%- assets
filters='rjsmin', filters='rjsmin',
output='gen/CorpusAnalysis.%(version)s.js', output='gen/CorpusAnalysis.%(version)s.js',
'js/CorpusAnalysis/QueryBuilder/ElementReferencesQueryBuilder.js', 'js/CorpusAnalysis/query-builder/index.js',
'js/CorpusAnalysis/QueryBuilder/GeneralFunctionsQueryBuilder.js', 'js/CorpusAnalysis/query-builder/element-references.js',
'js/CorpusAnalysis/QueryBuilder/StructuralAttributeBuilderFunctionsQueryBuilder.js', 'js/CorpusAnalysis/query-builder/general-query-builder-functions.js',
'js/CorpusAnalysis/QueryBuilder/TokenAttributeBuilderFunctionsQueryBuilder.js', 'js/CorpusAnalysis/query-builder/structural-attribute-builder-functions.js',
'js/CorpusAnalysis/query-builder/token-attribute-builder-functions.js',
'js/CorpusAnalysis/CorpusAnalysisApp.js', 'js/CorpusAnalysis/CorpusAnalysisApp.js',
'js/CorpusAnalysis/CorpusAnalysisConcordance.js', 'js/CorpusAnalysis/CorpusAnalysisConcordance.js',
'js/CorpusAnalysis/QueryBuilder.js',
'js/CorpusAnalysis/CorpusAnalysisReader.js', 'js/CorpusAnalysis/CorpusAnalysisReader.js',
'js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js' 'js/CorpusAnalysis/CorpusAnalysisStaticVisualization.js'
%} %}

View File

@ -130,21 +130,5 @@
<script> <script>
const corpusAnalysisConcordance = new CorpusAnalysisConcordance(corpusAnalysisApp); const corpusAnalysisConcordance = new CorpusAnalysisConcordance(corpusAnalysisApp);
const concordanceQueryBuilder = new ConcordanceQueryBuilder(); const concordanceQueryBuilder = new ConcordanceQueryBuilder();
let queryBuilderDisplay = document.getElementById("corpus-analysis-concordance-query-builder-display");
let expertModeDisplay = document.getElementById("corpus-analysis-concordance-expert-mode-display");
let expertModeSwitch = document.getElementById("corpus-analysis-concordance-expert-mode-switch");
expertModeSwitch.addEventListener("change", function() {
if (this.checked) {
queryBuilderDisplay.classList.add("hide");
expertModeDisplay.classList.remove("hide");
concordanceQueryBuilder.generalFunctions.switchToExpertModeParser();
} else {
queryBuilderDisplay.classList.remove("hide");
expertModeDisplay.classList.add("hide");
concordanceQueryBuilder.generalFunctions.switchToQueryBuilderParser();
}
});
</script> </script>
{% endmacro %} {% endmacro %}