lekoala/bootstrap5-tags

suggestion to order loaded tags

Closed this issue · 7 comments

First of all excellent project.

My query or suggestion is if it is feasible to order the labels that one already has selected. It is useful mainly when you want to edit for some change in the display order.

I imagine grag and drop the label.

image

From already thank you very much.

the drag&dropping side is easy using SortableJS is easy (i updated the demo.html for that :) )

the issue is that i'm using a select under the hood. the items there are not sorted regardless of the order when you picked or entered the tags

in order to do this, i would need to sort the options in the select to match whatever has been entered and reflect any drag and drop as well. Sounds doable but I don't have the use for that at the moment but if you want to make a PR you are welcome :-)

@DanielKimmich

actually... i found a solution you can simply sort the options based on in the order of the tags after drag and drop

        function sortSelect(elem, sort) {
        var tmp = [];
        var selectedValue = elem[elem.selectedIndex].value;
        for (var i = 0; i < elem.options.length; i++) {
          tmp.push(elem.options[i]);
        }
        tmp.sort(function (a, b) {
          if (sort.indexOf(a.value) === -1) {
            return tmp.length;
          }
          return sort.indexOf(a.value) - sort.indexOf(b.value);
        });
        while (elem.options.length > 0) {
          elem.options[0] = null;
        }
        for (var i = 0; i < tmp.length; i++) {
          elem.options[i] = tmp[i];
        }
      }

      const validationTags = document.querySelector("#validationTags");
      const example1 = validationTags.parentElement.querySelector("div.form-control").querySelector("div");
      new Sortable(example1, {
        animation: 150,
        filter: "input",
        draggable: "span",
        ghostClass: "blue-background-class",
        onEnd: function (evt) {
          var itemEl = evt.item; // dragged HTMLElement
          var oldIndex = evt.oldIndex; // element's old index within old parent
          var newIndex = evt.newIndex; // element's new index within new parent

          // Sort options according to tag orders
          if (oldIndex != newIndex) {
            const indexes = Array.from(validationTags.parentElement.querySelectorAll("span.badge")).map((badge) => {
              return (badge.dataset && badge.dataset.value) || -1;
            });
            sortSelect(validationTags, indexes);
          }
        },
      });

You're a genius, if that's what I was needing.

I'm testing it and if there are two tags it makes the change fine and saves it fine in the database.

In the case that it has more labels, it is giving me the following error:

Uncaught TypeError: Cannot read properties of undefined (reading 'value')
at edit:200:36
at Array.reduce ()
at Bt.onEnd (edit:199:101)
at W (Sortable.min.js:2:11012)
at U (Sortable.min.js:2:11592)
at Bt._onDrop (Sortable.min.js:2:28869)

It is in this line

return badge.dataset.value || -1;

I will continue testing
thank you.

@DanielKimmich mhh did you see I made an update to the script ? i don't get the issue with my code in demo.html

It is correct, I have tried "demo.html" and I copied part of that code and it works for me in my application and it works.
So I will continue analyzing because it does not work for me. I tell you that I am using the "Laravel Collective" library and I created my own form.


{{--Form::texttags('name', 'options', 'values', 'attributes')  --}}
@php
    $clase = $attributes['class'] ?? 'form-select';
    $new = $attributes['data-allow-new'] ?? 'false';
    $badge = $attributes['badgeStyle'] ?? 'primary';

    if (old($name) !== null){
        $values = old($name);
    }
    $selected = $values ? (implode(',',$values)) : '';

@endphp

