www.spaceplanner.app

Web client to the spaceplanner API
git clone git://jacobedwards.org/www.spaceplanner.app
Log | Files | Refs

main.js (9433B)


      1 import * as api from "/lib/api.js"
      2 import * as etc from "/lib/etc.js"
      3 import * as ui from "/lib/ui.js"
      4 
      5 // These are in the order they should appear
      6 const editables = [ "name", "address", "synopsis" ]
      7 
      8 etc.handle_wrap(init)
      9 
     10 function init() {
     11 	etc.authorize()
     12 	etc.bar()
     13 
     14 	let f = document.getElementById("filter")
     15 	f.removeAttribute("disabled")
     16 	f.addEventListener("input", function(ev) {
     17 		document.querySelectorAll("#floorplans > li").forEach(function(item) {
     18 			if (item.querySelector("#adder")) {
     19 				return
     20 			}
     21 
     22 			let data = {}
     23 			let h = item.querySelector(".floorplan > header")
     24 			const tc = function(sel) {
     25 				const e = h.querySelector(sel)
     26 				return e ? e.textContent : null
     27 			}
     28 			data.name = tc(".name_div > h2 > a")
     29 			data.address = tc(".address")
     30 			data.synopsis = tc(".synopsis")
     31 			item.hidden = !matchFloorplan(data, ev.target.value)
     32 		})
     33 	})
     34 
     35 	api.fetch("GET", "floorplans/:user")
     36 		.then(show_floorplans)
     37 		.catch(etc.error)
     38 }
     39 
     40 function commit_editable_floorplan_func(element, data) {
     41 	return function () {
     42 		let patches = []
     43 		let fields = Array.from(element.querySelectorAll(".fp_metadata"))
     44 		let updated = false
     45 		let newdata = {}
     46 		for (let i in fields) {
     47 			let name = fields[i].name
     48 			let value = fields[i].value
     49 			if (value.length === 0) {
     50 				value = null
     51 			}
     52 
     53 			console.debug(fields[i], name, value)
     54 			newdata[name] = value;
     55 			if (newdata[name] !== data[name]) {
     56 				updated = true
     57 			}
     58 		}
     59 
     60 		if (!updated) {
     61 			console.debug("No changes, skipping")
     62 			element.replaceWith(create_floorplan(data))
     63 			return
     64 		}
     65 
     66 		return api.fetch("PUT", `floorplans/:user/${data.id}`, newdata)
     67 			.then(function(rdata) {
     68 				for (let i in rdata) {
     69 					data[i] = rdata[i]
     70 				}
     71 				element.replaceWith(create_floorplan(data))
     72 			})
     73 			.catch(function(err) {
     74 				etc.error(err, element)
     75 				throw err
     76 			})
     77 	}
     78 }
     79 
     80 function editable_floorplan_create_func(element) {
     81 	return function () {
     82 		let data = {}
     83 		let fields = Array.from(element.querySelectorAll("header > input"))
     84 		for (let i in fields) {
     85 			let name = fields[i].name
     86 			let value = fields[i].value
     87 			console.debug(fields[i], name, value)
     88 			if (value) {
     89 				data[name] = value
     90 			}
     91 		}
     92 
     93 		return api.fetch("POST", "floorplans/:user", data)
     94 			.then(function(rdata) {
     95 				for (let i in rdata) {
     96 					data[i] = rdata[i]
     97 				}
     98 				for (let i in fields) {
     99 					fields[i].value = ""
    100 				}
    101 				/* NOTE: I was going to try and not
    102 				 * have these floorplans know anything
    103 				 * about where they are, but I'm living
    104 				 * with this.
    105 				 */
    106 				element.parentElement.after(create_floorplan_item(data))
    107 			})
    108 			.catch(function(err) {
    109 				etc.error(err, element)
    110 				throw err
    111 			})
    112 	}
    113 }
    114 
    115 function editable_floorplan_func(element, data) {
    116 	return function() {
    117 		let prev
    118 		let parent = element.querySelector("header")
    119 		for (let i in editables) {
    120 			let input
    121 			let memo = "Edit floorplan " + editables[i]
    122 			let e = parent.querySelector("." + editables[i])
    123 
    124 			input = ui.input(editables[i], memo, {
    125 				attributes: { value: e ? e.textContent : "" }
    126 			})
    127 			input.classList.add("fp_metadata")
    128 			input.classList.add(editables[i])
    129 			input.name = editables[i]
    130 
    131 			if (e) {
    132 				e.replaceWith(input)
    133 			} else {
    134 				if (prev) {
    135 					if (prev.name === "name") {
    136 						parent.append(input)
    137 					} else {
    138 						prev.after(input)
    139 					}
    140 				} else {
    141 					parent.append(input)
    142 				}
    143 			}
    144 			prev = input
    145 		}
    146 	}
    147 }
    148 
    149 function delete_floorplan_func(item, floorplan) {
    150 	return function() {
    151 		api.fetch("DELETE", `floorplans/:user/${floorplan.id}`)
    152 			.then(function() {
    153 				item.parentElement.remove()
    154 			})
    155 			.catch(function(err) {
    156 				etc.error("Unable to delete floorplan: " + err, item)
    157 			})
    158 	}
    159 }
    160 
    161 function ask_delete_floorplan_func(item, floorplan) {
    162 	return function() {
    163 		document.querySelectorAll(".delete_dialog").forEach(function(e) { e.remove() })
    164 		let c = document.body.appendChild(document.createElement("div"))
    165 		c.classList.add("delete_dialog")
    166 		let mkbutton = function(value) {
    167 			let b = document.createElement("input")
    168 			b.type = "button"
    169 			b.value = value
    170 			return b
    171 		}
    172 
    173 		let t = c.appendChild(document.createElement("p"))
    174 		t.appendChild(document.createTextNode("Delete "))
    175 		let q = t.appendChild(document.createElement("q"))
    176 		q.appendChild(document.createTextNode(floorplan.name))
    177 		t.append(document.createTextNode("?"))
    178 
    179 		let yes = c.appendChild(mkbutton("Yes"))
    180 		let no = c.appendChild(mkbutton("No"))
    181 
    182 		let p = new Promise(function(res, rej) {})
    183 		let hand = function(ev) {
    184 			if (ev.target.value == "Yes") {
    185 				delete_floorplan_func(item, floorplan)()
    186 			}
    187 			c.remove()
    188 		}
    189 		yes.addEventListener("click", hand)
    190 		no.addEventListener("click", hand)
    191 
    192 		return p
    193 	}
    194 }
    195 
    196 function create_floorplan_item(floorplan) {
    197 	let item = document.createElement("li")
    198 	item.append(create_floorplan(floorplan))
    199 	return item
    200 }
    201 
    202 function create_floorplan(floorplan) {
    203 	let root = document.createElement("div")
    204 	root.classList.add("class", "floorplan")
    205 
    206 	let aside = document.createElement("div")
    207 	aside.classList.add("fp_ops")
    208 	if (floorplan) {
    209 		let a = aside.appendChild(document.createElement("a"))
    210 		a.href = `./floorplan/?id=${floorplan.id}`
    211 		a.append(document.createTextNode("Editor"))
    212 
    213 		let ops = aside.appendChild(document.createElement("div"))
    214 		ops.classList.add("fp_buttons")
    215 
    216 		ops.append(ui.button("Copy", "Copy floorplan", null, { handlers: { click: function() { copy_floorplan(floorplan) } } }))
    217 		ops.append(ui.button("Delete", "Delete floorplan", null, { handlers: { click: ask_delete_floorplan_func(root, floorplan) } }))
    218 	} else {
    219 		root.id = "adder"
    220 		root.addEventListener("keydown", function(ev) {
    221 			if (ev.key === "Enter") {
    222 				ev.preventDefault()
    223 				editable_floorplan_create_func(root)()
    224 			}
    225 		})
    226 		aside.append(ui.button("Create", "Create floorplan", null, { handlers: { click: editable_floorplan_create_func(root) } }))
    227 	}
    228 
    229 	let header = document.createElement("header")
    230 	header.append(aside)
    231 	root.append(header)
    232 
    233 	if (!floorplan) {
    234 		editable_floorplan_func(root, {})()
    235 	} else {
    236 		if (!floorplan.name) {
    237 			throw new Error("Expected floorplan name")
    238 		}
    239 		let nameDiv = header.appendChild(document.createElement("div"))
    240 		nameDiv.classList.add("name_div")
    241 		nameDiv.append(create_field.name(floorplan.name, floorplan.id))
    242 		nameDiv.append(ui.toggle(
    243 			{ button: ui.button("Edit", "Edit floorplan metadata", "create"), func: editable_floorplan_func(root, floorplan) },
    244 			{ button: ui.button("Save", "Save floorplan metadata", "save"), func: commit_editable_floorplan_func(root, floorplan) },
    245 		))
    246 
    247 		if (floorplan.address) {
    248 			header.append(create_field.address(floorplan.address))
    249 		}
    250 		if (floorplan.synopsis) {
    251 			header.append(create_field.synopsis(floorplan.synopsis))
    252 		}
    253 
    254 		if (floorplan.user != localStorage.getItem("username")) {
    255 			let footer = document.createElement("footer")
    256 			// TODO: Link to user page, when it exists
    257 			footer.append(document.createTextNode("By " + floorplan.user))
    258 			root.append(footer)
    259 		}
    260 	}
    261 
    262 	return root
    263 }
    264 
    265 var create_field = {
    266 	name: function(text, id) {
    267 		let heading = document.createElement("h2")
    268 		heading.classList.add("fp_metadata")
    269 		heading.classList.add("name")
    270 		let link = document.createElement("a")
    271 		link.href = `./floorplan/?id=${id}`
    272 		link.appendChild(document.createTextNode(text))
    273 		heading.append(link)
    274 		return heading
    275 	},
    276 
    277 	synopsis: function(text) {
    278 		let synopsis = document.createElement("span")
    279 		synopsis.classList.add("fp_metadata")
    280 		synopsis.classList.add("synopsis")
    281 		synopsis.appendChild(document.createTextNode(text))
    282 		return synopsis
    283 	},
    284 
    285 	address: function(text) {
    286 		let address = document.createElement("address")
    287 		address.classList.add("fp_metadata")
    288 		address.classList.add("address")
    289 		address.appendChild(document.createTextNode(text))
    290 		return address
    291 	}
    292 }
    293 
    294 function show_floorplans(floorplans) {
    295 	let list = document.getElementById("floorplans")
    296 	if (!list) {
    297 		throw new Error("expected #floorplans")
    298 	}
    299 
    300 	list.append(create_floorplan_item())
    301 	for (let i in floorplans) {
    302 		list.append(create_floorplan_item(floorplans[i]))
    303 	}
    304 }
    305 
    306 function insertFloorplan(floorplan) {
    307 	let e = create_floorplan_item(floorplan)
    308 
    309 	let adder = document.getElementById("adder")
    310 	if (adder) {
    311 		adder.parentElement.after(e)
    312 	} else {
    313 		let list = document.getElementById("floorplans")
    314 		list.prepend(create_floorplan(floorplan))
    315 	}
    316 }
    317 
    318 function copy_floorplan(floorplan, name, depth) {
    319 	if (!name) {
    320 		name = floorplan.name + " (Copy)"
    321 	}
    322 	api.fetch("GET", `floorplans/${floorplan.user}/${floorplan.id}/data`)
    323 		.then(function(data) {
    324 			let f = structuredClone(floorplan)
    325 			f.name = name
    326 			return api.fetch("POST", "floorplans/:user", f)
    327 				.then(function(floorplan) {
    328 					insertFloorplan(floorplan)
    329 					return api.fetch("PUT", `floorplans/${floorplan.user}/${floorplan.id}/data`, data)
    330 						.catch(function(err) {
    331 							api.fetch("DELETE", `floorplans/:user/${floorplan.id}`)
    332 							throw err
    333 						})
    334 				})
    335 				.catch(function(err) {
    336 					depth = depth ?? 0
    337 					if (depth < 10 && err.message.indexOf('violates unique constraint "id"')) {
    338 						return copy_floorplan(floorplan, name + " (Copy)", depth + 1)
    339 					} else {
    340 						etc.error(err)
    341 						throw err
    342 					}
    343 				})
    344 		})
    345 }
    346 
    347 function matchFloorplan(floorplan, exp) {
    348 	const ms = function(s, e) {
    349 		return s ? s.toLowerCase().includes(e) : false
    350 	}
    351 
    352 	exp = exp.toLowerCase()
    353 	return ms(floorplan.name, exp) || ms(floorplan.address, exp) || ms(floorplan.synopsis, exp)
    354 }