diff --git a/src/SquareEdit/Grouping.ts b/src/SquareEdit/Grouping.ts index db9d5597..0ece55f4 100644 --- a/src/SquareEdit/Grouping.ts +++ b/src/SquareEdit/Grouping.ts @@ -7,26 +7,26 @@ import NeonView from '../NeonView'; import { EditorAction, ToggleLigatureAction } from '../Types'; import { removeHandler, deleteButtonHandler } from './SelectOptions'; - /** * The NeonView parent to access editor actions. */ let neonView: NeonView; - /** * Set the neonView member. */ -export function initNeonView (view: NeonView): void { +export function initNeonView(view: NeonView): void { neonView = view; } - /** * Check if selected elements can be grouped or not * @returns true if can be grouped, false otherwise */ -export function isGroupable(selectionType: string, elements: Array): boolean { +export function isGroupable( + selectionType: string, + elements: Array, +): boolean { const groups = Array.from(elements.values()) as SVGGraphicsElement[]; if (groups.length < 2) { @@ -34,10 +34,11 @@ export function isGroupable(selectionType: string, elements: Array) { +function containsLinked( + selectionType: string, + elements?: Array, +) { if (!elements) { - elements = Array.from(document.querySelectorAll('.selected')) as SVGGraphicsElement[]; + elements = Array.from( + document.querySelectorAll('.selected'), + ) as SVGGraphicsElement[]; } switch (selectionType) { case 'selBySyllable': for (const element of elements) { - if (element.hasAttribute('mei:follows') || element.hasAttribute('mei:precedes')) { - Notification.queueNotification('The action involves linked syllables, please untoggle them first', 'warning'); + if ( + element.hasAttribute('mei:follows') || + element.hasAttribute('mei:precedes') + ) { + Notification.queueNotification( + 'The action involves linked syllables, please untoggle them first', + 'warning', + ); return true; } } return false; - + case 'selByNeume': for (const element of elements) { - if (element.parentElement.hasAttribute('mei:follows') || element.parentElement.hasAttribute('mei:precedes')) { - Notification.queueNotification('The action involves linked syllables, please untoggle them first', 'warning'); + if ( + element.parentElement.hasAttribute('mei:follows') || + element.parentElement.hasAttribute('mei:precedes') + ) { + Notification.queueNotification( + 'The action involves linked syllables, please untoggle them first', + 'warning', + ); return true; } } return false; - + case 'selByNc': for (const element of elements) { - if (element.parentElement.parentElement.hasAttribute('mei:follows') || element.parentElement.parentElement.hasAttribute('mei:precedes')) { - Notification.queueNotification('The action involves linked syllables, please untoggle them first', 'warning'); + if ( + element.parentElement.parentElement.hasAttribute('mei:follows') || + element.parentElement.parentElement.hasAttribute('mei:precedes') + ) { + Notification.queueNotification( + 'The action involves linked syllables, please untoggle them first', + 'warning', + ); return true; } } @@ -85,131 +107,254 @@ function containsLinked (selectionType:string, elements?: Array, +): boolean { + for (let idx = 0; idx < elements.length; idx++) { + const syllable = elements.at(idx); + if (syllable.hasAttribute('mei:precedes')) { + // Get xml:id of the next syllable (without the #, if it exists) + const nextSyllableId = syllable + .getAttribute('mei:precedes') + .replace('#', ''); + + // Find the next syllable and its index in the array + let nextSyllableIdx: number; + const nextSyllable = elements.find((element, idx) => { + if (element.id === nextSyllableId) { + nextSyllableIdx = idx; + return true; + } + + return false; + }); + + // Condition 1: The next (following) syllable cannot be found + if (!nextSyllable) { + return true; + } + + // Condition 2: The next syllable has been found, but the @follows attribute does NOT EXIST + if (!nextSyllable.hasAttribute('mei:follows')) { + return true; + } + + // Condition 3: The next syllable's @follows attribute exists, but it is not in the correct format #id + if (nextSyllable.getAttribute('mei:follows') != '#' + syllable.id) { + return true; + } + + // Condition 4: + // Since the @follows value is correct, a pair of syllables exist for the toggle-linked syllable. + // Check if the @follows syllable is the next syllable (index-wise) in the array: + if (nextSyllableIdx !== idx + 1) { + return true; + } + } + if (syllable.hasAttribute('mei:follows')) { + const prevSyllableId = syllable + .getAttribute('mei:follows') + .replace('#', ''); + const prevSyllable = elements.find( + (syllable) => syllable.id === prevSyllableId, + ); + + // Condition 1: The previous syllable does not exist + if (!prevSyllable) { + return true; + } + + // Condition 2: The previous syllable exists, but the @precedes attribute does NOT EXIST + if (!prevSyllable.hasAttribute('mei:precedes')) { + return true; + } + + // Condition 3: The previous syllable's @precedes attribute exists, but it is not in the correct format #id + if (prevSyllable.getAttribute('mei:precedes') != '#' + syllable.id) { + return true; + } + } + + return false; + } +} + /** * Checks to see is a selection of elements is already linked * @param elements elements to be considered * @returns true is linked, false otherwise */ export function isLinked(elements: Array): boolean { + // every element should be linked + for (const element of elements) { + if ( + !element.hasAttribute('mei:precedes') && + !element.hasAttribute('mei:follows') + ) + return false; + } + return true; +} - // if number of elements is not 2, elements cannot be linked by definition - if (elements.length !== 2) return false; +/** + * Assuming all elements are valid, + * check if the elements can be linked: + * 1. sort elements based on their order in the DOM + * 2. ensure elements are adjacent in the DOM + * 3. ensure elements belong to adjacent staves + * 4. check if can be linked: + * 4.1 if exactly two elements, both needs to be unlinked + * 4.2 if more than two elements, only one element can be unlinked, + * at either beginning or end of the sequence + * @param elements elements to be considered + * @returns true can be linked + */ +export function canBeLinked(elements: Array): boolean { + // 1. Sort elements based on their order in the DOM + elements.sort((a, b) => { + const staffA = a.closest('.staff'); + const staffB = b.closest('.staff'); + return staffA.compareDocumentPosition(staffB) & + Node.DOCUMENT_POSITION_FOLLOWING + ? -1 + : 1; + }); - // if precedes and follows attributes exist and their IDs match - if (((elements[0].getAttribute('mei:follows') === `#${elements[1].id}`) && - (elements[1].getAttribute('mei:precedes') === `#${elements[0].id}`)) || - ((elements[0].getAttribute('mei:precedes') === '#' + elements[1].id) && - (elements[1].getAttribute('mei:follows') === '#' + elements[0].id))) { - return true; + const staves = Array.from(document.querySelectorAll('.staff')); + const syllables = Array.from(document.querySelectorAll('.syllable')); + + // 2. Ensure syllables are adjacent in the DOM + for (let i = 0; i < elements.length - 1; i++) { + const current = elements[i]; + const next = elements[i + 1]; + const currentSyllableIndex = syllables.indexOf(current); + const nextSyllableIndex = syllables.indexOf(next); + if (nextSyllableIndex - currentSyllableIndex !== 1) { + return false; // Staves must be adjacent + } + // 3. Ensure syllables belong to adjacent staves + const currentStaff = current.closest('.staff'); + const nextStaff = next.closest('.staff'); + + const currentStaffIndex = staves.indexOf(currentStaff as HTMLElement); + const nextStaffIndex = staves.indexOf(nextStaff as HTMLElement); + if (nextStaffIndex - currentStaffIndex !== 1) { + return false; // Staves must be adjacent + } } - else { - return false; + + // 4.1 Handle case for exactly two syllables + if (elements.length === 2) { + return elements.every( + (element) => + !element.hasAttribute('mei:precedes') && + !element.hasAttribute('mei:follows'), + ); } -} + // 4.2 Handle case for more than two syllables + const unlinkedElements = elements.filter( + (element) => + !element.hasAttribute('mei:precedes') && + !element.hasAttribute('mei:follows'), + ); + + if (unlinkedElements.length !== 1) { + return false; // Only one unlinked syllable is allowed + } + + // The unlinked syllable should be at the beginning or end + const firstUnlinked = unlinkedElements[0]; + + return ( + elements.indexOf(firstUnlinked) === 0 || + elements.indexOf(firstUnlinked) === elements.length - 1 + ); +} /** * Check if the selected elements can be linked or unlikned. * @param selectionType Current selection mode. Only certain elements can be linked - * @param elements The elements under question + * @param elements The elements under question * @returns true if user should be able to link or un-link elements, false otherwise */ -export function isLinkable(selectionType: string, elements: Array): boolean { - - switch(elements.length) { - // only a selection of length 2 can lead to option to toggle syllable link - case 2: - // only Syllables can be linked or unlinked (?) - if (selectionType !== 'selBySyllable') return false; - - // if ALREADY linked - if (isLinked([elements[0], elements[1]])) { - return true; - } - // if CAN be linked - else { - // Check if this *could* be a selection with a single logical syllable split by a staff break. - // Check if these are adjacent staves (logically) - if (SelectTools.isMultiStaveSelection(elements)) { - const staff0 = elements[0].closest('.staff'); - const staff1 = elements[1].closest('.staff'); - const staffChildren = Array.from(staff0.parentElement.children); - - // Check if one syllable is the last in the first staff and the other is the first in the second. - // Determine which staff is first. - const firstStaff = (staffChildren.indexOf(staff0) < staffChildren.indexOf(staff1)) ? staff0 : staff1; - const secondStaff = (firstStaff.id === staff0.id) ? staff1 : staff0; - const firstLayer = firstStaff.querySelector('.layer'); - const secondLayer = secondStaff.querySelector('.layer'); - - // Check that the first staff has either syllable as the last syllable - const firstSyllableChildren = Array.from(firstLayer.children).filter((elem: HTMLElement) => elem.classList.contains('syllable')); - const secondSyllableChildren = Array.from(secondLayer.children).filter((elem: HTMLElement) => elem.classList.contains('syllable')); - const lastSyllable = firstSyllableChildren[firstSyllableChildren.length - 1]; - const firstSyllable = secondSyllableChildren[0]; - - if (lastSyllable.id === elements[0].id && firstSyllable.id === elements[1].id) { - return true; - } - else if (lastSyllable.id === elements[1].id && firstSyllable.id === elements[0].id) { - return true; - } - } - } - - // cannot toggle link for syllable selection if number of selected - // syllables is not equal to 2 - default: - return false; +export function isLinkable( + selectionType: string, + elements: Array, +): boolean { + // cannot toggle link for syllable selection if number of selected + // syllables is smaller than 2 + if (elements.length < 2) { + return false; + } + + // only Syllables can be linked or unlinked (?) + if (selectionType !== 'selBySyllable') return false; + + // Check if has invalid linked syllables + if (hasInvalidLinkedSyllable(elements)) { + Notification.queueNotification( + 'The selected syllables include invalid linked syllable(s)!', + 'warning', + ); + return false; + } + + // if ALREADY linked + if (isLinked(elements)) { + return true; + } + // if CAN be linked + else if (canBeLinked(elements)) { + return true; } return false; } - /** * Merge selected staves */ -export function mergeStaves (): void { +export function mergeStaves(): void { const systems = document.querySelectorAll('.staff.selected'); const elementIds = []; - systems.forEach(staff => { + systems.forEach((staff) => { elementIds.push(staff.id); }); const editorAction: EditorAction = { action: 'merge', param: { - elementIds: elementIds - } + elementIds: elementIds, + }, }; - neonView.edit(editorAction, neonView.view.getCurrentPageURI()).then((result) => { - if (result) { - Notification.queueNotification('Staff Merged', 'success'); - SelectOptions.endOptionsSelection(); - neonView.updateForCurrentPage(); - } else { - Notification.queueNotification('Merge Failed', 'error'); - } - }); + neonView + .edit(editorAction, neonView.view.getCurrentPageURI()) + .then((result) => { + if (result) { + Notification.queueNotification('Staff Merged', 'success'); + SelectOptions.endOptionsSelection(); + neonView.updateForCurrentPage(); + } else { + Notification.queueNotification('Merge Failed', 'error'); + } + }); } - /** * Trigger the grouping selection menu. * @param type - The grouping type: nc, neume, syl, ligatureNc, or ligature */ -export function triggerGrouping (type: string): void { +export function triggerGrouping(type: string): void { const moreEdit = document.getElementById('moreEdit'); moreEdit.parentElement.classList.remove('hidden'); moreEdit.innerHTML += Contents.groupingMenu[type]; initGroupingListeners(); } - /** * Remove the grouping selection menu. */ -export function endGroupingSelection (): void { +export function endGroupingSelection(): void { const moreEdit = document.getElementById('moreEdit'); moreEdit.innerHTML = ''; moreEdit.parentElement.classList.add('hidden'); @@ -217,11 +362,10 @@ export function endGroupingSelection (): void { document.body.removeEventListener('keydown', keydownListener); } - /** * The grouping dropdown listener. */ -export function initGroupingListeners (): void { +export function initGroupingListeners(): void { const del = document.getElementById('delete'); del.removeEventListener('click', removeHandler); del.addEventListener('click', removeHandler); @@ -231,8 +375,8 @@ export function initGroupingListeners (): void { try { document.getElementById('mergeSyls').addEventListener('click', () => { if (containsLinked(SelectTools.getSelectionType())) return; - const elementIds = getChildrenIds().filter(e => - document.getElementById(e).classList.contains('neume') + const elementIds = getChildrenIds().filter((e) => + document.getElementById(e).classList.contains('neume'), ); groupingAction('group', 'neume', elementIds); }); @@ -241,8 +385,8 @@ export function initGroupingListeners (): void { try { document.getElementById('groupNeumes').addEventListener('click', () => { if (containsLinked(SelectTools.getSelectionType())) return; - const elementIds = getIds().filter(e => - document.getElementById(e).classList.contains('neume') + const elementIds = getIds().filter((e) => + document.getElementById(e).classList.contains('neume'), ); groupingAction('group', 'neume', elementIds); }); @@ -250,8 +394,8 @@ export function initGroupingListeners (): void { try { document.getElementById('groupNcs').addEventListener('click', () => { - const elementIds = getIds().filter(e => - document.getElementById(e).classList.contains('nc') + const elementIds = getIds().filter((e) => + document.getElementById(e).classList.contains('nc'), ); groupingAction('group', 'nc', elementIds); }); @@ -272,25 +416,29 @@ export function initGroupingListeners (): void { } catch (e) {} try { - document.getElementById('toggle-ligature').addEventListener('click', async () => { - const elementIds = getIds(); - - const editorAction: ToggleLigatureAction = { - action: 'toggleLigature', - param: { - elementIds: elementIds - } - }; - neonView.edit(editorAction, neonView.view.getCurrentPageURI()).then((result) => { - if (result) { - Notification.queueNotification('Ligature Toggled', 'success'); - } else { - Notification.queueNotification('Ligature Toggle Failed', 'error'); - } - endGroupingSelection(); - neonView.updateForCurrentPage(); + document + .getElementById('toggle-ligature') + .addEventListener('click', async () => { + const elementIds = getIds(); + + const editorAction: ToggleLigatureAction = { + action: 'toggleLigature', + param: { + elementIds: elementIds, + }, + }; + neonView + .edit(editorAction, neonView.view.getCurrentPageURI()) + .then((result) => { + if (result) { + Notification.queueNotification('Ligature Toggled', 'success'); + } else { + Notification.queueNotification('Ligature Toggle Failed', 'error'); + } + endGroupingSelection(); + neonView.updateForCurrentPage(); + }); }); - }); } catch (e) {} try { @@ -300,14 +448,15 @@ export function initGroupingListeners (): void { } catch (e) {} } - /** * Grouping/Ungrouping keybinding event listener */ -const keydownListener = function(e) { +const keydownListener = function (e) { if (e.key === 'g') { // get selected elements to check if they can be groupeds - const elements = Array.from(document.querySelectorAll('.selected')) as SVGGraphicsElement[]; + const elements = Array.from( + document.querySelectorAll('.selected'), + ) as SVGGraphicsElement[]; if (elements.length == 0) return; const selectionType = SelectTools.getSelectionType(); @@ -324,12 +473,11 @@ const keydownListener = function(e) { } // check if groupable before grouping else if (isGroupable(selectionType, elements)) { - const elementIds = getChildrenIds().filter(e => - document.getElementById(e).classList.contains('neume') + const elementIds = getChildrenIds().filter((e) => + document.getElementById(e).classList.contains('neume'), ); groupingAction('group', 'neume', elementIds); - - } + } // can only ungroup if length is 1 (one syllable selected) // cannot ungroup if multiple syllables are selected else if (elements.length === 1) { @@ -367,56 +515,141 @@ const keydownListener = function(e) { break; default: - console.error(`Can't perform grouping/ungrouping action on selection type ${selectionType}.`); + console.error( + `Can't perform grouping/ungrouping action on selection type ${selectionType}.`, + ); return; } } }; - /** * Form and execute a group/ungroup action. * @param action - The action to execute. Either "group" or "ungroup". * @param groupType - The type of elements to group. Either "neume" or "nc". * @param elementIds - The IDs of the elements. */ -function groupingAction (action: 'group' | 'ungroup', groupType: 'neume' | 'nc', elementIds: string[]): void { +function groupingAction( + action: 'group' | 'ungroup', + groupType: 'neume' | 'nc', + elementIds: string[], +): void { const editorAction: EditorAction = { action: action, param: { groupType: groupType, - elementIds: elementIds - } + elementIds: elementIds, + }, }; - neonView.edit(editorAction, neonView.view.getCurrentPageURI()).then((result) => { - if (result) { - if (action === 'group') { - Notification.queueNotification('Grouping Success', 'success'); + neonView + .edit(editorAction, neonView.view.getCurrentPageURI()) + .then((result) => { + if (result) { + if (action === 'group') { + Notification.queueNotification('Grouping Success', 'success'); + } else { + Notification.queueNotification('Ungrouping Success', 'success'); + } } else { - Notification.queueNotification('Ungrouping Success', 'success'); + if (action === 'group') { + Notification.queueNotification('Grouping Failed', 'error'); + } else { + Notification.queueNotification('Ungrouping Failed', 'error'); + } } - } else { - if (action === 'group') { - Notification.queueNotification('Grouping Failed', 'error'); - } else { - Notification.queueNotification('Ungrouping Failed', 'error'); + neonView.updateForCurrentPage(); + + // Prompt user to confirm if Neon does not re cognize contour + if (groupType === 'nc') { + const neumeParent = document.getElementById( + elementIds[0], + ).parentElement; + const ncs = Array.from(neumeParent.children) as SVGGraphicsElement[]; + const contour = neonView.info.getContour(ncs); + if (contour === undefined) { + Warnings.groupingNotRecognized(); + } } + endGroupingSelection(); + }); +} + +function unlink(elementIds: string[]): Array { + const param = new Array(); + for (const id of elementIds) { + const element = document.getElementById(id); + if (element.getAttribute('mei:precedes')) { + param.push({ + action: 'set', + param: { + elementId: id, + attrType: 'precedes', + attrValue: '', + }, + }); } - neonView.updateForCurrentPage(); - - // Prompt user to confirm if Neon does not re cognize contour - if (groupType === 'nc') { - const neumeParent = document.getElementById(elementIds[0]).parentElement; - const ncs = Array.from(neumeParent.children) as SVGGraphicsElement[]; - const contour = neonView.info.getContour(ncs); - if (contour === undefined) { - Warnings.groupingNotRecognized(); - } + if (element.getAttribute('mei:follows')) { + param.push({ + action: 'set', + param: { + elementId: id, + attrType: 'follows', + attrValue: '', + }, + }); + param.push({ + action: 'setText', + param: { + elementId: id, + text: '', + }, + }); } - endGroupingSelection(); - }); + } + + return param; } +// Utility function to get an element by ID and ensure it's an SVGGraphicsElement +function getSVGGraphicsElementById(id: string): SVGGraphicsElement | null { + const element = document.getElementById(id); + if (element instanceof SVGGraphicsElement) { + return element; + } + return null; +} + +function getToggleSyllableIds( + elements: Array, +): [SVGGraphicsElement, SVGGraphicsElement] { + // Sort elements based on their order in the DOM + elements.sort((a, b) => { + const staffA = a.closest('.staff'); + const staffB = b.closest('.staff'); + return staffA.compareDocumentPosition(staffB) & + Node.DOCUMENT_POSITION_FOLLOWING + ? -1 + : 1; + }); + + if (elements.length === 2) { + return [elements[0], elements[1]]; + } + + const unlinkedElement = elements.find( + (el) => !el.hasAttribute('mei:precedes') && !el.hasAttribute('mei:follows'), + ); + + if (unlinkedElement) { + const index = elements.indexOf(unlinkedElement); + + if (index === 0) { + return [elements[index], elements[index + 1]]; + } else if (index === elements.length - 1) { + return [elements[index - 1], elements[index]]; + } + } +} /** * Determine what action (link/unlink) to perform when user clicks on "Toggle Linked Syllable" @@ -426,90 +659,34 @@ function toggleLinkedSyllables() { const elementIds = getIds(); const chainAction: EditorAction = { action: 'chain', - param: [] + param: [], }; const param = new Array(); - if (document.getElementById(elementIds[0]).getAttribute('mei:precedes')) { - param.push({ - action: 'set', - param: { - elementId: elementIds[0], - attrType: 'precedes', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: elementIds[1], - attrType: 'follows', - attrValue: '' - } - }); - param.push({ - action: 'setText', - param: { - elementId: elementIds[1], - text: '' - } - }); - } else if (document.getElementById(elementIds[0]).getAttribute('mei:follows')) { - param.push({ - action: 'set', - param: { - elementId: elementIds[0], - attrType: 'follows', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: elementIds[1], - attrType: 'precedes', - attrValue: '' - } - }); - param.push({ - action: 'setText', - param: { - elementId: elementIds[0], - text: '' - } - }); - } else { - // Associate syllables. Will need to find which is first. Use staves. - const syllable0 = document.getElementById(elementIds[0]); - const syllable1 = document.getElementById(elementIds[1]); - const staff0 = syllable0.closest('.system'); - const staff1 = syllable1.closest('.system'); - const staffChildren = Array.from(staff0.parentElement.children).filter((elem: HTMLElement) => elem.classList.contains('system')); - - let firstSyllable, secondSyllable; - // Determine first syllable comes first by staff - if (staffChildren.indexOf(staff0) < staffChildren.indexOf(staff1)) { - firstSyllable = syllable0; - secondSyllable = syllable1; - } else { - firstSyllable = syllable1; - secondSyllable = syllable0; - } + const elements = elementIds + .map(getSVGGraphicsElementById) // Map IDs to SVGGraphicsElement or null + .filter((el): el is SVGGraphicsElement => el !== null); + + if (isLinked(elements)) { + param.push(...unlink(elementIds)); + } else { + const [firstSyllable, secondSyllable] = getToggleSyllableIds(elements); + console.log(firstSyllable.id, secondSyllable.id); param.push({ action: 'set', param: { elementId: firstSyllable.id, attrType: 'precedes', - attrValue: '#' + secondSyllable.id - } + attrValue: '#' + secondSyllable.id, + }, }); param.push({ action: 'set', param: { elementId: secondSyllable.id, attrType: 'follows', - attrValue: '#' + firstSyllable.id - } + attrValue: '#' + firstSyllable.id, + }, }); // Delete syl on second syllable const syl = secondSyllable.querySelector('.syl'); @@ -517,49 +694,57 @@ function toggleLinkedSyllables() { param.push({ action: 'remove', param: { - elementId: syl.id - } + elementId: syl.id, + }, }); } } + chainAction.param = param; - neonView.edit(chainAction, neonView.view.getCurrentPageURI()).then((result) => { - if (result) { - Notification.queueNotification('Toggled Syllable Link'); - } else { - Notification.queueNotification('Failed to Toggle Syllable Link'); - } - endGroupingSelection(); - neonView.updateForCurrentPage(); - }); + neonView + .edit(chainAction, neonView.view.getCurrentPageURI()) + .then((result) => { + if (result) { + Notification.queueNotification('Toggled Syllable Link', 'success'); + } else { + Notification.queueNotification( + 'Failed to Toggle Syllable Link', + 'error', + ); + } + endGroupingSelection(); + neonView.updateForCurrentPage(); + }); } - /** * @returns The IDs of selected elements. */ -function getIds (): string[] { +function getIds(): string[] { const ids = []; const elements = Array.from(document.getElementsByClassName('selected')); - elements.forEach(el => { + elements.forEach((el) => { ids.push(el.id); }); return ids; } - /** * @returns The IDs of the selected elements' children. */ -function getChildrenIds (): string[] { +function getChildrenIds(): string[] { const childrenIds = []; const elements = Array.from(document.getElementsByClassName('selected')); - elements.forEach(el => { - if (el.classList.contains('divLine') || el.classList.contains('accid') || el.classList.contains('clef')) { + elements.forEach((el) => { + if ( + el.classList.contains('divLine') || + el.classList.contains('accid') || + el.classList.contains('clef') + ) { return; } const children = Array.from(el.children); - children.forEach(ch => { + children.forEach((ch) => { childrenIds.push(ch.id); }); }); diff --git a/src/utils/ConvertMei.ts b/src/utils/ConvertMei.ts index 8ad93de9..ffff01f2 100644 --- a/src/utils/ConvertMei.ts +++ b/src/utils/ConvertMei.ts @@ -454,13 +454,8 @@ export function convertToVerovio(sbBasedMei: string): string { const syllableIdx = newSyllables.indexOf(syllable); // Validate toggle-linked syllable - if (syllable.hasAttribute('precedes') && syllable.hasAttribute('follows')) { - // Check if the syllable has both @precedes and @follows - invalidLinked = true; - invalidLinkedInfo += `- <${syllable.tagName}> (${getSyllableText(syllable)}) with xml:id: ${syllable.getAttribute('xml:id')} has both @precedes and @follows\n`; - } // Check the precedes syllable - else if (syllable.hasAttribute('precedes')) { + if (syllable.hasAttribute('precedes')) { const info = checkPrecedesSyllable(syllable, syllableIdx, newSyllables); if (info) { invalidLinked = true; @@ -468,7 +463,7 @@ export function convertToVerovio(sbBasedMei: string): string { } } // Check the follows syllable - else if (syllable.hasAttribute('follows')) { + if (syllable.hasAttribute('follows')) { const info = checkFollowsSyllable(syllable, newSyllables); if (info) { invalidLinked = true; diff --git a/src/utils/EditControls.ts b/src/utils/EditControls.ts index 1abd895e..7ade04c8 100644 --- a/src/utils/EditControls.ts +++ b/src/utils/EditControls.ts @@ -6,10 +6,11 @@ import { EditorAction } from '../Types'; /** * Set top navbar event listeners. */ -export function initNavbar (neonView: NeonView): void { - +export function initNavbar(neonView: NeonView): void { // setup navbar listeners - const navbarDropdowns = document.querySelectorAll('.navbar-item.has-dropdown.is-hoverable'); + const navbarDropdowns = document.querySelectorAll( + '.navbar-item.has-dropdown.is-hoverable', + ); Array.from(navbarDropdowns).forEach((dropDown) => { dropDown.addEventListener('mouseover', () => { // @@ -31,7 +32,7 @@ export function initNavbar (neonView: NeonView): void { }); document.getElementById('export').addEventListener('click', () => { - neonView.export().then(manifest => { + neonView.export().then((manifest) => { const link: HTMLAnchorElement = document.createElement('a'); link.href = manifest as string; link.download = neonView.name + '.jsonld'; @@ -46,402 +47,451 @@ export function initNavbar (neonView: NeonView): void { // Is an actual file with a valid URI except in local mode where it must be generated. document.getElementById('getmei').addEventListener('click', () => { const uri = neonView.view.getCurrentPageURI(); - neonView.getPageMEI(uri).then(mei => { - const data = 'data:application/mei+xml;base64,' + window.btoa(convertToNeon(mei)); + neonView.getPageMEI(uri).then((mei) => { + const data = + 'data:application/mei+xml;base64,' + window.btoa(convertToNeon(mei)); document.getElementById('getmei').setAttribute('href', data); - document.getElementById('getmei').setAttribute('download', neonView.view.getPageName() + '.mei'); + document + .getElementById('getmei') + .setAttribute('download', neonView.view.getPageName() + '.mei'); }); }); - /* "MEI ACTIONS" menu */ // Event listener for "Remove Empty Syllables" button inside "MEI Actions" dropdown - document.getElementById('remove-empty-syls').addEventListener('click', function() { - const uri = neonView.view.getCurrentPageURI(); + document + .getElementById('remove-empty-syls') + .addEventListener('click', function () { + const uri = neonView.view.getCurrentPageURI(); + + neonView.getPageMEI(uri).then((meiString) => { + const parser = new DOMParser(); + const meiDoc = parser.parseFromString(meiString, 'text/xml'); + const mei = meiDoc.documentElement; + const syllables = Array.from(mei.getElementsByTagName('syllable')); + + // Check for syllables without neumes + let hasEmptySyllables = false; + const removeSyllableActions = []; + for (const syllable of syllables) { + // if empty syllable found, create action object for removing it + if (syllable.getElementsByTagName('neume').length === 0) { + const toRemove: EditorAction = { + action: 'remove', + param: { + elementId: syllable.getAttribute('xml:id'), + }, + }; + // add action object to array (chain) of action objects + removeSyllableActions.push(toRemove); + hasEmptySyllables = true; + } + } - neonView.getPageMEI(uri).then(meiString => { - const parser = new DOMParser(); - const meiDoc = parser.parseFromString(meiString, 'text/xml'); - const mei = meiDoc.documentElement; - const syllables = Array.from(mei.getElementsByTagName('syllable')); - - // Check for syllables without neumes - let hasEmptySyllables = false; - const removeSyllableActions = []; - for (const syllable of syllables) { - // if empty syllable found, create action object for removing it - if (syllable.getElementsByTagName('neume').length === 0) { - const toRemove: EditorAction = { - action: 'remove', - param: { - elementId: syllable.getAttribute('xml:id') + // check if empty syllables were found + if (!hasEmptySyllables) { + Notification.queueNotification('No empty syllables found', 'warning'); + } else { + // create "chain action" object + const chainRemoveAction: EditorAction = { + action: 'chain', + param: removeSyllableActions, + }; + + // execute action that removes all empty syllables + // "result" value is true or false (true if chain of actions was successful) + neonView.edit(chainRemoveAction, uri).then((result) => { + if (result) { + neonView.updateForCurrentPage(); + Notification.queueNotification( + 'Removed empty Syllables', + 'success', + ); + } else { + Notification.queueNotification( + 'Failed to remove empty Syllables', + 'error', + ); } + }); + } + }); + }); + + // Event listener for "Remove Empty Neumes" button inside "MEI Actions" dropdown + document + .getElementById('remove-empty-neumes') + .addEventListener('click', function () { + const uri = neonView.view.getCurrentPageURI(); + + neonView.getPageMEI(uri).then((meiString) => { + const parser = new DOMParser(); + const meiDoc = parser.parseFromString(meiString, 'text/xml'); + const mei = meiDoc.documentElement; + const neumes = Array.from(mei.getElementsByTagName('neume')); + + // Check for neumes without neume components + let hasEmptyNeumes = false; + const removeNeumeActions = []; + for (const neume of neumes) { + // if empty neume found, create action object for removing it + if (neume.getElementsByTagName('nc').length === 0) { + const toRemove: EditorAction = { + action: 'remove', + param: { + elementId: neume.getAttribute('xml:id'), + }, + }; + // add action object to array (chain) of action objects + removeNeumeActions.push(toRemove); + hasEmptyNeumes = true; + } + } + + // check if empty neumes were found + if (!hasEmptyNeumes) { + Notification.queueNotification('No empty Neumes found', 'warning'); + } else { + // create "chain action" object + const chainRemoveAction: EditorAction = { + action: 'chain', + param: removeNeumeActions, }; - // add action object to array (chain) of action objects - removeSyllableActions.push(toRemove); - hasEmptySyllables = true; + + // execute action that removes all empty neumes + // "result" value is true or false (true if chain of actions was successful) + neonView.edit(chainRemoveAction, uri).then((result) => { + if (result) { + neonView.updateForCurrentPage(); + Notification.queueNotification('Removed empty Neumes', 'success'); + } else { + Notification.queueNotification( + 'Failed to remove empty Neumes', + 'error', + ); + } + }); } - } + }); + }); - // check if empty syllables were found - if (!hasEmptySyllables) { - Notification.queueNotification('No empty syllables found', 'warning'); - } - else { - // create "chain action" object - const chainRemoveAction: EditorAction = { - action: 'chain', - param: removeSyllableActions - }; + document + .getElementById('remove-out-of-bounds-glyphs') + .addEventListener('click', function () { + const uri = neonView.view.getCurrentPageURI(); + neonView.getPageMEI(uri).then((meiString) => { + // Load MEI document into parser + const parser = new DOMParser(); + const meiDoc = parser.parseFromString(meiString, 'text/xml'); + const mei = meiDoc.documentElement; + + // Get bounds of the MEI + const dimensions = mei.querySelector('surface'); + const meiLrx = Number(dimensions.getAttribute('lrx')), + meiLry = Number(dimensions.getAttribute('lry')); + + function isAttrOutOfBounds(zone: Element, attr: string): boolean { + const coord = Number(zone.getAttribute(attr)); + const comp = attr == 'lrx' || attr == 'ulx' ? meiLrx : meiLry; + return coord < 0 || coord > comp; + } - // execute action that removes all empty syllables - // "result" value is true or false (true if chain of actions was successful) - neonView.edit(chainRemoveAction, uri).then((result) => { - if (result) { - neonView.updateForCurrentPage(); - Notification.queueNotification('Removed empty Syllables', 'success'); - } - else { - Notification.queueNotification('Failed to remove empty Syllables', 'error'); + // Get array of zones that are out of bound, and create a hash map + // for fast retrieval + const zones = Array.from(mei.querySelectorAll('zone')); + const outOfBoundZones = zones.filter((zone) => + ['ulx', 'uly', 'lrx', 'lry'].some((attr) => + isAttrOutOfBounds(zone, attr), + ), + ); + const zoneMap = new Map( + outOfBoundZones.map((zone) => [zone.getAttribute('xml:id'), zone]), + ); + + // Filter out the neume components and divlines that have a zone out of bounds + const glyphs = Array.from( + mei.querySelectorAll('nc, divLine, clef, accid'), + ); + const outOfBoundGlyphs = glyphs.filter((glyph) => { + if (glyph.hasAttribute('facs')) { + const facsId = glyph.getAttribute('facs').slice(1); + return zoneMap.has(facsId); } + + return false; }); - } - }); - }); - // Event listener for "Remove Empty Neumes" button inside "MEI Actions" dropdown - document.getElementById('remove-empty-neumes').addEventListener('click', function() { - const uri = neonView.view.getCurrentPageURI(); + // Check if there are no out-of-bound glyphs, and + // exit, since no edit needs to be made. + if (outOfBoundGlyphs.length === 0) { + return Notification.queueNotification( + 'There are no out-of-bound glyphs to remove.', + 'warning', + ); + } - neonView.getPageMEI(uri).then(meiString => { - const parser = new DOMParser(); - const meiDoc = parser.parseFromString(meiString, 'text/xml'); - const mei = meiDoc.documentElement; - const neumes = Array.from(mei.getElementsByTagName('neume')); - - // Check for neumes without neume components - let hasEmptyNeumes = false; - const removeNeumeActions = []; - for (const neume of neumes) { - // if empty neume found, create action object for removing it - if (neume.getElementsByTagName('nc').length === 0) { - const toRemove: EditorAction = { + // Create remove actions and chain action to send to Verovio + const removeActions: EditorAction[] = outOfBoundGlyphs.map((glyph) => { + return { action: 'remove', param: { - elementId: neume.getAttribute('xml:id') - } + elementId: glyph.getAttribute('xml:id'), + }, }; - // add action object to array (chain) of action objects - removeNeumeActions.push(toRemove); - hasEmptyNeumes = true; - } - } + }); - // check if empty neumes were found - if (!hasEmptyNeumes) { - Notification.queueNotification('No empty Neumes found', 'warning'); - } - else { - // create "chain action" object - const chainRemoveAction: EditorAction = { + const chainAction: EditorAction = { action: 'chain', - param: removeNeumeActions, + param: removeActions, }; - // execute action that removes all empty neumes - // "result" value is true or false (true if chain of actions was successful) - neonView.edit(chainRemoveAction, uri).then((result) => { + neonView.edit(chainAction, uri).then((result) => { if (result) { neonView.updateForCurrentPage(); - Notification.queueNotification('Removed empty Neumes', 'success'); - } - else { - Notification.queueNotification('Failed to remove empty Neumes', 'error'); + Notification.queueNotification( + 'Successfully removed out-of-bounds syllables.', + 'success', + ); + } else { + Notification.queueNotification( + 'Failed to remove out-of-bound syllables.', + 'error', + ); } }); - } - }); - }); - - document.getElementById('remove-out-of-bounds-glyphs').addEventListener('click', function () { - const uri = neonView.view.getCurrentPageURI(); - neonView.getPageMEI(uri).then(meiString => { - // Load MEI document into parser - const parser = new DOMParser(); - const meiDoc = parser.parseFromString(meiString, 'text/xml'); - const mei = meiDoc.documentElement; - - // Get bounds of the MEI - const dimensions = mei.querySelector('surface'); - const meiLrx = Number(dimensions.getAttribute('lrx')), meiLry = Number(dimensions.getAttribute('lry')); - - function isAttrOutOfBounds(zone: Element, attr: string): boolean { - const coord = Number(zone.getAttribute(attr)); - const comp = (attr == 'lrx' || attr == 'ulx') ? meiLrx : meiLry; - return coord < 0 || coord > comp; - } - - // Get array of zones that are out of bound, and create a hash map - // for fast retrieval - const zones = Array.from(mei.querySelectorAll('zone')); - const outOfBoundZones = zones.filter(zone => - ['ulx', 'uly', 'lrx', 'lry'].some((attr) => isAttrOutOfBounds(zone, attr)) - ); - const zoneMap = new Map(outOfBoundZones.map(zone => [zone.getAttribute('xml:id'), zone])); - - // Filter out the neume components and divlines that have a zone out of bounds - const glyphs = Array.from(mei.querySelectorAll('nc, divLine, clef, accid')); - const outOfBoundGlyphs = glyphs.filter(glyph => { - if (glyph.hasAttribute('facs')) { - const facsId = glyph.getAttribute('facs').slice(1); - return zoneMap.has(facsId); - } - - return false; }); + }); - // Check if there are no out-of-bound glyphs, and - // exit, since no edit needs to be made. - if (outOfBoundGlyphs.length === 0) { - return Notification.queueNotification('There are no out-of-bound glyphs to remove.', 'warning'); - } - - // Create remove actions and chain action to send to Verovio - const removeActions: EditorAction[] = outOfBoundGlyphs.map(glyph => { - return { - action: 'remove', - param: { - elementId: glyph.getAttribute('xml:id'), - } + document + .getElementById('untoggle-invalid-oblique') + .addEventListener('click', function () { + const uri = neonView.view.getCurrentPageURI(); + neonView.getPageMEI(uri).then((meiString) => { + // Load MEI document into parser + const parser = new DOMParser(); + const meiDoc = parser.parseFromString(meiString, 'text/xml'); + const mei = meiDoc.documentElement; + const ncs = Array.from(mei.getElementsByTagName('nc')); + + let hasInvalidOblique = false; + const chainAction: EditorAction = { + action: 'chain', + param: [], }; - }); - - const chainAction: EditorAction = { - action: 'chain', - param: removeActions, - }; - - neonView.edit(chainAction, uri) - .then((result) => { - if (result) { - neonView.updateForCurrentPage(); - Notification.queueNotification('Successfully removed out-of-bounds syllables.', 'success'); - } - else { - Notification.queueNotification('Failed to remove out-of-bound syllables.', 'error'); + const param = new Array(); + let ncIdx = 0; + while (ncIdx < ncs.length) { + if (ncs[ncIdx].getAttribute('ligated')) { + if ( + (ncIdx < ncs.length - 1 && + !ncs[ncIdx + 1].getAttribute('ligated')) || + ncIdx == ncs.length - 1 + ) { + // If nc is ligated, and the next nc is not + // Or, nc is ligated, but already at the end (there is no next)\ + hasInvalidOblique = true; + param.push({ + action: 'set', + param: { + elementId: ncs[ncIdx].getAttribute('xml:id'), + attrType: 'ligated', + attrValue: '', + }, + }); + } + ncIdx += 2; } - }); - }); - }); + ncIdx += 1; + } - document.getElementById('untoggle-invalid-oblique').addEventListener('click', function () { - const uri = neonView.view.getCurrentPageURI(); - neonView.getPageMEI(uri).then(meiString => { - // Load MEI document into parser - const parser = new DOMParser(); - const meiDoc = parser.parseFromString(meiString, 'text/xml'); - const mei = meiDoc.documentElement; - const ncs = Array.from(mei.getElementsByTagName('nc')); - - let hasInvalidOblique = false; - const chainAction: EditorAction = { - action: 'chain', - param: [] - }; - const param = new Array(); - let ncIdx = 0; - while (ncIdx < ncs.length) { - if (ncs[ncIdx].getAttribute('ligated')) { - if ((ncIdx < ncs.length-1 && !ncs[ncIdx+1].getAttribute('ligated')) || (ncIdx == ncs.length-1)) { - // If nc is ligated, and the next nc is not - // Or, nc is ligated, but already at the end (there is no next)\ - hasInvalidOblique = true; - param.push({ - action: 'set', - param: { - elementId: ncs[ncIdx].getAttribute('xml:id'), - attrType: 'ligated', - attrValue: '' + if (!hasInvalidOblique) { + Notification.queueNotification( + 'No invalid obliques found', + 'warning', + ); + } else { + chainAction.param = param; + neonView + .edit(chainAction, neonView.view.getCurrentPageURI()) + .then((result) => { + if (result) { + Notification.queueNotification( + 'Untoggled invalid obliques', + 'success', + ); + } else { + Notification.queueNotification( + 'Failed to untoggle invalid obliques', + 'error', + ); } + neonView.updateForCurrentPage(); }); - } - ncIdx += 2; } - ncIdx += 1; - } - - if (!hasInvalidOblique) { - Notification.queueNotification('No invalid obliques found', 'warning'); - } - else { - chainAction.param = param; - neonView.edit(chainAction, neonView.view.getCurrentPageURI()).then((result) => { - if (result) { - Notification.queueNotification('Untoggled invalid obliques', 'success'); - } else { - Notification.queueNotification('Failed to untoggle invalid obliques', 'error'); - } - neonView.updateForCurrentPage(); - }); - } + }); }); - }); - document.getElementById('untoggle-invalid-syls').addEventListener('click', function () { - const uri = neonView.view.getCurrentPageURI(); - neonView.getPageMEI(uri).then(meiString => { - // Load MEI document into parser - const parser = new DOMParser(); - const meiDoc = parser.parseFromString(meiString, 'text/xml'); - const mei = meiDoc.documentElement; - const syllables = Array.from(mei.getElementsByTagName('syllable')); - - let hasInvalidSyllables = false; - const chainAction: EditorAction = { - action: 'chain', - param: [] - }; - const param = new Array(); - for (const syllable of syllables) { - if (syllable.hasAttribute('precedes') && syllable.hasAttribute('follows')) { - hasInvalidSyllables = true; - const precedesSyllable = syllables.find(element => element.getAttribute('xml:id') === syllable.getAttribute('precedes').substring(1)); - const followsSyllable = syllables.find(element => element.getAttribute('xml:id') === syllable.getAttribute('follows').substring(1)); - - param.push({ - action: 'set', - param: { - elementId: syllable.getAttribute('xml:id'), - attrType: 'precedes', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: syllable.getAttribute('xml:id'), - attrType: 'follows', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: precedesSyllable.getAttribute('xml:id'), - attrType: 'follows', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: followsSyllable.getAttribute('xml:id'), - attrType: 'precedes', - attrValue: '' + document + .getElementById('untoggle-invalid-syls') + .addEventListener('click', function () { + const uri = neonView.view.getCurrentPageURI(); + neonView.getPageMEI(uri).then((meiString) => { + // Load MEI document into parser + const parser = new DOMParser(); + const meiDoc = parser.parseFromString(meiString, 'text/xml'); + const mei = meiDoc.documentElement; + const syllables = Array.from(mei.getElementsByTagName('syllable')); + + const invalidSyllables = getInvalidSyllables(syllables); + + if (invalidSyllables.length === 0) { + Notification.queueNotification( + 'No invalid syllables found', + 'warning', + ); + } else { + const chainAction: EditorAction = { + action: 'chain', + param: [], + }; + const param = new Array(); + for (const syllable of invalidSyllables) { + if ( + syllable.hasAttribute('precedes') && + syllable.hasAttribute('follows') + ) { + const precedesSyllable = syllables.find( + (element) => + element.getAttribute('xml:id') === + syllable.getAttribute('precedes').substring(1), + ); + const followsSyllable = syllables.find( + (element) => + element.getAttribute('xml:id') === + syllable.getAttribute('follows').substring(1), + ); + + param.push({ + action: 'set', + param: { + elementId: syllable.getAttribute('xml:id'), + attrType: 'precedes', + attrValue: '', + }, + }); + param.push({ + action: 'set', + param: { + elementId: syllable.getAttribute('xml:id'), + attrType: 'follows', + attrValue: '', + }, + }); + param.push({ + action: 'set', + param: { + elementId: precedesSyllable.getAttribute('xml:id'), + attrType: 'follows', + attrValue: '', + }, + }); + param.push({ + action: 'set', + param: { + elementId: followsSyllable.getAttribute('xml:id'), + attrType: 'precedes', + attrValue: '', + }, + }); + + param.push( + ...addSylAction([syllable, precedesSyllable, followsSyllable]), + ); + } else if (syllable.hasAttribute('precedes')) { + const precedesSyllable = syllables.find( + (element) => + element.getAttribute('xml:id') === + syllable.getAttribute('precedes').substring(1), + ); + + param.push({ + action: 'set', + param: { + elementId: syllable.getAttribute('xml:id'), + attrType: 'precedes', + attrValue: '', + }, + }); + param.push({ + action: 'set', + param: { + elementId: precedesSyllable.getAttribute('xml:id'), + attrType: 'follows', + attrValue: '', + }, + }); + param.push(...addSylAction([syllable, precedesSyllable])); + } else if (syllable.hasAttribute('follows')) { + const followsSyllable = syllables.find( + (element) => + element.getAttribute('xml:id') === + syllable.getAttribute('follows').substring(1), + ); + + param.push({ + action: 'set', + param: { + elementId: syllable.getAttribute('xml:id'), + attrType: 'follows', + attrValue: '', + }, + }); + param.push({ + action: 'set', + param: { + elementId: followsSyllable.getAttribute('xml:id'), + attrType: 'precedes', + attrValue: '', + }, + }); + param.push(...addSylAction([syllable, followsSyllable])); } - }); - - param.push(...addSylAction([syllable, precedesSyllable, followsSyllable])); - } - else if (syllable.hasAttribute('precedes')) { - const precedesSyllable = syllables.find(element => element.getAttribute('xml:id') === syllable.getAttribute('precedes').substring(1)); - if (!precedesSyllable || !precedesSyllable.hasAttribute('follows')) { - hasInvalidSyllables = true; - param.push({ - action: 'set', - param: { - elementId: syllable.getAttribute('xml:id'), - attrType: 'precedes', - attrValue: '' - } - }); - param.push(...addSylAction([syllable, precedesSyllable])); - } - else if (precedesSyllable.getAttribute('follows') != '#' + syllable.getAttribute('xml:id')) { - hasInvalidSyllables = true; - param.push({ - action: 'set', - param: { - elementId: syllable.getAttribute('xml:id'), - attrType: 'precedes', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: precedesSyllable.getAttribute('xml:id'), - attrType: 'follows', - attrValue: '' - } - }); - param.push(...addSylAction([syllable, precedesSyllable])); } - } - else if (syllable.hasAttribute('follows')) { - const followsSyllable = syllables.find(element => element.getAttribute('xml:id') === syllable.getAttribute('follows').substring(1)); - if (!followsSyllable || !followsSyllable.hasAttribute('precedes')) { - hasInvalidSyllables = true; - param.push({ - action: 'set', - param: { - elementId: syllable.getAttribute('xml:id'), - attrType: 'follows', - attrValue: '' - } - }); - param.push(...addSylAction([syllable, followsSyllable])); - } - else if (followsSyllable.getAttribute('precedes') != '#' + syllable.getAttribute('xml:id')) { - hasInvalidSyllables = true; - param.push({ - action: 'set', - param: { - elementId: syllable.getAttribute('xml:id'), - attrType: 'follows', - attrValue: '' - } - }); - param.push({ - action: 'set', - param: { - elementId: followsSyllable.getAttribute('xml:id'), - attrType: 'precedes', - attrValue: '' + + chainAction.param = param; + neonView + .edit(chainAction, neonView.view.getCurrentPageURI()) + .then((result) => { + if (result) { + Notification.queueNotification( + 'Untoggled invalid syllables', + 'success', + ); + } else { + Notification.queueNotification( + 'Failed to untoggle invalid syllables', + 'error', + ); } + neonView.updateForCurrentPage(); }); - param.push(...addSylAction([syllable, followsSyllable])); - } } - } - - if (!hasInvalidSyllables) { - Notification.queueNotification('No invalid syllables found', 'warning'); - } - else { - chainAction.param = param; - neonView.edit(chainAction, neonView.view.getCurrentPageURI()).then((result) => { - if (result) { - Notification.queueNotification('Untoggled invalid syllables', 'success'); - } else { - Notification.queueNotification('Failed to untoggle invalid syllables', 'error'); - } - neonView.updateForCurrentPage(); - }); - } + }); }); - }); // Event listener for "Revert" button inside "MEI Actions" dropdown document.getElementById('revert').addEventListener('click', function () { - if (window.confirm('Reverting will cause all changes to be lost. Press OK to continue.')) { + if ( + window.confirm( + 'Reverting will cause all changes to be lost. Press OK to continue.', + ) + ) { neonView.deleteDb().then(() => { window.location.reload(); }); } }); - /* "VIEW" menu */ /* @@ -451,7 +501,7 @@ export function initNavbar (neonView: NeonView): void { const fitContentCheckmark = document.querySelector('#zoom-fit-content-icon'); const easyEditBtn = document.querySelector('#zoom-easy-edit'); const easyEditCheckmark = document.querySelector('#zoom-easy-edit-icon'); - + // fit content listener fitContentBtn.addEventListener('click', () => { easyEditBtn.classList.remove('checked'); @@ -461,7 +511,7 @@ export function initNavbar (neonView: NeonView): void { fitContentCheckmark.classList.add('selected'); // TODO: Save default zoom settings in local storage - + }); // easy edit listener easyEditBtn.addEventListener('click', () => { @@ -480,11 +530,11 @@ export function initNavbar (neonView: NeonView): void { /** * Initialize the undo/redo panel */ -export function initUndoRedoPanel (neonView: NeonView): void { +export function initUndoRedoPanel(neonView: NeonView): void { /** * Tries to undo an action and update the page if it succeeds. */ - function undoHandler (): void { + function undoHandler(): void { neonView.undo().then((result: boolean) => { if (result) { neonView.updateForCurrentPage(); @@ -496,9 +546,9 @@ export function initUndoRedoPanel (neonView: NeonView): void { } /** - * Tries to redo an action and update the page if it succeeds. + * Tries to redo an action and update the page if it succeeds. */ - function redoHandler (): void { + function redoHandler(): void { neonView.redo().then((result: boolean) => { if (result) { neonView.updateForCurrentPage(); @@ -518,7 +568,10 @@ export function initUndoRedoPanel (neonView: NeonView): void { document.getElementById('redo').addEventListener('click', redoHandler); document.body.addEventListener('keydown', (evt) => { - if ((evt.key === 'Z' || (evt.key === 'z' && evt.shiftKey)) && (evt.ctrlKey || evt.metaKey)) { + if ( + (evt.key === 'Z' || (evt.key === 'z' && evt.shiftKey)) && + (evt.ctrlKey || evt.metaKey) + ) { redoHandler(); } }); @@ -534,10 +587,82 @@ function addSylAction(syllables: Element[]): Array { param: { elementId: syllable.getAttribute('xml:id'), text: '', - } + }, }); } } return param; } + +function getInvalidSyllables(syllables: Element[]): Element[] { + const invalidSyllables: Element[] = []; + for (let idx = 0; idx < syllables.length; idx++) { + const syllable = syllables.at(idx); + if (syllable.hasAttribute('precedes')) { + // Get xml:id of the next syllable (without the #, if it exists) + const nextSyllableId = syllable.getAttribute('precedes').replace('#', ''); + + // Find the next syllable and its index in the array + let nextSyllableIdx: number; + const nextSyllable = syllables.find((element, idx) => { + if (element.getAttribute('xml:id') === nextSyllableId) { + nextSyllableIdx = idx; + return true; + } + + return false; + }); + + // Condition 1: The next (following) syllable cannot be found + if (!nextSyllable) { + invalidSyllables.push(syllable); + } + + // Condition 2: The next syllable has been found, but the @follows attribute does NOT EXIST + if (!nextSyllable.hasAttribute('follows')) { + invalidSyllables.push(syllable); + } + + // Condition 3: The next syllable's @follows attribute exists, but it is not in the correct format #id + if ( + nextSyllable.getAttribute('follows') != + '#' + syllable.getAttribute('xml:id') + ) { + invalidSyllables.push(syllable); + } + + // Condition 4: + // Since the @follows value is correct, a pair of syllables exist for the toggle-linked syllable. + // Check if the @follows syllable is the next syllable (index-wise) in the array: + if (nextSyllableIdx !== idx + 1) { + invalidSyllables.push(syllable); + } + } + if (syllable.hasAttribute('follows')) { + const prevSyllableId = syllable.getAttribute('follows').replace('#', ''); + const prevSyllable = syllables.find( + (syllable) => syllable.getAttribute('xml:id') === prevSyllableId, + ); + + // Condition 1: The previous syllable does not exist + if (!prevSyllable) { + invalidSyllables.push(syllable); + } + + // Condition 2: The previous syllable exists, but the @precedes attribute does NOT EXIST + if (!prevSyllable.hasAttribute('precedes')) { + invalidSyllables.push(syllable); + } + + // Condition 3: The previous syllable's @precedes attribute exists, but it is not in the correct format #id + if ( + prevSyllable.getAttribute('precedes') != + '#' + syllable.getAttribute('xml:id') + ) { + invalidSyllables.push(syllable); + } + } + } + return invalidSyllables; +} diff --git a/src/utils/SelectTools.ts b/src/utils/SelectTools.ts index fdad2550..2fc9fbfd 100644 --- a/src/utils/SelectTools.ts +++ b/src/utils/SelectTools.ts @@ -1,5 +1,8 @@ import * as Color from './Color'; -import { updateHighlight, getHighlightType } from '../DisplayPanel/DisplayControls'; +import { + updateHighlight, + getHighlightType, +} from '../DisplayPanel/DisplayControls'; import * as Grouping from '../SquareEdit/Grouping'; import { resize } from './Resize'; import { Attributes, SelectionType } from '../Types'; @@ -11,7 +14,7 @@ import * as d3 from 'd3'; /** * @returns The selection mode chosen by the user. */ -export function getSelectionType (): SelectionType { +export function getSelectionType(): SelectionType { const element = document.getElementsByClassName('sel-by is-active'); if (element.length !== 0) { return element[0].id as SelectionType; @@ -24,9 +27,10 @@ export function getSelectionType (): SelectionType { * Unselect all selected elements and run undo any extra * actions. */ -export function unselect (): void { - document.querySelectorAll('.no-moving').forEach((selected: SVGGElement) => - selected.classList.remove('no-moving')); +export function unselect(): void { + document + .querySelectorAll('.no-moving') + .forEach((selected: SVGGElement) => selected.classList.remove('no-moving')); document.querySelectorAll('.selected').forEach((selected: SVGGElement) => { selected.classList.remove('selected'); if (selected.classList.contains('staff')) { @@ -37,46 +41,57 @@ export function unselect (): void { selected.style.fill = ''; } - Array.from(selected.querySelectorAll('.divLine')).forEach((divLine: HTMLElement) => { - divLine.style.stroke = ''; - divLine.setAttribute('stroke-width', '30px'); - }); - - Array.from(selected.querySelectorAll('.neume')).forEach((neume: HTMLElement) => { - neume.style.fill = ''; - }); - - Array.from(selected.querySelectorAll('.sylTextRect-display')).forEach((sylRect: HTMLElement) => { - sylRect.style.fill = 'blue'; - }); + Array.from(selected.querySelectorAll('.divLine')).forEach( + (divLine: HTMLElement) => { + divLine.style.stroke = ''; + divLine.setAttribute('stroke-width', '30px'); + }, + ); + + Array.from(selected.querySelectorAll('.neume')).forEach( + (neume: HTMLElement) => { + neume.style.fill = ''; + }, + ); + + Array.from(selected.querySelectorAll('.sylTextRect-display')).forEach( + (sylRect: HTMLElement) => { + sylRect.style.fill = 'blue'; + }, + ); if (selected.parentElement.classList.contains('syllable-highlighted')) { selected.parentElement.style.fill = ''; selected.parentElement.classList.add('syllable'); selected.parentElement.classList.remove('syllable-highlighted'); - - Array.from(selected.parentElement.querySelectorAll('.divLine')).forEach((divLine: HTMLElement) => { - divLine.style.stroke = ''; - divLine.setAttribute('stroke-width', '30px'); - }); - - Array.from(selected.parentElement.querySelectorAll('.neume')).forEach((neume: HTMLElement) => { - neume.style.fill = ''; - }); + + Array.from(selected.parentElement.querySelectorAll('.divLine')).forEach( + (divLine: HTMLElement) => { + divLine.style.stroke = ''; + divLine.setAttribute('stroke-width', '30px'); + }, + ); + + Array.from(selected.parentElement.querySelectorAll('.neume')).forEach( + (neume: HTMLElement) => { + neume.style.fill = ''; + }, + ); } d3.selectAll('#resizeRect').remove(); d3.selectAll('.resizePoint').remove(); d3.selectAll('.rotatePoint').remove(); - }); - Array.from(document.querySelectorAll('.text-select')).forEach((el: SVGElement) => { - el.style.color = ''; - el.style.fontWeight = ''; - el.classList.remove('text-select'); - }); - + Array.from(document.querySelectorAll('.text-select')).forEach( + (el: SVGElement) => { + el.style.color = ''; + el.style.fontWeight = ''; + el.classList.remove('text-select'); + }, + ); + if (!document.getElementById('selByStaff').classList.contains('is-active')) { Grouping.endGroupingSelection(); } else { @@ -92,7 +107,10 @@ export function unselect (): void { * @param staff - The staff element in the DOM. * @param dragHandler - The drag handler in use. */ -export function selectStaff (staff: SVGGElement, dragHandler: DragHandler): void { +export function selectStaff( + staff: SVGGElement, + dragHandler: DragHandler, +): void { if (!staff.classList.contains('selected')) { staff.classList.add('selected'); Color.unhighlight(staff); @@ -108,7 +126,10 @@ export function selectStaff (staff: SVGGElement, dragHandler: DragHandler): void * @param layerElement - The layer element in the DOM. * @param dragHandler - The drag handler in use. */ -export function selectLayerElement (layerElement: SVGGElement, dragHandler: DragHandler): void { +export function selectLayerElement( + layerElement: SVGGElement, + dragHandler: DragHandler, +): void { if (!layerElement.classList.contains('selected')) { layerElement.classList.add('selected'); Color.unhighlight(layerElement); @@ -123,7 +144,11 @@ export function selectLayerElement (layerElement: SVGGElement, dragHandler: Drag * @param dragHandler - Only used for staves. * @param needsHighlightUpdate - Whether all the group's highlights should be updated */ -export function select (el: SVGGraphicsElement, dragHandler?: DragHandler, needsHighlightUpdate = true): void { +export function select( + el: SVGGraphicsElement, + dragHandler?: DragHandler, + needsHighlightUpdate = true, +): void { // If element does not exist, exit if (!el) return; @@ -134,18 +159,23 @@ export function select (el: SVGGraphicsElement, dragHandler?: DragHandler, needs return selectLayerElement(el, dragHandler); } - if (!el.classList.contains('selected') && !el.classList.contains('sylTextRect') && - !el.classList.contains('sylTextRect-display')) { + if ( + !el.classList.contains('selected') && + !el.classList.contains('sylTextRect') && + !el.classList.contains('sylTextRect-display') + ) { el.classList.add('selected'); // set fill to red // set stroke to red only if selected elem is a divLine el.style.fill = '#d00'; - el.style.stroke = (el.classList.contains('divLine'))? '#d00' : 'black'; + el.style.stroke = el.classList.contains('divLine') ? '#d00' : 'black'; if (el.querySelectorAll('.sylTextRect-display').length) { - el.querySelectorAll('.sylTextRect-display').forEach((elem: HTMLElement) => { - elem.style.fill = '#d00'; - }); + el.querySelectorAll('.sylTextRect-display').forEach( + (elem: HTMLElement) => { + elem.style.fill = '#d00'; + }, + ); } if (el.querySelectorAll('.divLine').length) { @@ -154,7 +184,7 @@ export function select (el: SVGGraphicsElement, dragHandler?: DragHandler, needs }); } - if(el.classList.contains('syllable')) { + if (el.classList.contains('syllable')) { el.querySelectorAll('.neume').forEach((elem: HTMLElement) => { elem.style.fill = '#d00'; }); @@ -180,13 +210,16 @@ export function select (el: SVGGraphicsElement, dragHandler?: DragHandler, needs updateHighlight(); } } - /** * Select an nc. * @param el - The neume component. */ -export async function selectNcs (el: SVGGraphicsElement, neonView: NeonView, dragHandler: DragHandler): Promise { +export async function selectNcs( + el: SVGGraphicsElement, + neonView: NeonView, + dragHandler: DragHandler, +): Promise { if (!el.parentElement.classList.contains('selected')) { const parent = el.parentElement as unknown as SVGGraphicsElement; unselect(); @@ -218,36 +251,46 @@ export async function selectNcs (el: SVGGraphicsElement, neonView: NeonView, dra * @param neonView - The [[NeonView]] for this instance. * @returns True if the neume component is part of a ligature. */ -export async function isLigature (nc: SVGGraphicsElement, neonView: NeonView): Promise { - const attributes: Attributes = await neonView.getElementAttr(nc.id, neonView.view.getCurrentPageURI()); +export async function isLigature( + nc: SVGGraphicsElement, + neonView: NeonView, +): Promise { + const attributes: Attributes = await neonView.getElementAttr( + nc.id, + neonView.view.getCurrentPageURI(), + ); return Boolean(attributes.ligated); } - /** * Check if list of elements of a certain type are logically adjacent to each other. * Includes elements that are on separate staves but would otherwise be next to each other. * Can not apply to elements of type neume component. - * + * * @param selectionType user selection mode * @param elements the elements of interest * @returns true if elements are adjacent, false otherwise */ -export function areAdjacent(selectionType: string, elements: SVGGraphicsElement[]): boolean { +export function areAdjacent( + selectionType: string, + elements: SVGGraphicsElement[], +): boolean { // 2 elements cannot be adjacent if there is only 1 element if (elements.length < 2) return false; // get all elements that are of the same type as selectionType let allElemsOfSelectionType: HTMLElement[]; - switch(selectionType) { + switch (selectionType) { case 'selBySyllable': - allElemsOfSelectionType = Array.from(document.querySelectorAll('.syllable')); + allElemsOfSelectionType = Array.from( + document.querySelectorAll('.syllable'), + ); break; case 'selByNeume': - // We automatically return 'true' for neumes because we want the user to be + // We automatically return 'true' for neumes because we want the user to be // allowed to group two neumes in separate syllabes without having to parform other - // steps first. Yes, there is a trade-off - this allows users to try and group any + // steps first. Yes, there is a trade-off - this allows users to try and group any // two non-adjacent neumes, but it was decided that the benefits (speed and efficiency) // outweigh the costs (user trying an illegal edit). return true; @@ -263,23 +306,23 @@ export function areAdjacent(selectionType: string, elements: SVGGraphicsElement[ default: return false; } - - // Sort SELECTED elements in order of appearance by + + // Sort SELECTED elements in order of appearance by // matching to order of ALL elements of selection type const sortedElements = []; - for (let i=0; i !(elem.classList.contains('divLine') || elem.classList.contains('accid') || elem.classList.contains('clef'))); +export function sharedLogicalParent( + selectionType: string, + elements: SVGGraphicsElement[], +): boolean { + elements = elements.filter( + (elem) => + !( + elem.classList.contains('divLine') || + elem.classList.contains('accid') || + elem.classList.contains('clef') + ), + ); if (!elementsHaveCorrectType(selectionType, elements)) return false; - switch(selectionType) { + switch (selectionType) { case 'selBySyllable': const referenceParentStaff = elements[0].closest('.staff'); - for (let i=0; i { if (!elem.classList.contains('syllable')) return false; @@ -373,12 +428,13 @@ export function elementsHaveCorrectType(selectionType: string, elements: SVGGrap return true; } - /** * @param elements - The elements to compare. * @returns True if the elements have the same parent up two levels, otherwise false. */ -export function sharedSecondLevelParent (elements: SVGGraphicsElement[]): boolean { +export function sharedSecondLevelParent( + elements: SVGGraphicsElement[], +): boolean { const tempElements = Array.from(elements); const firstElement = tempElements.pop(); const secondParent = firstElement.parentElement.parentElement; @@ -399,10 +455,10 @@ export function sharedSecondLevelParent (elements: SVGGraphicsElement[]): boolea export function isMultiStaveSelection(elements: SVGElement[]): boolean { const elementsArray = Array.from(elements); - for (let i=0; i { - const coordinates: number[] = path.getAttribute('d') + staff.querySelectorAll('path').forEach((path) => { + const coordinates: number[] = path + .getAttribute('d') .match(/\d+/g) - .map(element => Number(element)); + .map((element) => Number(element)); if (rotate === undefined) { - rotate = Math.atan((coordinates[3] - coordinates[1]) / - (coordinates[2] - coordinates[0])); + rotate = Math.atan( + (coordinates[3] - coordinates[1]) / (coordinates[2] - coordinates[0]), + ); } if (uly === undefined || Math.min(coordinates[1], coordinates[3]) < uly) { @@ -454,7 +511,7 @@ export function getStaffBBox (staff: SVGGElement): StaffBBox { } }); - return { id: staff.id, ulx, uly, lrx, lry, rotate, }; + return { id: staff.id, ulx, uly, lrx, lry, rotate }; } /** @@ -462,7 +519,11 @@ export function getStaffBBox (staff: SVGGElement): StaffBBox { * @param el - the bbox (sylTextRect) element in the DOM * @param dragHandler - the drag handler in use */ -export function selectBBox (el: SVGGraphicsElement, dragHandler: DragHandler, neonView: NeonView): void { +export function selectBBox( + el: SVGGraphicsElement, + dragHandler: DragHandler, + neonView: NeonView, +): void { const bbox = el; const syl = bbox.closest('.syl'); if (!syl.classList.contains('selected')) { @@ -471,7 +532,7 @@ export function selectBBox (el: SVGGraphicsElement, dragHandler: DragHandler, ne const closest = el.closest('.syllable') as HTMLElement; closest.style.fill = '#d00'; closest.classList.add('syllable-highlighted'); - if(closest.querySelectorAll('.divLine').length) { + if (closest.querySelectorAll('.divLine').length) { closest.querySelectorAll('.neume').forEach((elem: HTMLElement) => { elem.style.fill = '#d00'; }); @@ -480,7 +541,7 @@ export function selectBBox (el: SVGGraphicsElement, dragHandler: DragHandler, ne }); } - if (neonView !== undefined ){ + if (neonView !== undefined) { resize(syl as SVGGraphicsElement, neonView, dragHandler); } if (dragHandler !== undefined) { @@ -503,9 +564,11 @@ export function selectBBox (el: SVGGraphicsElement, dragHandler: DragHandler, ne * Select not neume elements. * @param notNeumes - An array of not neumes elements. */ -export function selectNn (notNeumes: SVGGraphicsElement[]): boolean { +export function selectNn(notNeumes: SVGGraphicsElement[]): boolean { if (notNeumes.length > 0) { - notNeumes.forEach(nn => { select(nn); }); + notNeumes.forEach((nn) => { + select(nn); + }); return false; } else { return true; @@ -515,7 +578,11 @@ export function selectNn (notNeumes: SVGGraphicsElement[]): boolean { /** * Handle selecting an array of elements based on the selection type. */ -export async function selectAll (elements: Array, neonView: NeonView, dragHandler: DragHandler): Promise { +export async function selectAll( + elements: Array, + neonView: NeonView, + dragHandler: DragHandler, +): Promise { const selectionType = getSelectionType(); unselect(); if (elements.length === 0) { @@ -558,7 +625,11 @@ export async function selectAll (elements: Array, neonView: // Check if we click-selected a clef or a custos or an accid or a divLine grouping = element.closest('.clef, .custos, .accid, .divLine'); if (grouping === null) { - console.warn('Element ' + element.id + ' is not part of specified group and is not a clef or custos or accid or divLine.'); + console.warn( + 'Element ' + + element.id + + ' is not part of specified group and is not a clef or custos or accid or divLine.', + ); continue; } containsLayerElements = containsLayerElements || true; @@ -568,10 +639,10 @@ export async function selectAll (elements: Array, neonView: groupsToSelect.add(grouping); // Check for precedes/follows - let selected = grouping; + let selected = grouping; let follows = selected.getAttribute('mei:follows'); let precedes = selected.getAttribute('mei:precedes'); - while (follows || precedes){ + while (follows || precedes) { if (follows) { selected = document.querySelector('#' + follows.slice(1)); if (groupsToSelect.has(selected)) { @@ -593,7 +664,9 @@ export async function selectAll (elements: Array, neonView: } } // Select the elements - groupsToSelect.forEach((group: SVGGraphicsElement) => select(group, dragHandler, false)); + groupsToSelect.forEach((group: SVGGraphicsElement) => + select(group, dragHandler, false), + ); /* Determine the context menu to display (if any) */ @@ -602,9 +675,9 @@ export async function selectAll (elements: Array, neonView: // Handle occurance of clef or custos or accid or divLine if (containsLayerElements && !containsNc) { // A context menu will only be displayed if there is a single clef - if (groupsToSelect.size === 1 ) { + if (groupsToSelect.size === 1) { SelectOptions.triggerLayerElementActions(groups[0]); - }else { + } else { if (selectionType == 'selBySyllable') { SelectOptions.triggerDefaultSylActions(); } else { @@ -631,36 +704,26 @@ export async function selectAll (elements: Array, neonView: case 'selByLayerElement': if (groupsToSelect.size === 1) { SelectOptions.triggerLayerElementActions(groups[0]); - }else { + } else { SelectOptions.triggerDefaultActions(); } break; case 'selBySyllable': - switch (groups.length) { case 1: // TODO change context if it is only a neume/nc. SelectOptions.triggerSyllableActions('singleSelect'); break; - case 2: + default: // Check if this is a linked syllable split by a staff break // if they are linkable, user can toggle linked-sylls - if (Grouping.isLinkable('selBySyllable', groups)) { + if (Grouping.isLinkable('selBySyllable', groups)) { SelectOptions.triggerSyllableActions('linkableSelect'); } - else if (Grouping.isGroupable('selBySyllable', groups)) { - SelectOptions.triggerSyllableActions('multiSelect'); - } - else { - SelectOptions.triggerSyllableActions('default'); - } - break; - - default: // if syllables are all located on one stave, they should be groupable - if (Grouping.isGroupable('selBySyllable', groups)) { + else if (Grouping.isGroupable('selBySyllable', groups)) { SelectOptions.triggerSyllableActions('multiSelect'); } // if sylls are accross multiple staves @@ -697,31 +760,41 @@ export async function selectAll (elements: Array, neonView: // Check if these neume components are part of the same neume if (groups[0].parentElement === groups[1].parentElement) { const children = Array.from(groups[0].parentElement.children); - + // Check that neume components are adjacent - if (Math.abs(children.indexOf(groups[0]) - children.indexOf(groups[1])) === 1) { - + if ( + Math.abs( + children.indexOf(groups[0]) - children.indexOf(groups[1]), + ) === 1 + ) { // Check that second neume component is lower than first. // Note that the order in the list may not be the same as the // order by x-position. - let firstNC = (groups[0].children[0] as SVGUseElement); - let secondNC = (groups[1].children[0] as SVGUseElement); + let firstNC = groups[0].children[0] as SVGUseElement; + let secondNC = groups[1].children[0] as SVGUseElement; - let firstNCX = firstNC.x.baseVal.value; + let firstNCX = firstNC.x.baseVal.value; let secondNCX = secondNC.x.baseVal.value; - let firstNCY = firstNC.y.baseVal.value; + let firstNCY = firstNC.y.baseVal.value; let secondNCY = secondNC.y.baseVal.value; // order nc's by x coord (left to right) - if ( (firstNCX > secondNCX) - || (firstNCX === secondNCX && firstNCY < secondNCY)) { + if ( + firstNCX > secondNCX || + (firstNCX === secondNCX && firstNCY < secondNCY) + ) { [firstNC, secondNC] = [secondNC, firstNC]; - [firstNCX, firstNCY, secondNCX, secondNCY] = [secondNCX, secondNCY, firstNCX, firstNCY]; + [firstNCX, firstNCY, secondNCX, secondNCY] = [ + secondNCX, + secondNCY, + firstNCX, + firstNCY, + ]; } // if stacked nc's/ligature (identical x), or descending nc's (y descends) if (firstNCX === secondNCX || firstNCY < secondNCY) { - Grouping.triggerGrouping('ligature'); + Grouping.triggerGrouping('ligature'); break; } } @@ -746,14 +819,14 @@ export async function selectAll (elements: Array, neonView: SelectOptions.triggerBBoxActions(); break; default: - groups.forEach(g => selectBBox(g, dragHandler, undefined)); + groups.forEach((g) => selectBBox(g, dragHandler, undefined)); break; } break; default: console.error('Unknown selection type. This should not have occurred.'); } - + // function changeStaffListener(): void { // try { // document.getElementById('changeStaff')