isaacHagoel / svelte-dnd-action

An action based drag and drop container for Svelte
MIT License
1.76k stars 105 forks source link

Icons being duplicated during drag events #575

Open badarsebard opened 3 months ago

badarsebard commented 3 months ago

I am having a difficult time reproducing this issue in a REPL so please bear with me. I am seeing an issue where icons are being duplicated when performing the drag actions. In the screenshot I have two columns in their starting positions and correctly rendered.

image

The plus icon in the yellow column has been duplicated after moving the blue column to its left.

image

I can also drag the blue column back and forth multiple times, and every time it moves to the left of the yellow column, another copy of the icon gets added to the yellow column.

image

There are other interesting behaviors I've observed that I don't know how to interpret, but may help. When either of the columns has duplicate icons, if I pick up and drop the column in place without moving it the duplicates are removed after being finalized.

image image

Below is the relevant part of the code.

<script>
    import Editor from "@tinymce/tinymce-svelte";
    import {Button, ButtonGroup, Input, InputAddon, Label} from "flowbite-svelte";
    import {CloseOutline, EnvelopeOpenOutline, PaperClipOutline} from "flowbite-svelte-icons";
    import {activeCanvasComponent, selectedCard} from "./stores";
    import {onMount, tick} from "svelte";
    import {AddColumn, GetColumnsWithCards} from "./wailsjs/go/main/App.js";
    import {EventsOn} from "./wailsjs/runtime";
    import Column from "./Column.svelte";
    import Card from "./Card.svelte";
    import {dndzone} from "svelte-dnd-action";
    import {flip} from "svelte/animate";

    const conf = {
        height: "100%",
        width: "100%",
        skin: "borderless",
        plugins: [
            'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
            'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
            'insertdatetime', 'media', 'table', 'help', 'wordcount'
        ],
        menubar: "",
        toolbar: "bold italic underline strikethrough | align numlist bullist | link image | table outdent indent | blocks fontfamily fontsize | forecolor backcolor removeformat | fullscreen preview",
        content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
        license_key: 'gpl',
        promotion: false,
        branding: false,
    };
    const doneColumn = {
        _id: "0",
        board: "default",
        position: -1,
        title: "Done",
        color: "#31c48d",  //  rgb(49 196 141)
        collapsed: true,
        sortType: "due",
        sortOrder: "asc",
        count: 0,
    };

    // _id: string;
    // done: boolean;
    // seen: boolean;
    // conversationId: string;
    // sender: EmailAddress;
    // title: string;
    // // Go type: time
    // time: any;
    // bodyPreview: string;
    // body: Body;
    // column: string;
    // tags: Tag;
    // // Go type: time
    // snoozeDate: any;
    // // Go type: time
    // dueDate: any;
    // todoItems: TodoItem[];
    // notes: Note[];
    // attachments: string[];
    // parentId: string;
    // hasChildren: boolean;
    let card = {};
    let columnsWithCards = [];
    let showComponent = "board";
    let hideCc = true;

    onMount(async () => {
        activeCanvasComponent.subscribe(value => {
            showComponent = value;
        });
        selectedCard.subscribe(value => {
            if (Object.keys(value).length !== 0) {
                console.log(value);
                activeCanvasComponent.set("viewer");
                card = value;
            }
        });
        EventsOn("columnUpdate", async () => {
            console.log("Column update event received");
            columnsWithCards = await GetColumnsWithCards();
            console.log(columnsWithCards);
        });
        columnsWithCards = await GetColumnsWithCards();
    });

    async function sendEmail() {
        // todo: send email
        activeCanvasComponent.set("board");
    }

    async function attachFile() {
        // todo: attach file
    }

    async function saveDraft() {
        // todo: save draft
        activeCanvasComponent.set("board");
    }

    async function addColumn() {
        await AddColumn({
            id: null,
            board: "default",
            position: Object.keys(columnsWithCards).length,
            title: null,
            color: "#2196f3",
            collapsed: false,
            sortType: null,
            sortOrder: "asc",
        });
    }

    const flipDurationMs = 200;

    function handleDndConsiderColumns(e) {
        console.log(e);
        columnsWithCards = e.detail.items;
    }

    function handleDndFinalizeColumns(e) {
        console.log(e.detail.items);
        columnsWithCards = e.detail.items;
    }

    function handleDndConsiderCards(cid, e) {
        // const colIdx = columnsWithCards.findIndex(c => c.id === cid);
        // columnsWithCards[colIdx].items = e.detail.items;
        // columnsWithCards = [...columnsWithCards];
    }

    function handleDndFinalizeCards(cid, e) {
        // const colIdx = columnsWithCards.findIndex(c => c.id === cid);
        // columnsWithCards[colIdx].items = e.detail.items;
        // columnsWithCards = [...columnsWithCards];
    }

    // todo: add contact autocomplete to the address fields (to, cc, bcc)
</script>

