Skip to content

Commit a29009b

Browse files
authored
Merge pull request #1 from live-codes/templates-search
2 parents 692fbd8 + 95fc88c commit a29009b

40 files changed

Lines changed: 454 additions & 169 deletions

package-lock.json

Lines changed: 199 additions & 93 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/livecodes/UI/command-menu-actions.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ export const getCommandMenuActions = ({
292292
'reason',
293293
'ocaml',
294294
'python',
295-
'pyodide',
296295
'python-wasm',
297296
'r',
298297
'ruby',
@@ -302,7 +301,6 @@ export const getCommandMenuActions = ({
302301
'php',
303302
'php-wasm',
304303
'cpp',
305-
'clang',
306304
'cpp-wasm',
307305
'java',
308306
'csharp-wasm',

src/livecodes/UI/import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,6 @@ export const createImportUI = ({
271271
showScreen('open');
272272
});
273273

274-
modal.show(importContainer, { isAsync: true, autoFocus: false });
274+
modal.show(importContainer, { isAsync: true, autoFocus: false, size: 'large-fixed' });
275275
getUrlImportInput(importContainer).focus();
276276
};

src/livecodes/UI/open.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const createOpenItem = (
2323
isTemplate = false,
2424
) => {
2525
const li = document.createElement('li');
26+
li.dataset.id = item.id;
2627
list.appendChild(li);
2728

2829
const link = document.createElement('a');

src/livecodes/UI/selectors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,9 @@ export const getStarterTemplatesList = /* @__PURE__ */ (templatesContainer: HTML
490490
export const getUserTemplatesScreen = /* @__PURE__ */ (templatesContainer: HTMLElement) =>
491491
templatesContainer.querySelector('#templates-user .modal-screen') as HTMLElement;
492492

493+
export const getTemplatesSearchInput = /* @__PURE__ */ (templatesContainer: HTMLElement) =>
494+
templatesContainer.querySelector('#templates-search-input') as HTMLInputElement;
495+
493496
export const getBulkImportButton = /* @__PURE__ */ (listContainer: HTMLElement) =>
494497
listContainer.querySelector('#bulk-import-button') as HTMLElement;
495498

src/livecodes/UI/templates.ts

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { templatesScreen } from '../html';
22
import type { EventsManager, Template } from '../models';
3-
import { debounce } from '../utils/utils';
3+
import { debounce, loadScript } from '../utils/utils';
4+
import { flexSearchUrl } from '../vendors';
5+
import { getTemplatesSearchInput } from './selectors';
46

5-
export const createTemplatesContainer = (
6-
eventsManager: EventsManager,
7-
loadUserTemplates: () => void,
8-
) => {
7+
let searchIndex: Promise<any> | undefined;
8+
9+
export const createTemplatesContainer = (eventsManager: EventsManager) => {
910
const div = document.createElement('div');
1011
div.innerHTML = templatesScreen;
1112
const templatesContainer = div.firstChild as HTMLElement;
@@ -23,21 +24,19 @@ export const createTemplatesContainer = (
2324
});
2425
const target = templatesContainer.querySelector('#' + link.dataset.target);
2526
target?.classList.add('active');
26-
if (link.dataset.target === 'templates-user') {
27-
loadUserTemplates();
28-
}
2927
});
3028
});
3129
setupTemplatesSearch(templatesContainer);
3230
return templatesContainer;
3331
};
3432

3533
export const createStarterTemplateLink = (
36-
template: Template,
34+
template: Template & { id: string },
3735
starterTemplatesList: HTMLElement | null,
3836
baseUrl: string,
3937
) => {
4038
const li = document.createElement('li');
39+
li.dataset.id = template.id;
4140
const link = document.createElement('a');
4241
link.href = '?template=' + template.name;
4342
link.innerHTML = `
@@ -54,36 +53,78 @@ export const noUserTemplates = () => `
5453
<div class="description alert">${window.deps.translateString('templates.noUserTemplates.heading', 'You have no saved templates.')}</div>
5554
<div class="description help">
5655
${window.deps.translateString(
57-
'templates.noUserTemplates.desc',
58-
'You can save a project as a template from <wbr />(App&nbsp;menu&nbsp;&gt;&nbsp;Save&nbsp;as&nbsp;&gt; Template).',
59-
{
60-
isHTML: true,
61-
},
62-
)}
56+
'templates.noUserTemplates.desc',
57+
'You can save a project as a template from <wbr />(App&nbsp;menu&nbsp;&gt;&nbsp;Save&nbsp;as&nbsp;&gt; Template).',
58+
{
59+
isHTML: true,
60+
},
61+
)}
6362
</div>
6463
</div>
6564
`;
6665

66+
export const initTemplatesSearchIndex = () => {
67+
searchIndex = loadScript(flexSearchUrl, 'FlexSearch').then(
68+
async (FlexSearch: any) =>
69+
new FlexSearch.Document({
70+
index: ['name', 'title', 'description', 'aliases', 'tags', 'languages'],
71+
tokenize: 'full',
72+
worker: true,
73+
}),
74+
);
75+
};
76+
77+
export const addTemplateToIndex = ({
78+
id,
79+
title,
80+
name = '',
81+
description = '',
82+
aliases = [],
83+
tags = [],
84+
languages = [],
85+
}: {
86+
id: string;
87+
title: string;
88+
name?: string;
89+
description?: string;
90+
aliases?: string[];
91+
tags?: string[];
92+
languages?: string[];
93+
}) => {
94+
searchIndex?.then((index) => {
95+
index.add({ id, name, title, description, aliases, tags, languages });
96+
});
97+
};
98+
6799
export const setupTemplatesSearch = (container: HTMLElement) => {
68-
const input = container.querySelector('#templates-search-input');
100+
const input = getTemplatesSearchInput(container);
69101
if (!input) return;
70102

71103
const filterTemplates = (query: string) => {
72-
const mainItems = container.querySelectorAll('#templates-starter li');
73-
const userItems = container.querySelectorAll('#templates-user li');
74-
const items = Array.from(mainItems).concat(Array.from(userItems));
75-
items.forEach((item) => {
76-
const text = item.textContent?.toLowerCase() || '';
77-
const matches = text.includes(query.toLowerCase());
78-
(item as HTMLElement).style.display = matches ? '' : 'none';
104+
searchIndex?.then(async (index) => {
105+
const mainItems = container.querySelectorAll(
106+
'#templates-starter li',
107+
) as NodeListOf<HTMLElement>;
108+
const userItems = container.querySelectorAll('#templates-user li') as NodeListOf<HTMLElement>;
109+
const items = Array.from(mainItems).concat(Array.from(userItems));
110+
111+
const result =
112+
query === ''
113+
? null
114+
: (await index.searchAsync(query)).map((field: any) => field.result).flat();
115+
116+
items.forEach((item) => {
117+
(item as HTMLElement).style.display =
118+
query === '' || result.includes(item.dataset.id as string) ? '' : 'none';
119+
});
79120
});
80121
};
81122

82123
const debouncedFilter = debounce((val: string) => {
83124
filterTemplates(val.trim());
84125
}, 150);
85126

86-
input.addEventListener('input', (e: Event) => {
127+
input.addEventListener('keyup', (e: Event) => {
87128
const val = (e.target as HTMLInputElement).value || '';
88129
debouncedFilter(val);
89130
});

src/livecodes/core.ts

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getPlaygroundUrl } from '../sdk';
22
import {
3+
addTemplateToIndex,
34
createLoginContainer,
45
createOpenItem,
56
createProjectInfoUI,
@@ -10,6 +11,7 @@ import {
1011
displayLoggedOut,
1112
getFullscreenButton,
1213
getResultElement,
14+
initTemplatesSearchIndex,
1315
loadingMessage,
1416
noUserTemplates,
1517
} from './UI';
@@ -3147,10 +3149,10 @@ const handleLogout = () => {
31473149
};
31483150

31493151
const handleNew = () => {
3150-
const templatesContainer = createTemplatesContainer(eventsManager, () => loadUserTemplates());
3151-
const userTemplatesScreen = UI.getUserTemplatesScreen(templatesContainer);
3152+
const templatesContainer = createTemplatesContainer(eventsManager);
31523153

31533154
const loadUserTemplates = async () => {
3155+
const userTemplatesScreen = UI.getUserTemplatesScreen(templatesContainer);
31543156
const defaultTemplate = getAppData()?.defaultTemplate;
31553157
const userTemplates = ((await stores.templates?.getList()) || []).sort((a, b) =>
31563158
a.id === defaultTemplate ? -1 : b.id === defaultTemplate ? 1 : 0,
@@ -3174,6 +3176,7 @@ const handleNew = () => {
31743176
getLanguageByAlias,
31753177
true,
31763178
);
3179+
addTemplateToIndex(item);
31773180

31783181
if (defaultTemplate === item.id) {
31793182
link.parentElement?.classList.add('selected');
@@ -3255,41 +3258,50 @@ const handleNew = () => {
32553258
});
32563259
};
32573260

3258-
let starterTemplatesCache: Template[];
32593261
const createTemplatesUI = async () => {
3262+
initTemplatesSearchIndex();
32603263
const starterTemplatesList = UI.getStarterTemplatesList(templatesContainer);
3264+
if (!starterTemplatesList) return;
3265+
starterTemplatesList.innerHTML = '';
3266+
const searchInput = UI.getTemplatesSearchInput(templatesContainer);
3267+
if (searchInput) {
3268+
searchInput.value = '';
3269+
}
32613270
const loadingText = starterTemplatesList?.firstElementChild;
3262-
if (!starterTemplatesCache) {
3263-
getTemplates()
3264-
.then((starterTemplates) => {
3265-
starterTemplatesCache = starterTemplates;
3266-
loadingText?.remove();
3267-
starterTemplates.forEach((template) => {
3268-
const link = createStarterTemplateLink(template, starterTemplatesList, baseUrl);
3269-
eventsManager.addEventListener(
3270-
link,
3271-
'click',
3272-
(event) => {
3273-
event.preventDefault();
3274-
loadStarterTemplate(template.name, /* checkSaved= */ false);
3275-
},
3276-
false,
3277-
);
3278-
});
3279-
})
3280-
.catch(() => {
3281-
loadingText?.remove();
3282-
notifications.error(
3283-
window.deps.translateString(
3284-
'core.error.failedToLoadTemplates',
3285-
'Failed loading starter templates',
3286-
),
3271+
getTemplates()
3272+
.then((starterTemplates) => {
3273+
loadingText?.remove();
3274+
starterTemplates.forEach((template, id) => {
3275+
const link = createStarterTemplateLink(
3276+
{ id: String(id), ...template },
3277+
starterTemplatesList,
3278+
baseUrl,
3279+
);
3280+
addTemplateToIndex({ id: String(id), ...template });
3281+
eventsManager.addEventListener(
3282+
link,
3283+
'click',
3284+
(event) => {
3285+
event.preventDefault();
3286+
loadStarterTemplate(template.name, /* checkSaved= */ false);
3287+
},
3288+
false,
32873289
);
32883290
});
3289-
}
3291+
})
3292+
.catch(() => {
3293+
loadingText?.remove();
3294+
notifications.error(
3295+
window.deps.translateString(
3296+
'core.error.failedToLoadTemplates',
3297+
'Failed loading starter templates',
3298+
),
3299+
);
3300+
});
32903301

3291-
setTimeout(() => UI.getStarterTemplatesTab(templatesContainer)?.click());
3292-
modal.show(templatesContainer, { isAsync: true });
3302+
loadUserTemplates();
3303+
requestAnimationFrame(() => UI.getStarterTemplatesTab(templatesContainer)?.click());
3304+
modal.show(templatesContainer, { isAsync: true, size: 'large-fixed' });
32933305
};
32943306

32953307
eventsManager.addEventListener(

src/livecodes/html/templates.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
</li>
1313
</ul>
1414
<div class="templates-search-container">
15-
<label for="templates-search-input" class="visually-hidden" data-i18n="templates.search.label"
15+
<label for="templates-search-input" data-i18n="templates.search.label"
1616
>Search templates</label
1717
>
1818
<input
1919
id="templates-search-input"
2020
type="search"
2121
placeholder="Search templates..."
22+
data-i18n="templates.search.placeholder"
23+
data-i18n-prop="placeholder"
2224
aria-label="Search templates by language"
2325
/>
2426
</div>

src/livecodes/i18n/locales/en/translation.lokalise.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,6 +2552,10 @@
25522552
"notes": "",
25532553
"translation": "Search templates"
25542554
},
2555+
"templates.search.placeholder": {
2556+
"notes": "",
2557+
"translation": "Search templates..."
2558+
},
25552559
"templates.starter.angular": {
25562560
"notes": "",
25572561
"translation": "Angular Starter"
@@ -2608,6 +2612,14 @@
26082612
"notes": "",
26092613
"translation": "C++ (Wasm) Starter"
26102614
},
2615+
"templates.starter.csharp-wasm": {
2616+
"notes": "",
2617+
"translation": "C# (Wasm) Starter"
2618+
},
2619+
"templates.starter.d3": {
2620+
"notes": "",
2621+
"translation": "D3 Starter"
2622+
},
26112623
"templates.starter.daisyui": {
26122624
"notes": "",
26132625
"translation": "daisyUI Starter"
@@ -2712,6 +2724,10 @@
27122724
"notes": "",
27132725
"translation": "Perl Starter"
27142726
},
2727+
"templates.starter.phaser": {
2728+
"notes": "",
2729+
"translation": "Phaser Starter"
2730+
},
27152731
"templates.starter.php": {
27162732
"notes": "",
27172733
"translation": "PHP Starter"
@@ -2736,6 +2752,10 @@
27362752
"notes": "",
27372753
"translation": "Python Starter"
27382754
},
2755+
"templates.starter.python-wasm": {
2756+
"notes": "",
2757+
"translation": "Python (Wasm) Starter"
2758+
},
27392759
"templates.starter.r": {
27402760
"notes": "",
27412761
"translation": "R Starter"

src/livecodes/i18n/locales/en/translation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,7 @@ const translation = {
984984
},
985985
search: {
986986
label: 'Search templates',
987+
placeholder: 'Search templates...',
987988
},
988989
starter: {
989990
angular: 'Angular Starter',
@@ -1000,6 +1001,8 @@ const translation = {
10001001
commonlisp: 'Common Lisp Starter',
10011002
cpp: 'C++ Starter',
10021003
'cpp-wasm': 'C++ (Wasm) Starter',
1004+
'csharp-wasm': 'C# (Wasm) Starter',
1005+
d3: 'D3 Starter',
10031006
daisyui: 'daisyUI Starter',
10041007
diagrams: 'Diagrams Starter',
10051008
fennel: 'Fennel Starter',
@@ -1026,12 +1029,14 @@ const translation = {
10261029
minizinc: 'MiniZinc Starter',
10271030
ocaml: 'Ocaml Starter',
10281031
perl: 'Perl Starter',
1032+
phaser: 'Phaser Starter',
10291033
php: 'PHP Starter',
10301034
'php-wasm': 'PHP (Wasm) Starter',
10311035
postgresql: 'PostgreSQL Starter',
10321036
preact: 'Preact Starter',
10331037
prolog: 'Prolog Starter',
10341038
python: 'Python Starter',
1039+
'python-wasm': 'Python (Wasm) Starter',
10351040
r: 'R Starter',
10361041
react: 'React Starter',
10371042
'react-native': 'React Native Starter',

0 commit comments

Comments
 (0)