UNPKG

40.1 kBJavaScriptView Raw
1(function () {
2 var unique_sockjs_string = '_connect_to_statebus_'
3
4 // ****************
5 // Connecting over the Network
6 function set_cookie (key, val) {
7 document.cookie = key + '=' + val + '; Expires=21 Oct 2025 00:0:00 GMT;'
8 }
9 function get_cookie (key) {
10 var c = document.cookie.match('(^|;)\\s*' + key + '\\s*=\\s*([^;]+)');
11 return c ? c.pop() : '';
12 }
13 try { document.cookie } catch (e) {get_cookie = set_cookie = function (){}}
14 function make_websocket (url) {
15 if (!url.match(/^\w{0,7}:\/\//))
16 url = location.protocol+'//'+location.hostname+(location.port ? ':'+location.port : '') + url
17
18 url = url.replace(/^state:\/\//, 'wss://')
19 url = url.replace(/^istate:\/\//, 'ws://')
20 url = url.replace(/^statei:\/\//, 'ws://')
21
22 url = url.replace(/^https:\/\//, 'wss://')
23 url = url.replace(/^http:\/\//, 'ws://')
24
25 // { // Convert to absolute
26 // var link = document.createElement("a")
27 // link.href = url
28 // url = link.href
29 // }
30 console.log('opening websocket to', url)
31 return new WebSocket(url + '/' + unique_sockjs_string + '/websocket')
32 // return new SockJS(url + '/' + unique_sockjs_string)
33 }
34 function client_creds (server_url) {
35 var me = bus.fetch('ls/me')
36 bus.log('connect: me is', me)
37 if (!me.client) {
38 // Create a client id if we have none yet.
39 // Either from a cookie set by server, or a new one from scratch.
40 var c = get_cookie('client')
41 me.client = c || (Math.random().toString(36).substring(2)
42 + Math.random().toString(36).substring(2)
43 + Math.random().toString(36).substring(2))
44 bus.save(me)
45 }
46
47 set_cookie('client', me.client)
48 return {clientid: me.client}
49 }
50
51
52 // ****************
53 // Manipulate Localstorage
54 function localstorage_client (prefix) {
55 try { localStorage } catch (e) { return }
56
57 // This doesn't yet trigger updates across multiple browser windows.
58 // We can do that by adding a list of dirty keys and
59
60 var bus = this
61 bus.log(this)
62
63 // Fetch returns the value immediately in a save
64 // Saves are queued up, to store values with a delay, in batch
65 var saves_are_pending = false
66 var pending_saves = {}
67
68 function save_the_pending_saves() {
69 bus.log('localstore: saving', pending_saves)
70 for (var k in pending_saves)
71 localStorage.setItem(k, JSON.stringify(pending_saves[k]))
72 saves_are_pending = false
73 }
74
75 bus(prefix).to_fetch = function (key) {
76 var result = localStorage.getItem(key)
77 return result ? JSON.parse(result) : {key: key}
78 }
79 bus(prefix).to_save = function (obj) {
80 // Do I need to make this recurse into the object?
81 bus.log('localStore: on_save:', obj.key)
82 pending_saves[obj.key] = obj
83 if (!saves_are_pending) {
84 setTimeout(save_the_pending_saves, 50)
85 saves_are_pending = true
86 }
87 bus.save.fire(obj)
88 return obj
89 }
90 bus(prefix).to_delete = function (key) { localStorage.removeItem(key) }
91
92
93 // Hm... this update stuff doesn't seem to work on file:/// urls in chrome
94 function update (event) {
95 bus.log('Got a localstorage update', event)
96 //this.get(event.key.substr('statebus '.length))
97 }
98 if (window.addEventListener) window.addEventListener("storage", update, false)
99 else window.attachEvent("onstorage", update)
100 }
101
102 // Stores state in the query string, as ?key1={obj...}&key2={obj...}
103 function url_store (prefix) {
104 var bus = this
105 function get_query_string_value (key) {
106 return unescape(window.location.search.replace(
107 new RegExp("^(?:.*[&\\?]"
108 + escape(key).replace(/[\.\+\*]/g, "\\$&")
109 + "(?:\\=([^&]*))?)?.*$", "i"),
110 "$1"))
111 }
112
113 // Initialize data from the URL on load
114
115 // Now the regular shit
116 var data = get_query_string_value(key)
117 data = (data && JSON.parse(data)) || {key : key}
118 // Then I would need to:
119 // - Change the key prefix
120 // - Save this into the cache
121
122 bus(prefix).to_save = function (obj) {
123 window.history.replaceState(
124 '',
125 '',
126 document.location.origin
127 + document.location.pathname
128 + escape('?'+key+'='+JSON.stringify(obj)))
129 bus.save.fire(obj)
130 }
131 }
132
133 function live_reload_from (prefix) {
134 if (!window.live_reload_initialized) {
135 var first_time = true
136 this(function () {
137 var re = new RegExp(".*/" + prefix + "/(.*)")
138 var file = window.location.href.match(re)[1]
139 var code = bus.fetch('/code/invisible.college/' + file).code
140 if (!code) return
141 if (first_time) {first_time = false; return}
142 var old_scroll_position = window.pageYOffset
143 document.body.innerHTML = code
144 var i = 0
145 var d = 100
146 var interval = setInterval(function () {
147 if (i > 500) clearInterval(interval)
148 i += d
149 window.scrollTo(0, old_scroll_position)
150 }, d)
151 })
152 window.live_reload_initialized = true
153 }
154 }
155
156
157 // ****************
158 // Wrapper for React Components
159
160 // XXX Currently assumes there's a statebus named "bus" in global
161 // XXX scope.
162
163 var components = {} // Indexed by 'component/0', 'component/1', etc.
164 var components_count = 0
165 var dirty_components = {}
166 function React_View(component) {
167 function wrap(name, new_func) {
168 var old_func = component[name]
169 component[name] = function wrapper () { return new_func.bind(this)(old_func) }
170 }
171
172 // Register the component's basic info
173 wrap('componentWillMount', function new_cwm (orig_func) {
174 if (component.displayName === undefined)
175 throw 'Component needs a displayName'
176 this.name = component.displayName.toLowerCase().replace(' ', '_')
177 this.key = 'component/' + components_count++
178 components[this.key] = this
179
180 function add_shortcut (obj, shortcut_name, to_key) {
181 delete obj[shortcut_name]
182 Object.defineProperty(obj, shortcut_name, {
183 get: function () { return bus.fetch(to_key) },
184 configurable: true })
185 }
186 add_shortcut(this, 'local', this.key)
187
188 orig_func && orig_func.apply(this, arguments)
189
190 // Make render reactive
191 var orig_render = this.render
192 this.render = bus.reactive(function () {
193 console.assert(this !== window)
194 if (this.render.called_directly) {
195 delete dirty_components[this.key]
196
197 // Add reactivity to any keys passed inside objects in props.
198 for (var k in this.props)
199 if (this.props.hasOwnProperty(k)
200 && this.props[k] !== null
201 && typeof this.props[k] === 'object'
202 && this.props[k].key)
203
204 bus.fetch(this.props[k].key)
205
206 // Call the renderer!
207 return orig_render.apply(this, arguments)
208 } else {
209 dirty_components[this.key] = true
210 schedule_re_render()
211 }
212 })
213 })
214
215 wrap('componentWillUnmount', function new_cwu (orig_func) {
216 orig_func && orig_func.apply(this, arguments)
217 // Clean up
218 bus.delete(this.key)
219 delete components[this.key]
220 delete dirty_components[this.key]
221 })
222
223 function shallow_clone(original) {
224 var clone = Object.create(Object.getPrototypeOf(original))
225 var i, keys = Object.getOwnPropertyNames(original)
226 for (i=0; i < keys.length; i++){
227 Object.defineProperty(clone, keys[i],
228 Object.getOwnPropertyDescriptor(original, keys[i])
229 )
230 }
231 return clone
232 }
233
234 component.shouldComponentUpdate = function new_scu (next_props, next_state) {
235 // This component definitely needs to update if it is marked as dirty
236 if (dirty_components[this.key] !== undefined) return true
237
238 // Otherwise, we'll check to see if its state or props
239 // have changed. But ignore React's 'children' prop,
240 // because it often has a circular reference.
241 next_props = shallow_clone(next_props)
242 this_props = shallow_clone(this.props)
243
244 delete next_props['children']; delete this_props['children']
245 // delete next_props['kids']; delete this_props['kids']
246
247 next_props = bus.clone(next_props)
248 this_props = bus.clone(this_props)
249
250
251 return !bus.deep_equals([next_state, next_props], [this.state, this_props])
252
253 // TODO:
254 //
255 // - Check children too. Right now we just silently fail
256 // on components with children. WTF?
257 //
258 // - A better method might be to mark a component dirty when
259 // it receives new props in the
260 // componentWillReceiveProps React method.
261 }
262
263 component.loading = function loading () {
264 return this.render.loading()
265 }
266
267 // Now create the actual React class with this definition, and
268 // return it.
269 var react_class = React.createClass(component)
270 var result = function (props, children) {
271 props = props || {}
272 props['data-key'] = props.key
273 props['data-widget'] = component.displayName
274
275 return (React.version >= '0.12.'
276 ? React.createElement(react_class, props, children)
277 : react_class(props, children))
278 }
279 // Give it the same prototype as the original class so that it
280 // passes React.isValidClass() inspection
281 result.prototype = react_class.prototype
282 return result
283 }
284 window.React_View = React_View
285 if (window.statebus) window.statebus.create_react_class = React_View
286
287 // *****************
288 // Re-rendering react components
289 var re_render_scheduled = false
290 re_rendering = false
291 function schedule_re_render() {
292 if (!re_render_scheduled) {
293 requestAnimationFrame(function () {
294 re_render_scheduled = false
295
296 // Re-renders dirty components
297 for (var comp_key in dirty_components) {
298 if (dirty_components[comp_key] // Since another component's update might update this
299 && components[comp_key]) // Since another component might unmount this
300
301 try {
302 re_rendering = true
303 components[comp_key].forceUpdate()
304 } finally {
305 re_rendering = false
306 }
307 }
308 })
309 re_render_scheduled = true
310 }
311 }
312
313 // ##############################################################################
314 // ###
315 // ### Full-featured single-file app methods
316 // ###
317
318 function make_client_statebus_maker () {
319 var extra_stuff = ['localstorage_client make_websocket client_creds',
320 'url_store components live_reload_from'].join(' ').split(' ')
321 if (window.statebus) {
322 var orig_statebus = statebus
323 window.statebus = function make_client_bus () {
324 var bus = orig_statebus()
325 for (var i=0; i<extra_stuff.length; i++)
326 bus[extra_stuff[i]] = eval(extra_stuff[i])
327 bus.localstorage_client('ls/*')
328 return bus
329 }
330 }
331 }
332
333 load_scripts() // This function could actually be inlined
334 function load_scripts() {
335 // console.info('Loading scripts!', window.statebus)
336 if (!window.statebus) {
337 var statebus_dir = script_elem().getAttribute('src').match(/(.*)[\/\\]/)
338 statebus_dir = (statebus_dir && statebus_dir[1] + '/')||''
339
340 var js_urls = {
341 react: statebus_dir + 'extras/react.js',
342 sockjs: statebus_dir + 'extras/sockjs.js',
343 coffee: statebus_dir + 'extras/coffee.js',
344 statebus: statebus_dir + 'statebus.js'
345 }
346 if (statebus_dir == 'https://stateb.us/')
347 js_urls.statebus = statebus_dir + 'statebus4.js'
348
349 for (var name in js_urls)
350 document.write('<script src="' + js_urls[name] + '" charset="utf-8"></script>')
351 }
352
353 document.addEventListener('DOMContentLoaded', scripts_ready, false)
354 }
355
356 function script_elem () {
357 return document.querySelector('script[src*="client"][src$=".js"]')
358 }
359 var loaded_from_file_url = window.location.href.match(/^file:\/\//)
360 window.statebus_server = window.statebus_server ||
361 script_elem().getAttribute('server') ||
362 (loaded_from_file_url ? 'https://stateb.us:3006' : '/')
363 window.statebus_backdoor = window.statebus_backdoor ||
364 script_elem().getAttribute('backdoor')
365 var react_render
366 function scripts_ready () {
367 react_render = React.version >= '0.14.' ? ReactDOM.render : React.render
368 make_client_statebus_maker()
369 window.bus = window.statebus()
370 window.bus.label = 'bus'
371 window.sb = bus.sb
372 statebus.widget = React_View
373 statebus.create_react_class = React_View
374
375 improve_react()
376 window.dom = window.ui = window.dom || window.ui || {}
377 window.ignore_flashbacks = false
378 if (statebus_server !== 'none')
379 bus.net_mount ('/*', statebus_server)
380
381 if (window.statebus_backdoor) {
382 window.master = statebus()
383 master.net_mount('*', statebus_backdoor)
384 }
385 bus.net_automount()
386
387 // bus('*').to_save = function (obj) { bus.save.fire(obj) }
388 bus('/new/*').to_save = function (o) {
389 if (o.key.split('/').length > 3) return
390
391 var old_key = o.key
392 o.key = old_key + '/' + Math.random().toString(36).substring(2,12)
393 statebus.cache[o.key] = o
394 delete statebus.cache[old_key]
395 bus.save(o)
396 }
397 load_coffee()
398
399 statebus.compile_coffee = compile_coffee
400 statebus.load_client_code = load_client_code
401 statebus.load_widgets = load_widgets
402
403 if (window.statebus_ready)
404 for (var i=0; i<statebus_ready.length; i++)
405 statebus_ready[i]()
406
407 load_widgets()
408 // if (dom.Body || dom.body || dom.BODY)
409 // react_render((window.Body || window.body || window.BODY)(), document.body)
410 }
411
412 function improve_react() {
413 function capitalize (s) {return s[0].toUpperCase() + s.slice(1)}
414 function camelcase (s) { var a = s.split(/[_-]/)
415 return a.slice(0,1).concat(a.slice(1).map(capitalize)).join('') }
416
417 // We used to get all_css_props like this:
418 //
419 // var all_css_props = Object.keys(document.body.style)
420 // if (all_css_props.length < 100) // Firefox
421 // all_css_props = Object.keys(document.body.style.__proto__)
422 //
423 // But now I've hard-coded them:
424 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"]
425
426 var ignore = {d:1, cx:1, cy:1, rx:1, ry:1, x:1, y:1,
427 content:1, fill:1, stroke:1, src:1}
428 var is_css_prop = {}
429 for (var i=0; i<all_css_props.length; i++)
430 if (!ignore[all_css_props[i]])
431 is_css_prop[all_css_props[i]] = true
432
433 function better_element(el) {
434 // To do:
435 // - Don't put all args into a children array, cause react thinks
436 // that means they need a key.
437
438 return function () {
439 var children = []
440 var attrs = {style: {}}
441
442 for (var i=0; i<arguments.length; i++) {
443 var arg = arguments[i]
444
445 // Strings and DOM nodes and undefined become children
446 if (typeof arg === 'string' // For "foo"
447 || arg instanceof String // For new String()
448 || arg && React.isValidElement(arg)
449 || arg === undefined)
450 children.push(arg)
451
452 // Arrays append onto the children
453 else if (arg instanceof Array)
454 Array.prototype.push.apply(children, arg)
455
456 // Pure objects get merged into object city
457 // Styles get redirected to the style field
458 else if (arg instanceof Object)
459 for (var k in arg)
460 if (is_css_prop[k]
461 && !(k in {width:1,height:1,size:1}
462 && el in {canvas:1, input:1, embed:1, object:1}))
463 attrs.style[k] = arg[k] // Merge styles
464 else if (k === 'style') // Merge insides of style tags
465 for (var k2 in arg[k])
466 attrs.style[k2] = arg[k][k2]
467 else {
468 attrs[k] = arg[k] // Or be normal.
469
470 if (k === 'key')
471 attrs['data-key'] = arg[k]
472 }
473 }
474 if (children.length === 0) children = undefined
475 if (attrs['ref'] === 'input')
476 bus.log(attrs, children)
477 return React.DOM[el](attrs, children)
478 }
479 }
480 for (var el in React.DOM)
481 window[el.toUpperCase()] = better_element(el)
482
483 function make_better_input (name, element) {
484 window[name] = React.createFactory(React.createClass({
485 getInitialState: function() {
486 return {value: this.props.value}
487 },
488 componentWillReceiveProps: function(new_props) {
489 this.setState({value: new_props.value})
490 },
491 onChange: function(e) {
492 this.props.onChange && this.props.onChange(e)
493 if (this.props.value)
494 this.setState({value: e.target.value})
495 },
496 render: function() {
497 var new_props = {}
498 for (var k in this.props)
499 if (this.props.hasOwnProperty(k))
500 new_props[k] = this.props[k]
501 if (this.state.value) new_props.value = this.state.value
502 new_props.onChange = this.onChange
503 return element(new_props)
504 }
505 }))
506 }
507
508 make_better_input("INPUT", window.INPUT)
509 make_better_input("TEXTAREA", window.TEXTAREA)
510 make_syncarea()
511
512 // Make IMG accept data from state:
513 var og_img = window.IMG
514 window.IMG = function () {
515 var args = []
516 for (var i=0; i<arguments.length; i++) {
517 args.push(arguments[i])
518 if (arguments[i].state)
519 args[i].src = 'data:;base64,' + fetch(args[i].state)._
520 }
521 return og_img.apply(this, args)
522 }
523
524
525 // Unfortunately, React's default STYLE and TITLE tags are useless
526 // unless you "dangerously set inner html" because they wrap strings
527 // inside useless spans.
528 function escape_html (s) {
529 // TODO: this will fail on '<' and '>' in CSS selectors
530 return s.replace(/</g, "&lt;").replace(/>/g, "&gt;")
531 }
532 window.STYLE = function (s) {
533 return React.DOM.style({dangerouslySetInnerHTML: {__html: escape_html(s)}})
534 }
535 window.TITLE = function (s) {
536 return React.DOM.title({dangerouslySetInnerHTML: {__html: escape_html(s)}})
537 }
538 }
539
540 function autodetect_args (func) {
541 if (func.args) return
542
543 // Get an array of the func's params
544 var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
545 params = /([^\s,]+)/g,
546 s = func.toString().replace(comments, '')
547 func.args = s.slice(s.indexOf('(')+1, s.indexOf(')')).match(params) || []
548 }
549
550
551 // Load the components
552 var users_widgets = {}
553 function make_component(name, safe_renders) {
554 // Define the component
555
556 window[name] = users_widgets[name] = window.React_View({
557 displayName: name,
558 render: function () {
559 var args = [], func = window.dom[name]
560
561 // Parse the function's args, and pass props into them directly
562 autodetect_args(func)
563 // this.props.kids = this.props.kids || this.props.children
564 for (var i=0; i<func.args.length; i++)
565 args.push(this.props[func.args[i]])
566
567 // Now run the function.
568 var vdom
569 if (safe_renders)
570 try {
571 vdom = func.apply(this, args)
572 } catch (error) {
573 console.error(error)
574 }
575 else // TODO: kill support for this safe_renders = false branch?
576 vdom = func.apply(this, args)
577
578 // This automatically adds two attributes "data-key" and
579 // "data-widget" to the root node of every react component.
580 // I think we might wanna find a better solution.
581 if (vdom && vdom.props) {
582 vdom.props['data-widget'] = name
583 vdom.props['data-key'] = this.props['data-key']
584 }
585
586 // Wrap plain JS values with SPAN, so react doesn't complain
587 if (!React.isValidElement(vdom))
588 // To do: should arrays be flattened into a SPAN's arguments?
589 vdom = React.DOM.span(null, (typeof vdom === 'string')
590 ? vdom : JSON.stringify(vdom))
591 return vdom
592 },
593 componentDidMount: function () {
594 var refresh = window.dom[name].refresh
595 refresh && refresh.bind(this)()
596 },
597 componentWillUnmount: function () {
598 var down = window.dom[name].down
599 return down && down.bind(this)()
600 },
601 componentDidUpdate: function () {
602 if (!this.initial_render_complete && !this.loading()) {
603 this.initial_render_complete = true
604 var up = window.dom[name].up
605 up && up.bind(this)()
606 }
607 var refresh = window.dom[name].refresh
608 return refresh && refresh.bind(this)()
609 },
610 getInitialState: function () { return {} }
611 })
612 }
613
614 function make_syncarea () {
615 // a textarea that syncs with other textareas via diffsync
616 // options:
617 // textarea_style : hashmap of styles to add to the textarea
618 // cursor_style : hashmap of styles to add to each peer's cursor
619 // autosize : true --> resizes the textarea vertically to fit the text inside it
620 // ws_url : websocket url for the diffsync server,
621 // e.g. 'wss://invisible.college:' + diffsync.port
622 // channel : 'diffsync_channel' --> diffsync channel to connect to
623 window['SYNCAREA'] = users_widgets['SYNCAREA'] = React.createClass({
624 getInitialState : function () {
625 return { cursor_positions : {} }
626 },
627 on_text_changed : function () {
628 if (this.props.autosize) {
629 var t = this.textarea_ref
630 t.style.height = null
631 while (t.rows > 1 && t.scrollHeight < t.offsetHeight) t.rows--
632 while (t.scrollHeight > t.offsetHeight) t.rows++
633 }
634 },
635 componentDidMount : function () {
636 var self = this
637 self.on_ranges = function (ranges) {
638 self.ranges = ranges
639 var cursor_positions = {}
640 Object.keys(ranges).forEach(function (k) {
641 var r = ranges[k]
642 var xy = getCaretCoordinates(self.textarea_ref, r[0])
643 var x = self.textarea_ref.offsetLeft - self.textarea_ref.scrollLeft + xy.left + 'px'
644 var y = self.textarea_ref.offsetTop - self.textarea_ref.scrollTop + xy.top + 'px'
645 cursor_positions[k] = [x, y]
646 })
647 self.setState({ cursor_positions : cursor_positions })
648 }
649
650 this.ds = diffsync.create_client({
651 ws_url : this.props.ws_url,
652 channel : this.props.channel,
653 get_text : function () {
654 return self.textarea_ref.value
655 },
656 get_range : function () {
657 var t = self.textarea_ref
658 return [t.selectionStart, t.selectionEnd]
659 },
660 on_text : function (s, range) {
661 self.textarea_ref.value = s
662 self.textarea_ref.setSelectionRange(range[0], range[1])
663 self.on_text_changed()
664 },
665 on_ranges : this.on_ranges
666 })
667 },
668 render : function () {
669 var self = this
670 var cursors = []
671 Object.keys(this.state.cursor_positions).forEach(function (k) {
672 var p = self.state.cursor_positions[k]
673 var style = {
674 position : 'absolute',
675 left : p[0],
676 top : p[1]
677 }
678 Object.keys(self.props.cursor_style).forEach(function (k) {
679 style[k] = self.props.cursor_style[k]
680 })
681 cursors.push(React.createElement('div', {
682 key : k,
683 style : style
684 }))
685 })
686 return React.createElement('div', {
687 style : {
688 clipPath : 'inset(0px 0px 0px 0px)'
689 },
690 }, React.createElement('textarea', {
691 ref : function (t) { self.textarea_ref = t },
692 style : this.props.textarea_style,
693 onChange : function (e) {
694 self.ds.on_change()
695 self.on_text_changed()
696 },
697 onMouseDown : function () {
698 setTimeout(function () { self.ds.on_change() }, 0)
699 },
700 onKeyDown : function () {
701 setTimeout(function () { self.ds.on_change() }, 0)
702 },
703 onScroll : function () {
704 self.on_ranges(self.ranges)
705 }
706 }), cursors)
707 }
708 })
709 }
710
711 function compile_coffee (coffee, filename) {
712 var compiled
713 try {
714 compiled = CoffeeScript.compile(coffee,
715 {bare: true,
716 sourceMap: true,
717 filename: filename})
718 var source_map = JSON.parse(compiled.v3SourceMap)
719 source_map.sourcesContent = coffee
720 compiled = compiled.js
721
722 // Base64 encode the source map
723 try {
724 compiled += '\n'
725 compiled += '//# sourceMappingURL=data:application/json;base64,'
726 compiled += btoa(JSON.stringify(source_map)) + '\n'
727 compiled += '//# sourceURL=' + filename
728 } catch (e) {} // btoa() fails on unicode. Give up for now.
729
730 } catch (error) {
731 if (error.location)
732 console.error('Syntax error in '+ filename + ' on line',
733 error.location.first_line
734 + ', column ' + error.location.first_column + ':',
735 error.message)
736 else throw error
737 }
738 return compiled
739 }
740 function load_client_code (code, safe) {
741 var dom = {}, ui = {}
742 if (code) eval(code)
743 else { dom = window.dom; ui = window.ui }
744 for (var k in ui) dom[k] = dom[k] || ui[k]
745 for (var widget_name in dom) {
746 window.dom[widget_name] = dom[widget_name]
747 make_component(widget_name, safe)
748 }
749 }
750 function load_coffee () {
751 load_client_code()
752 var scripts = document.getElementsByTagName("script")
753 var filename = location.pathname.substring(location.pathname.lastIndexOf('/') + 1)
754 for (var i=0; i<scripts.length; i++)
755 if (scripts[i].getAttribute('type')
756 in {'statebus':1, 'coffeedom':1,'statebus-js':1,
757 'coffee':1, 'coffeescript':1}) {
758 // Compile coffeescript to javascript
759 var compiled = scripts[i].text
760 if (scripts[i].getAttribute('type') !== 'statebus-js')
761 compiled = compile_coffee(scripts[i].text, filename)
762 if (compiled)
763 load_client_code(compiled)
764 }
765 }
766
767 function dom_to_widget (node) {
768 if (node.nodeName === '#text') return node.textContent
769
770 node.seen = true
771 var children = [], props = {}
772 // Recursively convert children
773 for (var i=0; i<node.childNodes.length; i++)
774 children.push(dom_to_widget(node.childNodes[i])) // recurse
775
776 // Convert attributes to props
777 var props = {}
778 for (var i=0; node.attributes && i<node.attributes.length; i++)
779 props[node.attributes[i].name] = node.attributes[i].value
780
781 var widge = (window[node.nodeName.toLowerCase()]
782 || window[node.nodeName.toUpperCase()])
783 console.assert(widge, node.nodeName + ' has not been defined as a UI widget.')
784
785 return widge(props, children)
786 }
787
788 window.users_widgets = users_widgets
789 function load_widgets () {
790 for (var w in users_widgets) {
791 var nodes = document.getElementsByTagName(w)
792 for (var i=0; i<nodes.length; i++)
793 if (!nodes[i].seen)
794 react_render(dom_to_widget(nodes[i]), nodes[i])
795 }
796 }
797})()