<select 
    class="{!!$clase!!}" 
    id="{{ $name }}" 
    name="{{ $name.'[]' }}" 
    multiple 
    data-full-width="true" 
    data-allow-clear="true" 
    data-allow-new="{!!$new!!}" 
    data-badge-style="{!!$badge!!}" 
    data-suggestions-threshold="0"
    data-selected="{!!$selected!!}"
    >


    @empty($options)
        @empty($values)
            <option value="">Escriba las etiquetas...</option><!-- you need at least one option with the placeholder -->
        @else
            <option value="" selected="selected" disabled hidden >Escriba las etiquetas...</option><!-- you need at least one option with the placeholder -->
            @foreach ($values as $value)  					
                <option value="{{$value}}">{{ $value }}</option>
            @endforeach
        @endempty
    @else 
        <option value="" selected="selected" disabled hidden >Seleccione las etiquetas...</option><!-- you need at least one option with the placeholder -->    
        @foreach ($options as $value)  					
            <option value="{{$value}}">{{ $value }}</option>
        @endforeach
    @endempty	

</select>

    <script src="{{ asset('packages/sortablejs/Sortable.js') }}" type="module"></script>
    <script type="module">
        import Tags from "{{ asset('packages/bootstrap5-tags/tags.js') }}"
        Tags.init("#{!!$name!!}");

        function sortSelect(elem, sort) {
        var tmp = [];
        var selectedValue = elem[elem.selectedIndex].value;
        for (var i = 0; i < elem.options.length; i++) {
          tmp.push(elem.options[i]);
        }
        tmp.sort(function (a, b) {
          return sort.indexOf(b.value) - sort.indexOf(a.value);
        });
        while (elem.options.length > 0) {
          elem.options[0] = null;
        }
        for (var i = 0; i < tmp.length; i++) {
          elem.options[i] = tmp[i];
        }
      }

      const sortingTags = document.querySelector("#{!!$name!!}");
      const example1 = sortingTags.parentElement.querySelector("div.form-control").querySelector("div");
      new Sortable(example1, {
        animation: 150,
        filter: "input",
        draggable: "span",
        ghostClass: "blue-background-class",
        onEnd: function (evt) {
          var itemEl = evt.item; // dragged HTMLElement
          var oldIndex = evt.oldIndex; // element's old index within old parent
          var newIndex = evt.newIndex; // element's new index within new parent

          // Sort options according to tag orders
          if (oldIndex != newIndex) {
            const indexes = Array.from(sortingTags.parentElement.querySelectorAll("span.badge")).reduce((badge) => {
              //  console.log (badge.dataset);
                return badge.dataset.value || -1;
            });
          //  console.log (indexes);
            sortSelect(sortingTags, indexes);
          }
        },
      });
    </script>

Thank you very much for taking care and helping me.
Regards,

i don't see anything wrong on the first look so unfortunately that's as much support I can provide as part of creator of this library :-)
i do freelance work as well if needed ;-)

your error means that there is no data attribute => cannot call value of undefined.
simply make a console log of badge console.log(badge) and check what you got there that has no data attribute. then it's a matter of tweaking your queryselector or reduce tag or simply make a forEach loop instead of a reduce tag to have better control

Thanks again, now it's working for me.
Make a few changes, replace the name of the elements "document.querySelector('#{!!$name!!}')", not using a variable in the script.

      const example1 = document.querySelector('#{!!$name!!}').parentElement.querySelector('div.form-control').querySelector('div');
      new Sortable(example1, {
        animation: 150,
        filter: 'input',
        draggable: 'span',
        ghostClass: "blue-background-class",
        onEnd: function (evt) {
          var itemEl = evt.item; // dragged HTMLElement
          var oldIndex = evt.oldIndex; // element's old index within old parent
          var newIndex = evt.newIndex; // element's new index within new parent

          // Sort options according to tag orders
          if (oldIndex != newIndex) {
            const indexes = Array.from(document.querySelector('#{!!$name!!}').parentElement.querySelectorAll("span.badge")).map((badge) => {
              // console.log (badge.dataset);
              return (badge.dataset && badge.dataset.value) || -1;
            });
            // console.log (indexes);
            sortSelect(document.querySelector('#{!!$name!!}'), indexes);
          }
        },
      });

For my part I consider closed
Regards,