commit 223ee24b507bb61b3c84a180bb1161732b16acee
parent cc0fc8361bdca00e47b434cd8ea20936a2137e0b
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date: Sun, 25 Aug 2024 14:13:15 -0700
Add selection system to editor and improve frontend
- Add new selection system to the editor, and use that to make a
single selection handler
- Add a general purpose editor remove function which will remove
the given elements or references from the backend
- Re-plot pointmaps when their points move
- Add and edit a few functions to work with refs in the editor
- Rewrite handlers in the front-end to work with the new systems
in the editor
Diffstat:
2 files changed, 418 insertions(+), 305 deletions(-)
diff --git a/files/floorplans/floorplan/editor.js b/files/floorplans/floorplan/editor.js
@@ -1,15 +1,80 @@
import { default as SVG } from "/lib/github.com/svgdotjs/svg.js/svg.js"
import * as backend from "./backend.js"
-SVG.extend(SVG.Element, {
- select: function() {
- console.debug("User selected", this.node)
- this.root().find(".last_selected")
+const selectEvent = new Event("select")
+const unselectEvent = new Event("unselect")
+
+SVG.extend(SVG.Svg, {
+ unselect: function(list) {
+ console.debug("Svg.unselect", list)
+
+ let selected
+ if (list) {
+ selected = list
+ } else {
+ selected = this.find(".selected")
+ }
+
+ // Should also test for selected class
+ if (!selected) {
+ console.debug("Nothing to unselect")
+ return
+ }
+
+ this.find(".last_selected")
.removeClass("last_selected")
- this.root().find(".selected")
+
+ let unselected = selected
.removeClass("selected")
.addClass("last_selected")
- return this.addClass("selected")
+
+ // NOTE: Could fire an event, but then I'd have to handle
+ // deletions, so I'll leave it until it's needed.
+ return unselected
+ },
+
+ select: function(list) {
+ console.debug("Svg.select", list)
+
+ this.unselect()
+
+ if (list) {
+ list.addClass("selected")
+ }
+ this.fire("select", { selected: list })
+ return list
+ },
+
+ reselect: function() {
+ this.fire("select", { selected: this.find(".selected") })
+ }
+})
+
+SVG.extend(SVG.List, {
+ selectList: function() {
+ let root
+ this.each(function(item) {
+ if (!root) {
+ root = item.root()
+ } else if (root != item.root())
+ throw new Error("Cannot select from different documents")
+ })
+
+ return root.select(this)
+ },
+
+ array: function() {
+ let a = []
+ this.each(function(item) {
+ a.push(item)
+ })
+ return a
+ }
+})
+
+SVG.extend(SVG.Element, {
+ select: function() {
+ return new SVG.List([this]).selectList()[0]
},
findOneMax: function(selector) {
@@ -240,6 +305,26 @@ export class FloorplanEditor {
editor.updateGrid()
})
resize.observe(editor.draw.node)
+
+ let selectionRemoval = new MutationObserver(function(mutations) {
+ for (const m of mutations) {
+ if (m.type === "childList" && m.removedNodes) {
+ m.removedNodes.forEach(function(node) {
+ if (node.classList.contains("selected")) {
+ console.debug("selectionRemoval",
+ "Detected selected node being removed")
+ editor.draw.reselect()
+ return
+ }
+ })
+ }
+ }
+ })
+ selectionRemoval.observe(this.draw.node, { childList: true, subtree: true })
+
+ this.draw.on("select", function(event) {
+ editor.selection = event.detail.selection
+ })
}
useUnits(system) {
@@ -383,27 +468,59 @@ export class FloorplanEditor {
return this.backend.addPoint(point)
}
- removePoint(point) {
- this.backend.removePoint(getId(point), { recurse: true })
+ remove(...elements) {
+ let later = []
+
+ for (let i in elements) {
+ let ref = getRef(elements[i])
+ if (ref.type === "pointmaps") {
+ this.backend.unmapPoints(ref.id)
+ } else {
+ later.push(ref)
+ }
+ }
+
+ for (let i in later) {
+ if (later[i].type === "points") {
+ this.backend.removePoint(later[i].id, { unmap: true })
+ } else {
+ throw new Error("Unsupported type")
+ }
+ }
+
+ this.backend.removeOrphans()
this.updateDisplay()
}
+ removePoints(...points) {
+ for (let i in points) {
+ points[i] = backend.newRef("points", getId(points[i]))
+ }
+ return remove(points)
+ }
+
pointAt(point) {
return this.thingAt(point, "#points")
}
-
thingAt(point, selector) {
+ return this.thingsAt(point, selector, 1)[0]
+ }
+
+ thingsAt(point, selector, max) {
let children = this.draw.find(selector ?? "*")
.children()
.toArray()
+ let inside = []
for (let i in children) {
if (children[i].inside(point.x, point.y)) {
- return children[i]
+ if (inside.push(children[i]) >= max) {
+ return inside
+ }
}
}
- return null
+ return inside
}
mapSelected(type) {
@@ -412,14 +529,9 @@ export class FloorplanEditor {
}
mapPoints(type, p1, p2) {
- let pointId = function(id) { return id.split("_")[1] }
-
- this.mapPointsById(type, pointId(p1.attr("id")), pointId(p2.attr("id")))
- }
-
- mapPointsById(type, p1, p2) {
- this.backend.mapPoints(type, p1, p2)
+ let ref = this.backend.mapPoints(type, getId(p1, "points"), getId(p2, "points"))
this.updateDisplay()
+ return ref
}
selectedPoints() {
@@ -461,7 +573,7 @@ export class FloorplanEditor {
let ops = {
add: {
- points: function(name, value) {
+ points: function(name, value, ref) {
let cur = editor.draw.findOneMax(byId(name))
// Update pointmaps
if (cur) {
@@ -474,14 +586,18 @@ export class FloorplanEditor {
.attr({ id: name })
.addClass("point")
.select()
- .on("click", function(event) {
- if (event.shiftKey) {
- this.select()
- event.preventDefault()
- }
- })
}
+ for (let oth in editor.backend.mappedPoints[ref.id]) {
+ let map = editor.backend.mappedPoints[ref.id][oth]
+ oth = editor.backend.reqId("points", oth)
+ map = editor.draw.findOneMax(byId(refId((backend.newRef("pointmaps", map)))))
+ if (map) {
+ // It's probably being added later, that said, this isn't a good solution
+ // because it doesn't allow for checking for errors.
+ map.plot(oth.x, oth.y, value.x, value.y)
+ }
+ }
},
pointmaps: function(name, value) {
if (value.type !== "wall" && value.type !== "door") {
@@ -528,7 +644,7 @@ export class FloorplanEditor {
if (!ops[diff.op][ref.type]) {
throw new Error("Unhandled patch")
}
- ops[diff.op][ref.type](refId(ref), diff.value)
+ ops[diff.op][ref.type](refId(ref), diff.value, ref)
}
updateId(ids) {
@@ -538,7 +654,7 @@ export class FloorplanEditor {
}
findRef(ref) {
- return this.draw.findExactlyOne(byId(refId(ref)))
+ return this.draw.findExactlyOne(byId(refId(getRef(ref))))
}
}
@@ -592,32 +708,51 @@ function gridSystem(editor, system) {
return last
}
-function getId(thing) {
- console.debug("getId", thing)
+export function getRef(thing, type) {
+ console.debug("getRef", thing, type)
+ let ref
if (typeof thing === "object") {
- return idRef(thing.attr("id")).id
+ if (typeof thing.attr === "function") {
+ ref = idRef(thing.attr("id"))
+ } else if (typeof thing.type === "string" && typeof thing.id === "number") {
+ ref = thing
+ }
+ } else if (typeof thing === "string") {
+ ref = idRef(thing)
}
- if (typeof thing === "string") {
- return idRef(thing).id
+
+ if (!ref) {
+ console.error("Couldn't get ref from", thing)
+ throw new Error("Invalid ref")
}
- if (typeof thing === "number") {
- return thing
+ if (type && ref.type != type) {
+ throw new Error(`${ref.type}: Invalid ref type (wanted ${type})`)
}
- throw new Error("Invalid ID")
+ return ref
}
-function byId(id) {
- return "#" + id
-}
+export function getId(thing, type) {
+ console.debug("getId", thing)
-function refId(ref) {
- return ref.type + "_" + ref.id
+ let n = Number(thing)
+ if (isNaN(n)) {
+ return getRef(thing, type).id
+ }
+ return n
}
export function idRef(id) {
let a = id.split("_")
if (a.length != 2) {
- throw new Error("Invalid id")
+ throw new Error(`${id}: Invalid id`)
}
return backend.newRef(a[0], a[1])
}
+
+function byId(id) {
+ return "#" + id
+}
+
+function refId(ref) {
+ return ref.type + "_" + ref.id
+}
diff --git a/files/floorplans/floorplan/main.js b/files/floorplans/floorplan/main.js
@@ -2,9 +2,10 @@ import { default as SVG } from "/lib/github.com/svgdotjs/svg.js/svg.js"
import "/lib/github.com/svgdotjs/svg.panzoom.js/svg.panzoom.js"
import * as ui from "/lib/ui.js"
import * as etc from "/lib/etc.js"
-import { FloorplanEditor as Editor, idRef } from "./editor.js"
+import * as lib from "./editor.js" // Confusing, but I don't want to fix variable conflict
import { Vector2 } from "/lib/github.com/ros2jsguy/threejs-math/math/Vector2.js"
import "./geometry.js"
+import * as backend from "./backend.js"
const messageTimeout = 4000
@@ -36,7 +37,7 @@ function init() {
zoomFactor: .5
})
- let editor = new Editor(draw,
+ let editor = new lib.FloorplanEditor(draw,
{ user: localStorage.getItem("username"), name: floorplan },
{ backend: {
callbacks: {
@@ -106,6 +107,8 @@ function init() {
)
))
+ editor.draw.on("select", function(event) { selectHandler(event, editor) })
+
editor.backend.pull()
.then(function() {
if (editor.draw.findExactlyOne("#points").children().length === 0) {
@@ -114,6 +117,63 @@ function init() {
})
}
+function selectHandler(event, editor) {
+ let old = document.getElementById("selOps")
+ if (!event.detail.selected) {
+ if (old) {
+ old.remove()
+ }
+ return
+ }
+ let a = event.detail.selected.array()
+ let c = document.createElement("li")
+ c.setAttribute("id", "selOps")
+
+ c.appendChild(document.createTextNode("Selection: "))
+ c.append(ui.input("Delete", "Delete selected objects", {
+ attributes: { type: "button" },
+ handlers: { click: function() {
+ editor.remove(...a)
+ }},
+ })
+ )
+
+ let refs = []
+ for (let i in a) {
+ refs[i] = lib.getRef(a[i])
+ }
+
+ let maps = []
+ for (let i in refs) {
+ if (refs[i].type === "pointmaps") {
+ maps.push(editor.backend.cache.pointmaps[refs[i].id])
+ }
+ }
+
+ if (maps.length > 0) {
+ c.appendChild(
+ radioMenu(editor, "Type", ["wall", "door"], null, {
+ nosubmit: true,
+ callbacks: {
+ change: function(newvalue) {
+ for (let i in maps) {
+ editor.mapPoints(newvalue, maps[i].a, maps[i].b)
+ editor.updateDisplay()
+ }
+ }
+ }
+ })
+ )
+ }
+
+ if (old) {
+ old.replaceWith(c)
+ } else {
+ document.querySelector(".toolbar")
+ .appendChild(c)
+ }
+}
+
function selector(editor, things, select, options) {
options = options ?? {}
@@ -165,108 +225,54 @@ let modes = {
* though.
*/
contextmenu: preventDefaultHandler,
- click: addWallHandler
+ mousedown: selectionHandler
}
},
Precise: {
points: true,
handlers: {
contextmenu: preventDefaultHandler,
- mousedown: precisePointHandler,
+ mousedown: [selectionHandler, precisePointHandler],
mousemove: precisePointHandler,
mouseup: precisePointHandler,
- keydown: [zoomKeysHandler, undoRedoHandler, pointMapTypeHandler, precisePointHandler],
- click: [precisePointHandler, pointMapTypeHandler],
+ keydown: [zoomKeysHandler, undoRedoHandler, precisePointHandler],
+ click: precisePointHandler,
dblclick: precisePointHandler,
}
}
}
-function zoomKeysHandler(event, editor) {
- if (event.key === "+") {
- editor.draw.zoom(editor.draw.zoom() * 1.25)
- } else if (event.key === "-" || event.key === "_") {
- editor.draw.zoom(editor.draw.zoom() / 1.25)
- } else {
+// mousedown
+function selectionHandler(event, editor) {
+ if (event.button != buttons.left) {
return
}
- event.preventDefault()
-}
-// click, keydown
-function pointMapTypeHandler(event, editor, state) {
- const cleanup = function() {
- state.menu.remove()
- for (let i in state) {
- delete state[i]
- }
- }
- const commit = function() {
- editor.finishAction()
- cleanup()
- }
- const cancel = function() {
- // NOTE: I would use editor.undo(), but I'm not sure
- // if I'll allow asynchronous menus,etc. in the future
- editor.mapPointsById(state.orig, state.map.a, state.map.b)
- cleanup()
- }
- const change = function(newvalue) {
- editor.mapPointsById(newvalue, state.map.a, state.map.b)
- editor.updateDisplay()
- }
+ let p = editor.draw.point(event.clientX, event.clientY)
- if (event.type === "keydown") {
- if (!state.menu) {
- return
- }
- if (event.key === "Enter") {
- commit(event)
- } else if (event.key === "Escape") {
- cancel(event)
- } else {
- return
- }
- event.preventDefault()
+ let x = editor.thingAt(p, "#points")
+ if (x) {
+ x.select()
return
}
- if (event.type != "click") {
+ x = editor.thingAt(p, "#pointmaps")
+ if (x) {
+ x.select()
return
}
- // No matter where the user clicks, the old
- // menu should canceled
- if (state.menu) {
- cancel()
- }
-
- let cursor = editor.draw.point(event.clientX, event.clientY)
- let point = editor.thingAt(cursor, "#points")
- if (point) {
- return
- }
+ editor.draw.select()
+}
- let map = editor.thingAt(cursor, "#pointmaps")
- if (!map) {
+function zoomKeysHandler(event, editor) {
+ if (event.key === "+") {
+ editor.draw.zoom(editor.draw.zoom() * 1.25)
+ } else if (event.key === "-" || event.key === "_") {
+ editor.draw.zoom(editor.draw.zoom() / 1.25)
+ } else {
return
}
-
- map.select()
- let ref = idRef(map.attr("id"))
- state.map = editor.backend.cache.pointmaps[ref.id]
- state.orig = state.map.type
- if (state.menu) {
- throw new Error("Menu should have already been removed")
- }
- state.menu = document.body.querySelector(".toolbar")
- .appendChild(
- item(radioMenu(editor, "Type", ["wall", "door"], state.orig, { callbacks: {
- commit: commit,
- change: change
- }}))
- )
-
event.preventDefault()
}
@@ -298,6 +304,10 @@ function radioMenu(editor, key, values, initial, options) {
container.append(radios[i])
}
+ if (options.nosubmit) {
+ return menu
+ }
+
container.appendChild(document.createTextNode(" "))
let submit = container.appendChild(document.createElement("input"))
submit.setAttribute("type", "submit")
@@ -351,124 +361,9 @@ function undoRedoHandler(event, editor) {
event.preventDefault()
}
+// mousedown, mousemove, mouseup
function precisePointHandler(event, editor, state) {
- const subs = {
- add: preciseAddPointHandler,
- edit: preciseEditPointHandler
- }
- const callSubWithEvent = function(name, event) {
- state[name] = state[name] ?? {}
- let done = subs[name](event, editor, state[name])
- if (event.type === "cleanup") {
- return
- }
- if (done) {
- delete state.handler
- } else if (event.defaultPrevented && name !== "add") {
- state.handler = name
- }
- return done
- }
- const callSub = function(name) {
- callSubWithEvent(name, event)
- }
- const cleanupSub = function(name) {
- callSubWithEvent(name, { type: "cleanup" })
- }
-
- if (state.handler) {
- if (callSub(state.handler)) {
- callSub("add")
- }
- }
-
- callSub("add")
- if (event.type === "dblclick" && !event.defaultPrevented) {
- callSub("edit")
- if (event.defaultPrevented) {
- callSubWithEvent("add", { type: "cleanup" })
- }
- }
-}
-
-// mousedown, mousemove, mouseup, keydown, dblclick
-function preciseAddPointHandler(event, editor, state) {
- const cleanup = function() {
- state.line.remove()
- state.point.remove()
- state.menu.remove()
- for (let i in state) {
- delete state[i]
- }
- }
- const updatePoint = function(p, options) {
- options = options ?? {}
- let origin = state.from.vec()
- state.point.move(p.x, p.y)
- let instead = editor.pointAt(p)
- if (instead && instead != state.from) {
- state.point.hide()
- p = instead.select().pos()
- state.gotsnapped = true
- } else if (state.gotsnapped) {
- state.gotsnapped = false
- state.point.show().select()
- }
- state.line.plot(origin.x, origin.y, p.x, p.y)
- if (!options.leave_input) {
- state.len.value = userLength(editor,
- editor.units.snapTo(origin.distanceTo(p), editor.unit))
- }
- }
- const addWall = function() {
- state.point.remove()
- let p = editor.addPoint(state.point.pos())
- editor.mapPoints("wall", state.from, p)
- cleanup()
- editor.finishAction()
- }
-
- if (event.type === "cleanup") {
- if (state.point) {
- cleanup()
- }
- return
- }
-
- if (!event.key && event.button !== buttons.left) {
- return
- }
-
- let p = editor.draw.point(event.clientX, event.clientY).vec()
- if (event.type === "dblclick") {
- if (state.point && state.from.vec().distanceTo(p) > 0) {
- addWall()
- event.preventDefault()
- }
- return
- }
-
- if (event.type === "mousedown") {
- if (state.point) {
- if (state.point.inside(p.x, p.y)) {
- state.moving = true
- }
- event.preventDefault()
- return
- }
-
- state.from = editor.pointAt(p)
- if (!state.from) {
- return
- }
- state.moving = true
- state.line = editor.ui.bottom.line()
- .addClass("wall")
- .addClass("preview")
- state.point = editor.ui.top.circle()
- .addClass("point")
- .addClass("preview")
- .select()
+ const init = function() {
state.menu = document.body.querySelector(".toolbar")
.appendChild(document.createElement("li"))
state.menu.classList.add("menu")
@@ -495,95 +390,174 @@ function preciseAddPointHandler(event, editor, state) {
updatePoint(vecs[1], { leave_input: true })
}
})
- event.preventDefault()
- return
}
-
- if (!state.line) {
- return
+ const cleanup = function() {
+ if (state.moveTimeout != null) {
+ clearTimeout(state.moveTimeout)
+ }
+ if (state.menu != undefined) {
+ state.menu.remove()
+ }
+ for (let i in state) {
+ if (i !== "lastUp") {
+ delete state[i]
+ }
+ }
}
+ const updatePoint = function(p, options) {
+ options = options ?? {}
- if (event.type === "mousemove") {
- if (!state.moving) {
- return
+ if (state.snapmap == null) {
+ editor.movePoint(state.to, p)
+ editor.updateDisplay()
}
- let sp = state.from.vec()
- p = snap(editor.units.snapTo(p, editor.unit), sp, 8)
- updatePoint(p)
- } else if (event.type === "mouseup") {
- if (state.from.inside(p.x, p.y)) {
- cleanup();
- } else {
- state.moving = false
- state.lastmoving = Date.now()
+
+ let points = editor.thingsAt(p, "#points")
+ let fid = lib.getId(state.from)
+ let tid
+ if (state.snapmap == null) {
+ tid = lib.getId(state.to)
}
- } else if (event.type === "keydown") {
- if (event.key === "Enter") {
- addWall()
- return
- } else if (event.key !== "Escape") {
- return
+ let instead
+ for (let i in points) {
+ let id = lib.getId(points[i])
+ if (id !== tid && id !== fid) {
+ instead = id
+ }
}
- cleanup();
- } else {
- return
- }
- event.preventDefault()
-}
-// mousedown, dblclick
-function preciseEditPointHandler(event, editor, state) {
- const cleanup = function() {
- state.menu.remove()
- for (let i in state) {
- delete state[i]
+ if (instead != undefined) {
+ if (instead !== state.to) {
+ if (state.snapmap == null) {
+ editor.remove(state.to)
+ } else {
+ editor.remove(state.snapmap)
+ }
+ state.to = editor.findRef(backend.newRef("points", instead))
+ state.snapmap = editor.mapPoints("wall", state.from, state.to)
+ }
+ } else if (state.snapmap != null) {
+ editor.remove(state.snapmap)
+ state.snapmap = null
+ state.to = editor.addPoint(p, true)
+ editor.mapPoints("wall", state.from, state.to)
+ editor.updateDisplay()
+ state.to = editor.findRef(state.to)
}
- state.done = true
- }
- if ((event.type !== "dblclick" && event.type !== "mousedown") ||
- event.button != buttons.left) {
- return state.done
+ if (!options.leave_input) {
+ state.len.value = userLength(editor,
+ editor.units.snapTo(state.origin.distanceTo(p), editor.unit))
+ }
+ }
+ const doMove = function() {
+ // This is racy
+ state.moveTimeout = null
+ updatePoint(snap(editor.units.snapTo(state.move, editor.unit), state.origin, 8))
+ }
+ const revert = function() {
+ /*
+ * NOTE: WARNING: If allowing asyncronous edits this would be bad
+ * I should introduce a revert function which takes diffs and reverts
+ * them specifically, and I suppose pass a diff id with every single action.
+ * I think asyncronous actions would add very little in terms of value,
+ * and take time to implement. Better to disallow for now.
+ */
+ editor.finishAction()
+ editor.undo()
+ cleanup()
+ }
+ const commit = function() {
+ editor.finishAction()
+ cleanup()
}
- let point = editor.pointAt(editor.draw.point(event.clientX, event.clientY))
- if (!point) {
- if (state.point) {
- cleanup()
- event.preventDefault()
- return true
- }
+ if (event.button !== buttons.left) {
return
}
- if (state.point && state.point != point) {
- cleanup()
- event.preventDefault()
- return true
+ let cursor = editor.draw.point(event.clientX, event.clientY).vec()
+ if (event.type === "mouseup") {
+ state.lastUp = Date.now()
}
- if (event.type === "dblclick") {
- if (state.menu) {
+ if (state.to == undefined) {
+ if (event.type === "mousedown") {
+ if (state.from != undefined) {
+ return
+ }
+
+ state.from = editor.selectedPoint()
+ if (!state.from) {
+ return
+ }
+
+ if (state.lastUp != null && elapsed(state.lastUp) <= 500) {
+ state.to = state.from
+ state.from = null
+
+ // I want the first pointmap defined, but this for now
+ let m = editor.backend.mappedPoints[lib.getId(state.to)]
+ for (let point in m) {
+ state.from = editor.findRef(backend.newRef("points", point))
+ break
+ }
+ if (!state.from) {
+ // I mean, there really shouldn't be an orphaned point,
+ // and I see no reason to move the only point in the plan
+ cleanup()
+ throw new Error("Can't move unmapped points")
+ }
+ init()
+ }
+
+ state.origin = state.from.vec()
+ } else if (event.type === "mouseup") {
cleanup()
+ return // Or should I preventDefault()?
+ } else if (event.type === "mousemove" && state.origin != undefined &&
+ state.origin.distanceTo(cursor) > 200) {
+ state.to = editor.addPoint(cursor, true)
+ editor.mapPoints("wall", state.from, state.to)
+ editor.updateDisplay()
+ state.to = editor.findRef(state.to)
+ init()
}
- state.done = false
- state.point = point
- state.menu = document.createElement("li")
- state.menu.appendChild(document.createTextNode("Point: "))
- state.menu.appendChild(
- ui.input("Delete", "Delete point", {
- attributes: { type: "button" },
- handlers: { click: function() {
- editor.removePoint(state.point)
- cleanup()
- }},
- })
- )
- state.point.select()
- document.querySelector(".toolbar")
- .appendChild(state.menu)
event.preventDefault()
+ return
}
+
+ if (state.to == undefined) {
+ return
+ }
+ if (!state.from) {
+ throw new Error("Hmm")
+ }
+
+ if (event.type === "mousemove") {
+ // This is still far too expensive, it runs up my fans in seconds.
+ state.move = cursor
+ if (state.moveTimeout == null) {
+ state.moveTimeout = setTimeout(doMove, 35)
+ }
+ } else if (event.type === "mouseup") {
+ if (state.from && state.from.inside(cursor.x, cursor.y)) {
+ revert()
+ } else {
+ // Not that it makes much difference, but should probably use
+ // state.to's position
+ if (state.to && state.origin.distanceTo(cursor) > 0) {
+ commit()
+ } else {
+ cleanup()
+ }
+ }
+ } else {
+ console.warn("Bit of a state mismatch, not that big of a deal though")
+ commit()
+ return
+ }
+ event.preventDefault()
}
function parseUserLength(editor, length) {
@@ -709,4 +683,8 @@ function item(node) {
return i
}
+function elapsed(since) {
+ return Date.now() - since
+}
+
window.onload = init