UNPKG

40.4 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 = clientjs_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 clientjs_option (option_name) {
369 // This function must be copy/paste synchronized with statebus.js. Be
370 // sure to clone all edits there.
371 var script_elem = (
372 document.querySelector('script[src*="/client"][src$=".js"]') ||
373 document.querySelector('script[src^="client"][src$=".js"]'))
374 return script_elem && script_elem.getAttribute(option_name)
375 }
376 var loaded_from_file_url = window.location.href.match(/^file:\/\//)
377 window.statebus_server = window.statebus_server || clientjs_option('server') ||
378 (loaded_from_file_url ? 'https://stateb.us:3006' : '/')
379 window.statebus_backdoor = window.statebus_backdoor || clientjs_option('backdoor')
380 var react_render
381 function scripts_ready () {
382 react_render = React.version >= '0.14.' ? ReactDOM.render : React.render
383 make_client_statebus_maker()
384 window.bus = window.statebus()
385 window.bus.label = 'bus'
386 window.sb = bus.sb
387 statebus.widget = React_View
388 statebus.create_react_class = React_View
389 statebus.createReactClass = React_View
390
391 improve_react()
392 window.ignore_flashbacks = false
393 if (statebus_server !== 'none')
394 bus.net_mount ('/*', statebus_server)
395
396 if (window.statebus_backdoor) {
397 window.master = statebus()
398 master.net_mount('*', statebus_backdoor)
399 }
400 bus.net_automount()
401
402 // bus('*').to_save = function (obj) { bus.save.fire(obj) }
403 bus('/new/*').to_save = function (o) {
404 if (o.key.split('/').length > 3) return
405
406 var old_key = o.key
407 o.key = old_key + '/' + Math.random().toString(36).substring(2,12)
408 statebus.cache[o.key] = o
409 delete statebus.cache[old_key]
410 bus.save(o)
411 }
412 load_coffee()
413
414 statebus.compile_coffee = compile_coffee
415 statebus.load_client_code = load_client_code
416 statebus.load_widgets = load_widgets
417
418 document.addEventListener('DOMContentLoaded', function () {
419 if (window.statebus_ready)
420 for (var i=0; i<statebus_ready.length; i++)
421 statebus_ready[i]()
422 }, false)
423
424 document.addEventListener('DOMContentLoaded', load_widgets, false)
425
426 // if (dom.Body || dom.body || dom.BODY)
427 // react_render((window.Body || window.body || window.BODY)(), document.body)
428 }
429
430 function improve_react() {
431 function capitalize (s) {return s[0].toUpperCase() + s.slice(1)}
432 function camelcase (s) { var a = s.split(/[_-]/)
433 return a.slice(0,1).concat(a.slice(1).map(capitalize)).join('') }
434
435 // We used to get all_css_props like this:
436 //
437 // var all_css_props = Object.keys(document.body.style)
438 // if (all_css_props.length < 100) // Firefox
439 // all_css_props = Object.keys(document.body.style.__proto__)
440 //
441 // But now I've hard-coded them:
442 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"]
443
444 var ignore = {d:1, cx:1, cy:1, rx:1, ry:1, x:1, y:1,
445 content:1, fill:1, stroke:1, src:1}
446 var is_css_prop = {}
447 for (var i=0; i<all_css_props.length; i++)
448 if (!ignore[all_css_props[i]])
449 is_css_prop[all_css_props[i]] = true
450
451 function better_element(el) {
452 // To do:
453 // - Don't put all args into a children array, cause react thinks
454 // that means they need a key.
455
456 return function () {
457 var children = []
458 var attrs = {style: {}}
459
460 for (var i=0; i<arguments.length; i++) {
461 var arg = arguments[i]
462
463 // Strings and DOM nodes and undefined become children
464 if (typeof arg === 'string' // For "foo"
465 || arg instanceof String // For new String()
466 || arg && React.isValidElement(arg)
467 || arg === undefined)
468 children.push(arg)
469
470 // Arrays append onto the children
471 else if (arg instanceof Array)
472 Array.prototype.push.apply(children, arg)
473
474 // Pure objects get merged into object city
475 // Styles get redirected to the style field
476 else if (arg instanceof Object)
477 for (var k in arg)
478 if (is_css_prop[k]
479 && !(k in {width:1,height:1,size:1}
480 && el in {canvas:1, input:1, embed:1, object:1}))
481 attrs.style[k] = arg[k] // Merge styles
482 else if (k === 'style') // Merge insides of style tags
483 for (var k2 in arg[k])
484 attrs.style[k2] = arg[k][k2]
485 else {
486 attrs[k] = arg[k] // Or be normal.
487
488 if (k === 'key')
489 attrs['data-key'] = arg[k]
490 }
491 }
492 if (children.length === 0) children = undefined
493 if (attrs['ref'] === 'input')
494 bus.log(attrs, children)
495 return React.DOM[el](attrs, children)
496 }
497 }
498 for (var el in React.DOM)
499 window[el.toUpperCase()] = better_element(el)
500
501 function make_better_input (name, element) {
502 window[name] = React.createFactory(React.createClass({
503 getInitialState: function() {
504 return {value: this.props.value}
505 },
506 componentWillReceiveProps: function(new_props) {
507 this.setState({value: new_props.value})
508 },
509 onChange: function(e) {
510 this.props.onChange && this.props.onChange(e)
511 if (this.props.value)
512 this.setState({value: e.target.value})
513 },
514 render: function() {
515 var new_props = {}
516 for (var k in this.props)
517 if (this.props.hasOwnProperty(k))
518 new_props[k] = this.props[k]
519 if (this.state.value) new_props.value = this.state.value
520 new_props.onChange = this.onChange
521 return element(new_props)
522 }
523 }))
524 }
525
526 make_better_input("INPUT", window.INPUT)
527 make_better_input("TEXTAREA", window.TEXTAREA)
528 make_syncarea()
529
530 // Make IMG accept data from state:
531 var og_img = window.IMG
532 window.IMG = function () {
533 var args = []
534 for (var i=0; i<arguments.length; i++) {
535 args.push(arguments[i])
536 if (arguments[i].state)
537 args[i].src = 'data:;base64,' + fetch(args[i].state)._
538 }
539 return og_img.apply(this, args)
540 }
541
542
543 // Unfortunately, React's default STYLE and TITLE tags are useless
544 // unless you "dangerously set inner html" because they wrap strings
545 // inside useless spans.
546 function escape_html (s) {
547 // TODO: this will fail on '<' and '>' in CSS selectors
548 return s.replace(/</g, "&lt;").replace(/>/g, "&gt;")
549 }
550 window.STYLE = function (s) {
551 return React.DOM.style({dangerouslySetInnerHTML: {__html: escape_html(s)}})
552 }
553 window.TITLE = function (s) {
554 return React.DOM.title({dangerouslySetInnerHTML: {__html: escape_html(s)}})
555 }
556 }
557
558 function autodetect_args (func) {
559 if (func.args) return
560
561 // Get an array of the func's params
562 var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
563 params = /([^\s,]+)/g,
564 s = func.toString().replace(comments, '')
565 func.args = s.slice(s.indexOf('(')+1, s.indexOf(')')).match(params) || []
566 }
567
568
569 // Load the components
570 var users_widgets = {}
571 function make_component(name, func) {
572 // Define the component
573
574 window[name] = users_widgets[name] = window.React_View({
575 displayName: name,
576 render: function () {
577 var args = []
578
579 // Parse the function's args, and pass props into them directly
580 autodetect_args(func)
581 // this.props.kids = this.props.kids || this.props.children
582 for (var i=0; i<func.args.length; i++)
583 args.push(this.props[func.args[i]])
584
585 // Now run the function.
586 var vdom = func.apply(this, args)
587
588 // This automatically adds two attributes "data-key" and
589 // "data-widget" to the root node of every react component.
590 // I think we might wanna find a better solution.
591 if (vdom && vdom.props) {
592 vdom.props['data-widget'] = name
593 vdom.props['data-key'] = this.props['data-key']
594 }
595
596 // Wrap plain JS values with SPAN, so react doesn't complain
597 if (!React.isValidElement(vdom))
598 // To do: should arrays be flattened into a SPAN's arguments?
599 vdom = React.DOM.span(null, (typeof vdom === 'string')
600 ? vdom : JSON.stringify(vdom))
601 return vdom
602 },
603 componentDidMount: function () {
604 var refresh = func.refresh
605 refresh && refresh.bind(this)()
606 },
607 componentWillUnmount: function () {
608 var down = func.down
609 return down && down.bind(this)()
610 },
611 componentDidUpdate: function () {
612 if (!this.initial_render_complete && !this.loading()) {
613 this.initial_render_complete = true
614 var up = func.up
615 up && up.bind(this)()
616 }
617 var refresh = func.refresh
618 return refresh && refresh.bind(this)()
619 },
620 getInitialState: function () { return {} }
621 })
622 }
623
624 function make_syncarea () {
625 // a textarea that syncs with other textareas via diffsync
626 // options:
627 // textarea_style : hashmap of styles to add to the textarea
628 // cursor_style : hashmap of styles to add to each peer's cursor
629 // autosize : true --> resizes the textarea vertically to fit the text inside it
630 // ws_url : websocket url for the diffsync server,
631 // e.g. 'wss://invisible.college:' + diffsync.port
632 // channel : 'diffsync_channel' --> diffsync channel to connect to
633 window['SYNCAREA'] = users_widgets['SYNCAREA'] = React.createClass({
634 getInitialState : function () {
635 return { cursor_positions : {} }
636 },
637 on_text_changed : function () {
638 if (this.props.autosize) {
639 var t = this.textarea_ref
640 t.style.height = null
641 while (t.rows > 1 && t.scrollHeight < t.offsetHeight) t.rows--
642 while (t.scrollHeight > t.offsetHeight) t.rows++
643 }
644 },
645 componentDidMount : function () {
646 var self = this
647 self.on_ranges = function (ranges) {
648 self.ranges = ranges
649 var cursor_positions = {}
650 Object.keys(ranges).forEach(function (k) {
651 var r = ranges[k]
652 var xy = getCaretCoordinates(self.textarea_ref, r[0])
653 var x = self.textarea_ref.offsetLeft - self.textarea_ref.scrollLeft + xy.left + 'px'
654 var y = self.textarea_ref.offsetTop - self.textarea_ref.scrollTop + xy.top + 'px'
655 cursor_positions[k] = [x, y]
656 })
657 self.setState({ cursor_positions : cursor_positions })
658 }
659
660 this.ds = diffsync.create_client({
661 ws_url : this.props.ws_url,
662 channel : this.props.channel,
663 get_text : function () {
664 return self.textarea_ref.value
665 },
666 get_range : function () {
667 var t = self.textarea_ref
668 return [t.selectionStart, t.selectionEnd]
669 },
670 on_text : function (s, range) {
671 self.textarea_ref.value = s
672 self.textarea_ref.setSelectionRange(range[0], range[1])
673 self.on_text_changed()
674 },
675 on_ranges : this.on_ranges
676 })
677 },
678 render : function () {
679 var self = this
680 var cursors = []
681 Object.keys(this.state.cursor_positions).forEach(function (k) {
682 var p = self.state.cursor_positions[k]
683 var style = {
684 position : 'absolute',
685 left : p[0],
686 top : p[1]
687 }
688 Object.keys(self.props.cursor_style).forEach(function (k) {
689 style[k] = self.props.cursor_style[k]
690 })
691 cursors.push(React.createElement('div', {
692 key : k,
693 style : style
694 }))
695 })
696 return React.createElement('div', {
697 style : {
698 clipPath : 'inset(0px 0px 0px 0px)'
699 },
700 }, React.createElement('textarea', {
701 ref : function (t) { self.textarea_ref = t },
702 style : this.props.textarea_style,
703 onChange : function (e) {
704 self.ds.on_change()
705 self.on_text_changed()
706 },
707 onMouseDown : function () {
708 setTimeout(function () { self.ds.on_change() }, 0)
709 },
710 onKeyDown : function () {
711 setTimeout(function () { self.ds.on_change() }, 0)
712 },
713 onScroll : function () {
714 self.on_ranges(self.ranges)
715 }
716 }), cursors)
717 }
718 })
719 }
720
721 function compile_coffee (coffee, filename) {
722 var compiled
723 try {
724 compiled = CoffeeScript.compile(coffee,
725 {bare: true,
726 sourceMap: true,
727 filename: filename})
728 var source_map = JSON.parse(compiled.v3SourceMap)
729 source_map.sourcesContent = coffee
730 compiled = compiled.js
731
732 // Base64 encode the source map
733 try {
734 compiled += '\n'
735 compiled += '//# sourceMappingURL=data:application/json;base64,'
736 compiled += btoa(JSON.stringify(source_map)) + '\n'
737 compiled += '//# sourceURL=' + filename
738 } catch (e) {} // btoa() fails on unicode. Give up for now.
739
740 } catch (error) {
741 if (error.location)
742 console.error('Syntax error in '+ filename + ' on line',
743 error.location.first_line
744 + ', column ' + error.location.first_column + ':',
745 error.message)
746 else throw error
747 }
748 return compiled
749 }
750 function load_client_code (code) {
751 var dom = {}, ui = {}
752 if (code) eval(code)
753 else { dom = window.dom; ui = window.ui }
754 for (var k in ui) dom[k] = dom[k] || ui[k]
755 for (var widget_name in dom)
756 window.dom[widget_name] = dom[widget_name]
757 }
758 function load_coffee () {
759 load_client_code()
760 var scripts = document.getElementsByTagName("script")
761 var filename = location.pathname.substring(location.pathname.lastIndexOf('/') + 1)
762 for (var i=0; i<scripts.length; i++)
763 if (scripts[i].getAttribute('type')
764 in {'statebus':1, 'coffeedom':1,'statebus-js':1,
765 'coffee':1, 'coffeescript':1}) {
766 // Compile coffeescript to javascript
767 var compiled = scripts[i].text
768 if (scripts[i].getAttribute('type') !== 'statebus-js')
769 compiled = compile_coffee(scripts[i].text, filename)
770 if (compiled)
771 load_client_code(compiled)
772 }
773 }
774
775 function dom_to_widget (node) {
776 if (node.nodeName === '#text') return node.textContent
777 if (!(node.nodeName in users_widgets)) return node
778
779 node.seen = true
780 var children = [], props = {}
781 // Recursively convert children
782 for (var i=0; i<node.childNodes.length; i++)
783 children.push(dom_to_widget(node.childNodes[i])) // recurse
784
785 // Convert attributes to props
786 var props = {}
787 for (var i=0; node.attributes && i<node.attributes.length; i++)
788 props[node.attributes[i].name] = node.attributes[i].value
789
790 var widge = (window[node.nodeName.toLowerCase()]
791 || window[node.nodeName.toUpperCase()])
792 console.assert(widge, node.nodeName + ' has not been defined as a UI widget.')
793
794 return widge(props, children)
795 }
796
797 window.users_widgets = users_widgets
798 function load_widgets () {
799 for (var w in users_widgets) {
800 var nodes = document.getElementsByTagName(w)
801 for (var i=0; i<nodes.length; i++)
802 if (!nodes[i].seen)
803 react_render(dom_to_widget(nodes[i]), nodes[i])
804 }
805 }
806
807 load_scripts()
808})()