From a637a7fed4ad5c2180e45d2ab6ffce7603b8b922 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 16 Nov 2024 22:58:37 -0800 Subject: [PATCH 1/4] fix: Make sure warnings are shown every time Formula changes --- src/sequences/Formula.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sequences/Formula.ts b/src/sequences/Formula.ts index 31c515fe..e6bab02f 100644 --- a/src/sequences/Formula.ts +++ b/src/sequences/Formula.ts @@ -2,7 +2,7 @@ import {Cached} from './Cached' import {SequenceExportModule} from './SequenceInterface' import {math, MathFormula} from '@/shared/math' -import type {GenericParamDescription} from '@/shared/Paramable' +import type {ParamValues, GenericParamDescription} from '@/shared/Paramable' import {ParamType} from '@/shared/ParamType' /** md @@ -114,6 +114,11 @@ class Formula extends Cached(paramDesc) { this.name = formulaMark + this.formula.source } + assignParameters(realized?: ParamValues) { + this.nErrors = 0 + return super.assignParameters(realized) + } + calculate(n: bigint) { let result = 0 let resultType = '' From a53d7ea43fb85596e60118e786f4d40a1a4b9b23 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sat, 16 Nov 2024 20:13:48 -0800 Subject: [PATCH 2/4] fix: Drag into sidebar always docks into an empty zone Prior to this PR, dragging a tab on top of a docked one would leave it floating over the docked tab. Moreover, if the empty zone in the sidebar was less than half the size of the tab being dragged, it would be impossible to dock the dragged tab at all. Now any drag into a sidebar with at least one empty zone results in a docking to the targeted zone if it is empty, or the first empty zone if not. Resolves #431. Note that this PR does not at the moment implement any resizing. There is resizing described in issue #431. However, now that this is working smoothly, it is making perfect visual sense to me that the dragged tab fits itself into the remaining empty space, leaving the already-docked tab alone, and in fact, this behavior seems to me preferable to what's described in #431. But if there remains a clear desire for the resizing as described in the issue, that can be implemented. Just let me know. --- src/views/Scope.vue | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/views/Scope.vue b/src/views/Scope.vue index eda6d32d..9003b55b 100644 --- a/src/views/Scope.vue +++ b/src/views/Scope.vue @@ -434,19 +434,21 @@ visualizers you can select. overlap: 0.5, // activates when a tab is dragged over a dropzone ondragenter: function (event: InteractEvent) { - event.target.classList.add('drop-hover') + event.target.parentElement?.parentElement?.classList.add( + 'drop-hover' + ) }, // activates when a tab is dragged out of a dropzone ondragleave: function (event: InteractEvent) { - event.target.classList.remove('drop-hover') - const dropzone = event.target const dropzoneContainer = dropzone.parentElement?.parentElement + if (!(dropzoneContainer instanceof HTMLElement)) return + dropzoneContainer.classList.remove('drop-hover') + const tab = event.relatedTarget?.parentElement if ( tab instanceof HTMLElement - && dropzoneContainer instanceof HTMLElement && tab.classList.contains('docked') ) { // Both individual dropzones and their containers have an @@ -469,16 +471,31 @@ visualizers you can select. // activates when tab is dropped in dropzone ondrop: function (event) { const tab = event.relatedTarget.parentElement - const dropzone = event.target + let dropzone = event.target const dropzoneContainer = dropzone.parentElement.parentElement if ( tab instanceof HTMLElement && dropzoneContainer instanceof HTMLElement - && dropzone.classList.contains('empty') ) { + if (!dropzone.classList.contains('empty')) { + // Drop into any empty dropzone in same container + const oldDropzone = dropzone + for (const wrapper of dropzoneContainer.children) { + const newDropzone = wrapper.children[0] + if ( + newDropzone !== oldDropzone + && newDropzone.classList.contains('empty') + ) { + dropzone = newDropzone + break + } + } + if (dropzone === oldDropzone) return // no empty zone here + } + // We have found an empty drop zone dropzone.classList.remove('empty') - dropzone.classList.remove('drop-hover') + dropzoneContainer.classList.remove('drop-hover') dropzoneContainer.classList.remove('empty') tab.classList.add('docked') tab.setAttribute('docked', dropzone.getAttribute('dropzone')) @@ -689,15 +706,15 @@ visualizers you can select. .dropzone { flex-grow: 1; - - &.drop-hover { - background-color: var(--ns-color-primary); - filter: brightness(120%); - } } } } + .dropzone-container.drop-hover .dropzone { + background-color: var(--ns-color-primary); + filter: brightness(120%); + } + .tab { width: 300px; position: absolute; From 46d1ac93edb2066e2a21110ce0e5b54a627d4f98 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 17 Nov 2024 15:41:40 -0800 Subject: [PATCH 3/4] chore: revert change to Formula.ts left over from Git manipulations --- src/sequences/Formula.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/sequences/Formula.ts b/src/sequences/Formula.ts index e6bab02f..31c515fe 100644 --- a/src/sequences/Formula.ts +++ b/src/sequences/Formula.ts @@ -2,7 +2,7 @@ import {Cached} from './Cached' import {SequenceExportModule} from './SequenceInterface' import {math, MathFormula} from '@/shared/math' -import type {ParamValues, GenericParamDescription} from '@/shared/Paramable' +import type {GenericParamDescription} from '@/shared/Paramable' import {ParamType} from '@/shared/ParamType' /** md @@ -114,11 +114,6 @@ class Formula extends Cached(paramDesc) { this.name = formulaMark + this.formula.source } - assignParameters(realized?: ParamValues) { - this.nErrors = 0 - return super.assignParameters(realized) - } - calculate(n: bigint) { let result = 0 let resultType = '' From af43302a5ec1960e3b2745ba51e6f869b7daaaac Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 20 Nov 2024 00:30:17 -0800 Subject: [PATCH 4/4] fix: tab drags and docking/undocking preserve size whenever possible Also fixes bug that sometimes prevented docking in an apparently empty spot (the `empty` class was not being properly maintained). --- src/components/Tab.vue | 12 ++--- src/views/Scope.vue | 103 ++++++++++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/src/components/Tab.vue b/src/components/Tab.vue index 00affc01..fa69ab0b 100644 --- a/src/components/Tab.vue +++ b/src/components/Tab.vue @@ -25,6 +25,7 @@ import interact from 'interactjs' import type {InteractEvent} from '@interactjs/types' import { + maybeClearDropzoneContainer, positionAndSizeTab, positionAndSizeAllTabs, selectTab, @@ -258,8 +259,7 @@ ) const dropzoneContainer = dropzone?.parentElement?.parentElement if ( - tab instanceof HTMLElement - && dropzone instanceof HTMLElement + dropzone instanceof HTMLElement && dropzoneContainer instanceof HTMLElement && tab.classList.contains('docked') ) { @@ -267,13 +267,7 @@ dropzone.classList.add('empty') tab.classList.remove('docked') tab.setAttribute('docked', 'none') - // if both dropzones are empty, - // make the dropzone container empty aswell - if ( - dropzoneContainer.querySelectorAll('.empty').length == 2 - ) { - dropzoneContainer.classList.add('empty') - } + maybeClearDropzoneContainer(dropzoneContainer) } return } diff --git a/src/views/Scope.vue b/src/views/Scope.vue index 9003b55b..c5395666 100644 --- a/src/views/Scope.vue +++ b/src/views/Scope.vue @@ -179,7 +179,10 @@ visualizers you can select. /** * Positions a tab to be inside a dropzone - * Resizes the tab to be the same size as the dropzone + * If the other dropzone in its container is empty, resizes the zone + * to be the same size as the tab; otherwise, resizes the tab to be the + * same size as the dropzone. + * * @param tab The HTML container that the draggable tab lives in * @param dropzone The dropzone the tab is docked to */ @@ -189,9 +192,38 @@ visualizers you can select. ): void { if (isMobile()) return - const dropzoneContainer = dropzone.parentElement?.parentElement + const dropzoneWrapper = dropzone.parentElement + if (!(dropzoneWrapper instanceof HTMLElement)) return + const dropzoneContainer = dropzoneWrapper.parentElement + if (!(dropzoneContainer instanceof HTMLElement)) return + const containerRect = dropzoneContainer.getBoundingClientRect() + const tabRect = tab.getBoundingClientRect() + + let resizeDropzone = !!dropzoneContainer.querySelector('.empty') + let toHeight = 0 + if (resizeDropzone) { + // Find the resizable dropzone wrapper + const resizeHandle = + dropzoneContainer.querySelector('.dropzone-resize') + if (!(resizeHandle instanceof HTMLElement)) return + const resizableWrapper = resizeHandle.parentElement + if (!(resizableWrapper instanceof HTMLElement)) return + if (resizableWrapper.classList.contains('resized')) { + resizeDropzone = false + } else { + if (resizableWrapper === dropzoneWrapper) { + toHeight = tabRect.height + 16 + } else { + toHeight = containerRect.height - tabRect.height + } + resizableWrapper.style.height = toHeight + 'px' + resizableWrapper.classList.add('resized') + } + } const dropzoneRect = dropzone.getBoundingClientRect() + if (!resizeDropzone) tab.style.height = dropzoneRect.height + 'px' + // Now that everything is the correct size, reposition the tab: const {x, y} = translateCoords(dropzoneRect.x, dropzoneRect.y) tab.style.top = y + 'px' @@ -203,11 +235,10 @@ visualizers you can select. tab.style.removeProperty('right') tab.style.left = x + 'px' } - tab.style.height = dropzoneRect.height + 'px' // update the classlist with "minimized" // if the height is less or equal than 110 - if (dropzoneRect.height <= 110) { + if (parseInt(getComputedStyle(tab).height) <= 110) { tab.classList.add('minimized') } else { tab.classList.remove('minimized') @@ -216,12 +247,7 @@ visualizers you can select. tab.setAttribute('data-x', x.toString()) tab.setAttribute('data-y', y.toString()) - if ( - tab instanceof HTMLElement - && dropzoneContainer instanceof HTMLElement - && dropzone instanceof HTMLElement - && dropzone.classList.contains('empty') - ) { + if (dropzone.classList.contains('empty')) { dropzone.classList.remove('empty') dropzone.classList.remove('drop-hover') dropzoneContainer.classList.remove('empty') @@ -258,12 +284,26 @@ visualizers you can select. return {x, y} } + /** + * Resets the state of a dropzone container if it empties out + */ + export function maybeClearDropzoneContainer(container: Element) { + if (container.querySelectorAll('.empty').length == 2) { + container.classList.add('empty') + const resizableWrapper = container.firstChild + if (!(resizableWrapper instanceof HTMLElement)) return + resizableWrapper.classList.remove('resized') + resizableWrapper.style.removeProperty('height') + } + } + /** * Places every docked tab back in its position and size. * Doesn't affect non-docked tabs. * Used when the window is resized. */ export function positionAndSizeAllTabs(): void { + // First reset the "empty" classes and recompute them, just in case: document .querySelectorAll('.dropzone') .forEach((dropzone: Element) => { @@ -279,19 +319,25 @@ visualizers you can select. ) if (!(dropzone instanceof HTMLElement)) return dropzone.classList.remove('empty') - positionAndSizeTab(tab, dropzone) }) document .querySelectorAll('.dropzone-container') - .forEach((container: Element) => { - if (container.querySelectorAll('.empty').length == 2) { - container.classList.add('empty') - } else { - container.classList.remove('empty') - } - }) + .forEach(dc => maybeClearDropzoneContainer(dc)) + + // Now actually position and resize all of the tabs + document.querySelectorAll('.tab').forEach((tab: Element) => { + if (!(tab instanceof HTMLElement)) return + if (tab.getAttribute('docked') === 'none') return + + const dropzone = document.querySelector( + '#' + tab.getAttribute('docked') + '-dropzone' + ) + if (!(dropzone instanceof HTMLElement)) return + positionAndSizeTab(tab, dropzone) + }) } + // selects a tab export function selectTab(tab: HTMLElement): void { deselectTab() @@ -302,6 +348,7 @@ visualizers you can select. drag.style.backgroundColor = 'var(--ns-color-primary)' tab.style.zIndex = '100' } + // deselects all tabs export function deselectTab(): void { const tabs = document.querySelectorAll('.tab') @@ -459,12 +506,7 @@ visualizers you can select. dropzone.classList.add('empty') tab.classList.remove('docked') tab.setAttribute('docked', 'none') - - if ( - dropzoneContainer.querySelectorAll('.empty').length == 2 - ) { - dropzoneContainer.classList.add('empty') - } + maybeClearDropzoneContainer(dropzoneContainer) } }, @@ -478,7 +520,14 @@ visualizers you can select. tab instanceof HTMLElement && dropzoneContainer instanceof HTMLElement ) { - if (!dropzone.classList.contains('empty')) { + let occupied = !dropzone.classList.contains('empty') + if ( + tab.classList.contains('docked') + && tab.getAttribute('docked') + === dropzone.getAttribute('dropzone') + ) + occupied = false // ok to just go back into zone you're in + if (occupied) { // Drop into any empty dropzone in same container const oldDropzone = dropzone for (const wrapper of dropzoneContainer.children) { @@ -528,9 +577,7 @@ visualizers you can select. move(event: InteractEvent) { const dropzoneWrapper = event.target - const dropzoneCont = - dropzoneWrapper.parentElement?.parentElement - + const dropzoneCont = dropzoneWrapper.parentElement if ( dropzoneWrapper instanceof HTMLElement && dropzoneCont instanceof HTMLElement