mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Fix glitchy animation of floating elements (#2224)
* Set max-height of 400px to scrollable Popover * Pass computed placement to user components to set different animation directions when the placement changes. * Move elevation effect from WithFloating/WithOverlay to Popover * Apply same changes as in WithFloating to WithOverlay * Adjust FloatingArrow CSS to Popover * Run eslint and formatter
This commit is contained in:
parent
1ab4ee38c6
commit
a48c96559d
8 changed files with 131 additions and 69 deletions
|
@ -18,19 +18,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
|
|
||||||
/* outer border */
|
/* outer border */
|
||||||
border: 1px solid #b6b6b6;
|
border: 1px solid var(--border-subtle);
|
||||||
|
|
||||||
&.dark {
|
|
||||||
border-color: #060606;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rotate the box to indicate the different directions */
|
/* Rotate the box to indicate the different directions */
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
||||||
/* inner border */
|
|
||||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
box-shadow: inset 1px 1px 0 0 #565656;
|
box-shadow: inset 1px 1px 0 0 #565656;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,52 +3,101 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { Placement } from "@floating-ui/dom";
|
||||||
|
import { getContext, onMount } from "svelte";
|
||||||
|
import { create_in_transition } from "svelte/internal";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
|
|
||||||
import { pageTheme } from "../sveltelib/theme";
|
import { floatingKey } from "./context-keys";
|
||||||
|
|
||||||
export let scrollable = false;
|
export let scrollable = false;
|
||||||
|
let element: HTMLDivElement;
|
||||||
|
let wrapper: HTMLDivElement;
|
||||||
|
let hidden = true;
|
||||||
|
let minHeight = 0;
|
||||||
|
|
||||||
|
let placement: Placement;
|
||||||
|
|
||||||
|
const placementStore = getContext<Writable<Promise<Placement>>>(floatingKey);
|
||||||
|
|
||||||
|
/* await computed placement of floating element to determine animation direction */
|
||||||
|
$: if ($placementStore !== undefined) {
|
||||||
|
$placementStore.then((computedPlacement) => {
|
||||||
|
if (placement != computedPlacement) {
|
||||||
|
placement = computedPlacement;
|
||||||
|
/* use internal function to animate popover */
|
||||||
|
create_in_transition(element, slide, { duration: 200 }).start();
|
||||||
|
hidden = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
/* set min-height on wrapper to ensure correct
|
||||||
|
popover placement at animation start */
|
||||||
|
minHeight = wrapper.offsetHeight;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="popover-wrapper d-flex"
|
||||||
|
style:--min-height="{minHeight}px"
|
||||||
|
bind:this={wrapper}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="popover"
|
class="popover"
|
||||||
class:scrollable
|
class:scrollable
|
||||||
class:dark={$pageTheme.isDark}
|
class:hidden
|
||||||
transition:slide={{ duration: 200 }}
|
class:top={placement === "top"}
|
||||||
|
class:right={placement === "right"}
|
||||||
|
class:bottom={placement === "bottom"}
|
||||||
|
class:left={placement === "left"}
|
||||||
|
bind:this={element}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "sass/vars" as *;
|
@use "sass/elevation" as elevation;
|
||||||
|
|
||||||
|
.popover-wrapper {
|
||||||
|
min-height: var(--min-height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
border-radius: 5px;
|
@include elevation.elevation(8);
|
||||||
background-color: color(canvas-elevated);
|
|
||||||
|
align-self: flex-start;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--canvas-elevated);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
|
||||||
min-width: var(--popover-width, 1rem);
|
min-width: var(--popover-width, 1rem);
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
|
|
||||||
/* Needs this much space for FloatingArrow to be positioned */
|
/* Needs this much space for FloatingArrow to be positioned */
|
||||||
padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px);
|
padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px);
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
color: color(fg);
|
|
||||||
|
|
||||||
/* outer border */
|
|
||||||
border: 1px solid palette(lightgray, 6);
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
border-color: palette(darkgray, 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* inner border */
|
|
||||||
box-shadow: inset 0 0 0 1px palette(lightgray, 3);
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
box-shadow: inset 0 0 0 1px palette(darkgray, 2);
|
|
||||||
}
|
|
||||||
&.scrollable {
|
&.scrollable {
|
||||||
max-height: 80vh;
|
max-height: 400px;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* alignment determines slide animation direction */
|
||||||
|
&.top,
|
||||||
|
&.left {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
&.bottom,
|
||||||
|
&.right {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,8 +10,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
} from "@floating-ui/dom";
|
} from "@floating-ui/dom";
|
||||||
import type { Callback } from "@tslib/typing";
|
import type { Callback } from "@tslib/typing";
|
||||||
import { singleCallback } from "@tslib/typing";
|
import { singleCallback } from "@tslib/typing";
|
||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
import { createEventDispatcher, onDestroy, setContext } from "svelte";
|
||||||
import type { ActionReturn } from "svelte/action";
|
import type { ActionReturn } from "svelte/action";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import isClosingClick from "../sveltelib/closing-click";
|
import isClosingClick from "../sveltelib/closing-click";
|
||||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||||
|
@ -23,12 +24,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
||||||
import positionFloating from "../sveltelib/position/position-floating";
|
import positionFloating from "../sveltelib/position/position-floating";
|
||||||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||||
|
import { floatingKey } from "./context-keys";
|
||||||
import FloatingArrow from "./FloatingArrow.svelte";
|
import FloatingArrow from "./FloatingArrow.svelte";
|
||||||
|
|
||||||
export let portalTarget: HTMLElement | null = null;
|
export let portalTarget: HTMLElement | null = null;
|
||||||
|
|
||||||
let placement: Placement = "bottom";
|
let placement: Placement = "bottom";
|
||||||
export { placement as preferredPlacement };
|
export { placement as preferredPlacement };
|
||||||
|
|
||||||
|
/* Used by Popover to set animation direction depending on placement */
|
||||||
|
const placementPromise = writable(undefined as Promise<Placement> | undefined);
|
||||||
|
setContext(floatingKey, placementPromise);
|
||||||
|
|
||||||
export let offset = 5;
|
export let offset = 5;
|
||||||
/* 30px box shadow from elevation(8) */
|
/* 30px box shadow from elevation(8) */
|
||||||
export let shift = 30;
|
export let shift = 30;
|
||||||
|
@ -72,8 +79,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
reference: ReferenceElement,
|
reference: ReferenceElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
position: PositionAlgorithm,
|
position: PositionAlgorithm,
|
||||||
): Promise<void> {
|
): Promise<Placement> {
|
||||||
return position(reference, floating);
|
const promise = position(reference, floating);
|
||||||
|
$placementPromise = promise;
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function position(
|
async function position(
|
||||||
|
@ -81,8 +90,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
reference: ReferenceElement,
|
reference: ReferenceElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
position: PositionAlgorithm,
|
position: PositionAlgorithm,
|
||||||
) => Promise<void> = applyPosition,
|
) => Promise<Placement> = applyPosition,
|
||||||
): Promise<void> {
|
): Promise<Placement | void> {
|
||||||
if (reference && floating) {
|
if (reference && floating) {
|
||||||
return callback(reference, floating, positionCurried);
|
return callback(reference, floating, positionCurried);
|
||||||
}
|
}
|
||||||
|
@ -97,9 +106,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
callback: PositioningCallback,
|
callback: PositioningCallback,
|
||||||
): Callback {
|
): Callback {
|
||||||
const innerFloating = floating;
|
const innerFloating = floating;
|
||||||
return callback(reference, innerFloating, () =>
|
return callback(reference, innerFloating, () => {
|
||||||
positionCurried(reference, innerFloating),
|
$placementPromise = positionCurried(reference, innerFloating);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleanup: Callback | null = null;
|
let cleanup: Callback | null = null;
|
||||||
|
@ -186,7 +195,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "sass/elevation" as elevation;
|
|
||||||
span.floating-reference {
|
span.floating-reference {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
@ -195,9 +203,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
z-index: 890;
|
z-index: 890;
|
||||||
&.show {
|
|
||||||
@include elevation.elevation(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-arrow {
|
&-arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -3,11 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
import type {
|
||||||
|
FloatingElement,
|
||||||
|
Placement,
|
||||||
|
ReferenceElement,
|
||||||
|
} from "@floating-ui/dom";
|
||||||
import type { Callback } from "@tslib/typing";
|
import type { Callback } from "@tslib/typing";
|
||||||
import { singleCallback } from "@tslib/typing";
|
import { singleCallback } from "@tslib/typing";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher, setContext } from "svelte";
|
||||||
import type { ActionReturn } from "svelte/action";
|
import type { ActionReturn } from "svelte/action";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import isClosingClick from "../sveltelib/closing-click";
|
import isClosingClick from "../sveltelib/closing-click";
|
||||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||||
|
@ -18,6 +23,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
||||||
import positionOverlay from "../sveltelib/position/position-overlay";
|
import positionOverlay from "../sveltelib/position/position-overlay";
|
||||||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||||
|
import { overlayKey } from "./context-keys";
|
||||||
|
|
||||||
|
/* Used by Popover to set animation direction depending on placement */
|
||||||
|
const placementPromise = writable(undefined as Promise<Placement> | undefined);
|
||||||
|
setContext(overlayKey, placementPromise);
|
||||||
|
|
||||||
export let padding = 0;
|
export let padding = 0;
|
||||||
export let inline = false;
|
export let inline = false;
|
||||||
|
@ -50,8 +60,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
reference: HTMLElement,
|
reference: HTMLElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
position: PositionAlgorithm,
|
position: PositionAlgorithm,
|
||||||
): Promise<void> {
|
): Promise<Placement> {
|
||||||
return position(reference, floating);
|
const promise = position(reference, floating);
|
||||||
|
$placementPromise = promise;
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function position(
|
async function position(
|
||||||
|
@ -59,8 +71,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
reference: HTMLElement,
|
reference: HTMLElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
position: PositionAlgorithm,
|
position: PositionAlgorithm,
|
||||||
) => Promise<void> = applyPosition,
|
) => Promise<Placement> = applyPosition,
|
||||||
): Promise<void> {
|
): Promise<Placement | void> {
|
||||||
if (reference && floating) {
|
if (reference && floating) {
|
||||||
return callback(reference, floating, positionCurried);
|
return callback(reference, floating, positionCurried);
|
||||||
}
|
}
|
||||||
|
@ -75,9 +87,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
callback: PositioningCallback,
|
callback: PositioningCallback,
|
||||||
): Callback {
|
): Callback {
|
||||||
const innerFloating = floating;
|
const innerFloating = floating;
|
||||||
return callback(reference, innerFloating, () =>
|
return callback(reference, innerFloating, () => {
|
||||||
positionCurried(reference, innerFloating),
|
$placementPromise = positionCurried(reference, innerFloating);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleanup: Callback;
|
let cleanup: Callback;
|
||||||
|
@ -147,15 +159,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "sass/elevation" as elevation;
|
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
&.show {
|
|
||||||
@include elevation.elevation(5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,3 +6,5 @@ export const sectionKey = Symbol("section");
|
||||||
export const buttonGroupKey = Symbol("buttonGroup");
|
export const buttonGroupKey = Symbol("buttonGroup");
|
||||||
export const dropdownKey = Symbol("dropdown");
|
export const dropdownKey = Symbol("dropdown");
|
||||||
export const modalsKey = Symbol("modals");
|
export const modalsKey = Symbol("modals");
|
||||||
|
export const floatingKey = Symbol("floating");
|
||||||
|
export const overlayKey = Symbol("overlay");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
import type { FloatingElement, Placement, ReferenceElement } from "@floating-ui/dom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface of a function that calls `computePosition` of floating-ui.
|
* The interface of a function that calls `computePosition` of floating-ui.
|
||||||
|
@ -9,4 +9,4 @@ import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
||||||
export type PositionAlgorithm = (
|
export type PositionAlgorithm = (
|
||||||
reference: ReferenceElement,
|
reference: ReferenceElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
) => Promise<void>;
|
) => Promise<Placement>;
|
||||||
|
|
|
@ -30,7 +30,7 @@ function positionFloating({
|
||||||
return async function(
|
return async function(
|
||||||
reference: ReferenceElement,
|
reference: ReferenceElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
): Promise<void> {
|
): Promise<Placement> {
|
||||||
const middleware: Middleware[] = [
|
const middleware: Middleware[] = [
|
||||||
flip(),
|
flip(),
|
||||||
offset(offsetArg),
|
offset(offsetArg),
|
||||||
|
@ -63,11 +63,13 @@ function positionFloating({
|
||||||
} = await computePosition(reference, floating, computeArgs);
|
} = await computePosition(reference, floating, computeArgs);
|
||||||
|
|
||||||
if (middlewareData.hide?.escaped) {
|
if (middlewareData.hide?.escaped) {
|
||||||
return hideCallback("escaped");
|
hideCallback("escaped");
|
||||||
|
return computedPlacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (middlewareData.hide?.referenceHidden) {
|
if (middlewareData.hide?.referenceHidden) {
|
||||||
return hideCallback("referenceHidden");
|
hideCallback("referenceHidden");
|
||||||
|
return computedPlacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(floating.style, {
|
Object.assign(floating.style, {
|
||||||
|
@ -102,6 +104,8 @@ function positionFloating({
|
||||||
top: arrowY ? `${arrowY}px` : "",
|
top: arrowY ? `${arrowY}px` : "",
|
||||||
transform: `rotate(${rotation}deg)`,
|
transform: `rotate(${rotation}deg)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return computedPlacement;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import type { ComputePositionConfig, FloatingElement, Middleware, ReferenceElement } from "@floating-ui/dom";
|
import type { ComputePositionConfig, FloatingElement, Middleware, Placement, ReferenceElement } from "@floating-ui/dom";
|
||||||
import { computePosition, inline, offset } from "@floating-ui/dom";
|
import { computePosition, inline, offset } from "@floating-ui/dom";
|
||||||
|
|
||||||
import type { PositionAlgorithm } from "./position-algorithm";
|
import type { PositionAlgorithm } from "./position-algorithm";
|
||||||
|
@ -20,7 +20,7 @@ function positionOverlay({
|
||||||
return async function(
|
return async function(
|
||||||
reference: ReferenceElement,
|
reference: ReferenceElement,
|
||||||
floating: FloatingElement,
|
floating: FloatingElement,
|
||||||
): Promise<void> {
|
): Promise<Placement> {
|
||||||
const middleware: Middleware[] = inlineArg ? [inline()] : [];
|
const middleware: Middleware[] = inlineArg ? [inline()] : [];
|
||||||
|
|
||||||
const { width, height } = reference.getBoundingClientRect();
|
const { width, height } = reference.getBoundingClientRect();
|
||||||
|
@ -35,7 +35,7 @@ function positionOverlay({
|
||||||
middleware,
|
middleware,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { x, y, middlewareData } = await computePosition(
|
const { x, y, middlewareData, placement } = await computePosition(
|
||||||
reference,
|
reference,
|
||||||
floating,
|
floating,
|
||||||
computeArgs,
|
computeArgs,
|
||||||
|
@ -57,6 +57,8 @@ function positionOverlay({
|
||||||
width: `${width + 2 * padding}px`,
|
width: `${width + 2 * padding}px`,
|
||||||
height: `${height + 2 * padding}px`,
|
height: `${height + 2 * padding}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return placement;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue