UNPKG

40.2 kBJavaScriptView Raw
1(function () {
2 var unique_sockjs_string = '_connect_to_statebus_'
3
4 window.dom = window.dom || new Proxy({}, {
5 get: function (o, k) { return o[k] },
6 set: function (o, k, v) {
7 o[k] = v
8 make_component(k, v)
9 return true
10 }
11 })
12
13 // ****************
14 // Connecting over the Network
15 function set_cookie (key, val) {
16 document.cookie = key + '=' + val + '; Expires=21 Oct 2025 00:0:00 GMT;'
17 }
18 function get_cookie (key) {
19 var c = document.cookie.match('(^|;)\\s*' + key + '\\s*=\\s*([^;]+)');
20 return c ? c.pop() : '';
21 }
22 try { document.cookie } catch (e) {get_cookie = set_cookie = function (){}}
23 function make_websocket (url) {
24 if (!url.match(/^\w{0,7}:\/\//))
25 url = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port : '') + url
26
27 url = url.replace(/^state:\/\//, 'wss://')
28 url = url.replace(/^istate:\/\//, 'ws://')
29 url = url.replace(/^statei:\/\//, 'ws://')
30
31 url = url.replace(/^https:\/\//, 'wss://')
32 url = url.replace(/^http:\/\//, 'ws://')
33
34 // { // Convert to absolute
35 // var link = document.createElement("a")
36 // link.href = url
37 // url = link.href
38 // }
39 console.log('opening websocket to', url)
40 return new WebSocket(url + '/' + unique_sockjs_string + '/websocket')
41 // return new SockJS(url + '/' + unique_sockjs_string)
42 }
43 function client_creds (server_url) {
44 var me = bus.fetch('ls/me')
45 bus.log('connect: me is', me)
46 if (!me.client) {
47 // Create a client id if we have none yet.
48 // Either from a cookie set by server, or a new one from scratch.
49 var c = get_cookie('client')
50 me.client = c || (Math.random().toString(36).substring(2)
51 + Math.random().toString(36).substring(2)
52 + Math.random().toString(36).substring(2))
53 bus.save(me)
54 }
55
56 set_cookie('client', me.client)
57 return {clientid: me.client}
58 }
59
60
61 // ****************
62 // Manipulate Localstorage
63 function localstorage_client (prefix) {
64 try { localStorage } catch (e) { return }
65
66 // This doesn't yet trigger updates across multiple browser windows.
67 // We can do that by adding a list of dirty keys and
68
69 var bus = this
70 bus.log(this)
71
72 // Fetch returns the value immediately in a save
73 // Saves are queued up, to store values with a delay, in batch
74 var saves_are_pending = false
75 var pending_saves = {}
76
77 function save_the_pending_saves() {
78 bus.log('localstore: saving', pending_saves)
79 for (var k in pending_saves)
80 localStorage.setItem(k, JSON.stringify(pending_saves[k]))
81 saves_are_pending = false
82 }
83
84 bus(prefix).to_fetch = function (key) {
85 var result = localStorage.getItem(key)
86 return result ? JSON.parse(result) : {key: key}
87 }
88 bus(prefix).to_save = function (obj) {
89 // Do I need to make this recurse into the object?
90 bus.log('localStore: on_save:', obj.key)
91 pending_saves[obj.key] = obj
92 if (!saves_are_pending) {
93 setTimeout(save_the_pending_saves, 50)
94 saves_are_pending = true
95 }
96 bus.save.fire(obj)
97 return obj
98 }
99 bus(prefix).to_delete = function (key) { localStorage.removeItem(key) }
100
101
102 // Hm... this update stuff doesn't seem to work on file:/// urls in chrome
103 function update (event) {
104 bus.log('Got a localstorage update', event)
105 //this.get(event.key.substr('statebus '.length))
106 }
107 if (window.addEventListener) window.addEventListener("storage", update, false)
108 else window.attachEvent("onstorage", update)
109 }
110
111 // Stores state in the query string, as ?key1={obj...}&key2={obj...}
112 function url_store (prefix) {
113 var bus = this
114 function get_query_string_value (key) {
115 return unescape(window.location.search.replace(
116 new RegExp("^(?:.*[&\\?]"
117 + escape(key).replace(/[\.\+\*]/g, "\\$&")
118 + "(?:\\=([^&]*))?)?.*$", "i"),
119 "$1"))
120 }
121
122 // Initialize data from the URL on load
123
124 // Now the regular shit
125 var data = get_query_string_value(key)
126 data = (data && JSON.parse(data)) || {key : key}
127 // Then I would need to:
128 // - Change the key prefix
129 // - Save this into the cache
130
131 bus(prefix).to_save = function (obj) {
132 window.history.replaceState(
133 '',
134 '',
135 document.location.origin
136 + document.location.pathname
137 + escape('?'+key+'='+JSON.stringify(obj)))
138 bus.save.fire(obj)
139 }
140 }
141
142 function live_reload_from (prefix) {
143 if (!window.live_reload_initialized) {
144 var first_time = true
145 this(function () {
146 var re = new RegExp(".*/" + prefix + "/(.*)")
147 var file = window.location.href.match(re)[1]
148 var code = bus.fetch('/code/invisible.college/' + file).code
149 if (!code) return
150 if (first_time) {first_time = false; return}
151 var old_scroll_position = window.pageYOffset
152 document.body.innerHTML = code
153 var i = 0
154 var d = 100
155 var interval = setInterval(function () {
156 if (i > 500) clearInterval(interval)
157 i += d
158 window.scrollTo(0, old_scroll_position)
159 }, d)
160 })
161 window.live_reload_initialized = true
162 }
163 }
164
165
166 // ****************
167 // Wrapper for React Components
168
169 // XXX Currently assumes there's a statebus named "bus" in global
170 // XXX scope.
171
172 var components = {} // Indexed by 'component/0', 'component/1', etc.
173 var components_count = 0
174 var dirty_components = {}
175 function React_View(component) {
176 function wrap(name, new_func) {
177 var old_func = component[name]
178 component[name] = function wrapper () { return new_func.bind(this)(old_func) }
179 }
180
181 // Register the component's basic info
182 wrap('componentWillMount', function new_cwm (orig_func) {
183 if (component.displayName === undefined)
184 throw 'Component needs a displayName'
185 this.name = component.displayName.toLowerCase().replace(' ', '_')
186 this.key = 'component/' + components_count++
187 components[this.key] = this
188
189 function add_shortcut (obj, shortcut_name, to_key) {
190 delete obj[shortcut_name]
191 Object.defineProperty(obj, shortcut_name, {
192 get: function () { return bus.fetch(to_key) },
193 configurable: true })
194 }
195 add_shortcut(this, 'local', this.key)
196
197 orig_func && orig_func.apply(this, arguments)
198
199 // Make render reactive
200 var orig_render = this.render
201 this.render = bus.reactive(function () {
202 console.assert(this !== window)
203 if (this.render.called_directly) {
204 delete dirty_components[this.key]
205
206 // Add reactivity to any keys passed inside objects in props.
207 for (var k in this.props)
208 if (this.props.hasOwnProperty(k)
209 && this.props[k] !== null
210 && typeof this.props[k] === 'object'
211 && this.props[k].key)
212
213 bus.fetch(this.props[k].key)
214
215 // Call the renderer!
216 return orig_render.apply(this, arguments)
217 } else {
218 dirty_components[this.key] = true
219 schedule_re_render()
220 }
221 })
222 })
223
224 wrap('componentWillUnmount', function new_cwu (orig_func) {
225 orig_func && orig_func.apply(this, arguments)
226 // Clean up
227 bus.delete(this.key)
228 delete components[this.key]
229 delete dirty_components[this.key]
230 })
231
232 function shallow_clone(original) {
233 var clone = Object.create(Object.getPrototypeOf(original))
234 var i, keys = Object.getOwnPropertyNames(original)
235 for (i=0; i < keys.length; i++){
236 Object.defineProperty(clone, keys[i],
237 Object.getOwnPropertyDescriptor(original, keys[i])
238 )
239 }
240 return clone
241 }
242
243 component.shouldComponentUpdate = function new_scu (next_props, next_state) {
244 // This component definitely needs to update if it is marked as dirty
245 if (dirty_components[this.key] !== undefined) return true
246
247 // Otherwise, we'll check to see if its state or props
248 // have changed. But ignore React's 'children' prop,
249 // because it often has a circular reference.
250 next_props = shallow_clone(next_props)
251 this_props = shallow_clone(this.props)
252
253 delete next_props['children']; delete this_props['children']
254 // delete next_props['kids']; delete this_props['kids']
255
256 next_props = bus.clone(next_props)
257 this_props = bus.clone(this_props)
258
259
260 return !bus.deep_equals([next_state, next_props], [this.state, this_props])
261
262 // TODO:
263 //
264 // - Check children too. Right now we just silently fail
265 // on components with children. WTF?
266 //
267 // - A better method might be to mark a component dirty when
268 // it receives new props in the
269 // componentWillReceiveProps React method.
270 }
271
272 component.loading = function loading () {
273 return this.render.loading()
274 }
275
276 // Now create the actual React class with this definition, and
277 // return it.
278 var react_class = React.createClass(component)
279 var result = function (props, children) {
280 props = props || {}
281 props['data-key'] = props.key
282 props['data-widget'] = component.displayName
283
284 return (React.version >= '0.12.'
285 ? React.createElement(react_class, props, children)
286 : react_class(props, children))
287 }
288 // Give it the same prototype as the original class so that it
289 // passes React.isValidClass() inspection
290 result.prototype = react_class.prototype
291 return result
292 }
293 window.React_View = React_View
294 if (window.statebus) window.statebus.create_react_class = window.statebus.createReactClass = React_View
295
296 // *****************
297 // Re-rendering react components
298 var re_render_scheduled = false
299 re_rendering = false
300 function schedule_re_render() {
301 if (!re_render_scheduled) {
302 requestAnimationFrame(function () {
303 re_render_scheduled = false
304
305 // Re-renders dirty components
306 for (var comp_key in dirty_components) {
307 if (dirty_components[comp_key] // Since another component's update might update this
308 && components[comp_key]) // Since another component might unmount this
309
310 try {
311 re_rendering = true
312 components[comp_key].forceUpdate()
313 } finally {
314 re_rendering = false
315 }
316 }
317 })
318 re_render_scheduled = true
319 }
320 }
321
322 // ##############################################################################
323 // ###
324 // ### Full-featured single-file app methods
325 // ###
326
327 function make_client_statebus_maker () {
328 var extra_stuff = ['localstorage_client make_websocket client_creds',
329 'url_store components live_reload_from'].join(' ').split(' ')
330 if (window.statebus) {
331 var orig_statebus = statebus
332 window.statebus = function make_client_bus () {
333 var bus = orig_statebus()
334 for (var i=0; i<extra_stuff.length; i++)
335 bus[extra_stuff[i]] = eval(extra_stuff[i])
336 bus.localstorage_client('ls/*')
337 return bus
338 }
339 }
340 }
341
342 function load_scripts() {
343 // console.info('Loading scripts! if', !!!window.statebus)
344 if (!window.statebus) {
345 var statebus_dir = script_option('src')
346 if (statebus_dir) statebus_dir = statebus_dir.match(/(.*)[\/\\]/)
347 if (statebus_dir) statebus_dir = statebus_dir[1] + '/'
348 else statebus_dir = ''
349
350 var js_urls = {
351 react: statebus_dir + 'extras/react.js',
352 sockjs: statebus_dir + 'extras/sockjs.js',
353 coffee: statebus_dir + 'extras/coffee.js',
354 statebus: statebus_dir + 'statebus.js'
355 }
356 if (statebus_dir == 'https://stateb.us/')
357 js_urls.statebus = statebus_dir + 'statebus4.js'
358
359 for (var name in js_urls)
360 document.write('<script src="' + js_urls[name] + '" charset="utf-8"></script>')
361
362 document.addEventListener('DOMContentLoaded', scripts_ready, false)
363 }
364 else
365 scripts_ready()
366 }
367
368 function script_option (option_name) {
369 var script_elem = document.querySelector('script[src*="client"][src$=".js"]')
370 return script_elem && script_elem.getAttribute(option_name)
371 }
372 var loaded_from_file_url = window.location.href.match(/^file:\/\//)
373 window.statebus_server = window.statebus_server || script_option('server') ||
374 (loaded_from_file_url ? 'https://stateb.us:3006' : '/')
375 window.statebus_backdoor = window.statebus_backdoor || script_option('backdoor')
376 var react_render
377 function scripts_ready () {
378 react_render = React.version >= '0.14.' ? ReactDOM.render : React.render
379 make_client_statebus_maker()
380 window.bus = window.statebus()
381 window.bus.label = 'bus'
382 window.sb = bus.sb
383 statebus.widget = React_View
384 statebus.create_react_class = React_View
385 statebus.createReactClass = React_View
386
387 improve_react()
388 window.ignore_flashbacks = false
389 if (statebus_server !== 'none')
390 bus.net_mount ('/*', statebus_server)
391
392 if (window.statebus_backdoor) {
393 window.master = statebus()
394 master.net_mount('*', statebus_backdoor)
395 }
396 bus.net_automount()
397
398 // bus('*').to_save = function (obj) { bus.save.fire(obj) }
399 bus('/new/*').to_save = function (o) {
400 if (o.key.split('/').length > 3) return
401
402 var old_key = o.key
403 o.key = old_key + '/' + Math.random().toString(36).substring(2,12)
404 statebus.cache[o.key] = o
405 delete statebus.cache[old_key]
406 bus.save(o)
407 }
408 load_coffee()
409
410 statebus.compile_coffee = compile_coffee
411 statebus.load_client_code = load_client_code
412 statebus.load_widgets = load_widgets
413
414 document.addEventListener('DOMContentLoaded', function () {
415 if (window.statebus_ready)
416 for (var i=0; i<statebus_ready.length; i++)
417 statebus_ready[i]()
418 }, false)
419
420 document.addEventListener('DOMContentLoaded', load_widgets, false)
421
422 // if (dom.Body || dom.body || dom.BODY)
423 // react_render((window.Body || window.body || window.BODY)(), document.body)
424 }
425
426 function improve_react() {
427 function capitalize (s) {return s[0].toUpperCase() + s.slice(1)}
428 function camelcase (s) { var a = s.split(/[_-]/)
429 return a.slice(0,1).concat(a.slice(1).map(capitalize)).join('') }
430
431 // We used to get all_css_props like this:
432 //
433 // var all_css_props = Object.keys(document.body.style)
434 // if (all_css_props.length < 100) // Firefox
435 // all_css_props = Object.keys(document.body.style.__proto__)
436 //
437 // But now I've hard-coded them:
438 var all_css_props = ["alignContent","alignItems","alignSelf","alignmentBaseline","all","animation","animationDelay","animationDirection","animationDuration","animationFillMode","animationIterationCount","animationName","animationPlayState","animationTimingFunction","backfaceVisibility","background","backgroundAttachment","backgroundBlendMode","backgroundClip","backgroundColor","backgroundImage","backgroundOrigin","backgroundPosition","backgroundPositionX","backgroundPositionY","backgroundRepeat","backgroundRepeatX","backgroundRepeatY","backgroundSize","baselineShift","blockSize","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","boxShadow","boxSizing","breakAfter","breakBefore","breakInside","bufferedRendering","captionSide","caretColor","clear","clip","clipPath","clipRule","color","colorInterpolation","colorInterpolationFilters","colorRendering","columnCount","columnFill","columnGap","columnRule","columnRuleColor","columnRuleStyle","columnRuleWidth","columnSpan","columnWidth","columns","contain","content","counterIncrement","counterReset","cursor","cx","cy","d","direction","display","dominantBaseline","emptyCells","fill","fillOpacity","fillRule","filter","flex","flexBasis","flexDirection","flexFlow","flexGrow","flexShrink","flexWrap","float","floodColor","floodOpacity","font","fontDisplay","fontFamily","fontFeatureSettings","fontKerning","fontSize","fontStretch","fontStyle","fontVariant","fontVariantCaps","fontVariantEastAsian","fontVariantLigatures","fontVariantNumeric","fontVariationSettings","fontWeight","gap","grid","gridArea","gridAutoColumns","gridAutoFlow","gridAutoRows","gridColumn","gridColumnEnd","gridColumnGap","gridColumnStart","gridGap","gridRow","gridRowEnd","gridRowGap","gridRowStart","gridTemplate","gridTemplateAreas","gridTemplateColumns","gridTemplateRows","height","hyphens","imageRendering","inlineSize","isolation","justifyContent","justifyItems","justifySelf","left","letterSpacing","lightingColor","lineBreak","lineHeight","listStyle","listStyleImage","listStylePosition","listStyleType","margin","marginBottom","marginLeft","marginRight","marginTop","marker","markerEnd","markerMid","markerStart","mask","maskType","maxBlockSize","maxHeight","maxInlineSize","maxWidth","maxZoom","minBlockSize","minHeight","minInlineSize","minWidth","minZoom","mixBlendMode","objectFit","objectPosition","offset","offsetDistance","offsetPath","offsetRotate","opacity","order","orientation","orphans","outline","outlineColor","outlineOffset","outlineStyle","outlineWidth","overflow","overflowAnchor","overflowWrap","overflowX","overflowY","overscrollBehavior","overscrollBehaviorX","overscrollBehaviorY","padding","paddingBottom","paddingLeft","paddingRight","paddingTop","page","pageBreakAfter","pageBreakBefore","pageBreakInside","paintOrder","perspective","perspectiveOrigin","placeContent","placeItems","placeSelf","pointerEvents","position","quotes","r","resize","right","rowGap","rx","ry","scrollBehavior","shapeImageThreshold","shapeMargin","shapeOutside","shapeRendering","size","speak","src","stopColor","stopOpacity","stroke","strokeDasharray","strokeDashoffset","strokeLinecap","strokeLinejoin","strokeMiterlimit","strokeOpacity","strokeWidth","tabSize","tableLayout","textAlign","textAlignLast","textAnchor","textCombineUpright","textDecoration","textDecorationColor","textDecorationLine","textDecorationSkipInk","textDecorationStyle","textIndent","textOrientation","textOverflow","textRendering","textShadow","textSizeAdjust","textTransform","textUnderlinePosition","top","touchAction","transform","transformBox","transformOrigin","transformStyle","transition","transitionDelay","transitionDuration","transitionProperty","transitionTimingFunction","unicodeBidi","unicodeRange","userSelect","userZoom","vectorEffect","verticalAlign","visibility","webkitAlignContent","webkitAlignItems","webkitAlignSelf","webkitAnimation","webkitAnimationDelay","webkitAnimationDirection","webkitAnimationDuration","webkitAnimationFillMode","webkitAnimationIterationCount","webkitAnimationName","webkitAnimationPlayState","webkitAnimationTimingFunction","webkitAppRegion","webkitAppearance","webkitBackfaceVisibility","webkitBackgroundClip","webkitBackgroundOrigin","webkitBackgroundSize","webkitBorderAfter","webkitBorderAfterColor","webkitBorderAfterStyle","webkitBorderAfterWidth","webkitBorderBefore","webkitBorderBeforeColor","webkitBorderBeforeStyle","webkitBorderBeforeWidth","webkitBorderBottomLeftRadius","webkitBorderBottomRightRadius","webkitBorderEnd","webkitBorderEndColor","webkitBorderEndStyle","webkitBorderEndWidth","webkitBorderHorizontalSpacing","webkitBorderImage","webkitBorderRadius","webkitBorderStart","webkitBorderStartColor","webkitBorderStartStyle","webkitBorderStartWidth","webkitBorderTopLeftRadius","webkitBorderTopRightRadius","webkitBorderVerticalSpacing","webkitBoxAlign","webkitBoxDecorationBreak","webkitBoxDirection","webkitBoxFlex","webkitBoxOrdinalGroup","webkitBoxOrient","webkitBoxPack","webkitBoxReflect","webkitBoxShadow","webkitBoxSizing","webkitClipPath","webkitColumnBreakAfter","webkitColumnBreakBefore","webkitColumnBreakInside","webkitColumnCount","webkitColumnGap","webkitColumnRule","webkitColumnRuleColor","webkitColumnRuleStyle","webkitColumnRuleWidth","webkitColumnSpan","webkitColumnWidth","webkitColumns","webkitFilter","webkitFlex","webkitFlexBasis","webkitFlexDirection","webkitFlexFlow","webkitFlexGrow","webkitFlexShrink","webkitFlexWrap","webkitFontFeatureSettings","webkitFontSizeDelta","webkitFontSmoothing","webkitHighlight","webkitHyphenateCharacter","webkitJustifyContent","webkitLineBreak","webkitLineClamp","webkitLocale","webkitLogicalHeight","webkitLogicalWidth","webkitMarginAfter","webkitMarginAfterCollapse","webkitMarginBefore","webkitMarginBeforeCollapse","webkitMarginBottomCollapse","webkitMarginCollapse","webkitMarginEnd","webkitMarginStart","webkitMarginTopCollapse","webkitMask","webkitMaskBoxImage","webkitMaskBoxImageOutset","webkitMaskBoxImageRepeat","webkitMaskBoxImageSlice","webkitMaskBoxImageSource","webkitMaskBoxImageWidth","webkitMaskClip","webkitMaskComposite","webkitMaskImage","webkitMaskOrigin","webkitMaskPosition","webkitMaskPositionX","webkitMaskPositionY","webkitMaskRepeat","webkitMaskRepeatX","webkitMaskRepeatY","webkitMaskSize","webkitMaxLogicalHeight","webkitMaxLogicalWidth","webkitMinLogicalHeight","webkitMinLogicalWidth","webkitOpacity","webkitOrder","webkitPaddingAfter","webkitPaddingBefore","webkitPaddingEnd","webkitPaddingStart","webkitPerspective","webkitPerspectiveOrigin","webkitPerspectiveOriginX","webkitPerspectiveOriginY","webkitPrintColorAdjust","webkitRtlOrdering","webkitRubyPosition","webkitShapeImageThreshold","webkitShapeMargin","webkitShapeOutside","webkitTapHighlightColor","webkitTextCombine","webkitTextDecorationsInEffect","webkitTextEmphasis","webkitTextEmphasisColor","webkitTextEmphasisPosition","webkitTextEmphasisStyle","webkitTextFillColor","webkitTextOrientation","webkitTextSecurity","webkitTextSizeAdjust","webkitTextStroke","webkitTextStrokeColor","webkitTextStrokeWidth","webkitTransform","webkitTransformOrigin","webkitTransformOriginX","webkitTransformOriginY","webkitTransformOriginZ","webkitTransformStyle","webkitTransition","webkitTransitionDelay","webkitTransitionDuration","webkitTransitionProperty","webkitTransitionTimingFunction","webkitUserDrag","webkitUserModify","webkitUserSelect","webkitWritingMode","whiteSpace","widows","width","willChange","wordBreak","wordSpacing","wordWrap","writingMode","x","y","zIndex","zoom"]
439
440 var ignore = {d:1, cx:1, cy:1, rx:1, ry:1, x:1, y:1,
441 content:1, fill:1, stroke:1, src:1}
442 var is_css_prop = {}
443 for (var i=0; i<all_css_props.length; i++)
444 if (!ignore[all_css_props[i]])
445 is_css_prop[all_css_props[i]] = true
446
447 function better_element(el) {
448 // To do:
449 // - Don't put all args into a children array, cause react thinks
450 // that means they need a key.
451
452 return function () {
453 var children = []
454 var attrs = {style: {}}
455
456 for (var i=0; i<arguments.length; i++) {
457 var arg = arguments[i]
458
459 // Strings and DOM nodes and undefined become children
460 if (typeof arg === 'string' // For "foo"
461 || arg instanceof String // For new String()
462 || arg && React.isValidElement(arg)
463 || arg === undefined)
464 children.push(arg)
465
466 // Arrays append onto the children
467 else if (arg instanceof Array)
468 Array.prototype.push.apply(children, arg)
469
470 // Pure objects get merged into object city
471 // Styles get redirected to the style field
472 else if (arg instanceof Object)
473 for (var k in arg)
474 if (is_css_prop[k]
475 && !(k in {width:1,height:1,size:1}
476 && el in {canvas:1, input:1, embed:1, object:1}))
477 attrs.style[k] = arg[k] // Merge styles
478 else if (k === 'style') // Merge insides of style tags
479 for (var k2 in arg[k])
480 attrs.style[k2] = arg[k][k2]
481 else {
482 attrs[k] = arg[k] // Or be normal.
483
484 if (k === 'key')
485 attrs['data-key'] = arg[k]
486 }
487 }
488 if (children.length === 0) children = undefined
489 if (attrs['ref'] === 'input')
490 bus.log(attrs, children)
491 return React.DOM[el](attrs, children)
492 }
493 }
494 for (var el in React.DOM)
495 window[el.toUpperCase()] = better_element(el)
496
497 function make_better_input (name, element) {
498 window[name] = React.createFactory(React.createClass({
499 getInitialState: function() {
500 return {value: this.props.value}
501 },
502 componentWillReceiveProps: function(new_props) {
503 this.setState({value: new_props.value})
504 },
505 onChange: function(e) {
506 this.props.onChange && this.props.onChange(e)
507 if (this.props.value)
508 this.setState({value: e.target.value})
509 },
510 render: function() {
511 var new_props = {}
512 for (var k in this.props)
513 if (this.props.hasOwnProperty(k))
514 new_props[k] = this.props[k]
515 if (this.state.value) new_props.value = this.state.value
516 new_props.onChange = this.onChange
517 return element(new_props)
518 }
519 }))
520 }
521
522 make_better_input("INPUT", window.INPUT)
523 make_better_input("TEXTAREA", window.TEXTAREA)
524 make_syncarea()
525
526 // Make IMG accept data from state:
527 var og_img = window.IMG
528 window.IMG = function () {
529 var args = []
530 for (var i=0; i<arguments.length; i++) {
531 args.push(arguments[i])
532 if (arguments[i].state)
533 args[i].src = 'data:;base64,' + fetch(args[i].state)._
534 }
535 return og_img.apply(this, args)
536 }
537
538
539 // Unfortunately, React's default STYLE and TITLE tags are useless
540 // unless you "dangerously set inner html" because they wrap strings
541 // inside useless spans.
542 function escape_html (s) {
543 // TODO: this will fail on '<' and '>' in CSS selectors
544 return s.replace(/</g, "&lt;").replace(/>/g, "&gt;")
545 }
546 window.STYLE = function (s) {
547 return React.DOM.style({dangerouslySetInnerHTML: {__html: escape_html(s)}})
548 }
549 window.TITLE = function (s) {
550 return React.DOM.title({dangerouslySetInnerHTML: {__html: escape_html(s)}})
551 }
552 }
553
554 function autodetect_args (func) {
555 if (func.args) return
556
557 // Get an array of the func's params
558 var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
559 params = /([^\s,]+)/g,
560 s = func.toString().replace(comments, '')
561 func.args = s.slice(s.indexOf('(')+1, s.indexOf(')')).match(params) || []
562 }
563
564
565 // Load the components
566 var users_widgets = {}
567 function make_component(name, func) {
568 // Define the component
569
570 window[name] = users_widgets[name] = window.React_View({
571 displayName: name,
572 render: function () {
573 var args = []
574
575 // Parse the function's args, and pass props into them directly
576 autodetect_args(func)
577 // this.props.kids = this.props.kids || this.props.children
578 for (var i=0; i<func.args.length; i++)
579 args.push(this.props[func.args[i]])
580
581 // Now run the function.
582 var vdom = func.apply(this, args)
583
584 // This automatically adds two attributes "data-key" and
585 // "data-widget" to the root node of every react component.
586 // I think we might wanna find a better solution.
587 if (vdom.props) {
588 vdom.props['data-widget'] = name
589 vdom.props['data-key'] = this.props['data-key']
590 }
591
592 // Wrap plain JS values with SPAN, so react doesn't complain
593 if (!React.isValidElement(vdom))
594 // To do: should arrays be flattened into a SPAN's arguments?
595 vdom = React.DOM.span(null, (typeof vdom === 'string')
596 ? vdom : JSON.stringify(vdom))
597 return vdom
598 },
599 componentDidMount: function () {
600 var refresh = func.refresh
601 refresh && refresh.bind(this)()
602 },
603 componentWillUnmount: function () {
604 var down = func.down
605 return down && down.bind(this)()
606 },
607 componentDidUpdate: function () {
608 if (!this.initial_render_complete && !this.loading()) {
609 this.initial_render_complete = true
610 var up = func.up
611 up && up.bind(this)()
612 }
613 var refresh = func.refresh
614 return refresh && refresh.bind(this)()
615 },
616 getInitialState: function () { return {} }
617 })
618 }
619
620 function make_syncarea () {
621 // a textarea that syncs with other textareas via diffsync
622 // options:
623 // textarea_style : hashmap of styles to add to the textarea
624 // cursor_style : hashmap of styles to add to each peer's cursor
625 // autosize : true --> resizes the textarea vertically to fit the text inside it
626 // ws_url : websocket url for the diffsync server,
627 // e.g. 'wss://invisible.college:' + diffsync.port
628 // channel : 'diffsync_channel' --> diffsync channel to connect to
629 window['SYNCAREA'] = users_widgets['SYNCAREA'] = React.createClass({
630 getInitialState : function () {
631 return { cursor_positions : {} }
632 },
633 on_text_changed : function () {
634 if (this.props.autosize) {
635 var t = this.textarea_ref
636 t.style.height = null
637 while (t.rows > 1 && t.scrollHeight < t.offsetHeight) t.rows--
638 while (t.scrollHeight > t.offsetHeight) t.rows++
639 }
640 },
641 componentDidMount : function () {
642 var self = this
643 self.on_ranges = function (ranges) {
644 self.ranges = ranges
645 var cursor_positions = {}
646 Object.keys(ranges).forEach(function (k) {
647 var r = ranges[k]
648 var xy = getCaretCoordinates(self.textarea_ref, r[0])
649 var x = self.textarea_ref.offsetLeft - self.textarea_ref.scrollLeft + xy.left + 'px'
650 var y = self.textarea_ref.offsetTop - self.textarea_ref.scrollTop + xy.top + 'px'
651 cursor_positions[k] = [x, y]
652 })
653 self.setState({ cursor_positions : cursor_positions })
654 }
655
656 this.ds = diffsync.create_client({
657 ws_url : this.props.ws_url,
658 channel : this.props.channel,
659 get_text : function () {
660 return self.textarea_ref.value
661 },
662 get_range : function () {
663 var t = self.textarea_ref
664 return [t.selectionStart, t.selectionEnd]
665 },
666 on_text : function (s, range) {
667 self.textarea_ref.value = s
668 self.textarea_ref.setSelectionRange(range[0], range[1])
669 self.on_text_changed()
670 },
671 on_ranges : this.on_ranges
672 })
673 },
674 render : function () {
675 var self = this
676 var cursors = []
677 Object.keys(this.state.cursor_positions).forEach(function (k) {
678 var p = self.state.cursor_positions[k]
679 var style = {
680 position : 'absolute',
681 left : p[0],
682 top : p[1]
683 }
684 Object.keys(self.props.cursor_style).forEach(function (k) {
685 style[k] = self.props.cursor_style[k]
686 })
687 cursors.push(React.createElement('div', {
688 key : k,
689 style : style
690 }))
691 })
692 return React.createElement('div', {
693 style : {
694 clipPath : 'inset(0px 0px 0px 0px)'
695 },
696 }, React.createElement('textarea', {
697 ref : function (t) { self.textarea_ref = t },
698 style : this.props.textarea_style,
699 onChange : function (e) {
700 self.ds.on_change()
701 self.on_text_changed()
702 },
703 onMouseDown : function () {
704 setTimeout(function () { self.ds.on_change() }, 0)
705 },
706 onKeyDown : function () {
707 setTimeout(function () { self.ds.on_change() }, 0)
708 },
709 onScroll : function () {
710 self.on_ranges(self.ranges)
711 }
712 }), cursors)
713 }
714 })
715 }
716
717 function compile_coffee (coffee, filename) {
718 var compiled
719 try {
720 compiled = CoffeeScript.compile(coffee,
721 {bare: true,
722 sourceMap: true,
723 filename: filename})
724 var source_map = JSON.parse(compiled.v3SourceMap)
725 source_map.sourcesContent = coffee
726 compiled = compiled.js
727
728 // Base64 encode the source map
729 try {
730 compiled += '\n'
731 compiled += '//# sourceMappingURL=data:application/json;base64,'
732 compiled += btoa(JSON.stringify(source_map)) + '\n'
733 compiled += '//# sourceURL=' + filename
734 } catch (e) {} // btoa() fails on unicode. Give up for now.
735
736 } catch (error) {
737 if (error.location)
738 console.error('Syntax error in '+ filename + ' on line',
739 error.location.first_line
740 + ', column ' + error.location.first_column + ':',
741 error.message)
742 else throw error
743 }
744 return compiled
745 }
746 function load_client_code (code) {
747 var dom = {}, ui = {}
748 if (code) eval(code)
749 else { dom = window.dom; ui = window.ui }
750 for (var k in ui) dom[k] = dom[k] || ui[k]
751 for (var widget_name in dom)
752 window.dom[widget_name] = dom[widget_name]
753 }
754 function load_coffee () {
755 load_client_code()
756 var scripts = document.getElementsByTagName("script")
757 var filename = location.pathname.substring(location.pathname.lastIndexOf('/') + 1)
758 for (var i=0; i<scripts.length; i++)
759 if (scripts[i].getAttribute('type')
760 in {'statebus':1, 'coffeedom':1,'statebus-js':1,
761 'coffee':1, 'coffeescript':1}) {
762 // Compile coffeescript to javascript
763 var compiled = scripts[i].text
764 if (scripts[i].getAttribute('type') !== 'statebus-js')
765 compiled = compile_coffee(scripts[i].text, filename)
766 if (compiled)
767 load_client_code(compiled)
768 }
769 }
770
771 function dom_to_widget (node) {
772 if (node.nodeName === '#text') return node.textContent
773 if (!(node.nodeName in users_widgets)) return node
774
775 node.seen = true
776 var children = [], props = {}
777 // Recursively convert children
778 for (var i=0; i<node.childNodes.length; i++)
779 children.push(dom_to_widget(node.childNodes[i])) // recurse
780
781 // Convert attributes to props
782 var props = {}
783 for (var i=0; node.attributes && i<node.attributes.length; i++)
784 props[node.attributes[i].name] = node.attributes[i].value
785
786 var widge = (window[node.nodeName.toLowerCase()]
787 || window[node.nodeName.toUpperCase()])
788 console.assert(widge, node.nodeName + ' has not been defined as a UI widget.')
789
790 return widge(props, children)
791 }
792
793 window.users_widgets = users_widgets
794 function load_widgets () {
795 for (var w in users_widgets) {
796 var nodes = document.getElementsByTagName(w)
797 for (var i=0; i<nodes.length; i++)
798 if (!nodes[i].seen)
799 react_render(dom_to_widget(nodes[i]), nodes[i])
800 }
801 }
802
803 load_scripts()
804})()