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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it compatible with Svelte 3? #9

Open
odoo-mastercore opened this issue Jun 21, 2019 · 14 comments
Open

Is it compatible with Svelte 3? #9

odoo-mastercore opened this issue Jun 21, 2019 · 14 comments

Comments

@odoo-mastercore
Copy link

No description provided.

@ghost
Copy link

ghost commented Jun 22, 2019

It's not. The syntax is very different. Is anyone working on updating this for the latest SvelteJS?

@elcobvg
Copy link
Owner

elcobvg commented Jun 22, 2019

Not updated for svelte v3 yet. As soon as I have the time...

@ghost
Copy link

ghost commented Jun 22, 2019

Happy to help if you could explain to me the logic behind it. I'm learning Svelte right now and this would be a great exercise for me, though I'm very beginner atm.

I'm trying to understand how it works in plain javascript here - https://www.w3schools.com/howto/howto_js_autocomplete.asp but it looks more complicated than it needs to be!

@northkode
Copy link

northkode commented Jul 17, 2019

Svelte 3.6.7 @mikeyhan1 @odoo-mastercore

<script>

		const regExpEscape = (s) => {
			return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&")
		}

			export let name= '';
			export let value= '';
			export let placeholder = '';
			export let required= false;
			export let disabled= false;

			// autocomplete props
			export let items= [];
			export let isOpen= false;
			export let results= [];
			export let search= '';
			export let isLoading= false;
			export let arrowCounter= 0;
	

      let className= '';
      let isAsync= false;
      let minChar= 2;
      let maxItems= 10;
      let fromStart= true; // Default type ahead
			let list;
			let input;
	
			async function onChange (event) {
				// Is the data given by an outside ajax request?
				if (isAsync) {
					isLoading = true;
				} else if (search.length >= Number(minChar)) {
					filterResults()
					isOpen = true;
				}
			}
			function filterResults () {
				results = items.filter(item => {
					if (typeof item !== 'string') {
						item = item.key || '' // Silent fail
					}
					return fromStart ? item.toUpperCase().startsWith(search.toUpperCase())
													 : item.toUpperCase().includes(search.toUpperCase())
				})
				.map(item => {
					const text = typeof item !== 'string' ? item.key : item
					return {
						key: text,
						value: item.value || item,
						label: search.trim() === '' ? text : text.replace(RegExp(regExpEscape(search.trim()), 'i'), "<span>$&</span>")
					}
				});

				const height = results.length > maxItems ? maxItems : results.length
				list.style.height = `${height * 2.25}rem`
			}
    function onKeyDown (event) {
      if (event.keyCode === 40 && arrowCounter < results.length) {
        // ArrowDown
        arrowCounter =  arrowCounter + 1 
      } else if (event.keyCode === 38 && arrowCounter > 0) {
        // ArrowUp
        arrowCounter =  arrowCounter - 1;
      } else if (event.keyCode === 13) {
        // Enter
        event.preventDefault()
        if (arrowCounter === -1) {
          arrowCounter = 0 // Default select first item of list
        }
        close(arrowCounter)
      } else if (event.keyCode === 27) {
        // Escape
        event.preventDefault()
        close()
      }
    }
    function close (index = -1) {
      isOpen = false; 
			arrowCounter = -1;
     	input.blur();
      if (index > -1) {
      	value = results[index].value;
				key = results[index].key;
      } else if (!value) {
        search = ''
      }
    }
  function onupdate ({ changed, current }) {
    if (isAsync && changed.items && current.items.length) {
       items = current.items;
       isLoading = false;
       isOpen = true;
       filterResults();
    }
	}
</script>

<style>
  * {
    box-sizing: border-box;
  }

  input {
    height: 2rem;
    font-size: 1rem;
    padding: 0.25rem 0.5rem;
  }

  .autocomplete {
    position: relative;
  }

  .hide-results {
    display: none;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #dbdbdb;
    height: 6rem;
    overflow: auto;
    width: 100%;

    background-color: white;
    box-shadow: 2px 2px 24px rgba(0, 0, 0, 0.1);
    position: absolute;
    z-index: 100;
  }

  .autocomplete-result {
    color: #7a7a7a;
    list-style: none;
    text-align: left;
    height: 2rem;
    padding: 0.25rem 0.5rem;
    cursor: pointer;
  }

  .autocomplete-result > :global(span) {
    background-color: none;
    color: #242424;
    font-weight: bold;
  }

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #dbdbdb;
  }
</style>
<svelte:window on:click="{()=>close()}" />
<div on:click="{(event)=>event.stopPropagation()}" class="autocomplete">
  <input
    type="text"
    class="{className}"
    {name}
    {placeholder}
    {required}
    {disabled}
    value="{value || ''}"
    autocomplete="{name}"
    bind:value="{search}"
    on:input="{(event)=>onChange(event)}"
    on:focus
    on:blur
    on:keydown="{(event)=>onKeyDown(event)}"
    bind:this={input}
  >
  <ul class="autocomplete-results{!isOpen ? ' hide-results' : ''}" bind:this={list}>
		{#each results as result, i}
				<li on:click="{()=>close(i)}" class="autocomplete-result{ i === arrowCounter ? ' is-active' : '' }">
				{@html result.label}
				</li>
		{/each}
  </ul>
{#if isLoading}
  <slot>
    <p class="fallback">Loading data...</p>
  </slot>
{/if}
</div>

https://svelte.dev/repl/72a022c606aa4509abc6b00401538235?version=3.6.7

Gets you 99% of the way there

@elcobvg
Copy link
Owner

elcobvg commented Jul 17, 2019

Thanks for this, @northkode ! Looks good, but why do you say 'gets you 99% there'?

@northkode
Copy link

northkode commented Jul 17, 2019

Some of the padding and styles aren't perfect.
I didnt have time to adjust them as svelte does slightly change some globle styles etc.

Didnt want to say it was perfect cause I only spent about 10 minutes on it! Lol

@MintyMods
Copy link

Perfect, Gonna steal this and tweak a little for my project.

Thanks Guys

@jdevine
Copy link

jdevine commented Jan 29, 2020

Wondering about the onupdate function

is that just a remnant of pre-v3 svelte? seems similar to the current store.update, but I'm not grokking it.

@elcobvg
Copy link
Owner

elcobvg commented Jan 29, 2020

Sorry @jdevine , it's not compatible with Svelte v3 yet. Haven't had the time yet to update.

@ghost
Copy link

ghost commented Mar 23, 2020

@northkode : Impressive :) Did you get async to work as well?

@northkode
Copy link

I did on a local copy. Not on this one above however. I could try find it and post an update.

@rrosiek
Copy link

rrosiek commented Apr 20, 2020

I took what @northkode did and tweaked it a bit for my local implementation. I removed a lot of the flexibility, again, for brevity and the fact I'm only using it for a specific project. Could put a PR in that's more aligned with the original if people are interested.

@tomzij this is how I handled the async piece, not sure if it's usable for everyone's scenario.

<script>
  import { scale } from "svelte/transition";
  import { quintOut } from "svelte/easing";

  const regExpEscape = s => {
    return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
  };

  export let asyncFunc = null;
  export let id = "";
  export let items = [];
  export let maxItems = 10;
  export let minChar = 2;
  export let name = "";
  export let results = [];
  export let value = {};

  let arrowCounter = 0;
  let input;
  let loading = false;
  let open = false;
  let search = "";
  let timer = null;

  const clickAway = event => {
    timer = setTimeout(() => {
      open = false;
    }, 200);
  };

  const clickOn = event => clearTimeout(timer);

  const onChange = event => {
    const term = event.target.value;

    if (term.length >= Number(minChar)) {
      if (asyncFunc !== null) {
        loading = true;

        clearTimeout(timer);

        timer = setTimeout(async () => {
          items = await asyncFunc(term);
          loading = false;
          open = true;
          filterResults(term);
        }, 500);
      } else {
        open = true;
        filterResults(term);
      }
    } else {
      results = [];
    }
  };

  const filterResults = term => {
    results = items
      .filter(item => {
        if (typeof item !== "string") item = item.value || "";

        return item.toUpperCase().includes(term.toUpperCase());
      })
      .map((item, i) => {
        const text = typeof item !== "string" ? item.value : item;

        return {
          key: item.key || i,
          value: text,
          label:
            term.trim() === ""
              ? text
              : text.replace(
                  RegExp(regExpEscape(term.trim()), "i"),
                  "<span class='font-semibold'>$&</span>"
                )
        };
      })
      .slice(0, maxItems - 1);
  };

  const onKeyDown = event => {
    if (event.keyCode === 40 && arrowCounter < results.length - 1) {
      arrowCounter++;
    } else if (event.keyCode === 38 && arrowCounter > 0) {
      arrowCounter--;
    } else if (event.keyCode === 13) {
      event.preventDefault();

      if (arrowCounter === -1) arrowCounter = 0;

      close(arrowCounter);
    } else if (event.keyCode === 27) {
      event.preventDefault();
      close();
    }
  };

  const close = (index = -1) => {
    open = false;
    arrowCounter = -1;
    input.blur();

    if (index > -1) {
      value = results[index];
      search = results[index].value;
    } else {
      search = "";
    }
  };
</script>

<div class="mt-1 relative rounded-md shadow-sm">
  <input
    on:blur={clickAway}
    on:focus={clickOn}
    on:keydown={event => onKeyDown(event)}
    on:input={event => onChange(event)}
    bind:value={search}
    bind:this={input}
    class="form-input block w-full pr-10 sm:text-sm sm:leading-5"
    type="text"
    {id}
    {name} />
  {#if loading}
    <div
      class="absolute inset-y-0 right-0 pr-3 flex items-center
      pointer-events-none">
      <img class="h-4 w-4 opacity-50" src="/bars_dark.svg" alt="Loading" />
    </div>
  {/if}
  {#if open}
    <div
      class="origin-top-right absolute right-0 mt-2 w-full rounded-md shadow-lg
      z-10"
      transition:scale={{ duration: 150, delay: 0, opacity: 0.2, start: 0.0, easing: quintOut }}>
      <div class="rounded-md bg-white shadow-xs">
        <div class="py-1">
          {#if results.length === 0}
            <div
              class="block px-4 py-2 text-sm cursor-pointer text-gray-700
              hover:bg-gray-100">
              No matches
            </div>
          {:else}
            {#each results as result, i}
              <div
                on:click={() => close(i)}
                class="block px-4 py-2 text-sm cursor-pointer text-gray-700
                hover:bg-gray-100 {i === arrowCounter ? 'bg-gray-100' : ''}">
                {@html result.label}
              </div>
            {/each}
          {/if}
        </div>
      </div>
    </div>
  {/if}
</div>

@svmartin
Copy link

I did on a local copy. Not on this one above however. I could try find it and post an update.

@northkode Could you post an update with async? That would be great!

@northkode
Copy link

@svmartin This is a simple example.
There are some things that would need to be fixed for your individual apps but this is one way to use it.
Styles could be fixed, but that is easy for your individual needs.

https://svelte.dev/repl/7641903573c943799c63f9772b0d033f?version=3.24.0

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

No branches or pull requests

7 participants