Merge pull request #269 from reuters-graphics/jon-framer

framer
This commit is contained in:
Jon McClure 2025-04-18 11:26:52 +01:00 committed by GitHub
commit 6470a386e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 448 additions and 329 deletions

View file

@ -0,0 +1,19 @@
import { Meta } from '@storybook/blocks';
import * as FramerStories from './Framer.stories.svelte';
<Meta of={FramerStories} />
# FeaturePhoto
An embed tool for development in the Graphics Kit.
```svelte
<script>
import { Framer } from '@reuters-graphics/graphics-components';
const embeds = ['/embeds/my-chart/index.html'];
</script>
<Framer {embeds} />
```

View file

@ -1,33 +1,19 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import Framer from './Framer.svelte';
import { withComponentDocs } from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Utilities/Framer',
component: Framer,
...withComponentDocs(componentDocs),
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
</script>
<Template>
{#snippet children({ args })}
<Framer {...args} />
{/snippet}
</Template>
<Story
name="Default"
name="Demo"
args={{
embeds: [
'https://graphics.reuters.com/USA-CONGRESS/FUNDRAISING/zjvqkawjlvx/embeds/en/embed/?zzz',
'https://www.reuters.com/graphics/UKRAINE-CRISIS/MAP/klvymdzdrvg/embeds/en/map/',
],
}}
/>

View file

@ -1,21 +1,31 @@
<!-- @migration-task Error while migrating Svelte code: Can't migrate code with afterUpdate. Please migrate by hand. -->
<script>
<script lang="ts">
// @ts-ignore Temporary
import Fa from 'svelte-fa/src/fa.svelte';
import { faDesktop, faLink } from '@fortawesome/free-solid-svg-icons';
import { onMount, afterUpdate } from 'svelte';
import pym from 'pym.js';
import urljoin from 'proper-url-join';
import Resizer from './Resizer/index.svelte';
import { width } from './stores.js';
import getUniqNames from './uniqNames.js';
import { width } from './stores';
import getUniqNames from './uniqNames';
import Typeahead from './Typeahead/index.svelte';
import ReutersGraphicsLogo from '../ReutersGraphicsLogo/ReutersGraphicsLogo.svelte';
export let embeds;
export let breakpoints = [330, 510, 660, 930, 1200];
export let minFrameWidth = 320;
export let maxFrameWidth = 1200;
interface Props {
embeds: string[];
breakpoints?: number[];
minFrameWidth?: number;
maxFrameWidth?: number;
}
const getDefaultEmbed = () => {
let {
embeds = [],
breakpoints = [330, 510, 660, 930, 1200],
minFrameWidth = 320,
maxFrameWidth = 1200,
}: Props = $props();
const getDefaultEmbed = (embeds: Props['embeds']) => {
if (embeds.length === 0) return '';
if (typeof window === 'undefined') return embeds[0];
const lastActiveEmbed = window.localStorage.getItem('framer-active-embed');
if (!lastActiveEmbed) return embeds[0];
@ -23,12 +33,16 @@
return embeds[0];
};
let activeEmbed = getDefaultEmbed();
let activeEmbedIndex = embeds.indexOf(activeEmbed);
let activeEmbed = $state(getDefaultEmbed(embeds));
let activeEmbedIndex = $derived(embeds.indexOf(activeEmbed));
$: embedTitles = getUniqNames(embeds);
let embedTitles = $derived.by(() => {
if (embeds.length === 0) return '';
return getUniqNames(embeds);
});
const reframe = (embed) => {
const reframe = (embed: string) => {
if (!embed) return;
// Bit of hack for handling adding query strings dynamically to embeds.
// cf. also the value prop on the Typeahead component...
const activeEmbed =
@ -43,55 +57,58 @@
);
};
onMount(() => {
reframe(activeEmbed);
});
afterUpdate(() => {
$effect(() => {
reframe(activeEmbed);
});
</script>
<div class="container">
<header>
<img
src="https://graphics.thomsonreuters.com/style-assets/images/logos/reuters-graphics-logo/svg/graphics-logo-dark.svg"
alt=""
/>
<ReutersGraphicsLogo width="120px" />
</header>
<div id="typeahead-container">
<div class="embed-link">
<a rel="external" target="_blank" href={activeEmbed} title={activeEmbed}>
Live link <Fa icon={faLink} />
</a>
{#if embeds.length === 0}
<div class="no-embeds">
<p>No embeds to show.</p>
</div>
{:else}
<div id="typeahead-container">
<div class="embed-link">
<a
rel="external"
target="_blank"
href={activeEmbed}
title={activeEmbed}
>
Live link <Fa icon={faLink} />
</a>
</div>
<Typeahead
label="Select an embed"
value={embedTitles[embeds.indexOf(activeEmbed)] ||
embedTitles[activeEmbedIndex] ||
embedTitles[0]}
extract={(d) => embedTitles[d.index]}
data={embeds.map((embed, index) => ({ index, embed }))}
showDropdownOnFocus={true}
onselect={(detail) => {
if (typeof window !== 'undefined') {
window.localStorage.setItem(
'framer-active-embed',
detail.original.embed
);
}
activeEmbed = detail.original.embed;
// activeEmbedIndex = detail.original.index;
}}
/>
</div>
<Typeahead
label="Select an embed"
value={embedTitles[embeds.indexOf(activeEmbed)] ||
embedTitles[activeEmbedIndex] ||
embedTitles[0]}
extract={(d) => embedTitles[d.index]}
data={embeds.map((embed, index) => ({ index, embed }))}
placeholder={'Search'}
showDropdownOnFocus={true}
on:select={({ detail }) => {
if (typeof window !== 'undefined') {
window.localStorage.setItem(
'framer-active-embed',
detail.original.embed
);
}
activeEmbed = detail.original.embed;
activeEmbedIndex = detail.original.index;
}}
/>
</div>
<div id="preview-label" style="width:{$width}px;">
<p>Preview</p>
</div>
<div id="frame-parent" style="width:{$width}px;"></div>
<div id="preview-label" style="width:{$width}px;">
<p>Preview</p>
</div>
<div id="frame-parent" style="width:{$width}px;"></div>
{/if}
</div>
<div id="home-link">
@ -100,18 +117,29 @@
</a>
</div>
<Resizer {breakpoints} {minFrameWidth} {maxFrameWidth} />
{#if embeds.length > 0}
<Resizer {breakpoints} {minFrameWidth} {maxFrameWidth} />
{/if}
<style lang="scss">
@use '../../scss/mixins' as mixins;
header {
font-family: 'Knowledge', 'Source Sans Pro', Arial, sans-serif;
font-size: 50px;
text-align: center;
text-transform: uppercase;
font-weight: 700;
margin: 20px 0;
}
.no-embeds {
background-color: #efefef;
border-radius: 4px;
padding: 5px 10px;
p {
text-align: center;
margin: 0;
@include mixins.font-note;
}
}
div#typeahead-container {
max-width: 660px;
margin: 0 auto 15px;

View file

@ -1,12 +1,11 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { faDesktop, faMobileAlt } from '@fortawesome/free-solid-svg-icons';
// @ts-ignore Temporary
import Fa from 'svelte-fa/src/fa.svelte';
import { width } from './../stores.js';
import { width } from '../stores.js';
interface Props {
breakpoints?: any;
breakpoints?: number[];
maxFrameWidth?: number;
minFrameWidth?: number;
}
@ -17,36 +16,36 @@
minFrameWidth = 320,
}: Props = $props();
let container = $state();
let container: HTMLElement | undefined = $state();
const sliderWidth = 90;
let windowInnerWidth = $state(1200);
let minWidth = $derived(minFrameWidth);
let maxWidth = $derived(Math.min(windowInnerWidth - 70, maxFrameWidth));
let pixelRange = $derived(maxWidth - minWidth);
run(() => {
$effect(() => {
if ($width > maxWidth) width.set(maxWidth);
});
let offset;
run(() => {
offset = ($width - minWidth) / pixelRange;
});
// svelte-ignore state_referenced_locally
let offset = $state(($width - minWidth) / pixelRange);
let sliding = $state(false);
let isFocused = $state(false);
const roundToNearestFive = (d) => Math.ceil(d / 5) * 5;
const roundToNearestFive = (d: number) => Math.ceil(d / 5) * 5;
const getPx = () => Math.round(pixelRange * offset + minWidth);
let pixelLabel = $state(null);
let pixelLabel: null | number = $state(null);
const move = (e) => {
const move = (e: MouseEvent) => {
if (!sliding || !container) return;
const { left } = container.getBoundingClientRect();
offset = Math.min(Math.max(0, e.pageX - left), sliderWidth) / sliderWidth;
pixelLabel = roundToNearestFive(getPx());
};
const handleKeyDown = (e) => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isFocused) return;
const { keyCode } = e;
const pixelWidth = sliderWidth / pixelRange;
@ -59,7 +58,7 @@
}
width.set(getPx());
};
const start = (e) => {
const start = (e: MouseEvent) => {
sliding = true;
move(e);
};

View file

@ -0,0 +1,101 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
interface Props {
value: string;
autofocus?: boolean;
debounce?: number;
label?: string | Snippet;
hideLabel?: boolean;
id?: string;
ref: HTMLElement;
removeFormAriaAttributes?: boolean;
ontype?: (value: string) => void;
onclear?: () => void;
}
let {
value = $bindable(''),
autofocus = false,
debounce = 0,
label = 'Label',
hideLabel = false,
id = 'search' + Math.random().toString(36),
ref = $bindable(),
removeFormAriaAttributes = false,
ontype = (_value: string) => {},
onclear = () => {},
...restProps
}: Props & HTMLInputAttributes = $props();
import { onMount } from 'svelte';
let prevValue = value;
let timeout: ReturnType<typeof setTimeout>;
let calling = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function debounceFn(fn: () => any) {
if (calling) return;
calling = true;
timeout = setTimeout(() => {
fn();
calling = false;
}, debounce);
}
onMount(() => {
if (autofocus) window.requestAnimationFrame(() => ref?.focus());
return () => clearTimeout(timeout);
});
$effect(() => {
if (value.length > 0 && value !== prevValue) {
if (debounce > 0) {
debounceFn(() => ontype(value));
} else {
ontype(value);
}
}
if (value.length === 0 && prevValue.length > 0) onclear();
prevValue = value;
});
</script>
<form
data-svelte-search
role={removeFormAriaAttributes ? null : 'search'}
aria-labelledby={removeFormAriaAttributes ? null : id}
action=""
onsubmit={(e) => {
e.preventDefault();
}}
>
<label
id="{id}-label"
for={id}
style={hideLabel ?
'position: absolute;height: 1px;width: 1px;overflow: hidden;clip: rect(1px 1px 1px 1px);clip: rect(1px, 1px, 1px, 1px);white-space: nowrap;'
: undefined}
>
{#if typeof label === 'string'}
{label}
{:else}
{@render label()}
{/if}
</label>
<input
bind:this={ref}
bind:value
{id}
name="search"
type="search"
placeholder="Search..."
autocomplete="off"
spellcheck="false"
{...restProps}
/>
</form>

View file

@ -1,133 +0,0 @@
/*
* Fuzzy
* https://github.com/myork/fuzzy
*
* Copyright (c) 2012 Matt York
* Licensed under the MIT license.
*/
const fuzzy = {};
// Return all elements of `array` that have a fuzzy
// match against `pattern`.
fuzzy.simpleFilter = function (pattern, array) {
return array.filter(function (str) {
return fuzzy.test(pattern, str);
});
};
// Does `pattern` fuzzy match `str`?
fuzzy.test = function (pattern, str) {
return fuzzy.match(pattern, str) !== null;
};
// If `pattern` matches `str`, wrap each matching character
// in `opts.pre` and `opts.post`. If no match, return null
fuzzy.match = function (pattern, str, opts) {
opts = opts || {};
let patternIdx = 0;
const result = [];
const len = str.length;
let totalScore = 0;
let currScore = 0;
// prefix
const pre = opts.pre || '';
// suffix
const post = opts.post || '';
// String to compare against. This might be a lowercase version of the
// raw string
const compareString = (opts.caseSensitive && str) || str.toLowerCase();
let ch;
pattern = (opts.caseSensitive && pattern) || pattern.toLowerCase();
// For each character in the string, either add it to the result
// or wrap in template if it's the next string in the pattern
for (let idx = 0; idx < len; idx++) {
ch = str[idx];
if (compareString[idx] === pattern[patternIdx]) {
ch = pre + ch + post;
patternIdx += 1;
// consecutive characters should increase the score more than linearly
currScore += 1 + currScore;
} else {
currScore = 0;
}
totalScore += currScore;
result[result.length] = ch;
}
// return rendered string if we have a match for every char
if (patternIdx === pattern.length) {
// if the string is an exact match with pattern, totalScore should be maxed
totalScore = compareString === pattern ? Infinity : totalScore;
return { rendered: result.join(''), score: totalScore };
}
return null;
};
// The normal entry point. Filters `arr` for matches against `pattern`.
// It returns an array with matching values of the type:
//
// [{
// string: '<b>lah' // The rendered string
// , index: 2 // The index of the element in `arr`
// , original: 'blah' // The original element in `arr`
// }]
//
// `opts` is an optional argument bag. Details:
//
// opts = {
// // string to put before a matching character
// pre: '<b>'
//
// // string to put after matching character
// , post: '</b>'
//
// // Optional function. Input is an entry in the given arr`,
// // output should be the string to test `pattern` against.
// // In this example, if `arr = [{crying: 'koala'}]` we would return
// // 'koala'.
// , extract: function(arg) { return arg.crying; }
// }
fuzzy.filter = function (pattern, arr, opts) {
if (!arr || arr.length === 0) {
return [];
}
if (typeof pattern !== 'string') {
return arr;
}
opts = opts || {};
return (
arr
.reduce(function (prev, element, idx, _arr) {
let str = element;
if (opts.extract) {
str = opts.extract(element);
}
const rendered = fuzzy.match(pattern, str, opts);
if (rendered != null) {
prev[prev.length] = {
string: rendered.rendered,
score: rendered.score,
index: idx,
original: element,
};
}
return prev;
}, [])
// Sort by score. Browsers are inconsistent wrt stable/unstable
// sorting, so force stable by using the index in the case of tie.
// See http://ofb.net/~sethml/is-sort-stable.html
.sort(function (a, b) {
const compare = b.score - a.score;
if (compare) return compare;
return a.index - b.index;
})
);
};
export default fuzzy;

View file

@ -0,0 +1,108 @@
interface MatchOptions {
pre?: string;
post?: string;
caseSensitive?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extract?: (arg: any) => string;
}
interface MatchResult {
rendered: string;
score: number;
}
interface FilterResult<T> {
string: string;
score: number;
index: number;
original: T;
}
const fuzzy = {
simpleFilter(pattern: string, array: string[]): string[] {
return array.filter((str) => this.test(pattern, str));
},
test(pattern: string, str: string): boolean {
return this.match(pattern, str) !== null;
},
match(
pattern: string,
str: string,
opts: MatchOptions = {}
): MatchResult | null {
let patternIdx = 0;
const result: string[] = [];
const len = str.length;
let totalScore = 0;
let currScore = 0;
const pre = opts.pre || '';
const post = opts.post || '';
const compareString = opts.caseSensitive ? str : str.toLowerCase();
pattern = opts.caseSensitive ? pattern : pattern.toLowerCase();
for (let idx = 0; idx < len; idx++) {
let ch = str[idx];
if (compareString[idx] === pattern[patternIdx]) {
ch = pre + ch + post;
patternIdx += 1;
currScore += 1 + currScore;
} else {
currScore = 0;
}
totalScore += currScore;
result[result.length] = ch;
}
if (patternIdx === pattern.length) {
totalScore = compareString === pattern ? Infinity : totalScore;
return { rendered: result.join(''), score: totalScore };
}
return null;
},
filter<T>(
pattern: string,
arr: T[],
opts: MatchOptions = {}
): FilterResult<T>[] {
if (!arr || arr.length === 0) {
return [];
}
if (typeof pattern !== 'string') {
return arr.map((element, index) => ({
string: element as unknown as string,
score: 0,
index,
original: element,
}));
}
return arr
.reduce<FilterResult<T>[]>((prev, element, idx) => {
let str = element as unknown as string;
if (opts.extract) {
str = opts.extract(element);
}
const rendered = this.match(pattern, str, opts);
if (rendered != null) {
prev.push({
string: rendered.rendered,
score: rendered.score,
index: idx,
original: element,
});
}
return prev;
}, [])
.sort((a, b) => {
const compare = b.score - a.score;
if (compare) return compare;
return a.index - b.index;
});
},
};
export default fuzzy;

View file

@ -1,86 +1,106 @@
<!-- @migration-task Error while migrating Svelte code: Can't migrate code with afterUpdate. Please migrate by hand. -->
<script>
/**
* @typedef {Object} TItem
* @property {number} index
* @property {string} embed
*/
<script lang="ts">
type TItem = {
index: number;
embed: string;
disabled?: boolean;
};
export let id = 'typeahead-' + Math.random().toString(36);
export let value = '';
type SelectedItem = {
selectedIndex: number;
searched: string;
selected: string;
original: TItem;
originalIndex: number;
};
/** @type {TItem[]} */
export let data = [];
/** @type {(item: TItem) => any} */
export let extract = (item) => item;
/** @type {(item: TItem) => boolean} */
export let disable = (_item) => false;
/** @type {(item: TItem) => boolean} */
export let filter = (_item) => false;
/** Set to `false` to prevent the first result from being selected */
export let autoselect = true;
/**
* Set to `keep` to keep the search field unchanged after select, set to `clear` to auto-clear search field
* @type {"update" | "clear" | "keep"}
*/
export let inputAfterSelect = 'update';
/** @type {{ original: TItem; index: number; score: number; string: string; disabled?: boolean; }[]} */
export let results = [];
/** Set to `true` to re-focus the input after selecting a result */
export let focusAfterSelect = false;
/** Set to `true` to only show results when the input is focused */
export let showDropdownOnFocus = false;
/**
* Specify the maximum number of results to return
* @type {number}
*/
export let limit = Infinity;
import fuzzy from './fuzzy.js';
import Search from 'svelte-search';
import { tick, createEventDispatcher, afterUpdate } from 'svelte';
const dispatch = createEventDispatcher();
let comboboxRef = null;
let searchRef = null;
let hideDropdown = false;
let selectedIndex = -1;
let prevResults = '';
let isFocused = false;
$: options = { pre: '<mark>', post: '</mark>', extract };
$: results =
value !== '' ?
fuzzy
.filter(value, data, options)
.filter(({ score }) => score > 0)
.slice(0, limit)
.filter((result) => !filter(result.original))
.map((result) => ({ ...result, disabled: disable(result.original) }))
: data.map((d) => ({ string: extract(d), original: d }));
$: resultsId = results.map((result) => extract(result.original)).join('');
$: showResults = !hideDropdown && results.length > 0 && isFocused;
$: if (showDropdownOnFocus) {
showResults = showResults && isFocused;
interface Props {
id?: string;
value: string;
label: string;
data: TItem[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extract?: (item: TItem) => any;
disable?: (item: TItem) => boolean;
filter?: (item: TItem) => boolean;
onselect: (item: SelectedItem) => void;
autoselect?: boolean;
inputAfterSelect?: 'update' | 'clear' | 'keep';
focusAfterSelect?: boolean;
showDropdownOnFocus?: boolean;
limit?: number;
noResults?: Snippet;
}
afterUpdate(() => {
let {
id = 'typeahead-' + Math.random().toString(36),
value = '',
label = '',
data = [],
extract = (item) => item,
disable = (_item) => false,
filter = (_item) => false,
autoselect = true,
// Set to `keep` to keep the search field unchanged after select, set to `clear` to auto-clear search field
inputAfterSelect = 'update',
/** Set to `true` to re-focus the input after selecting a result */
focusAfterSelect = false,
/** Set to `true` to only show results when the input is focused */
showDropdownOnFocus = false,
/** Specify the maximum number of results to return */
limit = Infinity,
noResults,
onselect,
...restProps
}: Props = $props();
import fuzzy from './fuzzy';
import Search from './Search.svelte';
import { tick, type Snippet } from 'svelte';
let comboboxRef: HTMLElement | null = $state(null);
let searchRef: HTMLElement | null = $state(null);
let hideDropdown = $state(true);
let selectedIndex = $state(-1);
let prevResults = $state('');
let isFocused = $state(false);
let options = $derived({ pre: '<mark>', post: '</mark>', extract });
let results = $derived.by(() => {
return value !== '' ?
fuzzy
.filter(value, data, options)
.filter(({ score }) => score > 0)
.slice(0, limit)
.filter((result) => !filter(result.original))
.map((result) => ({ ...result, disabled: disable(result.original) }))
: data.map((d, index) => ({
index,
string: extract(d),
original: d,
disabled: disable(d),
}));
});
let resultsId = $derived(
results.map((result) => extract(result.original)).join('')
);
let showResults: boolean = $state(
// svelte-ignore state_referenced_locally
!hideDropdown && results.length > 0 && isFocused
);
$effect(() => {
if (showDropdownOnFocus) {
showResults = showResults && isFocused;
}
});
$effect(() => {
if (prevResults !== resultsId && autoselect) {
selectedIndex = getNextNonDisabledIndex();
}
if (prevResults !== resultsId && !$$slots['no-results']) {
if (prevResults !== resultsId && !noResults) {
hideDropdown = results.length === 0;
}
@ -90,7 +110,7 @@
async function select() {
const result = results[selectedIndex];
if (result.disabled) return;
if (result.original.disabled) return;
const selectedValue = extract(result.original);
const searchedValue = value;
@ -98,7 +118,7 @@
if (inputAfterSelect === 'clear') value = '';
if (inputAfterSelect === 'update') value = selectedValue;
dispatch('select', {
onselect({
selectedIndex,
searched: searchedValue,
selected: selectedValue,
@ -108,11 +128,10 @@
await tick();
if (focusAfterSelect) searchRef.focus();
if (focusAfterSelect) searchRef?.focus();
close();
}
/** @type {() => number} */
function getNextNonDisabledIndex() {
let index = 0;
let disabled = results[index]?.disabled ?? false;
@ -130,8 +149,7 @@
return index;
}
/** @type {(direction: -1 | 1) => void} */
function change(direction) {
function change(direction: -1 | 1) {
let index =
direction === 1 && selectedIndex === results.length - 1 ?
0
@ -158,8 +176,9 @@
</script>
<svelte:window
on:click={({ target }) => {
if (!hideDropdown && !comboboxRef?.contains(target)) {
onclick={({ target }) => {
console.log('HELLO', !comboboxRef?.contains(target as Node));
if (!hideDropdown && !comboboxRef?.contains(target as Node)) {
close();
}
}}
@ -178,10 +197,11 @@
id="{id}-typeahead"
>
<Search
bind:value
{id}
{label}
removeFormAriaAttributes={true}
{...$$restProps}
bind:ref={searchRef}
bind:ref={searchRef!}
aria-autocomplete="list"
aria-controls="{id}-listbox"
aria-labelledby="{id}-label"
@ -190,23 +210,15 @@
) ?
`${id}-result-${selectedIndex}`
: null}
bind:value
on:type
on:input
on:change
on:focus
on:focus={() => {
onfocus={() => {
open();
if (showDropdownOnFocus) {
showResults = true;
isFocused = true;
}
}}
on:clear
on:clear={open}
on:blur
on:keydown
on:keydown={(e) => {
onclear={open}
onkeydown={(e: KeyboardEvent) => {
if (results.length === 0) return;
switch (e.key) {
@ -229,6 +241,7 @@
break;
}
}}
{...restProps}
/>
<ul
class:svelte-typeahead-list={true}
@ -236,7 +249,7 @@
aria-labelledby="{id}-label"
id="{id}-listbox"
>
{#if showResults}
{#if showResults && !hideDropdown}
{#each results as result, index}
<li
role="option"
@ -244,25 +257,23 @@
class:selected={selectedIndex === index}
class:disabled={result.disabled}
aria-selected={selectedIndex === index}
on:click={() => {
onclick={() => {
if (result.disabled) return;
selectedIndex = index;
select();
}}
on:keyup={(e) => {
onkeyup={(e) => {
if (e.key !== 'Enter') return;
if (result.disabled) return;
selectedIndex = index;
select();
}}
on:mouseenter={() => {
onmouseenter={() => {
if (result.disabled) return;
selectedIndex = index;
}}
>
<slot {result} {index} {value}>
{@html result.string}
</slot>
{@html result.string}
</li>
{/each}
{/if}

View file

@ -1,4 +1,4 @@
export default (embeds) => {
export default (embeds: string[]) => {
const nakedEmbeds = embeds
.map((e) => e.replace(/\?.+$/, ''))
.map((e) => e.replace(/index\.html$/, ''))