-
Notifications
You must be signed in to change notification settings - Fork 6
Example ToDo VirtualList File Save
David edited this page Oct 24, 2022
·
2 revisions
import { h, render } from "preact"
import { signal, computed } from "preact/signals";
import { File } from "System/IO";
import { Dom } from "OneJS/Dom";
import { useState, useEffect, useRef, useReducer } from "preact/hooks";
import { Style } from "preact/jsx";
import { ScrollView } from "UnityEngine/UIElements";
declare module "preact/jsx" {
namespace JSXInternal {
export interface IntrinsicElements {
textelement: TextElement
}
}
}
const STYLE_INNER: Style = {
position: "Relative", overflow: "Hidden", width: "100%", minHeight: "100%"
}
const STYLE_CONTENT: Style = {
position: "Absolute", left: 0, overflow: "Visible", width: "100%", height: "100%"
}
interface Props {
data: any[],
renderRow: Function,
rowHeight: number,
overscanCount: number,
style?: Style
}
export const VirtualList = (props: Props) => {
//const forceUpdate = useReducer(() => ({}), {})[1] as () => void
const scrollviewRef = useRef<Dom>()
const [height, setHeight] = useState(0)
const [start, setStart] = useState(0)
const resize = () => {
let sv = scrollviewRef.current.ve as ScrollView
let offetHeight = sv.resolvedStyle.height
if (height !== offetHeight) {
setHeight(offetHeight)
}
}
const handleScroll = (scrollTop: number) => {
let s = (scrollTop / props.rowHeight) | 0
s = Math.max(0, s - (s % props.overscanCount))
setStart(s);
}
useEffect(() => {
resize()
let sv = scrollviewRef.current.ve as ScrollView
sv.verticalScroller.add_valueChanged(handleScroll)
}, [])
let visibleRowCount = (height / props.rowHeight) | 0
if (props.overscanCount) {
visibleRowCount += props.overscanCount
}
let data = props.data;
let len = data.length;
let end = start + 1 + visibleRowCount;
end = Math.min(end, len);
const selection = [] as any
for (let i = start; i < end; i++) {
const d = data[i];
selection[i - start] = data[i]
}
return <scrollview ref={scrollviewRef} style={props.style}>
<div style={{ ...STYLE_INNER, height: len * props.rowHeight }}>
<div style={{ ...STYLE_CONTENT, top: start * props.rowHeight }}>
{selection.map(props.renderRow)}
</div>
</div>
</scrollview>
}
// default TODOFILE
let TODOFILE = __dirname + "/todo.json";
// Todo object
interface ITask {
id: number
task: string
due: Date
done?: Date
depends?: number[]
}
const currentTime = signal<Date>(new Date());
const currentTimeStr = computed(() =>
(currentTime.value.getMonth() + 1) + "/" + currentTime.value.getDate() + " " + currentTime.value.getHours() + ":" + currentTime.value.getMinutes() + ":" + currentTime.value.getSeconds() + " " + (currentTime.value.getHours() > 11 ? "PM" : "AM")
);
setInterval(() => {
currentTime.value = new Date();
}, 1000);
// To Do List (signal)
const todoList = signal<ITask[]>([{ id: 1, task: "Do the list", due: new Date() }]);
// current To Do
const todo = signal("");
// next available unique id
const nextId = signal(1);
// current count
const count = computed(() => todoList.value.length);
// number of completed
const completed = computed(() => {
return todoList.value.filter(todo => typeof todo.done != "undefined").length;
});
function onSave() {
try {
File.WriteAllText(TODOFILE, JSON.stringify(todoList.value, null, 2));
} catch (e) {
log("Error:" + e);
}
}
function onOpen() {
try {
const jsontxt = File.ReadAllText(TODOFILE);
if (jsontxt != null && jsontxt.length > 0) {
let list = JSON.parse(jsontxt);
todoList.value = [];
todoList.value = list;
} else {
log("Error: empty file");
}
} catch (e) {
log("Error:" + e);
}
}
function onAdd() {
let ids = todoList.value.map(t => t.id);
let maxId = ids.length == 0 ? 1 : Math.max(...ids) + 1;
todoList.value = [...todoList.value, { id: maxId, task: todo.value, due: new Date(), done: undefined }];
nextId.value = maxId;
todo.value = "";
}
function removeTodo(todo: ITask) {
if (todo != null) {
let val = todoList.value.filter(v => v.id != todo.id);
todoList.value = [...val];
}
}
interface ToDoProps {
row: ITask
}
const ToDo = (props: ToDoProps) => {
const todo = props.row;
const isDone = typeof todo.done != "undefined";
return (<div class="flex-row justify-between" style={{ width: "100%", height: "100%" }}>
<toggle
value={typeof todo.done != "undefined"}
onValueChanged={(e) => {
todo.done = e.newValue ? new Date() : undefined
todoList.value = [...todoList.value];
}}
/>
<div class="flex-none w-8 min-h-min py-2 m-1 bg-blue-600 text-white text-xs text-center">{todo.id}</div>
<textelement class="grow py-2" enableRichText={true} text={(isDone ? "<s>" : "") + todo.task} />
<button class="py-2" onClick={() => removeTodo(todo)} text="X" />
</div>)
}
const CLS_BTN = "inline-block w-16 min-h-min m-1 text-center py-2.5 bg-blue-600 text-white text-xs leading-tight rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg"
const ToDoList = (props: any) => {
const onInput = event => {
(todo.value = event.target.value);
}
return <div style={{ ...props.style }} class="bg-white block p-6 rounded-lg shadow-lg">
<h5 class="text-gray-900 text-xl leading-tight font-medium mb-2">ToDo List</h5>
<div class="text-gray-400 mb-4">{"Time: " + currentTimeStr.value}</div>
<div class="flex-row content-center"><button class={CLS_BTN} onClick={onSave} text="save" /><button class={CLS_BTN} onClick={onOpen} text="open" /></div>
<div class="flex-row content-center" style={{ alignItems: "Center" }}>
<textfield class="grow m-1 w-32" onInput={onInput} text={todo.value} />
<button onClick={onAdd} class={CLS_BTN} text="add" />
</div>
<div>{"Completed: " + completed.value + " of " + count.value}</div>
<VirtualList data={todoList.value}
renderRow={row => <ToDo row={row} />}
overscanCount={10}
rowHeight={30} />
</div>
}
render(<ToDoList style={{ width: "100%", height: "100%" }} />, document.body)