Problem z własnym przyciskiem 'select'

Odpowiedz Nowy wątek
2020-01-14 23:59

Rejestracja: 2 lata temu

Ostatnio: 4 tygodnie temu

Lokalizacja: Polska

0

EDIT: na potrzeby tego pytania stworzyłem fiddle: https://jsfiddle.net/8p45tf3k/

Paradoksalnie, przy hardkodowaniu niegenerycznych i nierenderowanych na serwerze wartości, wszystko działa OK. Natomiast u mnie wciąż ten sam bug, nawet z wartościami "na sztywno".

PYTANIE:

Hej, na początku samym zaznaczam, że nie mam za wiele do czynienia z JS, stąd moje pytanie do bardziej doświadczonych. W swoim projekcie nie chciałem korzystać z rozwiązań dużych bibliotek typu jQuery bądź Bootstrap, jednak zaszła potrzeba poradzenia sobie jakoś w miarę elegancko z elementami oznaczonymi tagiem select.

Nie udało mi się znaleźć jakiegoś dobrego gotowca, więc posiedziałem kilka godzin i udało mi się stworzyć coś od podstaw, w miarę generycznego, może nie do końca eleganckiego, choć moje wymagania spełnia i wykorzystuje jedynie vanilla JS i CSS .

Jednak pojawił się problem, ponieważ jak dwóch różnych elementach select na liście opcji pojawi się obiekt o tej samej wartości oraz z tym samym id (co ma prawo się zdarzyć), wtedy zaznaczany jest ten z pierwszego renderowanego bodajże elementu:

<input type="checkbox" id="@item" onchange="addSelection(this)" />@item

I zastanawiam się jak jeszcze mogę dodać wyjątkowości takiemu elementowi checkbox, jaki opcji. Zabrakło mi pomysłów. Poniżej kod dla przycisku oraz JS. CSS pomijam, wydaje mi się że nie ma tu wiele do czynienia.

<!--multiple select-->
            <div class="multiselect inputSelect barItem" id="dupa">
                <div class="selectBox">
                    <div id="arrowSel"></div>
                    <span class="oryginalText">Select flags</span>
                    <div>Select flags</div>
                    <select asp-for="Dupa" class="inputSelect" id="selectdupa" multiple>
                        @foreach (string item in ViewBag.Dupa)
                        {
                            <option>@item</option>
                        }
                    </select>
                </div>
                <div class="checkboxes" id="checkboxesdupa">

                    @foreach (string item in ViewBag.Dupa)
                    {
                    <label for="@item">
                        <input type="checkbox" id="@item" onchange="addSelection(this)" />@item
                    </label>
                    }
                </div>
            </div>
<script>

        //on checkbox change
        function addSelection(checkbox) {
            const checkboxes = checkbox.parentNode.parentNode.id;
            console.log(checkboxes);
            const selectionId = checkboxes.replace('checkboxes', 'select');

            if (checkbox.checked) {
                select(true, checkbox, selectionId);
            }
            else {
                select(false, checkbox, selectionId);
            }
        }

        //on checkbox check or uncheck, called by: addSelection
        function select(selection, checkbox, selectionId) {
            var sel = document.getElementById(selectionId);
            var opts = sel.options;
            for (var opt, j = 0; opt = opts[j]; j++) {
                if (opt.value === checkbox.id) {
                    opt.selected = selection;
                    console.log(selection + ' ' + checkbox.id + ' ' + selectionId + ' ' + opt.selected + ' ' + opt.value);
                    break;
                }
            }
            updatePlaceholder(opts, sel);

        }

        //on checkbox change
        function updatePlaceholder(opts, sel) {
            var counter = 0;
            for (var opt, j = 0; opt = opts[j]; j++) {
                if (opt.selected === true) {
                    counter++;
                }
            }
            if (counter === 0) {
                sel.previousElementSibling.style.color = '#bbbbbb';
                sel.previousElementSibling.innerHTML = sel.previousElementSibling.previousElementSibling.innerHTML;
            }
            else {
                sel.previousElementSibling.style.color = 'black';
                sel.previousElementSibling.innerHTML = 'Selected: ' + counter;
            }
        }

        //populate checkboxes on selections
        function verifyCheckboxes(selectionId, checkboxes) {
            var sel = document.getElementById(selectionId);
            var opts = sel.options;
            for (var opt, j = 0; opt = opts[j]; j++) {
                if (opt.selected === true) {
                    var checkboxesList = checkboxes.getElementsByTagName("input");
                    for (var i = 0; i < checkboxesList.length; i++) {
                        if (checkboxesList[i].id == opt.value) {
                            checkboxesList[i].checked = true;
                        }
                    }
                    updatePlaceholder(opts, sel);
                }
            }
        }

        //get all select tag objects
        const multiSelections = document.getElementsByClassName('multiselect');

        //on click of select tag object
        for (var multiSelect, j = 0; multiSelect = multiSelections[j]; j++) {
            const checkboxes = document.getElementById("checkboxes" + multiSelect.id);
            const selectionId = checkboxes.id.replace('checkboxes', 'select');
            const multi = multiSelect;
            verifyCheckboxes(selectionId, checkboxes);
            window.addEventListener('click', function (e) {
                if (multi.contains(e.target)) {
                    if (checkboxes.style.display === 'block') {
                        checkboxes.style.display = "none";
                    } else {
                        checkboxes.style.display = "block";
                    };
                }
                if (!checkboxes.contains(e.target) && !multi.contains(e.target)) {
                    checkboxes.style.display = "none";
                }
                if (checkboxes.contains(e.target)) {
                    checkboxes.style.display = "block";
                }
            });
        }
    </script>