<!-- Editor -->
{#if showComponent === 'editor'}
    <div class="h-full w-full flex flex-col">
        <div>
            <div class="bg-[#2196f3] h-10 content-center">
                <p class="ml-2 inline-block">Compose Email</p>
                <button class="black-text float-right mr-2" on:click={() => activeCanvasComponent.set("board")}>
                    <CloseOutline size="lg"></CloseOutline>
                </button>
            </div>
            <ButtonGroup class="w-full">
                <InputAddon class="w-12 first:rounded-s-none">
                    <Label class="w-6">To</Label>
                </InputAddon>
                <Input type="text"></Input>
                <Button class="whitespace-nowrap focus-within:ring-0" on:click={() => {hideCc = !hideCc}}>Cc Bcc
                </Button>
            </ButtonGroup>
            <ButtonGroup class="w-full {hideCc ? 'hidden' : ''}">
                <InputAddon class="w-12 first:rounded-s-none">
                    <Label class="w-6">Cc</Label>
                </InputAddon>
                <Input type="text"></Input>
            </ButtonGroup>
            <ButtonGroup class="w-full {hideCc ? 'hidden' : ''}">
                <InputAddon class="w-12 first:rounded-s-none">
                    <Label class="w-6">Bcc</Label>
                </InputAddon>
                <Input type="text"></Input>
            </ButtonGroup>
        </div>
        <Editor cssClass="w-full flex-1" scriptSrc="tinymce/tinymce.min.js" {conf}></Editor>
        <div class="content-center">
            <Button class="m-2 bg-[#2196f3] align-middle" pill on:click={sendEmail}>
                Submit
            </Button>
            <Button class="m-2 p-0 align-middle float-right">
                <EnvelopeOpenOutline size="lg" class="m-0 p-0 fill-black"></EnvelopeOpenOutline>
                <span class="text-black">Save Draft</span>
            </Button>
            <Button class="m-2 p-0 align-middle float-right">
                <PaperClipOutline size="lg" class="m-0 p-0 fill-black"></PaperClipOutline>
                <span class="text-black">Attach File</span>
            </Button>
        </div>
    </div>
    <!-- Viewer -->
{:else if showComponent === 'viewer' && card}
    <div class="h-full w-full flex flex-col">
        <div class="bg-[#2196f3] h-10 content-center">
            <p class="ml-2 inline-block">{card.title}</p>
            <button class="black-text float-right mr-2" on:click={() => activeCanvasComponent.set("board")}>
                <CloseOutline size="lg"></CloseOutline>
            </button>
        </div>
    </div>
    <!-- Board -->
{:else}
    <!-- user-defined columns -->
    <div class="flex" use:dndzone={{items: columnsWithCards, flipDurationMs}}
         on:consider={handleDndConsiderColumns} on:finalize={handleDndFinalizeColumns}>
        {#each columnsWithCards as column (column)}
<!--            <div class="flex" animate:flip="{{duration: flipDurationMs}}">-->
                <div class="h-auto bg-gradient-to-b from-gray-200 to-gray-50 border-t-8 m-2 flex flex-col min-w-80"
                     style="border-top-color: {column.color};">
                    <button class="w-full h-6 -mt-[8px]"></button>
                    <div class="flex flex-wrap gap-2 mx-2 mb-2 leading-8">
                        <button>{column.title}</button>
                        <button class="hover:bg-white h-8 w-8 rounded-full text-center ml-auto">
                            <i class="fa-solid fa-plus"></i>
                        </button>
                        <button class="hover:bg-white h-8 w-8 rounded-full text-center">
                            <i class="fa-solid fa-ellipsis-vertical"></i>
                        </button>
                    </div>
                </div>

<!--                <div class="flex" use:dndzone={{items: column.cards, flipDurationMs, type: "cards"}}-->
<!--                     on:consider={e => handleDndConsiderCards(column.id, e)}-->
<!--                     on:finalize={e => handleDndFinalizeCards(column.id, e)}>-->
<!--                    <Column columnId={column.id}/>-->
                    <!--                    <Column columnId={column.id}>-->
                    <!--                        {#each column.cards as card (card.id)}-->
                    <!--                            <div animate:flip="{{duration: flipDurationMs}}">-->
                    <!--                                <Card {card}></Card>-->
                    <!--                            </div>-->
                    <!--                        {/each}-->
                    <!--                    </Column>-->
<!--                </div>-->
<!--            </div>-->
        {/each}
    </div>
    <!-- column add button -->
    <button class="h-auto bg-gradient-to-b from-gray-200 to-gray-50 m-2 flex flex-col" on:click={addColumn}>
        <span class="mt-12"><i class="fa-solid fa-plus w-6"></i></span>
        <span class="mt-4" style="writing-mode: vertical-lr;">Add Column</span>
    </button>
    <!-- done column -->
    <Column columnId="0" isDone={true}></Column>
{/if}
badarsebard commented 3 months ago

On further troubleshooting this looks like the issue may be an odd interaction with Font Awesome's method for changing <i> to <svg>. I changed the FA Kit I am using to utilize Web Fonts instead of SVG and it has resolved the issue.

isaacHagoel commented 3 months ago

Do you still need help with this? I waited to see if there are additional comments but sounds like it's resolved

badarsebard commented 3 months ago

I am using the Web Fonts version of Font Awesome as a workaround, but ultimately the issue remains. It would be good to get this working with svg for the future since it provides additional capabilities within Font Awesome (custom icons, etc.).

I don't have enough understanding of how this library or FA work to really provide any insight on solution, though.

isaacHagoel commented 3 months ago

I see. Any chance you make a simple REPL reproducing the issue so that i can have a look?

badarsebard commented 3 months ago

https://svelte.dev/repl/a3873b548ea34798b75fbe5890c289dc

Observations: