Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,216 changes: 363 additions & 1,853 deletions functions/vendors/templates.js

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/livecodes/UI/command-menu-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ export const getCommandMenuActions = ({
'reason',
'ocaml',
'python',
'pyodide',
'python-wasm',
'r',
'ruby',
Expand All @@ -302,7 +301,6 @@ export const getCommandMenuActions = ({
'php',
'php-wasm',
'cpp',
'clang',
'cpp-wasm',
'java',
'csharp-wasm',
Expand Down
2 changes: 1 addition & 1 deletion src/livecodes/UI/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,6 @@ export const createImportUI = ({
showScreen('open');
});

modal.show(importContainer, { isAsync: true, autoFocus: false });
modal.show(importContainer, { isAsync: true, autoFocus: false, size: 'large-fixed' });
getUrlImportInput(importContainer).focus();
};
3 changes: 2 additions & 1 deletion src/livecodes/UI/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const createOpenItem = (
isTemplate = false,
) => {
const li = document.createElement('li');
li.dataset.id = item.id;
list.appendChild(li);

const link = document.createElement('a');
Expand Down Expand Up @@ -391,7 +392,7 @@ const organizeProjects = (

eventsManager.addEventListener(
searchProjectsInput,
'keyup',
'input',
async () => {
const result = await index.searchAsync(searchProjectsInput.value);
searchResults = result.map((field: any) => field.result).flat();
Expand Down
3 changes: 3 additions & 0 deletions src/livecodes/UI/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ export const getStarterTemplatesList = /* @__PURE__ */ (templatesContainer: HTML
export const getUserTemplatesScreen = /* @__PURE__ */ (templatesContainer: HTMLElement) =>
templatesContainer.querySelector('#templates-user .modal-screen') as HTMLElement;

export const getTemplatesSearchInput = /* @__PURE__ */ (templatesContainer: HTMLElement) =>
templatesContainer.querySelector('#templates-search-input') as HTMLInputElement;

export const getBulkImportButton = /* @__PURE__ */ (listContainer: HTMLElement) =>
listContainer.querySelector('#bulk-import-button') as HTMLElement;

Expand Down
84 changes: 76 additions & 8 deletions src/livecodes/UI/templates.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { templatesScreen } from '../html';
import type { EventsManager, Template } from '../models';
import { debounce, loadScript } from '../utils/utils';
import { flexSearchUrl } from '../vendors';
import { getTemplatesSearchInput } from './selectors';

export const createTemplatesContainer = (
eventsManager: EventsManager,
loadUserTemplates: () => void,
) => {
let searchIndex: Promise<any> | undefined;

export const createTemplatesContainer = (eventsManager: EventsManager) => {
const div = document.createElement('div');
div.innerHTML = templatesScreen;
const templatesContainer = div.firstChild as HTMLElement;
Expand All @@ -22,20 +24,19 @@ export const createTemplatesContainer = (
});
const target = templatesContainer.querySelector('#' + link.dataset.target);
target?.classList.add('active');
if (link.dataset.target === 'templates-user') {
loadUserTemplates();
}
});
});
setupTemplatesSearch(templatesContainer);
return templatesContainer;
};

export const createStarterTemplateLink = (
template: Template,
template: Template & { id: string },
starterTemplatesList: HTMLElement | null,
baseUrl: string,
) => {
const li = document.createElement('li');
li.dataset.id = template.id;
const link = document.createElement('a');
link.href = '?template=' + template.name;
link.innerHTML = `
Expand All @@ -61,3 +62,70 @@ export const noUserTemplates = () => `
</div>
</div>
`;

export const initTemplatesSearchIndex = () => {
searchIndex = loadScript(flexSearchUrl, 'FlexSearch').then(
async (FlexSearch: any) =>
new FlexSearch.Document({
index: ['name', 'title', 'description', 'aliases', 'tags', 'languages'],
tokenize: 'full',
worker: true,
}),
);
};

export const addTemplateToIndex = ({
id,
title,
name = '',
description = '',
aliases = [],
tags = [],
languages = [],
}: {
id: string;
title: string;
name?: string;
description?: string;
aliases?: string[];
tags?: string[];
languages?: string[];
}) => {
searchIndex?.then((index) => {
index.add({ id, name, title, description, aliases, tags, languages });
});
};

export const setupTemplatesSearch = (container: HTMLElement) => {
const input = getTemplatesSearchInput(container);
if (!input) return;

const filterTemplates = (query: string) => {
searchIndex?.then(async (index) => {
const mainItems = container.querySelectorAll(
'#templates-starter li',
) as NodeListOf<HTMLElement>;
const userItems = container.querySelectorAll('#templates-user li') as NodeListOf<HTMLElement>;
const items = Array.from(mainItems).concat(Array.from(userItems));

const result =
query === ''
? null
: (await index.searchAsync(query)).map((field: any) => field.result).flat();

items.forEach((item) => {
(item as HTMLElement).style.display =
query === '' || result.includes(item.dataset.id as string) ? '' : 'none';
});
});
};

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

input.addEventListener('input', (e: Event) => {
const val = (e.target as HTMLInputElement).value || '';
debouncedFilter(val);
});
Comment on lines +103 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Reapply filter when templates are loaded dynamically.

Filtering only runs on input events; if a user types a query and then loadUserTemplates() inserts items later, those new items won’t be filtered until the user types again. Consider reapplying the current query after templates load (e.g., call filterTemplates(input.value.trim()) or dispatch an input event) so the results stay consistent.

πŸ€– Prompt for AI Agents
In `@src/livecodes/UI/templates.ts` around lines 71 - 89, Filtering only runs on
user input so dynamically added templates from loadUserTemplates() bypass the
current query; after new templates are inserted call
filterTemplates(input.value.trim()) (or dispatch an 'input' event on input) to
reapply the current query, e.g., at the end of loadUserTemplates() or its
promise callback so filterTemplates, debouncedFilter and input stay in sync with
newly added items.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
74 changes: 43 additions & 31 deletions src/livecodes/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPlaygroundUrl } from '../sdk';
import {
addTemplateToIndex,
createLoginContainer,
createOpenItem,
createProjectInfoUI,
Expand All @@ -10,6 +11,7 @@ import {
displayLoggedOut,
getFullscreenButton,
getResultElement,
initTemplatesSearchIndex,
loadingMessage,
noUserTemplates,
} from './UI';
Expand Down Expand Up @@ -3147,10 +3149,10 @@ const handleLogout = () => {
};

const handleNew = () => {
const templatesContainer = createTemplatesContainer(eventsManager, () => loadUserTemplates());
const userTemplatesScreen = UI.getUserTemplatesScreen(templatesContainer);
const templatesContainer = createTemplatesContainer(eventsManager);

const loadUserTemplates = async () => {
const userTemplatesScreen = UI.getUserTemplatesScreen(templatesContainer);
const defaultTemplate = getAppData()?.defaultTemplate;
const userTemplates = ((await stores.templates?.getList()) || []).sort((a, b) =>
a.id === defaultTemplate ? -1 : b.id === defaultTemplate ? 1 : 0,
Expand All @@ -3174,6 +3176,7 @@ const handleNew = () => {
getLanguageByAlias,
true,
);
addTemplateToIndex(item);

if (defaultTemplate === item.id) {
link.parentElement?.classList.add('selected');
Expand Down Expand Up @@ -3255,41 +3258,50 @@ const handleNew = () => {
});
};

let starterTemplatesCache: Template[];
const createTemplatesUI = async () => {
initTemplatesSearchIndex();
const starterTemplatesList = UI.getStarterTemplatesList(templatesContainer);
if (!starterTemplatesList) return;
starterTemplatesList.innerHTML = '';
const searchInput = UI.getTemplatesSearchInput(templatesContainer);
if (searchInput) {
searchInput.value = '';
}
const loadingText = starterTemplatesList?.firstElementChild;
if (!starterTemplatesCache) {
getTemplates()
.then((starterTemplates) => {
starterTemplatesCache = starterTemplates;
loadingText?.remove();
starterTemplates.forEach((template) => {
const link = createStarterTemplateLink(template, starterTemplatesList, baseUrl);
eventsManager.addEventListener(
link,
'click',
(event) => {
event.preventDefault();
loadStarterTemplate(template.name, /* checkSaved= */ false);
},
false,
);
});
})
.catch(() => {
loadingText?.remove();
notifications.error(
window.deps.translateString(
'core.error.failedToLoadTemplates',
'Failed loading starter templates',
),
getTemplates()
.then((starterTemplates) => {
loadingText?.remove();
starterTemplates.forEach((template, id) => {
const link = createStarterTemplateLink(
{ id: String(id), ...template },
starterTemplatesList,
baseUrl,
);
addTemplateToIndex({ id: String(id), ...template });
eventsManager.addEventListener(
link,
'click',
(event) => {
event.preventDefault();
loadStarterTemplate(template.name, /* checkSaved= */ false);
},
false,
);
});
}
})
.catch(() => {
loadingText?.remove();
notifications.error(
window.deps.translateString(
'core.error.failedToLoadTemplates',
'Failed loading starter templates',
),
);
});

setTimeout(() => UI.getStarterTemplatesTab(templatesContainer)?.click());
modal.show(templatesContainer, { isAsync: true });
loadUserTemplates();
requestAnimationFrame(() => UI.getStarterTemplatesTab(templatesContainer)?.click());
modal.show(templatesContainer, { isAsync: true, size: 'large-fixed' });
};

eventsManager.addEventListener(
Expand Down
13 changes: 13 additions & 0 deletions src/livecodes/html/templates.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
<a href="#" data-target="templates-user" data-i18n="templates.user.heading">My Templates</a>
</li>
</ul>
<div class="templates-search-container">
<label for="templates-search-input" data-i18n="templates.search.label"
>Search templates</label
>
<input
id="templates-search-input"
type="search"
placeholder="Search templates..."
data-i18n="templates.search.placeholder"
data-i18n-prop="placeholder"
aria-label="Search templates by language"
/>
</div>

<div id="templates-starter" class="tab-content active">
<div class="modal-screen">
Expand Down
24 changes: 24 additions & 0 deletions src/livecodes/i18n/locales/en/translation.lokalise.json
Original file line number Diff line number Diff line change
Expand Up @@ -2548,6 +2548,14 @@
"notes": "",
"translation": "You have no saved templates."
},
"templates.search.label": {
"notes": "",
"translation": "Search templates"
},
"templates.search.placeholder": {
"notes": "",
"translation": "Search templates..."
},
"templates.starter.angular": {
"notes": "",
"translation": "Angular Starter"
Expand Down Expand Up @@ -2604,6 +2612,14 @@
"notes": "",
"translation": "C++ (Wasm) Starter"
},
"templates.starter.csharp-wasm": {
"notes": "",
"translation": "C# (Wasm) Starter"
},
"templates.starter.d3": {
"notes": "",
"translation": "D3 Starter"
},
"templates.starter.daisyui": {
"notes": "",
"translation": "daisyUI Starter"
Expand Down Expand Up @@ -2708,6 +2724,10 @@
"notes": "",
"translation": "Perl Starter"
},
"templates.starter.phaser": {
"notes": "",
"translation": "Phaser Starter"
},
"templates.starter.php": {
"notes": "",
"translation": "PHP Starter"
Expand All @@ -2732,6 +2752,10 @@
"notes": "",
"translation": "Python Starter"
},
"templates.starter.python-wasm": {
"notes": "",
"translation": "Python (Wasm) Starter"
},
"templates.starter.r": {
"notes": "",
"translation": "R Starter"
Expand Down
8 changes: 8 additions & 0 deletions src/livecodes/i18n/locales/en/translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,10 @@ const translation = {
desc: 'You can save a project as a template from <1></1>(App&nbsp;menu&nbsp;&gt;&nbsp;Save&nbsp;as&nbsp;&gt; Template).',
heading: 'You have no saved templates.',
},
search: {
label: 'Search templates',
placeholder: 'Search templates...',
},
starter: {
angular: 'Angular Starter',
assemblyscript: 'AssemblyScript Starter',
Expand All @@ -997,6 +1001,8 @@ const translation = {
commonlisp: 'Common Lisp Starter',
cpp: 'C++ Starter',
'cpp-wasm': 'C++ (Wasm) Starter',
'csharp-wasm': 'C# (Wasm) Starter',
d3: 'D3 Starter',
daisyui: 'daisyUI Starter',
diagrams: 'Diagrams Starter',
fennel: 'Fennel Starter',
Expand All @@ -1023,12 +1029,14 @@ const translation = {
minizinc: 'MiniZinc Starter',
ocaml: 'Ocaml Starter',
perl: 'Perl Starter',
phaser: 'Phaser Starter',
php: 'PHP Starter',
'php-wasm': 'PHP (Wasm) Starter',
postgresql: 'PostgreSQL Starter',
preact: 'Preact Starter',
prolog: 'Prolog Starter',
python: 'Python Starter',
'python-wasm': 'Python (Wasm) Starter',
r: 'R Starter',
react: 'React Starter',
'react-native': 'React Native Starter',
Expand Down
2 changes: 1 addition & 1 deletion src/livecodes/models.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type * from '../sdk/models';

export interface ModalOptions {
size?: 'large' | 'small' | 'full';
size?: 'large' | 'small' | 'full' | 'large-fixed';
closeButton?: boolean;
isAsync?: boolean;
onClose?: () => void;
Expand Down
Loading