Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

HighlightFirst does not play nice with ajax whitelist #884

Closed
3 tasks done
Nensec opened this issue Aug 14, 2021 · 3 comments
Closed
3 tasks done

HighlightFirst does not play nice with ajax whitelist #884

Nensec opened this issue Aug 14, 2021 · 3 comments

Comments

@Nensec
Copy link

Nensec commented Aug 14, 2021

Prerequisites

  • I am running the latest version
  • I checked the documentation and found no answer
  • I checked to make sure that this issue has not already been filed

馃挜 Demo Page

2021-08-14_03-36-08

Explanation

I have highlightFirst set to true, a custom template if there are no elements returned and have an input callback with a small debounce, if I hit enter before the debounce kicks in then the first tag in the list is added properly. If the debounce does kick in and the whitelist gets refreshed then the first element is not added when hitting enter.

  • What is the expected behavior?
    The first element should be added as it is highlighted

  • What is happening instead?
    The element is not added

  • What error message are you getting?
    No error

  • Code used

var tagifyObj = $('#tags')
                .tagify({
                    enforceWhitelist: true,
                    whitelist: @Html.Raw(Model.WhitelistedTagsJSON),
                    keepInvalidTags: false,
                    dropdown: {
                        highlightFirst: true,
                        enabled: 1,
                        position: 'text'
                    },
                    templates: {
                        dropdownItemNoMatch: function (data) {
                            return `<div class='${this.settings.classNames.dropdownItem}' tabindex="0" role="option">
    No tags found for: <strong>${data.value}</strong>
    </div>`
                        }
                    }
                });

tagifyObj.on('input', $.debounce(100, function (e, data)
{
    tagInput(e, data);
}));

tagInput = function(e, data) {
            var instance = $('#tags').data('tagify');
            var value = data.value;
            instance.whitelist = null // reset the whitelist

            // show loading animation and hide the suggestions dropdown
            instance.loading(true).dropdown.hide()

            $.get(`@Url.RouteUrl("TagsFetch")?tag=${value}`)
                .done(data => {
                    instance.whitelist = data;
                    instance.loading(false).dropdown.show(value);
                });
        };
@yairEO
Copy link
Owner

yairEO commented Aug 16, 2021

Please create a reproducible working demo (as asked in the issue template) so I could help. I don't want to spend my time creating a demo AND also figuring out what's wrong. Please, help me help you by creating a demo for me to examine.

@bdteo
Copy link

bdteo commented Nov 13, 2021

Hi @yairEO, a few minutes I experienced the same problem.
Here is most of my code (except for the back-end) with a fix in it:

function enableTheTagFilter($parent) {
  const $tagsFilterInput = $parent.find('#tags-filter-input')
  const tagsFilterInput = $tagsFilterInput.get(0)

  const tagify = new Tagify(
    tagsFilterInput,
    {
      whitelist: [],
      enforceWhitelist: true,
      dropdown: {
        // this is a numeral value which tells Tagify
        // when to show the suggestions dropdown,
        // when a minimum of N characters were typed
        enabled: 1,
        // position: "text",
        closeOnSelect: true,
        highlightFirst: true
      }
    }
  );

  // noinspection JSUnresolvedFunction
  tagify.on('input', debounce(onInput, 170))

  async function onInput(e) {
    const value = e.detail.value
    tagify.whitelist = null

    let controller = onInput['fetchController']
    if (controller) {
      controller.abort()
    }
    controller = onInput['fetchController'] = new AbortController()

    // show loading animation and hide the suggestions dropdown
    // noinspection JSUnresolvedFunction
    tagify.loading(true)
    // noinspection JSUnresolvedVariable
    // THE FOLLOWING LINE IS THE CULPRIT. THIS IS WHY IT IS COMMENTED OUT
    // tagify.dropdown.hide()

    const suggestionsUrl = $tagsFilterInput.data('suggestions-url')

    const response = await fetch(
      `${suggestionsUrl}?${$.param({value})}`,
      {signal: controller.signal}
    );
    tagify.whitelist = await response.json()
    // noinspection JSUnresolvedFunction
    tagify.loading(false)
    // await sleep(1000)
    // noinspection JSUnresolvedVariable
    tagify.dropdown.show(value)
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

I also don't have the time to set up a full demo but I found the culprit of the problem.
You can search for the THE FOLLOWING LINE IS THE CULPRIT. THIS IS WHY IT IS COMMENTED OUT line in the above snippet.

Using the sleep function that you can see above I was able to clearly see that the dropdown was opening, closing and then opening again on input.

Anyway the thing is that what I provided above is just a modified version of the example you have in the README.md
I mean this example:

var input = document.querySelector('input'),
    tagify = new Tagify(input, {whitelist:[]}),
    controller; // for aborting the call

// listen to any keystrokes which modify tagify's input
tagify.on('input', onInput)

function onInput( e ){
  var value = e.detail.value
  tagify.whitelist = null // reset the whitelist

  // https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
  controller && controller.abort()
  controller = new AbortController()

  // show loading animation and hide the suggestions dropdown
  tagify.loading(true).dropdown.hide()

  fetch('http://get_suggestions.com?value=' + value, {signal:controller.signal})
    .then(RES => RES.json())
    .then(function(newWhitelist){
      tagify.whitelist = newWhitelist // update inwhitelist Array in-place
      tagify.loading(false).dropdown.show(value) // render the suggestions dropdown
    })
}

I haven't gone into full depth to see why exactly it is happening but it seems that showing and hiding of the dropdown interferes with highlightFirst. Given the ways showing/hiding and highlightFirst work, it is probably not a good idea to hide the dropdown when initiating the loading.

In summary, @yairEO, you should probably edit this specific example not to include a call to dropdown.hide().

@bdteo
Copy link

bdteo commented Nov 13, 2021

TLDR for anyone experiencing the same problem:
Please, carefully check if you are calling dropdown.hide() somewhere where it should not happen.
It will probably depend on your specific implementation but most certainly removing the unneeded dropdown.hide() call is the solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants