inxilpro / IntellijAlpine

Alpine.js plugin for IntelliJ IDEs like PhpStorm and WebStorm.
MIT License
48 stars 10 forks source link

0.6.4 still creates bugs in the editor #63

Open joffreypersia opened 9 months ago

joffreypersia commented 9 months ago

Hi,

I tried to remove the maximum of useless plugins for my projects but still Alpine makes the editor crash on complex blade files like this one :

@props([
    'items' => '', // json of items
    'label' => '',
    'type' => 'default', // default, filterBar, modal
    'size' => 'md', // sm, md
    'searchBar' => false,
    'searchBarPlaceholder' => 'Search',
    'searchBarEmptyText' => 'No item found',
    'name' => '',
    'optGroup' => false,
])
@php
    $defaultClasses = $type === 'default' ? 'bg-white dark:bg-shade-800 border border-shade-200 rounded dark:border-shade-600 w-full shadow-sm' : '';
@endphp
<div
    x-data="{
        open: false,
        items: {{ $items }},
        itemSelected: null,
        name: '{{ $name }}',
        query: '',
        optGroup: {{ $optGroup ? 'true' : 'false' }},
        resetField() {
            this.itemSelected = this.items[0];
        },
        toggle() {
            if(this.open) {
                return this.close;
            }
            this.$refs.button.focus();
            this.open = true;
        },
        close(focusAfter) {
            if (!this.open) return
            this.open = false
            focusAfter && this.$refs.button.focus()
        },
        get displayItems() {
            return this.query === ''
                ? this.items.map(item => ({ ...item }))
                : this.items
                    .filter(item => item.label.toLowerCase().includes(this.query.toLowerCase()))
                    .map(item => ({ ...item }));
        },
        selectItem(value) {
            if (this.optGroup) {
                this.items.forEach(group => {
                    const foundItem = group.choices.find(item => item.value === value);
                    if (foundItem) this.itemSelected = foundItem;
                });
            } else {
                const selectedItem = this.items.find(item => item.value === value);
                this.itemSelected = selectedItem || null;
            }
            this.close(this.$refs.button);
        },
        resetSearch() {
            this.query = '';
        },
        init() {
            // get selected item from json / object
            if (this.optGroup) {
                this.items.forEach(group => {
                    group.choices.forEach(item => {
                        if (item.selected) {
                            this.itemSelected = item;
                        }
                    });
                });
            } else {
                // get selected item from json / object for non-optgroup
                this.items.forEach(item => {
                    if(item.selected === true) {
                        this.itemSelected = item;
                    }
                });
            }

            // if not specified in items, set to first item (0)
            if (!this.itemSelected && this.items.length > 0) {
                if (this.optGroup) {
                    this.itemSelected = this.items.find(group => group.choices.length > 0)?.choices[0];
                } else {
                    this.itemSelected = this.items[0];
                }
            };

            // filterBar only
            @if($type === 'filterBar')
                // Watch the itemSelected and update the filters array
                this.$watch('filtering', filtering => {
                    !filtering && this.resetField();
                });
            @endif

            // modal only
            @if($type === 'modal')
                // force item selected when LV updating $modalSelectsValues
                this.$watch('modalSelectsValues', modalSelectsValues => {
                    this.items.forEach( (item) => {
                        if(item.value == modalSelectsValues[this.name]) {
                            this.itemSelected = item;
                        }
                    });
                });
            @endif
        },
    }"
    {{ $attributes->merge(['class' => "min-w-[8rem] relative h-full $defaultClasses"]) }}
    x-on:keydown.escape.prevent.stop="close($refs.button)"
    :class="{'!border-primary-500': open}"
    wire:ignore