I wygląd po kliknięciu głównego przycisku:
multi select

edytowany 5x, ostatnio: bakunet, 2020-01-15 00:21

Pozostało 580 znaków

2020-01-15 08:57

Rejestracja: 4 lata temu

Ostatnio: 49 minut temu

1

Problem masz taki, że Twoje elementy mają te same ID, co jest niedozwolone. Jeżeli jest kilka takich samych ID na stronie, to JavaScript zawsze wybierze tylko ten pierwszy. Rozwiązaniem jest nieużywanie ID, tylko np. value i wybieranie elementów tylko w obrębie danego selecta, czyli nie document.getElementBy, tylko select.getElementBy.

Ja to zrobiłem w ten sposób JSFiddle. Użyłem m.in. inputa i tego, że ma atrybut placeholder i value, żeby trochę ułatwić sobie życie przy wyświetlaniu, co zostało zaznaczone. Jakbyś miał pytania, to wal śmiało :)

class MultiSelect {
  constructor(select) {
    this.options = this.extractOptions(select);
    this.dom = this.buildDom(select);
    this.bindEvents();
  }

  extractOptions(select) {
    // select.options zwraca HtmlOptionsCollection,
    // który nie ma metod filter, reduce itp.
    // Użycie ... konwertuje to na tablicę
    // Opcje trzymam w obiekcie, gdzie klucz, to wartość, a wartość, to obiekt HTML
    // { "a": "<option>...", "b": "<option>..." }
    // Dzięki temu później łatwiej mi ustawiać, czy coś jest zaznaczone
    // Bo robie po prostu this.options["a"].selected = true;
    return [...select.options]
      .filter(o => !o.disabled)
      .reduce((options, o) => {
        options[o.value] = o;

        return options;
      }, {});
  }

  buildDom(select) {
    // Tutaj buduję sobie cały dodatkowy HTML potrzebny
    // do działania multiselecta, żeby użytkownik nie musiał tego robić
    select.style.display = 'none';

    const wrapper = document.createElement('div');
    wrapper.classList.add('multiselect');

    const placeholder = document.createElement('input');
    placeholder.classList.add('multiselect-placeholder');
    placeholder.placeholder = select.options[0].innerText;
    placeholder.readOnly = true;
    wrapper.appendChild(placeholder);

    select.parentNode.insertBefore(wrapper, select);
    wrapper.appendChild(select);

    const multiselect = document.createElement('div');
    multiselect.classList.add('multiselect-select');
    wrapper.appendChild(multiselect);

    Object.values(this.options).forEach((opt, index) => {
      const optionWrapper = document.createElement('label');
      optionWrapper.classList.add('multiselect-option');

      optionWrapper.innerHTML = `
        <input type="checkbox" value="${opt.value}" />
        <span>${opt.innerText}</span>
      `;

      multiselect.appendChild(optionWrapper);
    });

    return {
      wrapper,
      select,
      placeholder,
      multiselect
    };
  }

  bindEvents() {
    // Tutaj się dzieje całe mięcho, które jak widać to jedynie kilka linijek
    // Pierwszy listener otwiera i zamyka multiselect
    // Drugi nasłuchuje na zmiany checkboxów
    // i odpowiednio zaznacza moje opcje we właściwym selecie
    // po czym aktualizuje placeholder
    this.dom.placeholder.addEventListener('click', () => {
      this.dom.wrapper.classList.toggle('multiselect--opened');
    });

    this.dom.multiselect.childNodes.forEach(node => {
      node.addEventListener('change', ({ target }) => {
        // Tutaj właśnie używam tego obiektu z pierwszej metody
        // { "a": "<option>...", "b": "<option>..." }
        const option = this.options[target.value];
        option.selected = target.checked;

        this.updatePlaceholder();
      });
    });
  }

  updatePlaceholder() {
    const { selectedOptions } = this.dom.select;

    if (selectedOptions.length === 0) {
      // Dzieki użyciu inputa, jak ustawimy mu wartość na '', to placeholder się wyświetla automatycznie
      this.dom.placeholder.value = '';
      return;
    }

    this.dom.placeholder.value = `Selected ${selectedOptions.length}`;
  }
}

