Skip to content

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)