diff --git a/Lara-JS/src-api/visualization/public/css/color-scheme.css b/Lara-JS/src-api/visualization/public/css/color-scheme.css index 9601655a8..5ed4761f1 100644 --- a/Lara-JS/src-api/visualization/public/css/color-scheme.css +++ b/Lara-JS/src-api/visualization/public/css/color-scheme.css @@ -2,12 +2,14 @@ :root { --white: #fff; + --translucid-white: #fff6; --lighter-gray: #e4e4e4; --light-gray: #d4d4d4; --gray: #a2a2a2; --dark-gray: #747474; --darker-gray: #313131; --black: #161616; + --translucid-black: #16161666; --light-blue: #19d8ff; --strong-translucid-light-blue: #19d8ff66; --weak-translucid-light-blue: #19d8ff33; @@ -23,6 +25,7 @@ :root { --bg-color: var(--white); + --translucid-bg-color: var(--translucid-white); --text-color: var(--darker-gray); --header-color: var(--lighter-gray); --border-color: var(--gray); @@ -50,6 +53,7 @@ @media (prefers-color-scheme: dark) { :root { --bg-color: var(--black); + --translucid-bg-color: var(--translucid-black); --text-color: var(--white); --header-color: var(--darker-gray); --border-color: var(--lighter-gray); diff --git a/Lara-JS/src-api/visualization/public/css/styles.css b/Lara-JS/src-api/visualization/public/css/styles.css index 5fd7e5a1d..65c9aa66d 100644 --- a/Lara-JS/src-api/visualization/public/css/styles.css +++ b/Lara-JS/src-api/visualization/public/css/styles.css @@ -241,7 +241,7 @@ main { #file-tabs { box-sizing: border-box; - height: 3em; + height: 3.5em; width: fit-content; max-width: 100%; padding: 0.125em; @@ -254,7 +254,10 @@ main { grid-area: file-tabs; position: relative; bottom: calc(-1em + 1px); +} +#file-tabs > div { + height: 100%; display: flex; align-items: start; justify-content: left; @@ -294,6 +297,31 @@ main { background-color: var(--tab-active-bg-color); } +.file-tabs-arrow { + padding: 0.625em; + background-color: var(--translucid-bg-color); + border: none; + + position: absolute; + top: 0; +} + +.file-tabs-arrow:hover { + background-color: var(--translucid-bg-color); +} + +#file-tabs-arrow-left { + left: 0; +} + +#file-tabs-arrow-right { + right: 0; +} + +.file-tabs-arrow:disabled { + display: none; +} + .loading { color: var(--text-color); diff --git a/Lara-JS/src-api/visualization/public/js/components.ts b/Lara-JS/src-api/visualization/public/js/components.ts index 607301bc5..d3f66da75 100644 --- a/Lara-JS/src-api/visualization/public/js/components.ts +++ b/Lara-JS/src-api/visualization/public/js/components.ts @@ -91,6 +91,10 @@ const getFileCodeElement = (filename: string): HTMLElement | null => { }; +const getFileTabsInternalDiv = (): HTMLDivElement | null => { + return getFileTabs().querySelector('div'); +} + const getFileTab = (filepath: string): HTMLButtonElement | null => { return getFileTabs().querySelector(`.file-tab[data-filepath="${filepath}"]`); }; @@ -99,6 +103,10 @@ const getActiveFileTab = (): HTMLButtonElement | null => { return getFileTabs().querySelector('.file-tab.active'); }; +const getFileTabsArrow = (direction: 'left' | 'right'): HTMLButtonElement | null => { + return document.querySelector(`#file-tabs-arrow-${direction}`); +} + const createIcon = (name: string): HTMLElement => { const icon = document.createElement('span'); @@ -212,6 +220,46 @@ const createFileTab = (filepath: string): HTMLButtonElement => { return fileTab; }; +const fileTabsArrowOnClick = (event: Event, direction: 'left' | 'right') => { + const fileTabsInternalDiv = getFileTabsInternalDiv()!; + const activeTabIndex = Array.from(fileTabsInternalDiv.children).findIndex(tab => tab.classList.contains('active')); + + if (direction === 'left' && activeTabIndex > 0) { + (fileTabsInternalDiv.children[activeTabIndex - 1] as HTMLButtonElement).click(); + } else if (direction === 'right' && activeTabIndex < fileTabsInternalDiv.children.length - 1) { + (fileTabsInternalDiv.children[activeTabIndex + 1] as HTMLButtonElement).click(); + } + + event.stopPropagation(); +}; + +const createFileTabsArrow = (direction: 'left' | 'right'): HTMLButtonElement => { + const arrow = document.createElement('button'); + arrow.classList.add('file-tabs-arrow'); + arrow.id = `file-tabs-arrow-${direction}`; + + arrow.appendChild(createIcon(`keyboard_arrow_${direction}`)); + arrow.addEventListener('click', event => fileTabsArrowOnClick(event, direction)); + return arrow; +} + +/** + * @brief Updates the file tabs arrows, enabling or disabling them based on the + * number of tabs and selected tab. + */ +const updateFileTabsArrows = (): void => { + const fileTabs = getFileTabs(); + const fileTabsInternalDiv = getFileTabsInternalDiv()!; + const activeFileTab = getActiveFileTab(); + + const fileTabsLeftArrow = getFileTabsArrow('left')!; + const fileTabsRightArrow = getFileTabsArrow('right')!; + + const fileTabsOverflow = fileTabs.scrollWidth < fileTabsInternalDiv.scrollWidth; + fileTabsLeftArrow.disabled = !fileTabsOverflow || fileTabsInternalDiv.children[0] === activeFileTab; + fileTabsRightArrow.disabled = !fileTabsOverflow || fileTabsInternalDiv.children[fileTabsInternalDiv.children.length - 1] === activeFileTab; +} + export { getAstContainer, getCodeContainer, @@ -229,7 +277,9 @@ export { getActiveCodeElement, getFileCodeElement, getFileTab, + getFileTabsInternalDiv, getActiveFileTab, + getFileTabsArrow, createIcon, createNodeDropdown, createNodeDropdownButton, @@ -240,4 +290,6 @@ export { createCodeElement, createCodeWrapper, createFileTab, + createFileTabsArrow, + updateFileTabsArrows, }; \ No newline at end of file diff --git a/Lara-JS/src-api/visualization/public/js/files.ts b/Lara-JS/src-api/visualization/public/js/files.ts index 0654129e2..d5901e91e 100644 --- a/Lara-JS/src-api/visualization/public/js/files.ts +++ b/Lara-JS/src-api/visualization/public/js/files.ts @@ -4,7 +4,7 @@ */ import { addFileCode, updateLines } from "./ast-import.js"; -import { createFileTab, getActiveCodeElement, getActiveFileTab, getFileCodeElement, getFileTab, getFileTabs, getMainCodeWrapper } from "./components.js"; +import { createFileTab, createFileTabsArrow, getActiveCodeElement, getActiveFileTab, getFileCodeElement, getFileTab, getFileTabsInternalDiv, getFileTabs, getMainCodeWrapper, updateFileTabsArrows } from "./components.js"; let selectedFilepath: string | null = null; @@ -21,7 +21,14 @@ const addFile = (path: string, code: string): void => { fileTab.addEventListener('click', () => selectFile(path)); const fileTabs = getFileTabs(); - fileTabs.appendChild(fileTab); + const fileTabsInternalDiv = getFileTabsInternalDiv()!; + if (fileTabsInternalDiv.children.length === 0) { + const fileTabsLeftArrow = createFileTabsArrow('left'); + const fileTabsRightArrow = createFileTabsArrow('right'); + fileTabs.append(fileTabsLeftArrow, fileTabsRightArrow); + } + + fileTabsInternalDiv.appendChild(fileTab); }; /** @@ -34,6 +41,7 @@ const clearFiles = (): void => { const fileTabs = getFileTabs(); fileTabs.innerHTML = ''; + fileTabs.appendChild(document.createElement('div')); codeWrapper.innerHTML = ''; selectedFilepath = null; @@ -62,6 +70,9 @@ const selectFile = (filepath: string): void => { fileCodeElement.classList.add('active'); updateLines(); + fileTab.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + updateFileTabsArrows(); + selectedFilepath = filepath; } }; diff --git a/Lara-JS/src-api/visualization/public/js/visualization.ts b/Lara-JS/src-api/visualization/public/js/visualization.ts index e3bd575c3..0a1d8ad80 100644 --- a/Lara-JS/src-api/visualization/public/js/visualization.ts +++ b/Lara-JS/src-api/visualization/public/js/visualization.ts @@ -3,7 +3,7 @@ * @brief Functions for handling the visualization behavior and events. */ -import { createCodeElement, createCodeWrapper, createNodeInfoAlert, createNodeInfoLine, getAstContainer, getCodeContainer, getContinueButton, getFirstNodeCodeElement, getHighlightableElements, getNodeElement, getNodeInfoContainer, getNodeText, getResizer } from "./components.js"; +import { createCodeElement, createCodeWrapper, createNodeInfoAlert, createNodeInfoLine, getAstContainer, getCodeContainer, getContinueButton, getFileTabsInternalDiv, getFirstNodeCodeElement, getHighlightableElements, getNodeElement, getNodeInfoContainer, getNodeText, getResizer, updateFileTabsArrows } from "./components.js"; import { selectFile } from "./files.js"; import JoinPoint from "./ToolJoinPoint.js"; @@ -225,6 +225,9 @@ const addResizerEventListeners = (): void => { else if (width > maxWidth) width = maxWidth; rootStyle.setProperty('--ast-container-width', `${width}px`); + + if (getFileTabsInternalDiv()) + updateFileTabsArrows(); } }); }; diff --git a/LaraApi/src-lara/visualization/public/js/components.js b/LaraApi/src-lara/visualization/public/js/components.js index dca91ee2f..16fb916da 100644 --- a/LaraApi/src-lara/visualization/public/js/components.js +++ b/LaraApi/src-lara/visualization/public/js/components.js @@ -72,12 +72,18 @@ const getActiveCodeElement = () => { const getFileCodeElement = (filename) => { return getCodeContainer().querySelector(`code[data-filepath="${filename}"]`); }; +const getFileTabsInternalDiv = () => { + return getFileTabs().querySelector('div'); +}; const getFileTab = (filepath) => { return getFileTabs().querySelector(`.file-tab[data-filepath="${filepath}"]`); }; const getActiveFileTab = () => { return getFileTabs().querySelector('.file-tab.active'); }; +const getFileTabsArrow = (direction) => { + return document.querySelector(`#file-tabs-arrow-${direction}`); +}; const createIcon = (name) => { const icon = document.createElement('span'); icon.classList.add('icon', 'material-symbols-outlined'); @@ -163,5 +169,38 @@ const createFileTab = (filepath) => { fileTab.textContent = filepath !== '' ? filepath.slice(filepath.lastIndexOf('/') + 1) : ''; return fileTab; }; -export { getAstContainer, getCodeContainer, getNodeInfoContainer, getContinueButton, getResizer, getFileTabs, getNodeElement, getNodeText, getFirstNodeCodeElement, getNodeCodeElements, getHighlightableElements, getMainCodeWrapper, getCodeLines, getActiveCodeElement, getFileCodeElement, getFileTab, getActiveFileTab, createIcon, createNodeDropdown, createNodeDropdownButton, createNodeElement, createNodeInfoLine, createNodeInfoAlert, createCodeLines, createCodeElement, createCodeWrapper, createFileTab, }; +const fileTabsArrowOnClick = (event, direction) => { + const fileTabsInternalDiv = getFileTabsInternalDiv(); + const activeTabIndex = Array.from(fileTabsInternalDiv.children).findIndex(tab => tab.classList.contains('active')); + if (direction === 'left' && activeTabIndex > 0) { + fileTabsInternalDiv.children[activeTabIndex - 1].click(); + } + else if (direction === 'right' && activeTabIndex < fileTabsInternalDiv.children.length - 1) { + fileTabsInternalDiv.children[activeTabIndex + 1].click(); + } + event.stopPropagation(); +}; +const createFileTabsArrow = (direction) => { + const arrow = document.createElement('button'); + arrow.classList.add('file-tabs-arrow'); + arrow.id = `file-tabs-arrow-${direction}`; + arrow.appendChild(createIcon(`keyboard_arrow_${direction}`)); + arrow.addEventListener('click', event => fileTabsArrowOnClick(event, direction)); + return arrow; +}; +/** + * @brief Updates the file tabs arrows, enabling or disabling them based on the + * number of tabs and selected tab. + */ +const updateFileTabsArrows = () => { + const fileTabs = getFileTabs(); + const fileTabsInternalDiv = getFileTabsInternalDiv(); + const activeFileTab = getActiveFileTab(); + const fileTabsLeftArrow = getFileTabsArrow('left'); + const fileTabsRightArrow = getFileTabsArrow('right'); + const fileTabsOverflow = fileTabs.scrollWidth < fileTabsInternalDiv.scrollWidth; + fileTabsLeftArrow.disabled = !fileTabsOverflow || fileTabsInternalDiv.children[0] === activeFileTab; + fileTabsRightArrow.disabled = !fileTabsOverflow || fileTabsInternalDiv.children[fileTabsInternalDiv.children.length - 1] === activeFileTab; +}; +export { getAstContainer, getCodeContainer, getNodeInfoContainer, getContinueButton, getResizer, getFileTabs, getNodeElement, getNodeText, getFirstNodeCodeElement, getNodeCodeElements, getHighlightableElements, getMainCodeWrapper, getCodeLines, getActiveCodeElement, getFileCodeElement, getFileTab, getFileTabsInternalDiv, getActiveFileTab, getFileTabsArrow, createIcon, createNodeDropdown, createNodeDropdownButton, createNodeElement, createNodeInfoLine, createNodeInfoAlert, createCodeLines, createCodeElement, createCodeWrapper, createFileTab, createFileTabsArrow, updateFileTabsArrows, }; //# sourceMappingURL=components.js.map \ No newline at end of file diff --git a/LaraApi/src-lara/visualization/public/js/files.js b/LaraApi/src-lara/visualization/public/js/files.js index 6339628a3..90610de92 100644 --- a/LaraApi/src-lara/visualization/public/js/files.js +++ b/LaraApi/src-lara/visualization/public/js/files.js @@ -3,7 +3,7 @@ * @brief Functions for handling files in the visualization. */ import { addFileCode, updateLines } from "./ast-import.js"; -import { createFileTab, getActiveCodeElement, getActiveFileTab, getFileCodeElement, getFileTab, getFileTabs, getMainCodeWrapper } from "./components.js"; +import { createFileTab, createFileTabsArrow, getActiveCodeElement, getActiveFileTab, getFileCodeElement, getFileTab, getFileTabsInternalDiv, getFileTabs, getMainCodeWrapper, updateFileTabsArrows } from "./components.js"; let selectedFilepath = null; /** * @brief Adds a new file, with the respective file tab and (hidden) code, to the visualization. @@ -16,7 +16,13 @@ const addFile = (path, code) => { const fileTab = createFileTab(path); fileTab.addEventListener('click', () => selectFile(path)); const fileTabs = getFileTabs(); - fileTabs.appendChild(fileTab); + const fileTabsInternalDiv = getFileTabsInternalDiv(); + if (fileTabsInternalDiv.children.length === 0) { + const fileTabsLeftArrow = createFileTabsArrow('left'); + const fileTabsRightArrow = createFileTabsArrow('right'); + fileTabs.append(fileTabsLeftArrow, fileTabsRightArrow); + } + fileTabsInternalDiv.appendChild(fileTab); }; /** * @brief Clears all files from the code container. @@ -27,6 +33,7 @@ const clearFiles = () => { throw new Error('Code container not initialized'); const fileTabs = getFileTabs(); fileTabs.innerHTML = ''; + fileTabs.appendChild(document.createElement('div')); codeWrapper.innerHTML = ''; selectedFilepath = null; }; @@ -49,6 +56,8 @@ const selectFile = (filepath) => { const fileCodeElement = getFileCodeElement(filepath); fileCodeElement.classList.add('active'); updateLines(); + fileTab.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + updateFileTabsArrows(); selectedFilepath = filepath; } }; diff --git a/LaraApi/src-lara/visualization/public/js/visualization.js b/LaraApi/src-lara/visualization/public/js/visualization.js index 07558917a..90c9ce295 100644 --- a/LaraApi/src-lara/visualization/public/js/visualization.js +++ b/LaraApi/src-lara/visualization/public/js/visualization.js @@ -2,7 +2,7 @@ * @file visualization.ts * @brief Functions for handling the visualization behavior and events. */ -import { createCodeElement, createCodeWrapper, createNodeInfoAlert, createNodeInfoLine, getAstContainer, getCodeContainer, getContinueButton, getFirstNodeCodeElement, getHighlightableElements, getNodeElement, getNodeInfoContainer, getNodeText, getResizer } from "./components.js"; +import { createCodeElement, createCodeWrapper, createNodeInfoAlert, createNodeInfoLine, getAstContainer, getCodeContainer, getContinueButton, getFileTabsInternalDiv, getFirstNodeCodeElement, getHighlightableElements, getNodeElement, getNodeInfoContainer, getNodeText, getResizer, updateFileTabsArrows } from "./components.js"; import { selectFile } from "./files.js"; /** * @brief Highlights the node with the given id. @@ -185,6 +185,8 @@ const addResizerEventListeners = () => { else if (width > maxWidth) width = maxWidth; rootStyle.setProperty('--ast-container-width', `${width}px`); + if (getFileTabsInternalDiv()) + updateFileTabsArrows(); } }); };