Fix Svelte regressions related to some input widgets
This commit is contained in:
parent
a993938d80
commit
964cf6df15
|
|
@ -306,7 +306,7 @@
|
|||
value={newColor.toHexOptionalAlpha() || "-"}
|
||||
on:commitText={({ detail }) => setColorCode(detail)}
|
||||
centered={true}
|
||||
tooltip="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."
|
||||
tooltip={"Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."}
|
||||
/>
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
|
|
@ -347,8 +347,8 @@
|
|||
unit={channel === "h" ? "°" : "%"}
|
||||
minWidth={56}
|
||||
tooltip={{
|
||||
h: "Hue component, the "color" along the rainbow",
|
||||
s: "Saturation component, the "colorfulness" from gray to vivid",
|
||||
h: `Hue component, the "color" along the rainbow`,
|
||||
s: `Saturation component, the "colorfulness" from gray to vivid`,
|
||||
v: "Value (or Brightness), the distance away from being darkened to black",
|
||||
}[channel]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
<svelte:options accessors={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
|
|
@ -28,32 +30,23 @@
|
|||
export let virtualScrollingEntryHeight = 0;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
let isOpen = open;
|
||||
let highlighted = activeEntry as MenuListEntry | undefined;
|
||||
let virtualScrollingEntriesStart = 0;
|
||||
|
||||
// Called only when `open` is changed from outside this component (with v-model)
|
||||
$: watchOpen(open);
|
||||
$: dispatch("open", isOpen);
|
||||
$: watchEntries(entries, self);
|
||||
$: watchDrawIcon(drawIcon, self);
|
||||
$: watchRemeasureWidth(entries, drawIcon);
|
||||
$: virtualScrollingTotalHeight = entries.length === 0 ? 0 : entries[0].length * virtualScrollingEntryHeight;
|
||||
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
|
||||
$: virtualScrollingEndIndex = entries.length === 0 ? 0 : Math.min(entries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
|
||||
|
||||
function watchOpen(open: boolean) {
|
||||
isOpen = open;
|
||||
highlighted = activeEntry;
|
||||
dispatch("open", open);
|
||||
}
|
||||
|
||||
// TODO: Svelte: fix infinite loop and reenable
|
||||
function watchEntries(_: MenuListEntry[][], floatingMenu: FloatingMenu) {
|
||||
// floatingMenu?.div().measureAndEmitNaturalWidth();
|
||||
}
|
||||
|
||||
// TODO: Svelte: fix infinite loop and reenable
|
||||
function watchDrawIcon(_: boolean, floatingMenu: FloatingMenu) {
|
||||
// floatingMenu?.div().measureAndEmitNaturalWidth();
|
||||
function watchRemeasureWidth(_: MenuListEntry[][], __: boolean) {
|
||||
self?.measureAndEmitNaturalWidth();
|
||||
}
|
||||
|
||||
function onScroll(e: Event) {
|
||||
|
|
@ -69,29 +62,29 @@
|
|||
dispatch("activeEntry", menuListEntry);
|
||||
|
||||
// Close the containing menu
|
||||
if (menuListEntry.ref) menuListEntry.ref.isOpen = false;
|
||||
if (menuListEntry.ref) menuListEntry.ref.open = false;
|
||||
dispatch("open", false);
|
||||
isOpen = false; // TODO: This is a hack for MenuBarInput submenus, remove it when we get rid of using `ref`
|
||||
open = false;
|
||||
}
|
||||
|
||||
function onEntryPointerEnter(menuListEntry: MenuListEntry): void {
|
||||
if (!menuListEntry.children?.length) return;
|
||||
|
||||
if (menuListEntry.ref) menuListEntry.ref.isOpen = true;
|
||||
if (menuListEntry.ref) menuListEntry.ref.open = true;
|
||||
else dispatch("open", true);
|
||||
}
|
||||
|
||||
function onEntryPointerLeave(menuListEntry: MenuListEntry): void {
|
||||
if (!menuListEntry.children?.length) return;
|
||||
|
||||
if (menuListEntry.ref) menuListEntry.ref.isOpen = false;
|
||||
if (menuListEntry.ref) menuListEntry.ref.open = false;
|
||||
else dispatch("open", false);
|
||||
}
|
||||
|
||||
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
|
||||
if (!menuListEntry.children?.length) return false;
|
||||
|
||||
return open;
|
||||
return menuListEntry.ref?.open || false;
|
||||
}
|
||||
|
||||
/// Handles keyboard navigation for the menu. Returns if the entire menu stack should be dismissed
|
||||
|
|
@ -99,13 +92,13 @@
|
|||
// Interactive menus should keep the active entry the same as the highlighted one
|
||||
if (interactive) highlighted = activeEntry;
|
||||
|
||||
const menuOpen = isOpen;
|
||||
const menuOpen = open;
|
||||
const flatEntries = entries.flat().filter((entry) => !entry.disabled);
|
||||
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.isOpen);
|
||||
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.open);
|
||||
|
||||
const openSubmenu = (highlighted: MenuListEntry): void => {
|
||||
if (highlighted.ref && highlighted.children?.length) {
|
||||
highlighted.ref.isOpen = true;
|
||||
highlighted.ref.open = true;
|
||||
|
||||
// Highlight first item
|
||||
highlighted.ref.setHighlighted(highlighted.children[0][0]);
|
||||
|
|
@ -114,7 +107,7 @@
|
|||
|
||||
if (!menuOpen && (e.key === " " || e.key === "Enter")) {
|
||||
// Allow opening menu with space or enter
|
||||
isOpen = true;
|
||||
open = true;
|
||||
highlighted = activeEntry;
|
||||
} else if (menuOpen && openChild >= 0) {
|
||||
// Redirect the keyboard navigation to a submenu if one is open
|
||||
|
|
@ -125,7 +118,7 @@
|
|||
|
||||
// Handle the child closing the entire menu stack
|
||||
if (shouldCloseStack) {
|
||||
isOpen = false;
|
||||
open = false;
|
||||
return true;
|
||||
}
|
||||
} else if ((menuOpen || interactive) && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
||||
|
|
@ -145,7 +138,7 @@
|
|||
setHighlighted(newEntry);
|
||||
} else if (menuOpen && e.key === "Escape") {
|
||||
// Close menu with escape key
|
||||
isOpen = false;
|
||||
open = false;
|
||||
|
||||
// Reset active to before open
|
||||
setHighlighted(activeEntry);
|
||||
|
|
@ -164,7 +157,7 @@
|
|||
openSubmenu(highlighted);
|
||||
} else if (menuOpen && e.key === "ArrowLeft") {
|
||||
// Left arrow closes a submenu
|
||||
if (submenu) isOpen = false;
|
||||
if (submenu) open = false;
|
||||
}
|
||||
|
||||
// By default, keep the menu stack open
|
||||
|
|
@ -177,20 +170,15 @@
|
|||
if (interactive && newHighlight?.value !== activeEntry?.value && newHighlight) dispatch("activeEntry", newHighlight);
|
||||
}
|
||||
|
||||
// TODO: Svelte: Re-enable the `export` prefix
|
||||
export function scrollViewTo(distanceDown: number): void {
|
||||
scroller.div().scrollTo(0, distanceDown);
|
||||
}
|
||||
|
||||
export function menuIsOpen(): boolean {
|
||||
return open;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FloatingMenu
|
||||
class="menu-list"
|
||||
open={isOpen}
|
||||
on:open={({ detail }) => (isOpen = detail)}
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
on:naturalWidth
|
||||
type="Dropdown"
|
||||
windowEdgeMargin={0}
|
||||
|
|
@ -248,7 +236,7 @@
|
|||
{/if}
|
||||
|
||||
{#if entry.children}
|
||||
<svelte:self on:naturalWidth open={entry.ref?.menuIsOpen() || false} direction="TopRight" entries={entry.children} {minWidth} {drawIcon} {scrollableY} bind:this={entry.ref} />
|
||||
<svelte:self on:naturalWidth open={entry.ref?.open || false} direction="TopRight" entries={entry.children} {minWidth} {drawIcon} {scrollableY} bind:this={entry.ref} />
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -104,10 +104,8 @@
|
|||
// Gets the client bounds of the elements and apply relevant styles to them
|
||||
// TODO: Use the Vue :style attribute more whilst not causing recursive updates
|
||||
afterUpdate(() => {
|
||||
// Turning measuring on and off both cause the component to change, which causes the `updated()` Vue event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
|
||||
if (measuringOngoingGuard) return;
|
||||
|
||||
positionAndStyleFloatingMenu();
|
||||
// Turning measuring on and off both causes the component to change, which causes the `updated()` Vue event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
|
||||
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
|
||||
});
|
||||
|
||||
function resizeObserverCallback(entries: ResizeObserverEntry[]) {
|
||||
|
|
@ -204,13 +202,13 @@
|
|||
|
||||
// To be called by the parent component. Measures the actual width of the floating menu content element and returns it in a promise.
|
||||
export async function measureAndEmitNaturalWidth(): Promise<void> {
|
||||
if (!measuringOngoingGuard) return;
|
||||
|
||||
// Wait for the changed content which fired the `updated()` Vue event to be put into the DOM
|
||||
await tick();
|
||||
|
||||
// Wait until all fonts have been loaded and rendered so measurements of content involving text are accurate
|
||||
// API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (document as any).fonts.ready;
|
||||
await document.fonts.ready;
|
||||
|
||||
// Make the component show itself with 0 min-width so it can be measured, and wait until the values have been updated to the DOM
|
||||
measuringOngoing = true;
|
||||
|
|
|
|||
|
|
@ -60,25 +60,20 @@
|
|||
let layerTreeOptionsLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => {
|
||||
rebuildLayerTree(updateDocumentLayerTreeStructure);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => {
|
||||
patchWidgetLayout(layerTreeOptionsLayout, updateLayerTreeOptionsLayout);
|
||||
layerTreeOptionsLayout = layerTreeOptionsLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
|
||||
const targetPath = updateDocumentLayerDetails.data.path;
|
||||
const targetLayer = updateDocumentLayerDetails.data;
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => {
|
||||
rebuildLayerTree(updateDocumentLayerTreeStructure);
|
||||
});
|
||||
|
||||
const layer = layerCache.get(targetPath.toString());
|
||||
if (layer) {
|
||||
Object.assign(layer, targetLayer);
|
||||
} else {
|
||||
layerCache.set(targetPath.toString(), targetLayer);
|
||||
}
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
|
||||
const targetLayer = updateDocumentLayerDetails.data;
|
||||
const targetPath = targetLayer.path;
|
||||
|
||||
updateLayerInTree(targetPath, targetLayer);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -134,34 +129,23 @@
|
|||
window.getSelection()?.removeAllRanges();
|
||||
}
|
||||
|
||||
// TODO: Svelte: test this works
|
||||
function selectLayerWithModifiers(e: MouseEvent, listing: LayerListingInfo) {
|
||||
const ctrl = e.ctrlKey;
|
||||
const meta = e.metaKey;
|
||||
const shift = e.shiftKey;
|
||||
const alt = e.altKey;
|
||||
// Get the pressed state of the modifier keys
|
||||
const [ctrl, meta, shift, alt] = [e.ctrlKey, e.metaKey, e.shiftKey, e.altKey];
|
||||
// Get the state of the platform's accel key and its opposite platform's accel key
|
||||
const [accel, oppositeAccel] = platformIsMac() ? [meta, ctrl] : [ctrl, meta];
|
||||
|
||||
if (!ctrl && !meta && !shift && !alt) selectLayer(false, false, false, listing, e);
|
||||
else if (!ctrl && !meta && shift && !alt) selectLayer(false, false, true, listing, e);
|
||||
else if (ctrl && !meta && !shift && !alt) selectLayer(true, false, false, listing, e);
|
||||
else if (ctrl && !meta && shift && !alt) selectLayer(true, false, true, listing, e);
|
||||
else if (!ctrl && meta && !shift && !alt) selectLayer(false, true, false, listing, e);
|
||||
else if (!ctrl && meta && shift && !alt) selectLayer(false, true, true, listing, e);
|
||||
else if ((ctrl && meta) || alt) e.stopPropagation();
|
||||
// Select the layer only if the accel and/or shift keys are pressed
|
||||
if (!oppositeAccel && !alt) selectLayer(accel, shift, listing);
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
async function selectLayer(ctrl: boolean, cmd: boolean, shift: boolean, listing: LayerListingInfo, event: Event) {
|
||||
function selectLayer(accel: boolean, shift: boolean, listing: LayerListingInfo) {
|
||||
// Don't select while we are entering text to rename the layer
|
||||
if (listing.editingName) return;
|
||||
|
||||
const ctrlOrCmd = platformIsMac() ? cmd : ctrl;
|
||||
// Pressing the Ctrl key on a Mac, or the Cmd key on another platform, is a violation of the `.exact` qualifier so we filter it out here
|
||||
const opposite = platformIsMac() ? ctrl : cmd;
|
||||
|
||||
if (!opposite) editor.instance.selectLayer(listing.entry.path, ctrlOrCmd, shift);
|
||||
|
||||
// We always want to stop propagation so the click event doesn't pass through the layer and cause a deselection by clicking the layer panel background
|
||||
// This is also why we cover the remaining cases not considered by the `.exact` qualifier, in the last two bindings on the layer element, with a `stopPropagation()` call
|
||||
event.stopPropagation();
|
||||
editor.instance.selectLayer(listing.entry.path, accel, shift);
|
||||
}
|
||||
|
||||
async function deselectAllLayers() {
|
||||
|
|
@ -243,7 +227,7 @@
|
|||
fakeHighlight = [layer.path];
|
||||
}
|
||||
const select = (): void => {
|
||||
if (!layer.layerMetadata.selected) selectLayer(false, false, false, listing, event);
|
||||
if (!layer.layerMetadata.selected) selectLayer(false, false, listing);
|
||||
};
|
||||
|
||||
const target = (event.target || undefined) as HTMLElement | undefined;
|
||||
|
|
@ -283,16 +267,19 @@
|
|||
const layerWithNameBeingEdited = layers.find((layer: LayerListingInfo) => layer.editingName);
|
||||
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
|
||||
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
|
||||
const path = [] as bigint[];
|
||||
layers = [] as LayerListingInfo[];
|
||||
const path: bigint[] = [];
|
||||
|
||||
const recurse = (folder: UpdateDocumentLayerTreeStructureJs, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
|
||||
// Clear the layer tree before rebuilding it
|
||||
layers = [];
|
||||
|
||||
// Build the new layer tree
|
||||
const recurse = (folder: UpdateDocumentLayerTreeStructureJs): void => {
|
||||
folder.children.forEach((item, index) => {
|
||||
// TODO: fix toString
|
||||
const layerId = BigInt(item.layerId.toString());
|
||||
path.push(layerId);
|
||||
|
||||
const mapping = cache.get(path.toString());
|
||||
const mapping = layerCache.get(path.toString());
|
||||
if (mapping) {
|
||||
layers.push({
|
||||
folderIndex: index,
|
||||
|
|
@ -303,13 +290,24 @@
|
|||
}
|
||||
|
||||
// Call self recursively if there are any children
|
||||
if (item.children.length >= 1) recurse(item, layers, cache);
|
||||
if (item.children.length >= 1) recurse(item);
|
||||
|
||||
path.pop();
|
||||
});
|
||||
};
|
||||
recurse(updateDocumentLayerTreeStructure);
|
||||
layers = layers;
|
||||
}
|
||||
|
||||
recurse(updateDocumentLayerTreeStructure, layers, layerCache);
|
||||
function updateLayerInTree(targetPath: BigUint64Array, targetLayer: LayerPanelEntry) {
|
||||
const path = targetPath.toString();
|
||||
layerCache.set(path, targetLayer);
|
||||
|
||||
const layer = layers.find((layer: LayerListingInfo) => layer.entry.path.toString() === path);
|
||||
if (layer) {
|
||||
layer.entry = targetLayer;
|
||||
layers = layers;
|
||||
}
|
||||
}
|
||||
|
||||
function getLayerTypeData(layerType: LayerType): LayerTypeData {
|
||||
|
|
|
|||
|
|
@ -52,5 +52,9 @@
|
|||
.sections {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 0 0 0;
|
||||
flex: 0 0 auto;
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let self: HTMLDivElement;
|
||||
let entries: MenuListEntry[] = [];
|
||||
|
||||
function clickEntry(menuListEntry: MenuListEntry, e: MouseEvent) {
|
||||
|
|
@ -36,7 +35,7 @@
|
|||
(e.target as HTMLElement | undefined)?.focus();
|
||||
|
||||
if (menuListEntry.ref) {
|
||||
menuListEntry.ref.isOpen = true;
|
||||
menuListEntry.ref.open = true;
|
||||
entries = entries;
|
||||
} else {
|
||||
throw new Error("The menu bar floating menu has no associated ref");
|
||||
|
|
@ -74,7 +73,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="menu-bar-input" bind:this={self} data-menu-bar-input>
|
||||
<div class="menu-bar-input" data-menu-bar-input>
|
||||
{#each entries as entry, index (index)}
|
||||
<div class="entry-container">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
|
|
@ -82,7 +81,7 @@
|
|||
on:click={(e) => clickEntry(entry, e)}
|
||||
on:keydown={(e) => entry.ref?.keydown(e, false)}
|
||||
class="entry"
|
||||
class:open={entry.ref?.isOpen}
|
||||
class:open={entry.ref?.open}
|
||||
tabindex="0"
|
||||
data-floating-menu-spawner={entry.children && entry.children.length > 0 ? "" : "no-hover-transfer"}
|
||||
>
|
||||
|
|
@ -96,9 +95,9 @@
|
|||
{#if entry.children && entry.children.length > 0}
|
||||
<MenuList
|
||||
on:open={(e) => {
|
||||
if (entry.ref) entry.ref.isOpen = e.detail;
|
||||
if (entry.ref) entry.ref.open = e.detail;
|
||||
}}
|
||||
open={entry.ref?.isOpen || false}
|
||||
open={entry.ref?.open || false}
|
||||
entries={entry.children || []}
|
||||
direction="Bottom"
|
||||
minWidth={240}
|
||||
|
|
|
|||
|
|
@ -1232,7 +1232,7 @@ export function defaultWidgetLayout(): WidgetLayout {
|
|||
}
|
||||
|
||||
// Updates a widget layout based on a list of updates, returning the new layout
|
||||
export function patchWidgetLayout(layout: WidgetLayout, updates: WidgetDiffUpdate): void {
|
||||
export function patchWidgetLayout(/* mut */ layout: WidgetLayout, updates: WidgetDiffUpdate): void {
|
||||
layout.layoutTarget = updates.layoutTarget;
|
||||
|
||||
updates.diff.forEach((update) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue