Problem z własnym przyciskiem 'select'

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

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>
0
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.

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/

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