>
    <label class="sr-only">{{ $label }}</label>

    <input
        type="hidden"
        :name="name"
        x-model="itemSelected.value"
        :id="name"
    />

    <button
        x-ref="button"
        x-on:click="toggle()"
        :aria-expanded="open"
        type="button"
        class="flex items-center justify-between h-full gap-2 w-full truncate text-shade-700 dark:text-shade-50 {{ $size === 'sm' ? 'pl-2 pr-1 py-1 text-sm' : 'pl-3 pr-3 py-2 text-shade-950' }}"
    >
        <span
            x-text="itemSelected ? itemSelected.label : 'Select item'"
            class="truncate"
        ></span>

        <x-heroicon-s-chevron-up-down class="{{ $size === 'sm' ? 'icon-s' : 'icon-md' }} ml-2 text-shade-500"/>
    </button>

    <div
        x-ref="panel"
        x-show="open"
        x-transition.origin.top.right
        x-on:click.outside="close($refs.button)"
        x-cloak
        class="dropdown-container right-0 top-full z-10 origin-top-right {{ $type === 'default' ? '!w-full' : '' }} {{ $size === 'sm' ? 'mt-1' : 'mt-2' }}"
        :class="{'!border-primary-500': open}"
        x-trap="open"
    >
        @if($searchBar)
            <div
                class="relative border-b border-primary-500"
                x-on:keyup.down.prevent.stop="$focus.within($refs.listboxOptions).first()"
                x-on:keyup.up.prevent.stop="$focus.within($refs.listboxOptions).last()"
                x-on:keydown.tab="$focus.next()"
            >
                <input
                    type="text"
                    class="unique-input text-sm text-shade-700 dark:text-shade-50 w-full border-none focus-visible:border-none focus:border-none focus:ring-primary-500 bg-transparent dark:bg-transparent {{ $size === 'md' ? 'px-4 py-2.5' : 'px-2 py-1.5 text-sm leading-none'}}"
                    placeholder="{{ $searchBarPlaceholder }}"
                    x-model="query"
                    x-ref="inputSearch"
                />
                <button
                    type="button"
                    class="absolute transform -translate-y-1/2 right-2 top-1/2"
                    x-show="query !== ''"
                    x-on:click="resetSearch()"
                    x-on:keydown.enter.stop="resetSearch(); $refs['inputSearch'].focus();"
                >
                    <x-heroicon-s-x-mark class="icon-s text-shade-500"/>
                </button>
            </div>
        @endif
        <!-- more divs and so here -->
    </div>
</div>

CleanShot 2024-01-16 at 15 23 17@2x CleanShot 2024-01-16 at 15 24 28@2x

Also, you can see this ticket : https://youtrack.jetbrains.com/issue/WEB-64951

My editor doesn't see and read the JS correctly

Thank you @inxilpro

inxilpro commented 9 months ago

@joffreypersia hm. Does it crash, or does it just not handle the syntax highlighting properly? There's a known bug when combining Alpine and Blade that is very tricky—it's hard to inject the correct language into the file when they're mixed in certain ways.

I pasted your example into PhpStorm and didn't get any IDE errors…

joffreypersia commented 9 months ago

Hi @inxilpro,

Try this code :

