Skip to content

Commit 7e230d2

Browse files
authored
Merge pull request #901 from a0m0rajab/develop
feat(Templates): add search functionality for user templates
2 parents 91797a2 + 0b1d802 commit 7e230d2

40 files changed

Lines changed: 646 additions & 1913 deletions

functions/vendors/templates.js

Lines changed: 363 additions & 1853 deletions
Large diffs are not rendered by default.

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: 2 additions & 1 deletion
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');
@@ -391,7 +392,7 @@ const organizeProjects = (
391392

392393
eventsManager.addEventListener(
393394
searchProjectsInput,
394-
'keyup',
395+
'input',
395396
async () => {
396397
const result = await index.searchAsync(searchProjectsInput.value);
397398
searchResults = result.map((field: any) => field.result).flat();

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: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { templatesScreen } from '../html';
22
import type { EventsManager, Template } from '../models';
3+
import { debounce, loadScript } from '../utils/utils';
4+
import { flexSearchUrl } from '../vendors';
5+
import { getTemplatesSearchInput } from './selectors';
36

4-
export const createTemplatesContainer = (
5-
eventsManager: EventsManager,
6-
loadUserTemplates: () => void,
7-
) => {
7+
let searchIndex: Promise<any> | undefined;
8+
9+
export const createTemplatesContainer = (eventsManager: EventsManager) => {
810
const div = document.createElement('div');
911
div.innerHTML = templatesScreen;
1012
const templatesContainer = div.firstChild as HTMLElement;
@@ -22,20 +24,19 @@ export const createTemplatesContainer = (
2224
});
2325
const target = templatesContainer.querySelector('#' + link.dataset.target);
2426
target?.classList.add('active');
25-
if (link.dataset.target === 'templates-user') {
26-
loadUserTemplates();
27-
}
2827
});
2928
});
29+
setupTemplatesSearch(templatesContainer);
3030
return templatesContainer;
3131
};
3232

3333
export const createStarterTemplateLink = (
34-
template: Template,
34+
template: Template & { id: string },
3535
starterTemplatesList: HTMLElement | null,
3636
baseUrl: string,
3737
) => {
3838
const li = document.createElement('li');
39+
li.dataset.id = template.id;
3940
const link = document.createElement('a');
4041
link.href = '?template=' + template.name;
4142
link.innerHTML = `
@@ -61,3 +62,70 @@ export const noUserTemplates = () => `
6162
</div>
6263
</div>
6364
`;
65+
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+
99+
export const setupTemplatesSearch = (container: HTMLElement) => {
100+
const input = getTemplatesSearchInput(container);
101+
if (!input) return;
102+
103+
const filterTemplates = (query: string) => {
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+
});
120+
});
121+
};
122+
123+
const debouncedFilter = debounce((val: string) => {
124+
filterTemplates(val.trim());
125+
}, 150);
126+
127+
input.addEventListener('input', (e: Event) => {
128+
const val = (e.target as HTMLInputElement).value || '';
129+
debouncedFilter(val);
130+
});
131+
};

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@
1111
<a href="#" data-target="templates-user" data-i18n="templates.user.heading">My Templates</a>
1212
</li>
1313
</ul>
14+
<div class="templates-search-container">
15+
<label for="templates-search-input" data-i18n="templates.search.label"
16+
>Search templates</label
17+
>
18+
<input
19+
id="templates-search-input"
20+
type="search"
21+
placeholder="Search templates..."
22+
data-i18n="templates.search.placeholder"
23+
data-i18n-prop="placeholder"
24+
aria-label="Search templates by language"
25+
/>
26+
</div>
1427

1528
<div id="templates-starter" class="tab-content active">
1629
<div class="modal-screen">

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,6 +2548,14 @@
25482548
"notes": "",
25492549
"translation": "You have no saved templates."
25502550
},
2551+
"templates.search.label": {
2552+
"notes": "",
2553+
"translation": "Search templates"
2554+
},
2555+
"templates.search.placeholder": {
2556+
"notes": "",
2557+
"translation": "Search templates..."
2558+
},
25512559
"templates.starter.angular": {
25522560
"notes": "",
25532561
"translation": "Angular Starter"
@@ -2604,6 +2612,14 @@
26042612
"notes": "",
26052613
"translation": "C++ (Wasm) Starter"
26062614
},
2615+
"templates.starter.csharp-wasm": {
2616+
"notes": "",
2617+
"translation": "C# (Wasm) Starter"
2618+
},
2619+
"templates.starter.d3": {
2620+
"notes": "",
2621+
"translation": "D3 Starter"
2622+
},
26072623
"templates.starter.daisyui": {
26082624
"notes": "",
26092625
"translation": "daisyUI Starter"
@@ -2708,6 +2724,10 @@
27082724
"notes": "",
27092725
"translation": "Perl Starter"
27102726
},
2727+
"templates.starter.phaser": {
2728+
"notes": "",
2729+
"translation": "Phaser Starter"
2730+
},
27112731
"templates.starter.php": {
27122732
"notes": "",
27132733
"translation": "PHP Starter"
@@ -2732,6 +2752,10 @@
27322752
"notes": "",
27332753
"translation": "Python Starter"
27342754
},
2755+
"templates.starter.python-wasm": {
2756+
"notes": "",
2757+
"translation": "Python (Wasm) Starter"
2758+
},
27352759
"templates.starter.r": {
27362760
"notes": "",
27372761
"translation": "R Starter"

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,10 @@ const translation = {
982982
desc: 'You can save a project as a template from <1></1>(App&nbsp;menu&nbsp;&gt;&nbsp;Save&nbsp;as&nbsp;&gt; Template).',
983983
heading: 'You have no saved templates.',
984984
},
985+
search: {
986+
label: 'Search templates',
987+
placeholder: 'Search templates...',
988+
},
985989
starter: {
986990
angular: 'Angular Starter',
987991
assemblyscript: 'AssemblyScript Starter',
@@ -997,6 +1001,8 @@ const translation = {
9971001
commonlisp: 'Common Lisp Starter',
9981002
cpp: 'C++ Starter',
9991003
'cpp-wasm': 'C++ (Wasm) Starter',
1004+
'csharp-wasm': 'C# (Wasm) Starter',
1005+
d3: 'D3 Starter',
10001006
daisyui: 'daisyUI Starter',
10011007
diagrams: 'Diagrams Starter',
10021008
fennel: 'Fennel Starter',
@@ -1023,12 +1029,14 @@ const translation = {
10231029
minizinc: 'MiniZinc Starter',
10241030
ocaml: 'Ocaml Starter',
10251031
perl: 'Perl Starter',
1032+
phaser: 'Phaser Starter',
10261033
php: 'PHP Starter',
10271034
'php-wasm': 'PHP (Wasm) Starter',
10281035
postgresql: 'PostgreSQL Starter',
10291036
preact: 'Preact Starter',
10301037
prolog: 'Prolog Starter',
10311038
python: 'Python Starter',
1039+
'python-wasm': 'Python (Wasm) Starter',
10321040
r: 'R Starter',
10331041
react: 'React Starter',
10341042
'react-native': 'React Native Starter',

0 commit comments

Comments
 (0)