forked from cory/tildefriends
		
	+todo
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4058 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		
							
								
								
									
										1
									
								
								apps/cory/todo.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/cory/todo.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"type":"tildefriends-app","files":{"app.js":"&QUR1tKa15B5Or8AfPX/8Zs87teSeX0Mh/HF7PEPBom0=.sha256","index.html":"&QXhwvxhHc9fa8iL6088hGDu9FgWdY7wkXgvU2BMNv0A=.sha256","lit-core.min.js":"&tP9KhbgwF1chFqPtkNZ12Yx9AfkpnSjFiPcX5Pw5J9g=.sha256","script.js":"&9RTYD4Le7b6WvqJ8M2TRhZEM0oo6gRVbquZfbvtVIBQ=.sha256"}} | ||||
							
								
								
									
										82
									
								
								apps/cory/todo/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								apps/cory/todo/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| let g_db; | ||||
|  | ||||
| tfrpc.register(async function todo_get_all() { | ||||
| 	let names = await todo_get_names(); | ||||
| 	let result = []; | ||||
| 	for (let name of names) { | ||||
| 		result.push({ | ||||
| 			name: name, | ||||
| 			items: await todo_get(name), | ||||
| 		}); | ||||
| 	} | ||||
| 	return result; | ||||
| }); | ||||
|  | ||||
| async function todo_get_names() { | ||||
| 	return JSON.parse((await g_db.get('files')) ?? '[]'); | ||||
| } | ||||
|  | ||||
| async function todo_add(list) { | ||||
| 	let exchanged = false; | ||||
| 	let tries = 10; | ||||
| 	while (!exchanged && tries-- > 0) { | ||||
| 		let original = await g_db.get('files'); | ||||
| 		let names = JSON.parse(original ?? '[]'); | ||||
| 		let set = new Set(names); | ||||
| 		set.add(list); | ||||
| 		names = JSON.stringify([...set].sort()); | ||||
| 		exchanged = original === names || await g_db.exchange('files', original, names); | ||||
| 	} | ||||
| 	return exchanged; | ||||
| } | ||||
| tfrpc.register(todo_add); | ||||
|  | ||||
| async function todo_remove(list) { | ||||
| 	let exchanged = false; | ||||
| 	let tries = 10; | ||||
| 	while (!exchanged && tries-- > 0) { | ||||
| 		let original = await g_db.get('files'); | ||||
| 		let names = JSON.parse(original ?? '[]'); | ||||
| 		let set = new Set(names); | ||||
| 		set.delete(list); | ||||
| 		names = JSON.stringify([...set].sort()); | ||||
| 		exchanged = original === names || await g_db.exchange('files', original, names); | ||||
| 	} | ||||
| 	await g_db.remove('list:' + list); | ||||
| 	return exchanged; | ||||
| } | ||||
| tfrpc.register(todo_remove); | ||||
|  | ||||
| tfrpc.register(async function todo_rename(old_name, new_name) { | ||||
| 	if (await g_db.get('list:' + new_name)) { | ||||
| 		throw RuntimeError(`${new_name} already exists.`); | ||||
| 	} | ||||
| 	let list = await todo_get(old_name); | ||||
| 	await todo_set(new_name, list); | ||||
| 	await todo_add(new_name); | ||||
| 	await todo_remove(old_name); | ||||
| }); | ||||
|  | ||||
| async function todo_get(list) { | ||||
| 	try { | ||||
| 		let value = await g_db.get('list:' + list); | ||||
| 		return JSON.parse(value ?? '[]'); | ||||
| 	} catch (error) { | ||||
| 		print(error); | ||||
| 		return []; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function todo_set(list, value) { | ||||
| 	await g_db.set('list:' + list, JSON.stringify(value)); | ||||
| } | ||||
| tfrpc.register(todo_set); | ||||
|  | ||||
| async function main() { | ||||
| 	g_db = await database('todo'); | ||||
| 	await app.setDocument(utf8Decode(getFile('index.html'))); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										11
									
								
								apps/cory/todo/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/cory/todo/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| 	<head> | ||||
| 		<title>TODO</title> | ||||
| 	</head> | ||||
| 	<body style="color: #fff"> | ||||
| 		<h1>TODO</h1> | ||||
| 		<tf-todos></tf-todos> | ||||
| 	</body> | ||||
| 	<script src="script.js" type="module"></script> | ||||
| </html> | ||||
							
								
								
									
										29
									
								
								apps/cory/todo/lit-core.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								apps/cory/todo/lit-core.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										182
									
								
								apps/cory/todo/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								apps/cory/todo/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import {LitElement, html} from './lit-core.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TodosElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			lists: {type: Array} | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.lists = []; | ||||
| 		let self = this; | ||||
| 		tfrpc.rpc.todo_get_all().then(function(lists) { | ||||
| 			self.lists = lists; | ||||
| 		}).catch(function(error) { | ||||
| 			console.log(error); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async new_list() { | ||||
| 		await tfrpc.rpc.todo_add('new list'); | ||||
| 		await this.refresh(); | ||||
| 	} | ||||
|  | ||||
| 	async refresh() { | ||||
| 		this.lists = await tfrpc.rpc.todo_get_all(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<div> | ||||
| 				<div style="display: flex"> | ||||
| 					${this.lists.map(x => html` | ||||
| 						<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list> | ||||
| 					`)} | ||||
| 				</div> | ||||
| 				<input type="button" @click=${this.new_list} value="+ List"></input> | ||||
| 			</div>`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| class TodoListElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			name: {type: String}, | ||||
| 			items: {type: Array}, | ||||
| 			editing: {type: Number}, | ||||
| 			editing_name: {type: Boolean}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.items = []; | ||||
| 	} | ||||
|  | ||||
| 	save() { | ||||
| 		tfrpc.rpc.todo_set(this.name, this.items).catch(function(error) { | ||||
| 			console.log(error); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	remove_item(item) { | ||||
| 		let index = this.items.indexOf(item); | ||||
| 		this.items = [].concat(this.items.slice(0, index), this.items.slice(index + 1)); | ||||
| 		this.save(); | ||||
| 	} | ||||
|  | ||||
| 	handle_check(event, item) { | ||||
| 		item.x = event.srcElement.checked; | ||||
| 		this.save(); | ||||
| 	} | ||||
|  | ||||
| 	input_blur(item) { | ||||
| 		this.save(); | ||||
| 		this.editing = undefined; | ||||
| 	} | ||||
|  | ||||
| 	input_change(event, item) { | ||||
| 		item.text = event.srcElement.value; | ||||
| 	} | ||||
|  | ||||
| 	input_keydown(event, item) { | ||||
| 		if (event.key === 'Enter' || event.key === 'Escape') { | ||||
| 			item.text = event.srcElement.value; | ||||
| 			this.editing = undefined; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	updated() { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		if (edit) { | ||||
| 			edit.select(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render_item(item) { | ||||
| 		let index = this.items.indexOf(item); | ||||
| 		let self = this; | ||||
| 		if (index === this.editing) { | ||||
| 			return html` | ||||
| 				<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input> | ||||
| 				<input | ||||
| 					id="edit" | ||||
| 					type="text" | ||||
| 					value=${item.text} | ||||
| 					@change=${event => self.input_change(event, item)} | ||||
| 					@keydown=${event => self.input_keydown(event, item)} | ||||
| 					@blur=${x => self.input_blur(item)}></input> | ||||
| 				<span @click=${x => self.remove_item(item)}>x</span></div> | ||||
| 			`; | ||||
| 		} else { | ||||
| 			return html` | ||||
| 				<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input> | ||||
| 				<span @click=${x => self.editing = index}>${item.text}</span> | ||||
| 				<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	add_item() { | ||||
| 		this.items = [].concat(this.items || [], [{text: 'new item'}]); | ||||
| 		this.editing = this.items.length - 1; | ||||
| 		this.save(); | ||||
| 	} | ||||
|  | ||||
| 	async remove_list() { | ||||
| 		if (confirm(`Are you sure you want to remove "${this.name}"?`)) { | ||||
| 			await tfrpc.rpc.todo_remove(this.name); | ||||
| 			this.dispatchEvent(new Event('change')); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	rename(new_name) { | ||||
| 		let self = this; | ||||
| 		return tfrpc.rpc.todo_rename(this.name, new_name).then(function() { | ||||
| 			self.dispatchEvent(new Event('change')); | ||||
| 			self.editing_name = false; | ||||
| 		}).catch(function(error) { | ||||
| 			console.log(error); | ||||
| 			alert(error.message); | ||||
| 			self.editing_name = false; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	name_blur(new_name) { | ||||
| 		this.rename(new_name); | ||||
| 	} | ||||
|  | ||||
| 	name_keydown(event, item) { | ||||
| 		let self = this; | ||||
| 		if (event.key == 'Enter' || event.key === 'Escape') { | ||||
| 			let new_name = event.srcElement.value; | ||||
| 			this.rename(new_name); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		let name = this.editing_name ? | ||||
| 			html`<input | ||||
| 				type="text" | ||||
| 				id="edit" | ||||
| 				@keydown=${event => self.name_keydown(event)} | ||||
| 				@blur=${event => self.name_blur(event.srcElement.value)} | ||||
| 				value=${this.name}></input>` : | ||||
| 			html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`; | ||||
| 		return html` | ||||
| 			<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444"> | ||||
| 				${name} | ||||
| 				${(this.items || []).map(x => self.render_item(x))} | ||||
| 				<button @click=${self.add_item}>+ Item</button> | ||||
| 				<button @click=${self.remove_list}>- List</button> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-todo-list', TodoListElement); | ||||
| customElements.define('tf-todos', TodosElement); | ||||
		Reference in New Issue
	
	Block a user