@props([
    'tableOnly' => false,
])
<div
    x-data="{
        viewTableMosaic: {{ $tableOnly ? '\'table\'' : '(localStorage.getItem(\'table-filter\') ? localStorage.getItem(\'table-filter\') : \'table\')' }},
        tableOnly: {{ $tableOnly ? 'true' : 'false'}},
        allowClickingOnAllRow: false,
        showFilters: false,
        showDeleteModal: @entangle('showDeleteModal'),
        nbOfRows: 0,
        selected: @entangle('selected'),
        firstItem: null,
        lastItem: null,

        init() {
            this.nbOfRows = document.querySelectorAll('tbody tr').length;
            this.checkSelectedItems();

            $nextTick(() => {
                // Call the shadowScrolling function
                this.shadowScrolling();

                let isUpdated = false;

                Livewire.hook('morph.updated', ({ el, component }) => {
                    if (!isUpdated) {
                        isUpdated = true;

                        setTimeout(() => {

                            // Call checkSelectedItems
                            this.checkSelectedItems();
                            this.checkNumberOfRows();

                            // Reset the flag
                            isUpdated = false;

                            // Call the shadowScrolling function
                            this.shadowScrolling();

                        }, 10);
                    }
                });
            });
        },
        resetSelectAll() {
            document.querySelector('thead tr input[type=checkbox]').checked = false;
        },
        checkSelectedItems() {
            // timeout of 100ms to be sure that the function will run after the wire:model has been updated
            setTimeout(() => {
                let nbOfChecked = this.selected.length;
                if (nbOfChecked === this.nbOfRows) {
                    document.querySelector('thead tr input[type=checkbox]').checked = true;
                } else {
                    document.querySelector('thead tr input[type=checkbox]').checked = false;
                }
            }, 100);
        },
        toggleAllCheckboxes(el) {
            document.querySelectorAll('tbody tr input[type=checkbox]').forEach((checkbox) => {
                const isChecked = el.checked;
                if (checkbox.checked !== isChecked) {
                    checkbox.checked = isChecked;
                    // Manually dispatch the change event
                    checkbox.dispatchEvent(new Event('change'));
                }
            });
        },
        checkNumberOfRows() {
            this.nbOfRows = document.querySelectorAll('tbody tr').length;
        },
        shadowScrolling() {
            const scrollBoxes = document.querySelectorAll('.scroll-shadows');
            let isShadowBottomActive = false; // Flag to track the current state of the shadow

            scrollBoxes.forEach(scrollBox => {
                // Check if the element is currently visible (not having display: none)
                if (getComputedStyle(scrollBox).display !== 'none') {
                    // At page load / refresh
                    if (scrollBox.scrollHeight > scrollBox.clientHeight) {
                        const el = document.querySelector('#shadowContainer');
                        el.classList.add('shadowBottom');
                        isShadowBottomActive = true;
                    } else {
                        const el = document.querySelector('#shadowContainer');
                        el.classList.remove('shadowBottom');
                        isShadowBottomActive = false;
                    }

                    scrollBox.addEventListener('scroll', function(e) {
                        const shouldShowShadow = e.currentTarget.scrollHeight - e.currentTarget.clientHeight - e.currentTarget.scrollTop > 30;

                        if (shouldShowShadow && !isShadowBottomActive) {
                            const el = document.querySelector('#shadowContainer');
                            el.classList.add('shadowBottom');
                            isShadowBottomActive = true;
                        } else if (!shouldShowShadow && isShadowBottomActive) {
                            const el = document.querySelector('#shadowContainer');
                            el.classList.remove('shadowBottom');
                            isShadowBottomActive = false;
                        }
                    });
                }
            });
        }
    }"
    class="space-y-4 flex-grow flex flex-col"
>
    @include('model-index-pages.table-filter')
    <div class="relative flex flex-col flex-grow">
        <!-- Shadow container -->
        <div id="shadowContainer" class="absolute w-full h-full z-10 pointer-events-none rounded"></div>

        <!-- Table view -->
        @include('model-index-pages.table', array('tableStyle' => ' divide-y divide-shade-200 dark:divide-shade-700')) {{-- tableStyle is used here to style header / body separation --}}
        @if($tableOnly === false )
            <!-- Mosaic view -->
            @include('model-index-pages.mosaique')
        @endif
    </div>
    @include('model-index-pages.pagination')

    <!-- Delete Modal -->
    @teleport('body')
    <form wire:submit="deleteSelected">
        <x-modal.confirmation wire:model="showDeleteModal" maxWidth="xl">
            <x-slot name="title">Delete Confirmation</x-slot>

            <x-slot name="content">
                <p>Are you sure you want to delete these items ? This action is irreversible.</p>
            </x-slot>

            <x-slot name="footer">
                <button type="button" x-on:click="show = false" class="btn btn-white w-full sm:w-fit">Cancel</button>
                <button type="submit" class="btn btn-danger w-full sm:w-fit">Delete</button>
            </x-slot>
        </x-modal.confirmation>
    </form>
    @endteleport
</div>

I documented this subject in the ticket https://youtrack.jetbrains.com/issue/WEB-64951