mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Collapsible patch (#2052)
* Animate Collapsible in both directions
* Fix field margin issues
* Fix code style issues
* Make duration prop optional
* Implement reduced motion mode for Collapsible
* Refactor Collapsible and add comments
* Fix LabelContainer badges disappearing when field is still hovered
* Remove reducedMotion store and use body class instead
* Export optional animated boolean
* Do not export duration
* Add 5px top padding to Fields.svelte
to make it look like it used to.
* Revert "Add 5px top padding to Fields.svelte"
This reverts commit f30026149a
.
* Add top margin of 5px to Fields.svelte
This commit is contained in:
parent
9d9d4a97c7
commit
35431c5944
3 changed files with 90 additions and 59 deletions
|
@ -3,53 +3,89 @@ 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 { cubicOut } from "svelte/easing";
|
import { tick } from "svelte";
|
||||||
|
import { cubicIn, cubicOut } from "svelte/easing";
|
||||||
import { tweened } from "svelte/motion";
|
import { tweened } from "svelte/motion";
|
||||||
|
|
||||||
import { removeStyleProperties } from "../lib/styling";
|
|
||||||
|
|
||||||
export let duration = 300;
|
|
||||||
|
|
||||||
export let collapse = false;
|
export let collapse = false;
|
||||||
|
export let animated = !document.body.classList.contains("reduced-motion");
|
||||||
|
|
||||||
let collapsed = false;
|
let collapsed = false;
|
||||||
|
let contentHeight = 0;
|
||||||
|
|
||||||
const size = tweened<number>(undefined, {
|
function dynamicDuration(height: number): number {
|
||||||
duration,
|
return 100 + Math.pow(height, 1 / 4) * 25;
|
||||||
easing: cubicOut,
|
|
||||||
});
|
|
||||||
|
|
||||||
function doCollapse(collapse: boolean): void {
|
|
||||||
if (collapse) {
|
|
||||||
size.set(0);
|
|
||||||
} else {
|
|
||||||
collapsed = false;
|
|
||||||
size.set(1, { duration: 0 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
$: duration = dynamicDuration(contentHeight);
|
||||||
|
|
||||||
$: doCollapse(collapse);
|
const size = tweened<number>(undefined);
|
||||||
|
|
||||||
let collapsibleElement: HTMLElement;
|
async function transition(collapse: boolean): Promise<void> {
|
||||||
let clientHeight: number;
|
if (collapse) {
|
||||||
|
contentHeight = collapsibleElement.clientHeight;
|
||||||
function updateHeight(percentage: number): void {
|
size.set(0, {
|
||||||
collapsibleElement.style.overflow = "hidden";
|
duration: duration,
|
||||||
|
easing: cubicOut,
|
||||||
if (percentage === 1) {
|
});
|
||||||
removeStyleProperties(collapsibleElement, "height", "overflow");
|
|
||||||
} else if (percentage === 0) {
|
|
||||||
collapsed = true;
|
|
||||||
removeStyleProperties(collapsibleElement, "height", "overflow");
|
|
||||||
} else {
|
} else {
|
||||||
collapsibleElement.style.height = `${percentage * clientHeight}px`;
|
/* Tell content to show and await response */
|
||||||
|
collapsed = false;
|
||||||
|
await tick();
|
||||||
|
/* Measure content height to tween to */
|
||||||
|
contentHeight = collapsibleElement.clientHeight;
|
||||||
|
size.set(1, {
|
||||||
|
duration: duration,
|
||||||
|
easing: cubicIn,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (collapsibleElement) {
|
$: if (collapsibleElement) {
|
||||||
updateHeight($size);
|
if (animated) {
|
||||||
|
transition(collapse);
|
||||||
|
} else {
|
||||||
|
collapsed = collapse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let collapsibleElement: HTMLElement;
|
||||||
|
|
||||||
|
$: collapsed = $size === 0;
|
||||||
|
$: expanded = $size === 1;
|
||||||
|
$: height = $size * contentHeight;
|
||||||
|
$: transitioning = $size > 0 && !(collapsed || expanded);
|
||||||
|
$: measuring = !(collapsed || transitioning || expanded);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={collapsibleElement} class="collapsible" bind:clientHeight>
|
<div
|
||||||
|
bind:this={collapsibleElement}
|
||||||
|
class="collapsible"
|
||||||
|
class:animated
|
||||||
|
class:expanded
|
||||||
|
class:measuring
|
||||||
|
class:transitioning
|
||||||
|
style:--height="{height}px"
|
||||||
|
>
|
||||||
<slot {collapsed} />
|
<slot {collapsed} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if measuring}
|
||||||
|
<!-- Maintain document flow while collapsible height is measured -->
|
||||||
|
<div class="collapsible-placeholder" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.collapsible.animated {
|
||||||
|
&.measuring {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
&.transitioning {
|
||||||
|
overflow: hidden;
|
||||||
|
height: var(--height);
|
||||||
|
&.expanded {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -85,34 +85,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
onDestroy(() => api?.destroy());
|
onDestroy(() => api?.destroy());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot name="field-label" />
|
<div class="field-container" on:mouseenter on:mouseleave>
|
||||||
|
<slot name="field-label" />
|
||||||
|
|
||||||
<Collapsible collapse={collapsed} let:collapsed={hidden}>
|
<Collapsible collapse={collapsed} let:collapsed={hidden}>
|
||||||
<div
|
<div use:elementResolve class="editor-field" on:focusin on:focusout {hidden}>
|
||||||
use:elementResolve
|
<EditingArea
|
||||||
class="editor-field"
|
{content}
|
||||||
on:focusin
|
fontFamily={field.fontFamily}
|
||||||
on:focusout
|
fontSize={field.fontSize}
|
||||||
on:mouseenter
|
api={editingArea}
|
||||||
on:mouseleave
|
>
|
||||||
{hidden}
|
{#if flipInputs}
|
||||||
>
|
<slot name="plain-text-input" />
|
||||||
<EditingArea
|
<slot name="rich-text-input" />
|
||||||
{content}
|
{:else}
|
||||||
fontFamily={field.fontFamily}
|
<slot name="rich-text-input" />
|
||||||
fontSize={field.fontSize}
|
<slot name="plain-text-input" />
|
||||||
api={editingArea}
|
{/if}
|
||||||
>
|
</EditingArea>
|
||||||
{#if flipInputs}
|
</div>
|
||||||
<slot name="plain-text-input" />
|
</Collapsible>
|
||||||
<slot name="rich-text-input" />
|
</div>
|
||||||
{:else}
|
|
||||||
<slot name="rich-text-input" />
|
|
||||||
<slot name="plain-text-input" />
|
|
||||||
{/if}
|
|
||||||
</EditingArea>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.editor-field {
|
.editor-field {
|
||||||
|
|
|
@ -12,6 +12,7 @@ Contains the fields. This contains the scrollable area.
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.fields {
|
.fields {
|
||||||
|
margin-top: 5px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: min-content;
|
grid-auto-rows: min-content;
|
||||||
grid-gap: 6px;
|
grid-gap: 6px;
|
||||||
|
|
Loading…
Reference in a new issue