-
Notifications
You must be signed in to change notification settings - Fork 7
/
boydog-client.js
141 lines (118 loc) · 4.29 KB
/
boydog-client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const sharedb = require("sharedb/lib/client")
const ReconnectingWebSocket = require("reconnecting-websocket")
const Binding = require("sharedb-string-binding")
const $ = require("cash-dom")
const Swal = require("sweetalert2")
const _toPath = require("lodash.topath")
const acorn = require("acorn")
const acornWalk = require("acorn-walk")
const astring = require("astring")
const attrModel = "bd-value"
const attrCalculables = ["bd-id", "bd-class", "bd-html"]
// Boydog front-end scope
let valScope = {} // Always the latest value of the path
let calcScope = {} // Post OP event of a path
const init = (root = "html", host = window.location.host) => {
// Open WebSocket connection to ShareDB server
const socket = new ReconnectingWebSocket("ws://" + host)
const connection = new sharedb.Connection(socket)
// Status messages // TODO: Move to a small status icon at the bottom-right part of the screen?
socket.addEventListener("open", function () {
Swal.fire({
text: "⠀Boydog connected",
icon: "info",
toast: true,
timer: 5000,
showConfirmButton: false,
position: "bottom-end",
})
})
socket.addEventListener("close", function () {
Swal.fire({
text: "⠀Boydog disconnected",
icon: "warning",
toast: true,
showConfirmButton: false,
position: "bottom-end",
})
})
const updateRelatedCalculables = (path) => {
const c = calcScope[path]
if (!c) return
c.forEach((e) => (e.el[e.prop] = e.fn(valScope))) // Perform calculable final string
}
const updateLatestVal = (doc, path) => {
doc.fetch((err) => {
if (err) throw err
if (doc && doc.data) valScope[path] = doc.data.content
updateRelatedCalculables(path)
})
}
const subscribeDoc = (doc, path, el) => {
if (valScope[path] === undefined) {
// Reliable `doc.subscribed` alternative
valScope[path] = null // Null is a valid path t has not been assigned a value yet
doc.subscribe((err) => {
if (err) throw err
updateLatestVal(doc, path, () => {})
doc.on("op", (op) => {
updateLatestVal(doc, path)
if (el.getAttribute("bd-verbose")) console.log("Boydog operation on:", path, op)
})
})
}
}
const processModel = (path, el) => {
path = _toPath(path).join(">")
const doc = connection.get("boydog", path)
subscribeDoc(doc, path, el)
doc.fetch((err) => {
if (err) throw err
try {
new Binding(el, doc, ["content"]).setup()
} catch (err) {
console.log(`Warning: Unrecognized '${path}' path. Either a typo or path is not a leaf.`)
}
})
}
const processCalculable = (path, attr, attrVal, el) => {
path = _toPath(path).join(">")
const fn = new Function("sandbox", `with (sandbox) {const r = ${attrVal}; return r;}`)
const eqs = {
"bd-html": "innerHTML",
"bd-class": "className",
"bd-id": "id",
}
if (!Array.isArray(calcScope[path])) calcScope[path] = []
calcScope[path].push({ prop: eqs[attr], fn, el })
const doc = connection.get("boydog", path)
subscribeDoc(doc, path, el)
}
;[...attrCalculables, attrModel].forEach((attr) => {
$(`[${attr}]`).each((i, el) => {
const attrVal = el.getAttribute(attr)
const p = acorn.parse(attrVal)
if (attr === attrModel) {
if (p.body.length !== 1) return console.log("Error: Malformed bd-model. The bd-model attribute allows only one line expressions.")
if (p.body[0].expression.type !== "Identifier" && p.body[0].expression.type !== "MemberExpression")
return console.log("Error: Malformed bd-model. The bd-model attribute allows only expressions that don't require any calculations.")
const path = astring.generate(p.body[0]).slice(0, -1)
processModel(path, el)
} else {
acornWalk.simple(p, {
Identifier(node) {
const path = node.name
processCalculable(path, attr, attrVal, el)
},
MemberExpression(node) {
const path = astring.generate(node)
processCalculable(path, attr, attrVal, el)
},
})
}
})
})
console.log("Boydog client connected.")
}
console.log("Boydog client found. Now run `boydog.init(scope, host)` to begin sharing in real-time.")
window.boydog = { init }