new MultiSelect(document.querySelector('#select-a'));

HTML po zbudowaniu przez metodę buildDom wygląda tak:

<div class="multiselect">
    <input class="multiselect-placeholder" placeholder="Select something cool" readonly="">
    <select id="select-a" multiple="" style="display: none;">
        <option disabled="disabled">Select something cool</option>
        <option value="a">A</option>
        <option value="b">B</option>
        <option value="c">C</option>
        <option value="d">D</option>
    </select>
    <div class="multiselect-select">
        <label class="multiselect-option">
            <input type="checkbox" value="a">
            <span>A</span>
        </label>
        <label class="multiselect-option">
            <input type="checkbox" value="b">
            <span>B</span>
        </label>
        <label class="multiselect-option">
            <input type="checkbox" value="c">
            <span>C</span>
        </label>
        <label class="multiselect-option">
            <input type="checkbox" value="d">
            <span>D</span>
        </label>
    </div>
</div>
edytowany 14x, ostatnio: Desu, 2020-01-15 09:10

Pozostało 580 znaków

2020-01-15 09:10

Rejestracja: 2 lata temu

Ostatnio: 4 tygodnie temu

Lokalizacja: Polska

Desu napisał(a):

(...)
Ja to zrobiłem w ten sposób JSFiddle. Użyłem m.in. inputa i tego, że ma atrybut placeholder i value, żeby trochę ułatwić sobie życie przy wyświetlaniu, co zostało zaznaczone. Jakbyś miał pytania, to wal śmiało :)

Przyznaję, pomysł z placeholder byłby przedni, szczególnie że u mnie każdy będzie inny. Tak samo zbudowanie obiektu HTML, bo jednak się robi bałagan w pliku.

Choć zdążyłem znaleźć inne obejście. Za <label>...</label> dodałem element:

<img src onerror="changeAttributes(this)" /> który automatycznie uruchamia funkcję:

@foreach (string item in ViewBag.VesselTypes)
                    {
                        <label for="@item">
                            <input type="checkbox" id="@item" onchange="addSelection(this)" />@item
                        </label>
                        <img src onerror="changeAttributes(this)" />
                    }

A changeAttributes(this) wygląda tak:

<script>
        function changeAttributes(sibling) {
            const label = sibling.previousElementSibling;
            const checkbox = sibling.previousElementSibling.children[0];
            const parent = sibling.parentNode;

            console.log(label.tagName + ' ' + checkbox.tagName);
            console.log(label.htmlFor + ' ' + checkbox.id);
            checkbox.id = label.htmlFor + parent.id;
            label.setAttribute("for", label.htmlFor + parent.id);
            console.log(label.tagName + ' ' + checkbox.tagName);
            console.log(label.htmlFor + ' ' + checkbox.id);
        }
    </script>

W moim przypadku każdy parentNode ma unikatowe id, więc zwyczajnie generycznie dopisuję je do label.htmlFor oraz checkbox.id, żeby mogły ze sobą maczować w obrębie całego dokumentu.

edytowany 2x, ostatnio: bakunet, 2020-01-15 09:14

Pozostało 580 znaków

2020-01-15 09:13

Rejestracja: 4 lata temu

Ostatnio: 49 minut temu

1

Wystarczy, że owiniesz checkbox w label i już nie musisz mieć ID, tak jak zrobiłem to w moim przypadku.

<img src onerror="changeAttributes(this)" />

Propsy za znalezienie rozwiązania, ale to jakiś straszny hack, nie rób w ten sposób :D
Edytowałem swój post, bo trochę tam wprowadziłem bałagan, teraz jest finalnie tak, jak to miałem na fiddlu, moim zdaniem działa to całkiem spoko.

Możesz to dalej customizować, np. wartość placeholdera można przekazać tak:

new MultiSelect(document.querySelector('#select-a'), {
  placeholder: (items) => {
    return `Wybrano super elementy w liczbie ${items.length}`;
  }
});

new MultiSelect(document.querySelector('#select-b'), {
  placeholder: (items) => {
    return `Tutaj inny placeholder, wybrano: ${items.length}`;
  }
});

I wtedy w metodzie updatePlaceholder robisz cos takiego:

this.dom.placeholder.value = this.config.placeholder(selectedOptions);

https://jsfiddle.net/d76Lxq1n/

edytowany 5x, ostatnio: Desu, 2020-01-15 09:21
Ale mój hack nie wyrzuca żadnych błędów w kosoli (˵ ͡° ͜ʖ ͡°˵). Twoje rozwiązanie wygląda zwięźlej. Jak się zabiorę za optymalizację, to chętnie wrócę do niego. - bakunet 2020-01-15 09:21
A jakie moje rzuca? - Desu 2020-01-15 09:22
Żadnych, nic nie sugerowałem. - bakunet 2020-01-15 09:35

Pozostało 580 znaków

Odpowiedz

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0