add experiment what Karax would look like without DOM diff'ing but with barebones reactive programming
This commit is contained in:
parent
dd0ea311a3
commit
75739f8170
3 changed files with 946 additions and 0 deletions
752
experiments/trello/knete.nim
Normal file
752
experiments/trello/knete.nim
Normal file
|
|
@ -0,0 +1,752 @@
|
||||||
|
## Raw DOM manipulation. No DOM diff'ing, no cry.
|
||||||
|
|
||||||
|
import macros, tables
|
||||||
|
from strutils import startsWith, toLowerAscii, toUpperAscii
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
|
type kstring* = cstring
|
||||||
|
else:
|
||||||
|
type kstring* = string
|
||||||
|
|
||||||
|
type
|
||||||
|
Tag* {.pure.} = enum
|
||||||
|
text = "#text",
|
||||||
|
|
||||||
|
html, head, title, base, link, meta, style,
|
||||||
|
script, noscript,
|
||||||
|
body, section, nav, article, aside,
|
||||||
|
h1, h2, h3, h4, h5, h6,
|
||||||
|
header, footer, address, main
|
||||||
|
|
||||||
|
p, hr, pre, blockquote, ol, ul, li,
|
||||||
|
dl, dt, dd,
|
||||||
|
figure, figcaption,
|
||||||
|
|
||||||
|
tdiv = "div",
|
||||||
|
|
||||||
|
a, em, strong, small,
|
||||||
|
strikethrough = "s", cite, quote,
|
||||||
|
dfn, abbr, data, time, code, `var` = "var", samp,
|
||||||
|
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
|
||||||
|
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
|
||||||
|
ins, del, img, iframe, embed, `object` = "object",
|
||||||
|
param, video, audio, source, track, canvas, map,
|
||||||
|
area, svg, math,
|
||||||
|
|
||||||
|
table, caption, colgroup, col, tbody, thead,
|
||||||
|
tfoot, tr, td, th,
|
||||||
|
|
||||||
|
form, fieldset, legend, label, input, button,
|
||||||
|
select, datalist, optgroup, option, textarea,
|
||||||
|
keygen, output, progress, meter,
|
||||||
|
details, summary, command, menu
|
||||||
|
|
||||||
|
type
|
||||||
|
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
|
||||||
|
onclick, ## An element is clicked.
|
||||||
|
oncontextmenu, ## An element is right-clicked.
|
||||||
|
ondblclick, ## An element is double clicked.
|
||||||
|
onkeyup, ## A key was released.
|
||||||
|
onkeydown, ## A key is pressed.
|
||||||
|
onkeypressed, # A key was pressed.
|
||||||
|
onfocus, ## An element got the focus.
|
||||||
|
onblur, ## An element lost the focus.
|
||||||
|
onchange, ## The selected value of an element was changed.
|
||||||
|
onscroll, ## The user scrolled within an element.
|
||||||
|
|
||||||
|
onmousedown, ## A pointing device button (usually a mouse) is pressed
|
||||||
|
## on an element.
|
||||||
|
onmouseenter, ## A pointing device is moved onto the element that
|
||||||
|
## has the listener attached.
|
||||||
|
onmouseleave, ## A pointing device is moved off the element that
|
||||||
|
## has the listener attached.
|
||||||
|
onmousemove, ## A pointing device is moved over an element.
|
||||||
|
onmouseout, ## A pointing device is moved off the element that
|
||||||
|
## has the listener attached or off one of its children.
|
||||||
|
onmouseover, ## A pointing device is moved onto the element that has
|
||||||
|
## the listener attached or onto one of its children.
|
||||||
|
onmouseup, ## A pointing device button is released over an element.
|
||||||
|
|
||||||
|
ondrag, ## An element or text selection is being dragged (every 350ms).
|
||||||
|
ondragend, ## A drag operation is being ended (by releasing a mouse button
|
||||||
|
## or hitting the escape key).
|
||||||
|
ondragenter, ## A dragged element or text selection enters a valid drop target.
|
||||||
|
ondragleave, ## A dragged element or text selection leaves a valid drop target.
|
||||||
|
ondragover, ## An element or text selection is being dragged over a valid
|
||||||
|
## drop target (every 350ms).
|
||||||
|
ondragstart, ## The user starts dragging an element or text selection.
|
||||||
|
ondrop, ## An element is dropped on a valid drop target.
|
||||||
|
|
||||||
|
onsubmit, ## A form is submitted
|
||||||
|
oninput, ## An input value changes
|
||||||
|
|
||||||
|
onkeyupenter, ## vdom extension: an input field received the ENTER key press
|
||||||
|
onkeyuplater ## vdom extension: a key was pressed and some time
|
||||||
|
## passed (useful for on-the-fly text completions)
|
||||||
|
|
||||||
|
|
||||||
|
macro buildLookupTables(): untyped =
|
||||||
|
var a = newTree(nnkBracket)
|
||||||
|
for i in low(Tag)..high(Tag):
|
||||||
|
let x = $i
|
||||||
|
let y = if x[0] == '#': x else: toUpperAscii(x)
|
||||||
|
a.add(newCall("kstring", newLit(y)))
|
||||||
|
var e = newTree(nnkBracket)
|
||||||
|
for i in low(EventKind)..high(EventKind):
|
||||||
|
e.add(newCall("kstring", newLit(substr($i, 2))))
|
||||||
|
|
||||||
|
template tmpl(a, e) {.dirty.} =
|
||||||
|
const
|
||||||
|
toTagName*: array[Tag, kstring] = a
|
||||||
|
toEventName*: array[EventKind, kstring] = e
|
||||||
|
|
||||||
|
result = getAst tmpl(a, e)
|
||||||
|
|
||||||
|
buildLookupTables()
|
||||||
|
|
||||||
|
# ---------------- CSS ----------------------------------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
StyleAttr* {.pure.} = enum
|
||||||
|
## The style attributes supported by the virtual DOM.
|
||||||
|
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
|
||||||
|
alignContent
|
||||||
|
alignItems
|
||||||
|
alignSelf
|
||||||
|
animation
|
||||||
|
animationDelay
|
||||||
|
animationDirection
|
||||||
|
animationDuration
|
||||||
|
animationFillMode
|
||||||
|
animationIterationCount
|
||||||
|
animationName
|
||||||
|
animationTimingFunction
|
||||||
|
animationPlayState
|
||||||
|
background
|
||||||
|
backgroundAttachment
|
||||||
|
backgroundColor
|
||||||
|
backgroundImage
|
||||||
|
backgroundPosition
|
||||||
|
backgroundRepeat
|
||||||
|
backgroundClip
|
||||||
|
backgroundOrigin
|
||||||
|
backgroundSize
|
||||||
|
backfaceVisibility
|
||||||
|
border
|
||||||
|
borderBottom
|
||||||
|
borderBottomColor
|
||||||
|
borderBottomLeftRadius
|
||||||
|
borderBottomRightRadius
|
||||||
|
borderBottomStyle
|
||||||
|
borderBottomWidth
|
||||||
|
borderCollapse
|
||||||
|
borderColor
|
||||||
|
borderImage
|
||||||
|
borderImageOutset
|
||||||
|
borderImageRepeat
|
||||||
|
borderImageSlice
|
||||||
|
borderImageSource
|
||||||
|
borderImageWidth
|
||||||
|
borderLeft
|
||||||
|
borderLeftColor
|
||||||
|
borderLeftStyle
|
||||||
|
borderLeftWidth
|
||||||
|
borderRadius
|
||||||
|
borderRight
|
||||||
|
borderRightColor
|
||||||
|
borderRightStyle
|
||||||
|
borderRightWidth
|
||||||
|
borderSpacing
|
||||||
|
borderStyle
|
||||||
|
borderTop
|
||||||
|
borderTopColor
|
||||||
|
borderTopLeftRadius
|
||||||
|
borderTopRightRadius
|
||||||
|
borderTopStyle
|
||||||
|
borderTopWidth
|
||||||
|
borderWidth
|
||||||
|
bottom
|
||||||
|
boxDecorationBreak
|
||||||
|
boxShadow
|
||||||
|
boxSizing
|
||||||
|
captionSide
|
||||||
|
clear
|
||||||
|
clip
|
||||||
|
color
|
||||||
|
columnCount
|
||||||
|
columnFill
|
||||||
|
columnGap
|
||||||
|
columnRule
|
||||||
|
columnRuleColor
|
||||||
|
columnRuleStyle
|
||||||
|
columnRuleWidth
|
||||||
|
columns
|
||||||
|
columnSpan
|
||||||
|
columnWidth
|
||||||
|
content
|
||||||
|
counterIncrement
|
||||||
|
counterReset
|
||||||
|
cursor
|
||||||
|
direction
|
||||||
|
display
|
||||||
|
emptyCells
|
||||||
|
filter
|
||||||
|
flex
|
||||||
|
flexBasis
|
||||||
|
flexDirection
|
||||||
|
flexFlow
|
||||||
|
flexGrow
|
||||||
|
flexShrink
|
||||||
|
flexWrap
|
||||||
|
cssFloat
|
||||||
|
font
|
||||||
|
fontFamily
|
||||||
|
fontSize
|
||||||
|
fontSizeAdjust
|
||||||
|
fontStretch
|
||||||
|
fontStyle
|
||||||
|
fontVariant
|
||||||
|
fontWeight
|
||||||
|
hangingPunctuation
|
||||||
|
height
|
||||||
|
hyphens
|
||||||
|
icon
|
||||||
|
imageOrientation
|
||||||
|
justifyContent
|
||||||
|
left
|
||||||
|
letterSpacing
|
||||||
|
lineHeight
|
||||||
|
listStyle
|
||||||
|
listStyleImage
|
||||||
|
listStylePosition
|
||||||
|
listStyleType
|
||||||
|
margin
|
||||||
|
marginBottom
|
||||||
|
marginLeft
|
||||||
|
marginRight
|
||||||
|
marginTop
|
||||||
|
maxHeight
|
||||||
|
maxWidth
|
||||||
|
minHeight
|
||||||
|
minWidth
|
||||||
|
navDown
|
||||||
|
navIndex
|
||||||
|
navLeft
|
||||||
|
navRight
|
||||||
|
navUp
|
||||||
|
opacity
|
||||||
|
order
|
||||||
|
orphans
|
||||||
|
outline
|
||||||
|
outlineColor
|
||||||
|
outlineOffset
|
||||||
|
outlineStyle
|
||||||
|
outlineWidth
|
||||||
|
overflow
|
||||||
|
overflowX
|
||||||
|
overflowY
|
||||||
|
padding
|
||||||
|
paddingBottom
|
||||||
|
paddingLeft
|
||||||
|
paddingRight
|
||||||
|
paddingTop
|
||||||
|
pageBreakAfter
|
||||||
|
pageBreakBefore
|
||||||
|
pageBreakInside
|
||||||
|
perspective
|
||||||
|
perspectiveOrigin
|
||||||
|
pointerEvents
|
||||||
|
position
|
||||||
|
quotes
|
||||||
|
resize
|
||||||
|
right
|
||||||
|
scrollbar3dLightColor # note: missing in w3schools reference
|
||||||
|
scrollbarArrowColor # note: missing in w3schools reference
|
||||||
|
scrollbarBaseColor # note: missing in w3schools reference
|
||||||
|
scrollbarDarkshadowColor # note: missing in w3schools reference
|
||||||
|
scrollbarFaceColor # note: missing in w3schools reference
|
||||||
|
scrollbarHighlightColor # note: missing in w3schools reference
|
||||||
|
scrollbarShadowColor # note: missing in w3schools reference
|
||||||
|
scrollbarTrackColor # note: missing in w3schools reference
|
||||||
|
tableLayout
|
||||||
|
tabSize
|
||||||
|
textAlign
|
||||||
|
textAlignLast
|
||||||
|
textDecoration
|
||||||
|
textDecorationColor
|
||||||
|
textDecorationLine
|
||||||
|
textDecorationStyle
|
||||||
|
textIndent
|
||||||
|
textJustify
|
||||||
|
textOverflow
|
||||||
|
textShadow
|
||||||
|
textTransform
|
||||||
|
top
|
||||||
|
transform
|
||||||
|
transformOrigin
|
||||||
|
transformStyle
|
||||||
|
transition
|
||||||
|
transitionDelay
|
||||||
|
transitionDuration
|
||||||
|
transitionProperty
|
||||||
|
transitionTimingFunction
|
||||||
|
unicodeBidi
|
||||||
|
userSelect
|
||||||
|
verticalAlign
|
||||||
|
visibility
|
||||||
|
whiteSpace
|
||||||
|
width
|
||||||
|
wordBreak
|
||||||
|
wordSpacing
|
||||||
|
wordWrap
|
||||||
|
widows
|
||||||
|
zIndex
|
||||||
|
|
||||||
|
macro buildStyleLookupTable(): untyped =
|
||||||
|
var e = newTree(nnkBracket)
|
||||||
|
for i in low(StyleAttr)..high(StyleAttr):
|
||||||
|
e.add(newCall("kstring", newLit($i)))
|
||||||
|
template tmpl(e) {.dirty.} =
|
||||||
|
const
|
||||||
|
toStyleAttrName: array[StyleAttr, kstring] = e
|
||||||
|
result = getAst tmpl(e)
|
||||||
|
|
||||||
|
buildStyleLookupTable()
|
||||||
|
|
||||||
|
type
|
||||||
|
StyleObj {.importc.} = object
|
||||||
|
background*: cstring
|
||||||
|
backgroundAttachment*: cstring
|
||||||
|
backgroundColor*: cstring
|
||||||
|
backgroundImage*: cstring
|
||||||
|
backgroundPosition*: cstring
|
||||||
|
backgroundRepeat*: cstring
|
||||||
|
border*: cstring
|
||||||
|
borderBottom*: cstring
|
||||||
|
borderBottomColor*: cstring
|
||||||
|
borderBottomStyle*: cstring
|
||||||
|
borderBottomWidth*: cstring
|
||||||
|
borderColor*: cstring
|
||||||
|
borderLeft*: cstring
|
||||||
|
borderLeftColor*: cstring
|
||||||
|
borderLeftStyle*: cstring
|
||||||
|
borderLeftWidth*: cstring
|
||||||
|
borderRight*: cstring
|
||||||
|
borderRightColor*: cstring
|
||||||
|
borderRightStyle*: cstring
|
||||||
|
borderRightWidth*: cstring
|
||||||
|
borderStyle*: cstring
|
||||||
|
borderTop*: cstring
|
||||||
|
borderTopColor*: cstring
|
||||||
|
borderTopStyle*: cstring
|
||||||
|
borderTopWidth*: cstring
|
||||||
|
borderWidth*: cstring
|
||||||
|
bottom*: cstring
|
||||||
|
captionSide*: cstring
|
||||||
|
clear*: cstring
|
||||||
|
clip*: cstring
|
||||||
|
color*: cstring
|
||||||
|
cursor*: cstring
|
||||||
|
direction*: cstring
|
||||||
|
display*: cstring
|
||||||
|
emptyCells*: cstring
|
||||||
|
cssFloat*: cstring
|
||||||
|
font*: cstring
|
||||||
|
fontFamily*: cstring
|
||||||
|
fontSize*: cstring
|
||||||
|
fontStretch*: cstring
|
||||||
|
fontStyle*: cstring
|
||||||
|
fontVariant*: cstring
|
||||||
|
fontWeight*: cstring
|
||||||
|
height*: cstring
|
||||||
|
left*: cstring
|
||||||
|
letterSpacing*: cstring
|
||||||
|
lineHeight*: cstring
|
||||||
|
listStyle*: cstring
|
||||||
|
listStyleImage*: cstring
|
||||||
|
listStylePosition*: cstring
|
||||||
|
listStyleType*: cstring
|
||||||
|
margin*: cstring
|
||||||
|
marginBottom*: cstring
|
||||||
|
marginLeft*: cstring
|
||||||
|
marginRight*: cstring
|
||||||
|
marginTop*: cstring
|
||||||
|
maxHeight*: cstring
|
||||||
|
maxWidth*: cstring
|
||||||
|
minHeight*: cstring
|
||||||
|
minWidth*: cstring
|
||||||
|
overflow*: cstring
|
||||||
|
padding*: cstring
|
||||||
|
paddingBottom*: cstring
|
||||||
|
paddingLeft*: cstring
|
||||||
|
paddingRight*: cstring
|
||||||
|
paddingTop*: cstring
|
||||||
|
pageBreakAfter*: cstring
|
||||||
|
pageBreakBefore*: cstring
|
||||||
|
pointerEvents*: cstring
|
||||||
|
position*: cstring
|
||||||
|
right*: cstring
|
||||||
|
scrollbar3dLightColor*: cstring
|
||||||
|
scrollbarArrowColor*: cstring
|
||||||
|
scrollbarBaseColor*: cstring
|
||||||
|
scrollbarDarkshadowColor*: cstring
|
||||||
|
scrollbarFaceColor*: cstring
|
||||||
|
scrollbarHighlightColor*: cstring
|
||||||
|
scrollbarShadowColor*: cstring
|
||||||
|
scrollbarTrackColor*: cstring
|
||||||
|
tableLayout*: cstring
|
||||||
|
textAlign*: cstring
|
||||||
|
textDecoration*: cstring
|
||||||
|
textIndent*: cstring
|
||||||
|
textTransform*: cstring
|
||||||
|
transform*: cstring
|
||||||
|
top*: cstring
|
||||||
|
verticalAlign*: cstring
|
||||||
|
visibility*: cstring
|
||||||
|
width*: cstring
|
||||||
|
wordSpacing*: cstring
|
||||||
|
zIndex*: int
|
||||||
|
Style* = ref StyleObj
|
||||||
|
|
||||||
|
ElementObj {.importc.} = object
|
||||||
|
id*, class*, value*: cstring
|
||||||
|
disabled*: bool
|
||||||
|
style*: Style
|
||||||
|
Element* = ref ElementObj
|
||||||
|
|
||||||
|
DocumentObj {.importc.} = object
|
||||||
|
Document = ref DocumentObj
|
||||||
|
|
||||||
|
proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
|
||||||
|
|
||||||
|
proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) =
|
||||||
|
for x in pairs:
|
||||||
|
e.style.setStyle(toStyleAttrName[x[0]], x[1])
|
||||||
|
|
||||||
|
# ------- Tree manipulation ---------------------------
|
||||||
|
|
||||||
|
var document {.importc.}: Document
|
||||||
|
|
||||||
|
proc parent*(x: Element): Element {.importcpp: "#.parentNode".}
|
||||||
|
|
||||||
|
proc up*(x: Element; className: cstring): Element =
|
||||||
|
result = x
|
||||||
|
while result != nil and result.class != className:
|
||||||
|
result = result.parent
|
||||||
|
|
||||||
|
proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
|
||||||
|
proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
|
||||||
|
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
|
||||||
|
proc add*(n, child: Element) {.importcpp: "appendChild".}
|
||||||
|
|
||||||
|
proc removeChild(n, child: Element) {.importcpp.}
|
||||||
|
proc replaceChild(n, newNode, oldNode: Element) {.importcpp.}
|
||||||
|
|
||||||
|
proc replace*(self, by: Element) = replaceChild(self.parent, by, self)
|
||||||
|
proc delete*(self: Element) = removeChild(self.parent, self)
|
||||||
|
|
||||||
|
proc insertBefore(n, newNode, before: Element) {.importcpp.}
|
||||||
|
proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before)
|
||||||
|
|
||||||
|
proc createTextNode(d: Document, text: cstring): Element {.importcpp.}
|
||||||
|
proc text*(s: kstring): Element = createTextNode(document, s)
|
||||||
|
|
||||||
|
iterator items*(n: Element): Element =
|
||||||
|
for i in 0..<n.len: yield n[i]
|
||||||
|
|
||||||
|
proc createElement(d: Document, identifier: cstring): Element {.importcpp.}
|
||||||
|
proc setAttr*(n: Element; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
|
||||||
|
|
||||||
|
proc tree*(kind: Tag; kids: varargs[Element]): Element =
|
||||||
|
result = createElement(document, toTagName[kind])
|
||||||
|
for k in kids: result.add k
|
||||||
|
|
||||||
|
proc tree*(kind: Tag; attrs: openarray[(kstring, kstring)];
|
||||||
|
kids: varargs[Element]): Element =
|
||||||
|
result = tree(kind, kids)
|
||||||
|
for a in attrs: result.setAttr(a[0], a[1])
|
||||||
|
|
||||||
|
# other arbitrary stuff belonging to Element
|
||||||
|
proc focus*(e: Element) {.importcpp.}
|
||||||
|
proc scrollIntoView*(e: Element) {.importcpp.}
|
||||||
|
proc blur*(e: Element) {.importcpp.}
|
||||||
|
|
||||||
|
proc toChecked*(checked: bool): cstring =
|
||||||
|
(if checked: cstring"checked" else: cstring(nil))
|
||||||
|
|
||||||
|
proc toDisabled*(disabled: bool): cstring =
|
||||||
|
(if disabled: cstring"disabled" else: cstring(nil))
|
||||||
|
|
||||||
|
# -------------- Timer handling --------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
Timeout* {.importc.} = ref object of RootObj
|
||||||
|
|
||||||
|
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
|
||||||
|
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
|
||||||
|
|
||||||
|
# -------------- Event handling --------------------
|
||||||
|
|
||||||
|
type
|
||||||
|
EventObj {.importc.} = object
|
||||||
|
target*: Element
|
||||||
|
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
|
||||||
|
code*: cstring
|
||||||
|
isComposing*: bool
|
||||||
|
key*: cstring
|
||||||
|
keyCode*: int
|
||||||
|
location*: int
|
||||||
|
Event* = ref EventObj
|
||||||
|
|
||||||
|
EventHandler* = proc (ev: Event) {.closure.}
|
||||||
|
|
||||||
|
proc addEventListener(e: Element, ev: cstring, cb: proc(ev: Event),
|
||||||
|
useCapture = false) {.importcpp.}
|
||||||
|
|
||||||
|
proc addEventHandler*(e: Element; k: EventKind; action: EventHandler) =
|
||||||
|
case k
|
||||||
|
of EventKind.onkeyuplater:
|
||||||
|
proc laterWrapper(): EventHandler =
|
||||||
|
let action = action
|
||||||
|
var timer: Timeout
|
||||||
|
result = proc (ev: Event) =
|
||||||
|
proc wrapper() = action(ev)
|
||||||
|
if timer != nil: clearTimeout(timer)
|
||||||
|
timer = setTimeout(wrapper, 400)
|
||||||
|
|
||||||
|
e.addEventListener("keyup", laterWrapper())
|
||||||
|
of EventKind.onkeyupenter:
|
||||||
|
proc enterWrapper(): EventHandler =
|
||||||
|
let action = action
|
||||||
|
result = proc (ev: Event) =
|
||||||
|
if ev.keyCode == 13: action(ev)
|
||||||
|
|
||||||
|
e.addEventListener("keyup", enterWrapper())
|
||||||
|
else:
|
||||||
|
e.addEventListener(toEventName[k], action)
|
||||||
|
|
||||||
|
proc addEventHandler*(e: Element; k: EventKind; action: proc()) =
|
||||||
|
addEventHandler e, k, (proc (ev: Event) = action())
|
||||||
|
|
||||||
|
proc prepareDragData*(ev: Event; datatype, data: cstring)
|
||||||
|
{.importcpp: "#.dataTransfer.setData(@)".}
|
||||||
|
|
||||||
|
proc recvDragData*(ev: Event; datatype: cstring): cstring
|
||||||
|
{.importcpp: "#.dataTransfer.getData(@)".}
|
||||||
|
|
||||||
|
proc preventDefault*(ev: Event) {.importcpp.}
|
||||||
|
|
||||||
|
# ------------------ Init handling -----------------------------------
|
||||||
|
|
||||||
|
proc replaceById(id: cstring; newTree: Element) =
|
||||||
|
let x = getElementById(id)
|
||||||
|
x.parent.replaceChild(newTree, x)
|
||||||
|
newTree.id = id
|
||||||
|
|
||||||
|
proc setWindowOnload(h: EventHandler) {.importcpp: "window.onload = #".}
|
||||||
|
proc setInitializer*(initializer: proc (hashPart: kstring): Element;
|
||||||
|
root: cstring = "ROOT") =
|
||||||
|
var onhashChange {.importc: "window.onhashchange".}: proc()
|
||||||
|
var hashPart {.importc: "window.location.hash".}: cstring
|
||||||
|
|
||||||
|
setWindowOnload proc (ev: Event) =
|
||||||
|
replaceById root, initializer(hashPart)
|
||||||
|
onhashchange = proc () =
|
||||||
|
replaceById root, initializer(hashPart)
|
||||||
|
|
||||||
|
# ------------------ DSL section -------------------------------------
|
||||||
|
|
||||||
|
const
|
||||||
|
StmtContext = ["inc", "echo", "dec", "!"]
|
||||||
|
SpecialAttrs = ["id", "class", "value"]
|
||||||
|
|
||||||
|
type
|
||||||
|
ComponentKind {.pure.} = enum
|
||||||
|
None,
|
||||||
|
Tag
|
||||||
|
|
||||||
|
var
|
||||||
|
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
|
||||||
|
|
||||||
|
proc isComponent(x: string): ComponentKind {.compileTime.} =
|
||||||
|
allcomponents.getOrDefault(x)
|
||||||
|
|
||||||
|
proc addTags() {.compileTime.} =
|
||||||
|
let x = (bindSym"Tag").getTypeImpl
|
||||||
|
expectKind(x, nnkEnumTy)
|
||||||
|
for i in ord(Tag.html)..ord(Tag.high):
|
||||||
|
# +1 because of empty node at the start of the enum AST:
|
||||||
|
let tag = $x[i+1]
|
||||||
|
allcomponents[tag] = ComponentKind.Tag
|
||||||
|
|
||||||
|
static:
|
||||||
|
addTags()
|
||||||
|
|
||||||
|
|
||||||
|
proc getName(n: NimNode): string =
|
||||||
|
case n.kind
|
||||||
|
of nnkIdent:
|
||||||
|
result = $n.ident
|
||||||
|
of nnkAccQuoted:
|
||||||
|
result = ""
|
||||||
|
for i in 0..<n.len:
|
||||||
|
result.add getName(n[i])
|
||||||
|
of nnkStrLit..nnkTripleStrLit:
|
||||||
|
result = n.strVal
|
||||||
|
else:
|
||||||
|
#echo repr n
|
||||||
|
expectKind(n, nnkIdent)
|
||||||
|
|
||||||
|
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
|
||||||
|
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
||||||
|
|
||||||
|
proc toKstring(n: NimNode): NimNode =
|
||||||
|
if n.kind == nnkStrLit:
|
||||||
|
result = newCall(bindSym"kstring", n)
|
||||||
|
else:
|
||||||
|
result = copyNimNode(n)
|
||||||
|
for child in n:
|
||||||
|
result.add toKstring(child)
|
||||||
|
|
||||||
|
proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||||
|
# we need to distinguish statement and expression contexts:
|
||||||
|
# every call statement 's' needs to be transformed to 'dest.add s'.
|
||||||
|
# If expressions need to be distinguished from if statements. Since
|
||||||
|
# we know we start in a statement context, it's pretty simple to
|
||||||
|
# figure out expression contexts: In calls everything is an expression
|
||||||
|
# (except for the last child of the macros we consider here),
|
||||||
|
# lets, consts, types can be considered as expressions
|
||||||
|
# case is complex, calls are assumed to produce a value.
|
||||||
|
when defined(js):
|
||||||
|
template evHandler(): untyped = bindSym"addEventHandler"
|
||||||
|
else:
|
||||||
|
template evHandler(): untyped = ident"addEventHandler"
|
||||||
|
|
||||||
|
case n.kind
|
||||||
|
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
||||||
|
if tmpContext != nil:
|
||||||
|
result = newCall(bindSym"add", tmpContext, n)
|
||||||
|
else:
|
||||||
|
result = n
|
||||||
|
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
|
||||||
|
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
|
||||||
|
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
|
||||||
|
# recurse for the last son:
|
||||||
|
result = copyNimTree(n)
|
||||||
|
let L = n.len
|
||||||
|
assert n.len == result.len
|
||||||
|
if L > 0:
|
||||||
|
result[L-1] = tcall2(result[L-1], tmpContext)
|
||||||
|
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
|
||||||
|
nnkFinally:
|
||||||
|
# recurse for every child:
|
||||||
|
result = copyNimNode(n)
|
||||||
|
for x in n:
|
||||||
|
result.add tcall2(x, tmpContext)
|
||||||
|
of nnkCaseStmt:
|
||||||
|
# recurse for children, but don't add call for case ident
|
||||||
|
result = copyNimNode(n)
|
||||||
|
result.add n[0]
|
||||||
|
for i in 1 ..< n.len:
|
||||||
|
result.add tcall2(n[i], tmpContext)
|
||||||
|
of nnkProcDef:
|
||||||
|
let name = getName n[0]
|
||||||
|
if name.startsWith"on":
|
||||||
|
# turn it into an anon proc:
|
||||||
|
let anon = copyNimTree(n)
|
||||||
|
anon[0] = newEmptyNode()
|
||||||
|
if tmpContext == nil:
|
||||||
|
error "no Element to attach the event handler to"
|
||||||
|
else:
|
||||||
|
result = newCall(evHandler(), tmpContext,
|
||||||
|
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
|
||||||
|
else:
|
||||||
|
result = n
|
||||||
|
of nnkVarSection, nnkLetSection, nnkConstSection:
|
||||||
|
result = n
|
||||||
|
of nnkCallKinds - {nnkInfix}:
|
||||||
|
let op = getName(n[0])
|
||||||
|
if isComponent(op) == ComponentKind.Tag:
|
||||||
|
let tmp = genSym(nskLet, "tmp")
|
||||||
|
let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0]))
|
||||||
|
result = newTree(
|
||||||
|
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
|
||||||
|
newLetStmt(tmp, call))
|
||||||
|
for i in 1 ..< n.len:
|
||||||
|
# named parameters are transformed into attributes or events:
|
||||||
|
let x = n[i]
|
||||||
|
if x.kind == nnkExprEqExpr:
|
||||||
|
let key = getName x[0]
|
||||||
|
if key.startsWith("on"):
|
||||||
|
result.add newCall(evHandler(),
|
||||||
|
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
|
||||||
|
elif key in SpecialAttrs:
|
||||||
|
result.add newDotAsgn(tmp, key, x[1])
|
||||||
|
elif eqIdent(key, "style"):
|
||||||
|
result.add newCall(bindSym"applyStyles", tmp, toKstring x[1])
|
||||||
|
elif eqIdent(key, "setFocus"):
|
||||||
|
result.add newCall(bindSym"focus", tmp)
|
||||||
|
else:
|
||||||
|
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
|
||||||
|
elif eqIdent(x, "setFocus"):
|
||||||
|
result.add newCall(bindSym"focus", tmp)
|
||||||
|
else:
|
||||||
|
result.add tcall2(x, tmp)
|
||||||
|
if tmpContext == nil:
|
||||||
|
result.add tmp
|
||||||
|
else:
|
||||||
|
result.add newCall(bindSym"add", tmpContext, tmp)
|
||||||
|
elif tmpContext != nil and op notin StmtContext:
|
||||||
|
var hasEventHandlers = false
|
||||||
|
for i in 1..<n.len:
|
||||||
|
let it = n[i]
|
||||||
|
if it.kind in {nnkProcDef, nnkStmtList}:
|
||||||
|
hasEventHandlers = true
|
||||||
|
break
|
||||||
|
if not hasEventHandlers:
|
||||||
|
result = newCall(bindSym"add", tmpContext, n)
|
||||||
|
else:
|
||||||
|
let tmp = genSym(nskLet, "tmp")
|
||||||
|
var slicedCall = newCall(n[0])
|
||||||
|
let ex = newTree(nnkStmtListExpr)
|
||||||
|
ex.add newEmptyNode() # will become the let statement
|
||||||
|
for i in 1..<n.len:
|
||||||
|
let it = n[i]
|
||||||
|
if it.kind in {nnkProcDef, nnkStmtList}:
|
||||||
|
ex.add tcall2(it, tmp)
|
||||||
|
else:
|
||||||
|
slicedCall.add it
|
||||||
|
ex[0] = newLetStmt(tmp, slicedCall)
|
||||||
|
ex.add tmp
|
||||||
|
result = newCall(bindSym"add", tmpContext, ex)
|
||||||
|
elif op == "!" and n.len == 2:
|
||||||
|
result = n[1]
|
||||||
|
else:
|
||||||
|
result = n
|
||||||
|
else:
|
||||||
|
result = n
|
||||||
|
|
||||||
|
macro buildHtml*(tag, children: untyped): Element =
|
||||||
|
let kids = newProc(procType=nnkDo, body=children)
|
||||||
|
expectKind kids, nnkDo
|
||||||
|
var call: NimNode
|
||||||
|
if tag.kind in nnkCallKinds:
|
||||||
|
call = tag
|
||||||
|
else:
|
||||||
|
call = newCall(tag)
|
||||||
|
call.add body(kids)
|
||||||
|
result = tcall2(call, nil)
|
||||||
|
when defined(debugKaraxDsl):
|
||||||
|
echo repr result
|
||||||
|
|
||||||
|
macro buildHtml*(children: untyped): Element =
|
||||||
|
let kids = newProc(procType=nnkDo, body=children)
|
||||||
|
expectKind kids, nnkDo
|
||||||
|
result = tcall2(body(kids), nil)
|
||||||
|
when defined(debugKaraxDsl):
|
||||||
|
echo repr result
|
||||||
|
|
||||||
|
macro flatHtml*(tag: untyped): Element =
|
||||||
|
result = tcall2(tag, nil)
|
||||||
|
when defined(debugKaraxDsl):
|
||||||
|
echo repr result
|
||||||
151
experiments/trello/trello.nim
Normal file
151
experiments/trello/trello.nim
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
|
||||||
|
import knete, widgets
|
||||||
|
import karax / jstrutils
|
||||||
|
|
||||||
|
type
|
||||||
|
Attachable* = ref object of RootObj ## an element that is attachable to
|
||||||
|
## DOM elements.
|
||||||
|
attachedTo*: seq[Element]
|
||||||
|
|
||||||
|
TaskId = int
|
||||||
|
Task = ref object
|
||||||
|
id: TaskId
|
||||||
|
name, desc: kstring
|
||||||
|
|
||||||
|
ColumnId = distinct int
|
||||||
|
Column = ref object of Attachable
|
||||||
|
id: ColumnId
|
||||||
|
header: kstring
|
||||||
|
tasks: seq[Task]
|
||||||
|
|
||||||
|
Board = ref object
|
||||||
|
columns: seq[Column]
|
||||||
|
|
||||||
|
var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"),
|
||||||
|
Column(id: ColumnId 1, header: "Doing"),
|
||||||
|
Column(id: ColumnId 2, header: "Done")])
|
||||||
|
|
||||||
|
proc removeTask(t: TaskId) =
|
||||||
|
for c in b.columns:
|
||||||
|
for i in 0 ..< c.tasks.len:
|
||||||
|
if c.tasks[i].id == t:
|
||||||
|
delete(c.tasks, i)
|
||||||
|
break
|
||||||
|
|
||||||
|
proc moveTask(t: TaskId; dest: Column) =
|
||||||
|
for c in b.columns:
|
||||||
|
for i in 0 ..< c.tasks.len:
|
||||||
|
let task = c.tasks[i]
|
||||||
|
if task.id == t:
|
||||||
|
delete(c.tasks, i)
|
||||||
|
dest.tasks.add task
|
||||||
|
return
|
||||||
|
|
||||||
|
var nextTaskId = 0 ## negative because they are not yet backed
|
||||||
|
## up by the database
|
||||||
|
|
||||||
|
proc createTask(c: Column; name, desc: kstring): Task =
|
||||||
|
dec nextTaskId
|
||||||
|
result = Task(id: TaskId nextTaskId, name: name, desc: desc)
|
||||||
|
c.tasks.add result
|
||||||
|
|
||||||
|
# --------------------- UI -----------------------------------------------
|
||||||
|
|
||||||
|
proc renderTask(t: Task): Element =
|
||||||
|
proc dodelete =
|
||||||
|
removeTask t.id
|
||||||
|
delete result
|
||||||
|
|
||||||
|
proc dragstart(ev: Event) =
|
||||||
|
ev.prepareDragData("taskid", ev.target.id)
|
||||||
|
|
||||||
|
result = buildHtml():
|
||||||
|
tdiv(draggable="true", ondragstart=dragstart, id = &t.id):
|
||||||
|
bindField t.name
|
||||||
|
br()
|
||||||
|
bindField t.desc
|
||||||
|
button(onclick = dodelete):
|
||||||
|
text cross
|
||||||
|
|
||||||
|
type
|
||||||
|
NewTaskPanel = object
|
||||||
|
c: Column
|
||||||
|
e: Element
|
||||||
|
|
||||||
|
proc open(p: var NewTaskPanel; c: Column) =
|
||||||
|
p.e.style.display = "block"
|
||||||
|
p.c = c
|
||||||
|
|
||||||
|
proc newTaskDialog(): NewTaskPanel =
|
||||||
|
var nameInp = buildHtml():
|
||||||
|
input(setFocus = true)
|
||||||
|
var descInp = buildHtml():
|
||||||
|
input()
|
||||||
|
|
||||||
|
proc close =
|
||||||
|
result.e.style.display = "none"
|
||||||
|
|
||||||
|
proc submit =
|
||||||
|
let t = createTask(result.c, nameInp.value, descInp.value)
|
||||||
|
close()
|
||||||
|
result.c.attachedTo[0].add renderTask(t)
|
||||||
|
|
||||||
|
result.e = buildHtml():
|
||||||
|
tdiv(style={display: "none", position: "fixed",
|
||||||
|
left: "0", top: "0", width: "100%", height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
backgroundColor: "rgb(0,0,0)",
|
||||||
|
backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}):
|
||||||
|
tdiv(style={backgroundColor: "#fefefe",
|
||||||
|
margin: "15% auto", padding: "20px", border: "1px solid #888",
|
||||||
|
width: "80%"}):
|
||||||
|
span(onclick = close, style={color: "#aaa",
|
||||||
|
cssFloat: "right",
|
||||||
|
fontSize: "28px",
|
||||||
|
fontWeight: "bold"}):
|
||||||
|
text cross
|
||||||
|
p:
|
||||||
|
text "Task name"
|
||||||
|
nameInp
|
||||||
|
p:
|
||||||
|
text "Task description"
|
||||||
|
descInp
|
||||||
|
span(onclick = submit, style={color: "#0f0",
|
||||||
|
cssFloat: "left",
|
||||||
|
fontWeight: "bold"}):
|
||||||
|
text "Submit"
|
||||||
|
|
||||||
|
var dialog = newTaskDialog()
|
||||||
|
|
||||||
|
proc renderColumn(c: Column): Element =
|
||||||
|
proc doadd(c: Column): proc () =
|
||||||
|
result = proc() = dialog.open(c)
|
||||||
|
|
||||||
|
proc allowDrop(ev: Event) = ev.preventDefault()
|
||||||
|
proc drop(ev: Event) =
|
||||||
|
ev.preventDefault()
|
||||||
|
let data = ev.recvDragData("taskid")
|
||||||
|
moveTask(parseInt data, c)
|
||||||
|
ev.target.up("mycolumn").add(getElementById(data))
|
||||||
|
|
||||||
|
result = buildHtml():
|
||||||
|
tdiv(class = "mycolumn", style = {cssFloat: "left", width: "33%"},
|
||||||
|
ondrop = drop, ondragover = allowDrop):
|
||||||
|
tdiv(class = "myheader"):
|
||||||
|
bindField c.header
|
||||||
|
span(onclick = doadd(c)):
|
||||||
|
text plus
|
||||||
|
for t in c.tasks:
|
||||||
|
renderTask(t)
|
||||||
|
c.attachedTo.add result
|
||||||
|
|
||||||
|
proc renderBoard(b: Board): Element =
|
||||||
|
result = buildHtml(tdiv):
|
||||||
|
dialog.e
|
||||||
|
for c in b.columns:
|
||||||
|
renderColumn(c)
|
||||||
|
|
||||||
|
proc main(hashPart: kstring): Element =
|
||||||
|
result = renderBoard(b)
|
||||||
|
|
||||||
|
setInitializer main
|
||||||
43
experiments/trello/widgets.nim
Normal file
43
experiments/trello/widgets.nim
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
## Common widget implemenations
|
||||||
|
|
||||||
|
import knete
|
||||||
|
|
||||||
|
const
|
||||||
|
cross* = "x"
|
||||||
|
plus* = kstring"+"
|
||||||
|
|
||||||
|
const
|
||||||
|
LEFT* = 37
|
||||||
|
UP* = 38
|
||||||
|
RIGHT* = 39
|
||||||
|
DOWN* = 40
|
||||||
|
TAB* = 9
|
||||||
|
ESC* = 27
|
||||||
|
ENTER* = 13
|
||||||
|
|
||||||
|
proc editable*(x: kstring; onchanged: proc (value: kstring);
|
||||||
|
isEdited = false): Element =
|
||||||
|
proc onenter(ev: Event) = ev.target.blur()
|
||||||
|
proc submit(ev: Event) =
|
||||||
|
onchanged(ev.target.value)
|
||||||
|
# close the loop: This is a common pattern in Knete.
|
||||||
|
replace(result, editable(ev.target.value, onchanged, false))
|
||||||
|
|
||||||
|
proc makeEditable() =
|
||||||
|
# close the loop: This is a common pattern in Knete.
|
||||||
|
replace(result, editable(x, onchanged, true))
|
||||||
|
|
||||||
|
result = buildHtml():
|
||||||
|
if isEdited:
|
||||||
|
input(class = "edit",
|
||||||
|
onblur = submit,
|
||||||
|
onkeyupenter = onenter,
|
||||||
|
value = x,
|
||||||
|
setFocus = true)
|
||||||
|
else:
|
||||||
|
span(onclick = makeEditable):
|
||||||
|
text x
|
||||||
|
|
||||||
|
template bindField*(field): Element =
|
||||||
|
editable field, proc (value: kstring) =
|
||||||
|
field = value
|
||||||
Loading…
Add table
Add a link
Reference in a new issue