Packages

An easy to use frontend framework written in Gleam.

Current section

Files

Jump to
novdom src document_ffi.mjs
Raw

src/document_ffi.mjs

// import {} from "./globals.mjs"
import prototypes from "./prototypes.mjs"
import { List, Ok, Error } from "../prelude.mjs"
import { Hotkey, Shift, Alt, Short } from "./novdom/hotkey.mjs"
import { StateItem } from "./novdom/state.mjs"
import { OnStart, OnEnd } from "./novdom/internals/parameter.mjs"
import { twMerge } from "tailwind-merge"
import { unique_id } from "./utils_ffi.mjs"
const TEXT = "_TEXT_"
const HTML = "_HTML_"
// ------------------------------- VIEWPORT --------------------------------
export function init() {
prototypes()
globalThis.state_map = new Map() // state_id: String => value: a
globalThis.last_value_map = new Map() // id: String => last_value: a
globalThis.state_listener = new Map()
// using state_listener to store listeners for state_map.
// Tried to use events on components, but this was slower and more complicated.
globalThis.state_list_elements = new Map() // id: String => List(HTMLElement)
globalThis.reference_map = new Map() // reference_id: String => type: String
globalThis.hotkey_key_map = new Map() // key: Hotkey => ids: List(String)
globalThis.hotkey_id_map = new Map() // id: String => keys: List(Hotkey)
globalThis.hotkey_listener = new Map() // id: String => listener: Function
}
export function add_to_viewport(elem, id) {
// render_element(elem)
const viewport = document.getElementById(id)
viewport.appendChild(elem)
execute_simple_modifier(elem, "render", true)
}
// export function clear_viewport(id) {
// const viewport = document.getElementById(id)
// viewport.replaceChildren()
// }
// ------------------------------- ELEMENT --------------------------------
export function create_element(tag, children) {
const id = unique_id()
const elem = document.createElement(tag)
elem.setAttribute("id", id)
children.forEach((child) => elem.appendChild(child))
return elem
}
export function create_text_element(value) {
const id = unique_id()
const elem = document.createElement("p")
elem.setAttribute("id", id)
elem.textContent = value.replace(/^\s+|\s+$/g, function (match) {
return "\u00A0".repeat(match.length)
})
return elem
}
export function create_copy(elem) {
const id = unique_id()
const copy = elem.cloneNode(true)
copy.setAttribute("id", id)
return copy
}
export function for_each_child(elem, fn) {
;[...elem.children].forEach(fn)
}
// ------------------------------- PARAMETER --------------------------------
// ------------------------------- ATTRIBUTES --------------------------------
export function add_attribute(elem, name, value) {
handle_attribute(elem, name, value, false)
return elem
}
export function remove_attribute(elem, name, value) {
handle_attribute(elem, name, value, true)
return elem
}
function handle_attribute(elem, name, value, remove) {
// if (value === "") {
// elem.removeAttribute(name)
// return
// }
switch (name) {
case "tailwind":
if (!remove) {
elem.className = twMerge(elem.className, value)
return
}
value.smartSplit(" ").forEach((cls) => {
elem.classList.remove(cls)
})
return
case "class":
value.smartSplit(" ").forEach((cls) => {
if (remove) {
elem.classList.remove(cls)
return
}
elem.classList.add(cls)
})
return
case "style":
value.smartSplit(";").forEach((style) => {
if (!style.includes(":")) {
elem.style.setProperty(style, "")
return
}
const [k, v] = style.smartSplit(":")
if (remove) {
elem.style.removeProperty(k)
return
}
elem.style.setProperty(k, v.trim())
})
return
default:
if (remove) {
elem.removeAttribute(name)
return
}
elem.setAttribute(name, value)
// console.error("add_attribute: unknown attribute name / not implemented yet: " + name)
return
}
}
export function show(elem, fn = "insert") {
elem.ontransitionend = null
elem.removeEventListener("transitionend")
elem.removeEventListener("transitionstart")
elem.hidden = false
execute_simple_modifier(elem, fn)
}
export function hide(elem, transition = true) {
if (!transition) {
elem.hidden = true
return
}
execute_complex_modifier(elem, "remove", {
onend: () => {
elem.hidden = true
},
})
}
export function swap_vision(this_elem, for_elem) {
if (this_elem.hidden) {
show(for_comp)
return
}
execute_complex_modifier(this_elem, "replacement", {
onend: () => {
this_elem.hidden = true
},
onnew: () => {
show(for_elem, "replace")
},
})
}
// ------------------------------- LISTENER --------------------------------
export function add_listener(elem, name, callback) {
elem.addEventListener(name, callback)
return elem
}
export function remove_listener(elem, name, callback) {
elem.removeEventListener(name, callback)
return elem
}
export function create_once(callback) {
const fn = (event) => {
callback(event)
event.target.removeEventListener(event.type, fn)
}
return fn
}
export function add_global_listener(name, callback) {
document.addEventListener(name, callback)
}
// ------------------------------- CHILDREN --------------------------------
/**
* WARNING: Can only be used if the parent has only one child
*/
export function replace_child(elem, new_child) {
const old_child = elem.lastChild
execute_complex_modifier(old_child, "replacement", {
onend: () => {
elem.removeChild(old_child)
},
onnew: () => {
elem.appendChild(new_child)
execute_simple_modifier(new_child, "replace")
},
})
return elem
}
function create_new_state_wrapper(child) {
const parent = document.createElement("div")
parent.className = "replacement-transition"
parent.appendChild(child)
return parent
}
// export function remove_child(elem) {
// const old_child = elem.lastChild
// execute_complex_modifier(old_child, "remove", {
// onend: () => {
// elem.removeChild(old_child)
// },
// })
// return elem
// }
// ------------------------------- LIST CHILDREN --------------------------------
function tranistionDOM(elem, manipulationFn) {
const oldChildren = [...elem.children]
oldChildren.forEach((child) => {
child.oldBoundingClientRect = child.getBoundingClientRect()
})
manipulationFn()
oldChildren.forEach((child) => {
child.style.transition = "none"
const newBoundingClientRect = child.getBoundingClientRect()
const deltaX = child.oldBoundingClientRect.left - newBoundingClientRect.left
const deltaY = child.oldBoundingClientRect.top - newBoundingClientRect.top
child.style.transform = `translate(${deltaX}px, ${deltaY}px)`
})
requestAnimationFrame(() => {
oldChildren.forEach((child) => {
child.style.transition = ""
elem.ontranslate_onchildren_change(child)
child.style.transform = ""
})
})
}
function replace_list_child(wrapper, new_child) {
const old_child = wrapper.firstChild
execute_complex_modifier(old_child, "replacement", {
onend: () => {
wrapper.removeChild(old_child)
},
onnew: () => {
wrapper.appendChild(new_child)
execute_simple_modifier(new_child, "replace")
},
})
}
function set_list_children(elem, new_children, transition = true) {
if (!transition) {
elem.replaceChildren(...new_children.map(create_new_state_wrapper))
return
}
const old_children = [...elem.children]
if (!elem.onchildren_replacement) {
elem.replaceChildren(...new_children.map(create_new_state_wrapper))
new_children.forEach((child) => execute_simple_modifier(child.lastChild, "children_replace"))
return
}
if (elem.onchildren_replacement.render_call == "OnStart") {
let idx = 0
for (const wrapper of old_children) {
const old_child = wrapper.lastChild
const new_child = new_children[idx]
idx++
if (new_child) {
execute_complex_modifier(old_child, "children_replacement", {
onend: () => old_child.remove(),
onnew: () => {
wrapper.appendChild(new_child)
execute_simple_modifier(new_child, "children_replace")
},
})
continue
}
execute_complex_modifier(old_child, "children_replacement", {
onend: () => wrapper.remove(),
})
}
while (new_children.length > idx) {
const new_child = new_children[idx]
elem.appendChild(create_new_state_wrapper(new_child))
execute_simple_modifier(new_child, "children_replace")
idx++
}
return
}
const old_classes = elem.className
execute_complex_modifier(elem, "children_replacement", {
onend: () => {
elem.className = old_classes
elem.replaceChildren(...new_children.map(create_new_state_wrapper))
new_children.forEach((child) => execute_simple_modifier(child, "children_replace"))
},
})
}
function insert_list_child_before(elem, child_elem, idx) {
const wrapper = create_new_state_wrapper(child_elem)
const after_child = elem.children[idx]
const render_fn = after_child
? () => elem.insertBefore(wrapper, after_child)
: () => elem.appendChild(wrapper)
elem.ontranslate_onchildren_change ? tranistionDOM(elem, render_fn) : render_fn()
execute_simple_modifier(child_elem, "insert")
}
function move_list_child_before(elem, child_wrapper, idx) {
const after_child = elem.children[idx]
const render_fn = after_child
? () => elem.insertBefore(child_wrapper, after_child)
: () => elem.appendChild(child_wrapper)
elem.ontranslate_onchildren_change ? tranistionDOM(elem, render_fn) : render_fn()
}
function remove_list_child_at(elem, at) {
const wrapper = elem.children[at]
execute_complex_modifier(wrapper.firstChild, "remove", {
onend: () => {
elem.ontranslate_onchildren_change
? tranistionDOM(elem, () => elem.removeChild(wrapper))
: elem.removeChild(wrapper)
},
})
}
// ------------------------------- MODIFIER --------------------------------
export function forward_modifier(name) {
return (elem) => {
elem[`on${name}`]()
}
}
export function add_simple_modifier(elem, name, fn) {
if (!elem.onrender) {
switch (name) {
case "insert":
case "replace":
elem.onrender = fn
}
}
if (!elem.onchildren_replace) {
switch (name) {
case "replace":
elem.onchildren_replace = fn
}
}
elem[`on${name}`] = fn
return elem
}
export function add_complex_modifier(elem, name, fn, render_call = new OnEnd()) {
const complex_callback_fn = get_complex_callback_fn(elem, render_call)
const callback = (args) => {
complex_callback_fn(args)
fn(args.oldElem)
}
if (!elem.onremove) {
switch (name) {
case "replacement":
elem.onremove = callback
}
}
// if (!elem.onchildren_replacement) {
// switch (name) {
// case "replacement":
// elem.onchildren_replacement = fn
// }
// }
elem[`on${name}`] = callback
elem[`on${name}`].render_call = render_call.constructor.name
return elem
}
function get_complex_callback_fn(elem, render_call) {
// TODO: Add animationend??
switch (render_call.constructor.name) {
case "OnStart":
return ({ onend, onnew }) => {
// if (elem !== elem.renderRoot) {
// return
// }
onnew()
elem.addEventListener("transitionend", create_once(onend))
}
case "OnEnd":
return ({ onend, onnew }) => {
// if (elem !== elem.renderRoot) {
// return
// }
elem.ontransitionend = () => {
onend()
onnew()
elem.ontransitionend = null
}
}
}
}
// function get_replace_callback(fn) {
// return (e) => {
// const currentTarget = e.currentTarget
// if (currentTarget.renderRoot !== currentTarget) {
// return
// }
// fn()
// }
// }
function execute_simple_modifier(elem, name, recursive = false) {
if (elem.hidden) {
return
}
if (recursive) {
;[...elem.children].forEach((child) => {
execute_simple_modifier(child, name, true)
})
}
if (elem[`on${name}`]) {
requestAnimationFrame(() => {
setTimeout(() => {
elem[`on${name}`](elem)
})
})
}
}
function execute_complex_modifier(elem, name, args) {
const safe_args = {
oldElem: elem,
onend: args.onend || (() => {}),
onnew: args.onnew || (() => {}),
}
if (!elem) {
safe_args.onnew()
return
}
if (!elem[`on${name}`]) {
// TODO: Add check for chain_listener
safe_args.onend()
safe_args.onnew()
return
}
// elem.renderRoot = root
elem[`on${name}`](safe_args)
}
// function execute_replace_iter(elem, onend, onnew, root = elem) {
// ;[...elem.children].forEach((child) => {
// execute_replace_iter(child, onend, onnew, root)
// })
// if (elem.onreplace) {
// elem.renderRoot = root
// elem.onreplace(onend, onnew)
// }
// }
// ------------------------------- STATE --------------------------------
function call_listener(id, value) {
;(globalThis.state_listener.get(id) || []).forEach((callback) => callback(value))
}
function delete_state(id) {
globalThis.state_map.delete(id)
globalThis.state_listener.delete(id)
}
export function set_state(id, value) {
globalThis.state_map.set(id, value)
call_listener(id, value)
}
export function get_state_value(id) {
return globalThis.state_map.get(id)
}
export function add_state_listener(id, callback) {
let current = globalThis.state_listener.get(id) || []
globalThis.state_listener.set(id, [callback, ...current])
}
export function set_last_value(id, value) {
globalThis.last_value_map.set(id, value)
}
export function get_last_value(id) {
return globalThis.last_value_map.get(id)
}
// ------------------------------- STATE LIST --------------------------------
function current_state_list(id) {
return globalThis.state_map.get(id) || []
}
function create_state_item(value) {
return { id: unique_id(), value }
}
function get_item_idx(id, root_id) {
return current_state_list(root_id).findIndex((item) => item.id === id)
}
export function set_state_list(id, list_value, item_array = list_value.map(create_state_item)) {
globalThis.state_map.set(id, item_array)
call_listener(id, list_value)
return item_array
}
function set_state_items(
root_id,
item_array,
list_value = item_array.map((item) => item.value).toList()
) {
set_state_list(root_id, list_value, item_array)
return list_value
}
export function get_state_list_value(id) {
return current_state_list(id)
.map((item) => item.value)
.toList()
}
export function get_state_list_item_value(id, root_id) {
return current_state_list(root_id).find((item) => item.id === id).value
}
export function get_state_list_items(root_id) {
return current_state_list(root_id)
.map((item) => new StateItem(item.id, root_id))
.toList()
}
export function get_state_list_item(id, idx) {
const item = current_state_list(id)[idx]
if (!item) {
return new Error(undefined)
}
return new Ok(new StateItem(current_state_list(id)[idx].id, id))
}
export function overwrite_state_list(id, list_value) {
;(globalThis.state_map.get(id) || []).forEach((item) => {
delete_state(item.id)
})
const items = set_state_list(id, list_value).map((item) => new StateItem(item.id, id))
;(globalThis.state_list_elements.get(id) || []).forEach((elem) => {
set_list_children(
elem,
items.map((item) => elem.create_item_element(item))
)
})
}
export function set_state_list_item(id, root_id, value) {
set_state_items(
root_id,
current_state_list(root_id).map((item) => (item.id === id ? { id, value } : item))
)
call_listener(id, value)
}
export function make_state_list_component(elem, id, render_fn) {
elem.create_item_element = (...args) => {
const child = render_fn(...args)
if (elem.onchildren_replace) {
child.onchildren_replace = elem.onchildren_replace
}
child.onchildren_replacement = elem.onchildren_replacement
return child
}
set_list_children(
elem,
get_state_list_items(id).map((item) => elem.create_item_element(item)),
false
)
const current = globalThis.state_list_elements.get(id) || []
globalThis.state_list_elements.set(id, [...current, elem])
return elem
}
export function delete_state_item(id, root_id) {
const old_items = current_state_list(root_id)
const idx = get_item_idx(id, root_id)
const new_items = old_items.toSpliced(idx, 1)
set_state_items(root_id, new_items)
;(globalThis.state_list_elements.get(root_id) || []).forEach((elem) =>
remove_list_child_at(elem, idx)
)
}
export function insert_state_item_before(before_id, root_id, value) {
const old_items = current_state_list(root_id)
const idx = get_item_idx(before_id, root_id)
const new_items = old_items.toSpliced(idx, 0, { id: unique_id(), value })
set_state_items(root_id, new_items)
;(globalThis.state_list_elements.get(root_id) || []).forEach((elem) =>
insert_list_child_before(
elem,
elem.create_item_element(new StateItem(new_items[idx].id, root_id)),
idx
)
)
}
export function insert_state_item_after(after_id, root_id, value) {
const old_items = current_state_list(root_id)
const idx = get_item_idx(after_id, root_id)
const new_items = old_items.toSpliced(idx + 1, 0, { id: unique_id(), value })
set_state_items(root_id, new_items)
;(globalThis.state_list_elements.get(root_id) || []).forEach((elem) =>
insert_list_child_before(
elem,
elem.create_item_element(new StateItem(new_items[idx + 1].id, root_id)),
idx + 1
)
)
}
export function replace_state_item(id, root_id, value) {
const old_items = current_state_list(root_id)
const idx = get_item_idx(id, root_id)
const new_items = old_items.toSpliced(idx, 1, { id, value })
set_state_items(root_id, new_items)
;(globalThis.state_list_elements.get(root_id) || []).forEach((elem) => {
replace_list_child(
elem.children[idx],
elem.create_item_element(new StateItem(new_items[idx].id, root_id))
)
})
}
export function move_state_item_before(id, before_id, root_id) {
if (id === before_id) return
const old_items = current_state_list(root_id)
const idx = get_item_idx(id, root_id)
const before_idx = get_item_idx(before_id, root_id)
const item = old_items[idx]
// do toSpliced for bigger index first
const new_items =
idx > before_idx
? old_items.toSpliced(idx, 1).toSpliced(before_idx, 0, item)
: old_items.toSpliced(before_idx, 0, item).toSpliced(idx, 1)
set_state_items(root_id, new_items)
;(globalThis.state_list_elements.get(root_id) || []).forEach((elem) =>
move_list_child_before(elem, elem.children[idx], before_idx)
)
}
export function move_state_item_after(id, after_id, root_id) {
if (id === after_id) return
const old_items = current_state_list(root_id)
const idx = get_item_idx(id, root_id)
const after_idx = get_item_idx(after_id, root_id)
const item = old_items[idx]
const new_items =
idx > after_idx
? old_items.toSpliced(idx, 1).toSpliced(after_idx + 1, 0, item)
: old_items.toSpliced(after_idx + 1, 0, item).toSpliced(idx, 1)
set_state_items(root_id, new_items)
;(globalThis.state_list_elements.get(root_id) || []).forEach((elem) =>
move_list_child_before(elem, elem.children[idx], after_idx + 1)
)
}
// ------------------------------- REFERENCE --------------------------------
export function add_reference(ref, elem, type) {
globalThis.reference_map.set(ref.id, {
type: type.constructor.name,
elem,
})
}
export function read_reference(ref) {
const { type, elem } = globalThis.reference_map.get(ref.id)
if (type === "InnerHTML") {
return elem.innerHTML
}
if (type === "Value") {
if (elem.value !== undefined) {
return elem.value
}
throw new Error("read_reference: value not found")
}
}
// ------------------------------- HOTKEY --------------------------------
export function override_hotkey(id, key) {
update_hotkey_map_remove(globalThis.hotkey_id_map.get(id) || [], id)
const key_string = encode_key(key)
globalThis.hotkey_id_map.set(id, [key_string])
update_hotkey_map_add([key_string], id)
}
export function add_hotkey(id, key) {
const current = globalThis.hotkey_id_map.get(id) || []
const key_string = encode_key(key)
globalThis.hotkey_id_map.set(id, [key_string, ...current])
update_hotkey_map_add([key_string], id)
}
export function remove_hotkey(id, key) {
const current = globalThis.hotkey_id_map.get(id) || []
const key_string = encode_key(key)
globalThis.hotkey_id_map.set(
id,
current.filter((k) => k !== key_string)
)
update_hotkey_map_remove([key_string], id)
}
function update_hotkey_map_add(hotkeys, id) {
hotkeys.forEach((k) => {
const current = globalThis.hotkey_key_map.get(k) || []
globalThis.hotkey_key_map.set(k, [id, ...current])
})
}
function update_hotkey_map_remove(hotkeys, id) {
hotkeys.forEach((k) => {
const current = globalThis.hotkey_key_map.get(k) || []
globalThis.hotkey_key_map.set(
k,
current.filter((i) => i !== id)
)
})
}
export function get_hotkeys(id) {
return (globalThis.hotkey_id_map.get(id) || []).map(decode_key).toList()
}
export function get_hotkey_ids(key) {
const key_string = encode_key(key)
return (globalThis.hotkey_key_map.get(key_string) || []).toList()
}
function encode_key(key) {
let is_short = key.ctrlKey || key.metaKey
let is_shift = key.shiftKey
let is_alt = key.altKey
if (key.modifiers) {
const modifier_names = key.modifiers.map((m) => m.constructor.name)
is_short = modifier_names.includes("Short")
is_shift = modifier_names.includes("Shift")
is_alt = modifier_names.includes("Alt")
}
return (
key.code +
";" +
(is_shift ? "Shift," : "") +
(is_alt ? "Alt," : "") +
(is_short ? "Short," : "")
)
}
function decode_key(key_string) {
const [keycode, modifier_string] = key_string.split(";")
const modifiers = (
modifier_string
? modifier_string.smartSplit(",").map((k) => {
switch (k) {
case "Shift":
return new Shift()
case "Alt":
return new Alt()
case "Short":
return new Short()
// case "meta":
// return new Meta()
// case "ctrl":
// return new Ctrl()
default:
throw new Error("decode_key: unknown modifier: " + k)
}
})
: []
).toList()
return new Hotkey(keycode, modifiers)
}
export function keypress_callback(event) {
const pressed_key = encode_key(event)
const ids = globalThis.hotkey_key_map.get(pressed_key) || []
if (ids.length === 0) {
return
}
event.preventDefault()
ids.forEach((id) => globalThis.hotkey_listener.get(id)(event))
}
export function set_hotkey_listener(id, listener) {
globalThis.hotkey_listener.set(id, listener)
}
// ------------------------------- OTHER --------------------------------
export function store_mouse_position(e) {
const drag = document.getElementById("_drag_")
drag.style.setProperty("--mouse-x", e.clientX + "px")
drag.style.setProperty("--mouse-y", e.clientY + "px")
}