├── .gitignore ├── Fluxify.js ├── README.md └── examples └── chat ├── .gitignore ├── css └── chatapp.css ├── images └── loading.png ├── index.html ├── js ├── actions │ ├── ChatServerActionCreator.js │ └── ChatViewActionCreator.js ├── constants │ └── Constants.js ├── index.js ├── stores │ ├── MessageStore.js │ ├── ThreadStore.js │ └── UnreadThreadStore.js ├── utils │ ├── ChatMessageUtil.js │ ├── Events.js │ ├── WebAPIUtils.js │ └── mockdata.js └── views │ ├── MessageComposer.js │ ├── MessageComposer.jsx │ ├── MessageListItem.js │ ├── MessageListItem.jsx │ ├── MessageSection.js │ ├── MessageSection.jsx │ ├── ThreadListItem.js │ ├── ThreadListItem.jsx │ ├── ThreadSection.js │ ├── ThreadSection.jsx │ ├── index.js │ └── index.jsx └── lib ├── Fluxify.js ├── react.js └── sea.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /Fluxify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kpaxqin@github on 15-2-6. 3 | */ 4 | (function(global, factory){ 5 | // CMD & CMD-like 6 | if (typeof define ==="function"){ 7 | define(function(require, exports, module){ 8 | factory(global, exports); 9 | }) 10 | }else { 11 | factory(global, {}); 12 | } 13 | })(this, function(root, Fluxify){ 14 | 15 | // var Fluxify = window.Fluxify = {}; 16 | 17 | var _OP = Object.prototype, 18 | _AP = Array.prototype, 19 | hasOwn = _OP.hasOwnProperty; 20 | 21 | Fluxify.utils = { 22 | /* 23 | * change {foo: null, bar: null} to {foo: "foo", bar: "bar"} 24 | * keyMirror({foo: 1}) ==> {"foo": 1}, we'll not overwrite existed value by default 25 | * keyMirror({foo: 1}, true) ==> {"foo" : "foo"} 26 | * */ 27 | keyMirror: function(object, overwrite){ 28 | overwrite = overwrite || false; 29 | 30 | var result = {}; 31 | 32 | for (var key in object){ 33 | if (!hasOwn.call(object, key)){ 34 | continue; 35 | } 36 | 37 | if (object[key] != null){ 38 | result[key] = overwrite ? key : object[key]; 39 | }else{ 40 | result[key] = key; 41 | } 42 | } 43 | 44 | return result; 45 | }, 46 | /* 47 | * Object.assign polyfill from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 48 | * */ 49 | objectAssign: function(target, firstSource) { 50 | "use strict"; 51 | if (target === undefined || target === null) 52 | throw new TypeError("Cannot convert first argument to object"); 53 | var to = Object(target); 54 | for (var i = 1; i < arguments.length; i++) { 55 | var nextSource = arguments[i]; 56 | if (nextSource === undefined || nextSource === null) continue; 57 | var keysArray = Object.keys(Object(nextSource)); 58 | for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { 59 | var nextKey = keysArray[nextIndex]; 60 | var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); 61 | if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey]; 62 | } 63 | } 64 | return to; 65 | } 66 | } 67 | 68 | var ACTION_TYPES = {}; 69 | /* 70 | * 可以多次调用,自动merge 71 | * */ 72 | Fluxify.setActionTypes = function(types){ 73 | ACTION_TYPES = Fluxify.utils.objectAssign(ACTION_TYPES, Fluxify.utils.keyMirror(types)); 74 | 75 | return ACTION_TYPES; 76 | }; 77 | 78 | Fluxify.getActionTypes = function(){ 79 | return ACTION_TYPES; 80 | }; 81 | 82 | /* 83 | * 移植自 Backbone.Event 84 | * todo: 改用eventemitter 或重写此部分 85 | * */ 86 | Fluxify.Events = (function(){ 87 | var hasOwnProperty = _OP.hasOwnProperty, 88 | toString = _OP.toString, 89 | slice = _AP.slice; 90 | 91 | 92 | var _ = {//methods from underscore that Events in use 93 | once: function (func) { 94 | var ran = false, memo; 95 | return function() { 96 | if (ran) return memo; 97 | ran = true; 98 | memo = func.apply(this, arguments); 99 | func = null; 100 | return memo; 101 | }; 102 | }, 103 | isObject: function (obj) { 104 | return obj === Object(obj); 105 | }, 106 | isArray : Array.isArray || function(obj) { 107 | return toString.call(obj) == '[object Array]'; 108 | }, 109 | isString: function(obj){ 110 | return toString.call(obj) == '[object String]'; 111 | }, 112 | has: function(obj, key) { 113 | return hasOwnProperty.call(obj, key); 114 | }, 115 | keys : function (obj) { 116 | if (!_.isObject(obj)) return []; 117 | if (nativeKeys) return nativeKeys(obj); 118 | var keys = []; 119 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 120 | return keys; 121 | }, 122 | isEmpty : function (obj) { 123 | if (obj == null) return true; 124 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 125 | for (var key in obj) if (_.has(obj, key)) return false; 126 | return true; 127 | } 128 | } 129 | 130 | var Events = { 131 | 132 | // Bind an event to a `callback` function. Passing `"all"` will bind 133 | // the callback to all events fired. 134 | on: function(name, callback, context) { 135 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 136 | this._events || (this._events = {}); 137 | var events = this._events[name] || (this._events[name] = []); 138 | events.push({callback: callback, context: context, ctx: context || this}); 139 | return this; 140 | }, 141 | 142 | // Bind an event to only be triggered a single time. After the first time 143 | // the callback is invoked, it will be removed. 144 | once: function(name, callback, context) { 145 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 146 | var self = this; 147 | var once = _.once(function() { 148 | self.off(name, once); 149 | callback.apply(this, arguments); 150 | }); 151 | once._callback = callback; 152 | return this.on(name, once, context); 153 | }, 154 | 155 | // Remove one or many callbacks. If `context` is null, removes all 156 | // callbacks with that function. If `callback` is null, removes all 157 | // callbacks for the event. If `name` is null, removes all bound 158 | // callbacks for all events. 159 | off: function(name, callback, context) { 160 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 161 | 162 | // Remove all callbacks for all events. 163 | if (!name && !callback && !context) { 164 | this._events = void 0; 165 | return this; 166 | } 167 | 168 | var names = name ? [name] : _.keys(this._events); 169 | for (var i = 0, length = names.length; i < length; i++) { 170 | name = names[i]; 171 | 172 | // Bail out if there are no events stored. 173 | var events = this._events[name]; 174 | if (!events) continue; 175 | 176 | // Remove all callbacks for this event. 177 | if (!callback && !context) { 178 | delete this._events[name]; 179 | continue; 180 | } 181 | 182 | // Find any remaining events. 183 | var remaining = []; 184 | for (var j = 0, k = events.length; j < k; j++) { 185 | var event = events[j]; 186 | if ( 187 | callback && callback !== event.callback && 188 | callback !== event.callback._callback || 189 | context && context !== event.context 190 | ) { 191 | remaining.push(event); 192 | } 193 | } 194 | 195 | // Replace events if there are any remaining. Otherwise, clean up. 196 | if (remaining.length) { 197 | this._events[name] = remaining; 198 | } else { 199 | delete this._events[name]; 200 | } 201 | } 202 | 203 | return this; 204 | }, 205 | 206 | // Trigger one or many events, firing all bound callbacks. Callbacks are 207 | // passed the same arguments as `trigger` is, apart from the event name 208 | // (unless you're listening on `"all"`, which will cause your callback to 209 | // receive the true name of the event as the first argument). 210 | trigger: function(name) { 211 | if (!this._events) return this; 212 | var args = slice.call(arguments, 1); 213 | if (!eventsApi(this, 'trigger', name, args)) return this; 214 | var events = this._events[name]; 215 | var allEvents = this._events.all; 216 | if (events) triggerEvents(events, args); 217 | if (allEvents) triggerEvents(allEvents, arguments); 218 | return this; 219 | }, 220 | 221 | // Tell this object to stop listening to either specific events ... or 222 | // to every object it's currently listening to. 223 | stopListening: function(obj, name, callback) { 224 | var listeningTo = this._listeningTo; 225 | if (!listeningTo) return this; 226 | var remove = !name && !callback; 227 | if (!callback && typeof name === 'object') callback = this; 228 | if (obj) (listeningTo = {})[obj._listenId] = obj; 229 | for (var id in listeningTo) { 230 | obj = listeningTo[id]; 231 | obj.off(name, callback, this); 232 | if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; 233 | } 234 | return this; 235 | } 236 | 237 | }; 238 | 239 | // Regular expression used to split event strings. 240 | var eventSplitter = /\s+/; 241 | 242 | // Implement fancy features of the Events API such as multiple event 243 | // names `"change blur"` and jQuery-style event maps `{change: action}` 244 | // in terms of the existing API. 245 | var eventsApi = function(obj, action, name, rest) { 246 | if (!name) return true; 247 | 248 | // Handle event maps. 249 | if (typeof name === 'object') { 250 | for (var key in name) { 251 | obj[action].apply(obj, [key, name[key]].concat(rest)); 252 | } 253 | return false; 254 | } 255 | 256 | // Handle space separated event names. 257 | if (eventSplitter.test(name)) { 258 | var names = name.split(eventSplitter); 259 | for (var i = 0, length = names.length; i < length; i++) { 260 | obj[action].apply(obj, [names[i]].concat(rest)); 261 | } 262 | return false; 263 | } 264 | 265 | return true; 266 | }; 267 | 268 | // A difficult-to-believe, but optimized internal dispatch function for 269 | // triggering events. Tries to keep the usual cases speedy (most internal 270 | // Backbone events have 3 arguments). 271 | var triggerEvents = function(events, args) { 272 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 273 | switch (args.length) { 274 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 275 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 276 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 277 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 278 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 279 | } 280 | }; 281 | 282 | // Aliases for backwards compatibility. 283 | Events.bind = Events.on; 284 | Events.unbind = Events.off; 285 | 286 | return Events; 287 | })(); 288 | 289 | Fluxify.dispatcher = (function(){ 290 | 291 | var tokenIndex = 0, 292 | tokenPrefix = "ID_"; 293 | 294 | var Dispatcher = function(){ 295 | this._isDispatching = false; 296 | this._currentPayload = undefined; 297 | this._callbacks = {}; 298 | this._pending = {}; 299 | this._handled = {}; 300 | }; 301 | 302 | Dispatcher.prototype = { 303 | constructor : Dispatcher, 304 | register: function(callback){ 305 | var token = tokenPrefix + tokenIndex++; 306 | 307 | this._callbacks[token] = callback; 308 | 309 | return token; 310 | }, 311 | unregister: function(token){ 312 | delete this._callbacks[token]; 313 | }, 314 | dispatch : function(payload){ 315 | if (this._isDispatching){ 316 | console.error("cannot dispatch in middle of a dispatch"); 317 | return; 318 | } 319 | 320 | this._startDispatching(payload); 321 | 322 | for (var token in this._callbacks){ 323 | if (!hasOwn.call(this._callbacks, token)){ 324 | continue; 325 | } 326 | this._invokeCallback(token); 327 | 328 | } 329 | this._afterDispatching(); 330 | }, 331 | _startDispatching: function(payload){ 332 | this._currentPayload = payload; 333 | this._isDispatching = true; 334 | }, 335 | _afterDispatching: function(){ 336 | this._pending = {}; 337 | this._handled = {}; 338 | this._currentPayload = undefined; 339 | this._isDispatching = false; 340 | }, 341 | _invokeCallback: function(token){ 342 | var callback = this._callbacks[token]; 343 | 344 | this._pending[token] = true; 345 | callback.call(this, this._currentPayload); 346 | this._handled[token] = true; 347 | }, 348 | waitFor : function(tokens){ 349 | for (var i = 0, len = tokens.length; i < len; i++){ 350 | var token = tokens[i]; 351 | 352 | if (typeof token === "object"){ 353 | token = token.dispatchToken; 354 | } 355 | 356 | //check callback exist 357 | if (this._callbacks[token]){ 358 | //已经开始执行 359 | if (this._pending[token]){ 360 | //没有执行完,依赖一个执行中的callback说明成环 361 | if (!this._handled[token]){ 362 | throw Error("circle dependencies found while waitFor :" + token); 363 | } 364 | }else{//没开始执行 365 | this._invokeCallback(token); 366 | } 367 | }else{ 368 | console.error("callback not found for token: " + token); 369 | } 370 | } 371 | } 372 | } 373 | 374 | return new Dispatcher(); 375 | })(); 376 | 377 | var CHANGE_EVENT = "change"; 378 | Fluxify.createStore = function(name, methods, onDispatching){ 379 | var Store = Fluxify.utils.objectAssign({ 380 | emitChange: function(data){ 381 | this.trigger(CHANGE_EVENT, data); 382 | }, 383 | onChange: function(listener, context){ 384 | this.on(CHANGE_EVENT, listener, context); 385 | }, 386 | offChange: function(listener){ 387 | this.off(CHANGE_EVENT, listener); 388 | } 389 | }, Fluxify.Events, methods); 390 | 391 | if (typeof onDispatching === "function"){ 392 | Store.dispatchToken = Fluxify.dispatcher.register(onDispatching.bind(Store)); 393 | } 394 | 395 | return Store; 396 | } 397 | 398 | Fluxify.ReactMixins = (function(){ 399 | var storeListenerHandler = function(method){ 400 | 401 | var watchingStores = this.watchingStores, 402 | onStoreChange = this.onStoreChange || this.forceUpdate; 403 | 404 | if (!watchingStores || watchingStores.length < 0){ 405 | console.log("no valid stores watching"); 406 | } 407 | 408 | for (var i = 0, len = watchingStores.length, store = null; i < len; i++){ 409 | store = watchingStores[i]; 410 | if (store && typeof store[method] === "function"){ 411 | store[method](onStoreChange, this); 412 | } 413 | } 414 | }; 415 | 416 | return { 417 | componentDidMount : function(){ 418 | storeListenerHandler.call(this, "onChange"); 419 | }, 420 | componentWillUnmount : function(){ 421 | storeListenerHandler.call(this, "offChange"); 422 | } 423 | } 424 | })(); 425 | 426 | root.Fluxify = Fluxify; 427 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fluxify: 构建Flux应用的轻量化工具库 2 | ================== 3 | 4 | ##概述 5 | Fluxify是一个轻量化的(目前源码不足500行),协助构建[Flux][Flux]结构Web应用的工具库 6 | 7 | 由于facebook的[Flux][Flux]库更偏向于理念展示,公布的实现中只有Dispatcher部分。实际构建中有大量的重复工作可以由统一的工具库代劳,这也是Fluxify出现的原因。 8 | 9 | Fluxify目前做了以下事情: 10 | 11 | * ActionTypes的配置与访问 12 | * 内置单例的dispatcher实例,用户不必new Dispatcher 13 | * 提供createStore工厂方法用于生成Store,封装了事件和注册dispatcher回调的代码 14 | * 提供ReactMixins,简化React组件监听Store的代码 15 | 16 | ##使用 17 | ### 配置ActionTypes 18 | 提供了`Fluxify.setActionTypes`和`Fluxify.getActionTypes`两个工具函数,实际使用示例如下 19 | ```js 20 | // constant.js 21 | module.exports = { 22 | ACTION_TYPE: { 23 | THREAD_CLICK: null, 24 | CREATE_MESSAGE: null 25 | } 26 | } 27 | 28 | //appIndex.js 29 | var constant = require("constant"); 30 | 31 | Fluxify.setActionTypes(constant.ACTION_TYPE); 32 | 33 | //then you can get ActionTypes anywhere by Fluxify.getActionTypes 34 | Fluxify.getActionTypes();// => Object {THREAD_CLICK: "THREAD_CLICK", CREATE_MESSAGE: "CREATE_MESSAGE"} 35 | 36 | 37 | ``` 38 | 其中`Fluxify.setActionTypes`支持多次调用,最终保存的将是merge后的结果 39 | 40 | 为了简化配置,`Fluxify.setActionTypes`还提供了自动的KeyMirror功能 41 | 42 | 如下 43 | ```js 44 | Fluxify.setActionTypes({FOO: null}); 45 | Fluxify.setActionTypes({BAR: 1}); 46 | 47 | console.log(Fluxify.getActionTypes()); 48 | // => Object {FOO: "FOO", BAR: 1} 49 | //KeyMirror只对值为null或undefined的key生效,避免覆盖用户配置 50 | ``` 51 | 52 | ### 内置dispatcher 53 | 54 | 由于在Flux中dispatcher是单例的action转发器,因此没有由用户构建实例的必要,直接内置了一个dispatcher实例并通过`Fluxify.dispatcher`访问 55 | 56 | 保留了`register, unregister, waitFor, dispatch`等函数接口,其中waitFor不仅支持回调ID数组,也支持Store对象数组(由createStore构建的Store),如下: 57 | 58 | ```js 59 | var FooStore = Fluxify.createStore("FooStore", {/*methods*/}, function(payload){}); 60 | 61 | //BarStore.js 62 | /*省略代码若干*/ 63 | 64 | dispatcher.waitFor([FooStore.dispatchToken]);//等效于 dispatcher.waitFor([FooStore]); 65 | 66 | ``` 67 | 68 | ###createStore工厂方法 69 | 70 | createStore对事件相关的代码进行了封装,同时规范了构造Store对象的过程。注意createStore构建出的Store是一个简单对象。 71 | 72 | createStore(name, methods, onDispatching)提供了三个参数: 73 | 74 | * `name` Store名,此参数目前还没有被用到,留作备用 75 | * `methods` Store的业务方法集合,与业务相关的方法应该放在这个对象中,最终将被以Object.assign的方式merge进返回的Store对象 76 | * `onDispatching(payload)` 注册在dispatcher上的回调函数,处理action相关的逻辑,此函数的this指向当前Store 77 | 78 | createStore返回的Store对象具备如下函数与属性: 79 | 80 | * `dispatchToken` dispatcher回调函数的回调ID 81 | * `emitChange()` 触发Store的change事件 82 | * `onChange(listener, context)` 添加change事件监听器 83 | * `offChange(listener)` 移除change事件监听器 84 | 85 | ###ReactMixins 86 | 87 | 使用示例 88 | ```js 89 | React.createClass({ 90 | mixins: [Fluxify.ReactMixins], 91 | watchingStores: [FooStore, BarStore], 92 | /* 93 | 当watchingStores数组中的任一Store触发change事件时,onStoreChange函数即会被调用,this指向当前react组件。 94 | 可以省略onStoreChange函数,默认会调用this.forceUpdate 95 | */ 96 | onStoreChange : function(){ 97 | this.setState({ 98 | foo: "foo" 99 | }); 100 | }, 101 | render: function(){/*...*/} 102 | }); 103 | 104 | ``` 105 | ###存在的问题与展望: 106 | 107 | 1. createStore保留了onDispatching函数,这意味着在onDispatching函数中一定会用`switch-case`语句来分发action逻辑,这种模式的缺点明显:`switch-case`本身的语法不够健壮,不同的action处理逻辑共享同一个作用域也存在风险(虽然可以用即时函数解决)。
之所以没有采用类似[Fluxxor][Fluxxor]的配置化转发Action,是存在几方面的考虑: 108 | * ActionTypes都是变量,无法通过对象字面量Mapping的方式书写(变量不能作为 字面量对象的key)。Fluxxor的解决方案是用bindActions函数参数奇偶位做区分,但个人认为这种方式在工程化的应用中显得不够严谨。 109 | * 采用声名式配置意味着每种Action都需要一个函数进行处理,这意味着Store会混杂业务函数与action处理函数。多数情况下每种Action的处理代码只有少数几行,而Store要监听的Action种类可能会很多,大量的action处理函数可能造成代码难以阅读和维护 110 | 2. 无论是官方库还是第三方实现,Flux架构目前缺少一块重要的拼图:**“异步调用前置化、Store逻辑同步化”是Flux的核心,但是目前为止没有足够健壮的手段来维护这种核心理念**。
有很多不理解Web API call前置理念的开发者,在声称自己实现了一个Flux框架时直接省略掉了ActionCreator,导致Store异步化并且迅速掉入复杂的store关系维护陷阱,无论用多么强大的组合事件机制都难以挽回。
Fluxify中依然存在着上面提到的**健壮**性缺失的问题,一句setTimeout都会让管理Store依赖的核心函数waitFor失效。或许像facebook这样工程师平均素质足够高的团队可以不用在乎,但从更普遍的工程角度这无疑是难以接受的。而这也将是Fluxify下一步思考的重点 111 | 112 | [Flux]: https://github.com/facebook/flux 113 | 114 | [Fluxxor]: http://fluxxor.com/ 115 | -------------------------------------------------------------------------------- /examples/chat/.gitignore: -------------------------------------------------------------------------------- 1 | /js/views/.module-cache -------------------------------------------------------------------------------- /examples/chat/css/chatapp.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is provided by Facebook for testing and evaluation purposes 3 | * only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 9 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | .chatapp { 14 | font-family: 'Muli', 'Helvetica Neue', helvetica, arial; 15 | max-width: 760px; 16 | margin: 20px auto; 17 | overflow: hidden; 18 | } 19 | 20 | .message-list, .thread-list { 21 | border: 1px solid #ccf; 22 | font-size: 16px; 23 | height: 400px; 24 | margin: 0; 25 | overflow-y: auto; 26 | padding: 0; 27 | } 28 | 29 | .message-section { 30 | float: right; 31 | width: 65%; 32 | } 33 | 34 | .thread-section { 35 | float: left; 36 | width: 32.5%; 37 | } 38 | 39 | .message-thread-heading, 40 | .thread-count { 41 | height: 40px; 42 | margin: 0; 43 | } 44 | 45 | .message-list-item, .thread-list-item { 46 | list-style: none; 47 | padding: 12px 14px 14px; 48 | } 49 | 50 | .thread-list-item { 51 | border-bottom: 1px solid #ccc; 52 | cursor: pointer; 53 | } 54 | 55 | .thread-list:hover .thread-list-item:hover { 56 | background-color: #f8f8ff; 57 | } 58 | 59 | .thread-list:hover .thread-list-item { 60 | background-color: #fff; 61 | } 62 | 63 | .thread-list-item.active, 64 | .thread-list:hover .thread-list-item.active, 65 | .thread-list:hover .thread-list-item.active:hover { 66 | background-color: #efefff; 67 | cursor: default; 68 | } 69 | 70 | .message-author-name, 71 | .thread-name { 72 | color: #66c; 73 | float: left; 74 | font-size: 13px; 75 | margin: 0; 76 | } 77 | 78 | .message-time, .thread-time { 79 | color: #aad; 80 | float: right; 81 | font-size: 12px; 82 | } 83 | 84 | .message-text, .thread-last-message { 85 | clear: both; 86 | font-size: 14px; 87 | padding-top: 10px; 88 | } 89 | 90 | .message-composer { 91 | box-sizing: border-box; 92 | font-family: inherit; 93 | font-size: 14px; 94 | height: 5em; 95 | width: 100%; 96 | margin: 20px 0 0; 97 | padding: 10px; 98 | } 99 | 100 | .message-sending { 101 | float: left; 102 | width: 1em; 103 | height: 1em; 104 | margin: 0 3px 0 0; 105 | background: url(../images/loading.png) -49px -33px no-repeat; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /examples/chat/images/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpaxqin/Fluxify/4ec8b1246123eb8a44ea1dbc01da7723c32117ca/examples/chat/images/loading.png -------------------------------------------------------------------------------- /examples/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | my chat 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /examples/chat/js/actions/ChatServerActionCreator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(require){ 5 | var Fluxify = require("../../lib/Fluxify"); 6 | 7 | var dispatcher = Fluxify.dispatcher, 8 | ACTION_TYPE = Fluxify.getActionTypes(); 9 | 10 | return { 11 | receiveAll: function(msgs){ 12 | dispatcher.dispatch({ 13 | action: { 14 | actionType: ACTION_TYPE.RECEIVE_RAW_MESSAGE, 15 | rawMessages: msgs 16 | } 17 | }) 18 | }, 19 | addMessage: function(message){ 20 | 21 | dispatcher.dispatch({ 22 | action: { 23 | actionType: ACTION_TYPE.ADD_RAW_MESSAGE, 24 | rawMessage: message 25 | } 26 | }) 27 | } 28 | } 29 | }); -------------------------------------------------------------------------------- /examples/chat/js/actions/ChatViewActionCreator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-5. 3 | */ 4 | define(function(require){ 5 | var Fluxify = require("../../lib/Fluxify"); 6 | 7 | var MessageStore = require("../stores/MessageStore"); 8 | var ThreadStore = require("../stores/ThreadStore"); 9 | 10 | var WebAPIUtils = require("../utils/WebAPIUtils"); 11 | 12 | var dispatcher = Fluxify.dispatcher, 13 | ACTION_TYPE = Fluxify.getActionTypes(); 14 | 15 | return { 16 | threadClick: function(thread){ 17 | dispatcher.dispatch({ 18 | action: { 19 | actionType: ACTION_TYPE.THREAD_CLICK, 20 | thread: thread 21 | } 22 | }); 23 | 24 | }, 25 | createMessage: function(text){ 26 | var message = MessageStore.getCreatedMessage(text); 27 | 28 | message.isSending = true; 29 | 30 | dispatcher.dispatch({ 31 | action: { 32 | actionType: ACTION_TYPE.CREATE_MESSAGE, 33 | message: message 34 | } 35 | }); 36 | 37 | WebAPIUtils.addMessage(message, ThreadStore.getCurrent()); 38 | } 39 | } 40 | }); -------------------------------------------------------------------------------- /examples/chat/js/constants/Constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kpaxqin@github on 15-2-4. 3 | */ 4 | define(function(){ 5 | return { 6 | ACTION_TYPE: { 7 | RECEIVE_RAW_MESSAGE: null, 8 | ADD_RAW_MESSAGE : null, 9 | THREAD_CLICK: null, 10 | CREATE_MESSAGE: null 11 | }, 12 | ACTION_SOURCE_TYPE: { 13 | SERVER: "SERVER", 14 | USER_INTERACTION: "USER_INTERACTION" 15 | } 16 | 17 | } 18 | 19 | }) -------------------------------------------------------------------------------- /examples/chat/js/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(require){ 5 | var Fluxify = require("../lib/Fluxify"); 6 | 7 | var Constants = require("./constants/Constants"); 8 | 9 | var IndexView = require("./views/index"); 10 | 11 | var mockdata = require("./utils/mockdata"); 12 | 13 | var WebAPIUtils = require("./utils/WebAPIUtils"); 14 | 15 | mockdata.init(); 16 | 17 | Fluxify.setActionTypes(Constants.ACTION_TYPE); 18 | 19 | return { 20 | init: function(){ 21 | WebAPIUtils.getAllMessage(); 22 | 23 | React.render( 24 | React.createElement(IndexView, null), 25 | document.getElementById("chat") 26 | ) 27 | } 28 | } 29 | 30 | }); -------------------------------------------------------------------------------- /examples/chat/js/stores/MessageStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(require){ 5 | 6 | var Fluxify = require("../../lib/Fluxify"); 7 | 8 | var dispatcher = Fluxify.dispatcher, 9 | ACTION_TYPE = Fluxify.getActionTypes(); 10 | 11 | var ChatMessageUtil = require("../utils/ChatMessageUtil"); 12 | var ThreadStore = require("./ThreadStore"); 13 | 14 | var _messages = {}; 15 | 16 | function _addMessages(raws){ 17 | raws.forEach(function(msg){ 18 | _messages[msg.id] = ChatMessageUtil.convertRawMessage(msg); 19 | }, this) 20 | } 21 | 22 | function _markReadByThread(threadID){ 23 | for (var id in _messages){ 24 | var msg = _messages[id]; 25 | if (msg.threadID === threadID){ 26 | msg.isRead = true; 27 | } 28 | } 29 | } 30 | 31 | var MessageStore = Fluxify.createStore("MessageStore", { 32 | getCreatedMessage: function(text){ 33 | var timestamp = +new Date(); 34 | return { 35 | id: "m_" + timestamp, 36 | threadID: ThreadStore.getCurrentID(), 37 | authorName: "Bill", 38 | date: new Date(timestamp), 39 | text: text, 40 | isRead: true 41 | } 42 | }, 43 | getAll: function(){ 44 | return _messages; 45 | }, 46 | getAllForCurrentThread: function(){ 47 | var currendThreadID = ThreadStore.getCurrentID(); 48 | var result = []; 49 | 50 | for (var id in _messages){ 51 | var msg = _messages[id]; 52 | 53 | if (msg.threadID === currendThreadID){ 54 | result.push(msg) 55 | } 56 | } 57 | 58 | return result; 59 | } 60 | }, function(payload){ 61 | var action = payload.action; 62 | 63 | switch (action.actionType){ 64 | case ACTION_TYPE.RECEIVE_RAW_MESSAGE : 65 | _addMessages(action.rawMessages); 66 | dispatcher.waitFor([ThreadStore.dispatchToken]); 67 | _markReadByThread(ThreadStore.getCurrentID()); 68 | break; 69 | case ACTION_TYPE.THREAD_CLICK : 70 | dispatcher.waitFor([ThreadStore.dispatchToken]); 71 | _markReadByThread(ThreadStore.getCurrentID()); 72 | this.emitChange(); 73 | break; 74 | case ACTION_TYPE.CREATE_MESSAGE: 75 | _messages[action.message.id] = action.message; 76 | this.emitChange(); 77 | break; 78 | case ACTION_TYPE.ADD_RAW_MESSAGE: 79 | delete _messages[action.rawMessage.id].isSending; 80 | this.emitChange(); 81 | break; 82 | default : 83 | } 84 | }); 85 | 86 | return MessageStore; 87 | }) -------------------------------------------------------------------------------- /examples/chat/js/stores/ThreadStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(require){ 5 | var Fluxify = require("../../lib/Fluxify"); 6 | 7 | var dispatcher = Fluxify.dispatcher, 8 | ACTION_TYPE = Fluxify.getActionTypes(); 9 | 10 | var ChatMessageUtil = require("../utils/ChatMessageUtil"); 11 | 12 | var _threads = {}, 13 | _currentID; 14 | 15 | var ThreadStore = Fluxify.createStore("ThreadStore", { 16 | init: function(rawMessages){ 17 | 18 | //转换得到threads 19 | rawMessages.forEach(function(message){ 20 | var threadID = message.threadID; 21 | var thread = _threads[threadID]; 22 | 23 | //thread已保存且当前message时间不是最新 24 | if (thread && thread.lastTimestamp > message.timestamp){ 25 | return; 26 | } 27 | _threads[threadID] = { 28 | id: threadID, 29 | name: message.threadName, 30 | lastMessage: ChatMessageUtil.convertRawMessage(message), 31 | lastTimestamp : message.timestamp 32 | } 33 | }, this); 34 | 35 | //计算current 36 | if (!_currentID){ 37 | var sortedThreads = this.getAllChrono(); 38 | this.setCurrentByID(sortedThreads[sortedThreads.length - 1].id); 39 | } 40 | }, 41 | setCurrentByID: function(id){ 42 | if (_threads[id]){ 43 | _currentID = id; 44 | _threads[id].lastMessage.isRead = true; 45 | } 46 | }, 47 | getByID: function(id){ 48 | return _threads[id]; 49 | }, 50 | getAll: function(){ 51 | return _threads; 52 | }, 53 | getAllChrono: function(){ 54 | var result = []; 55 | 56 | for (var id in _threads){ 57 | var thread = _threads[id]; 58 | result.push(thread) 59 | } 60 | 61 | result.sort(function(a, b){ 62 | if (a.lastMessage.timestamp < b.lastMessage.timestamp) { 63 | return -1; 64 | } else if (a.lastMessage.timestamp > b.lastMessage.timestamp) { 65 | return 1; 66 | } 67 | return 0; 68 | }) 69 | 70 | return result; 71 | }, 72 | getCurrent: function(){ 73 | return this.getByID(_currentID); 74 | }, 75 | getCurrentID: function(){ 76 | return _currentID; 77 | }, 78 | /* 79 | * 由于标记message的isRead的工作在MessageStore中,此函数执行时计数出错,认为全部未读 80 | * */ 81 | getUnreadCount: function(){ 82 | var count = 0; 83 | for (var id in _threads){ 84 | var thread = _threads[id]; 85 | 86 | if (!thread.lastMessage.isRead){ 87 | count++; 88 | } 89 | } 90 | 91 | return count; 92 | } 93 | }, function(payload){//通过createStore构造的dispatch监听函数this指向构造出的Store 94 | var action = payload.action; 95 | 96 | switch (action.actionType){ 97 | case ACTION_TYPE.RECEIVE_RAW_MESSAGE : 98 | this.init(action.rawMessages); 99 | 100 | break; 101 | case ACTION_TYPE.THREAD_CLICK : 102 | this.setCurrentByID(action.thread.id); 103 | this.emitChange(); 104 | break; 105 | case ACTION_TYPE.CREATE_MESSAGE: 106 | var currentThread = this.getCurrent(); 107 | 108 | currentThread.lastMessage = action.message; 109 | currentThread.lastTimestamp = +action.message.date; 110 | this.emitChange(); 111 | 112 | default : 113 | } 114 | }); 115 | 116 | return ThreadStore; 117 | 118 | }) -------------------------------------------------------------------------------- /examples/chat/js/stores/UnreadThreadStore.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var Fluxify = require("../../lib/Fluxify"); 3 | 4 | var dispatcher = Fluxify.dispatcher, 5 | ACTION_TYPE = Fluxify.getActionTypes(); 6 | 7 | var ThreadStore = require("./ThreadStore"); 8 | var MessageStore = require("./MessageStore"); 9 | 10 | var UnreadThreadStore = Fluxify.createStore("UnreadThreadStore", { 11 | getUnreadThreadCount: function(){ 12 | var count = 0, 13 | _threads = ThreadStore.getAll(); 14 | for (var id in _threads){ 15 | var thread = _threads[id]; 16 | 17 | if (!thread.lastMessage.isRead){ 18 | count++; 19 | } 20 | } 21 | 22 | return count; 23 | } 24 | }, function(payload){ 25 | var action = payload.action; 26 | 27 | switch (action.actionType){ 28 | case ACTION_TYPE.RECEIVE_RAW_MESSAGE: 29 | dispatcher.waitFor([ThreadStore.dispatchToken, MessageStore.dispatchToken]) 30 | break; 31 | } 32 | }); 33 | 34 | return UnreadThreadStore; 35 | }) -------------------------------------------------------------------------------- /examples/chat/js/utils/ChatMessageUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kpaxqin@github on 15-2-4. 3 | */ 4 | define(function(require){ 5 | return { 6 | convertRawMessage: function(rawMessage){ 7 | return { 8 | id: rawMessage.id, 9 | threadID: rawMessage.threadID, 10 | authorName: rawMessage.authorName, 11 | date: new Date(rawMessage.timestamp), 12 | text: rawMessage.text, 13 | isRead: false//default to be false 14 | } 15 | } 16 | } 17 | }); -------------------------------------------------------------------------------- /examples/chat/js/utils/Events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(){ 5 | 6 | var Events = (function(){ 7 | var hasOwnProperty = Object.prototype.hasOwnProperty, 8 | toString = Object.prototype.toString, 9 | slice = Array.prototype.slice; 10 | 11 | 12 | var _ = {//methods from underscore that Events in use 13 | once: function (func) { 14 | var ran = false, memo; 15 | return function() { 16 | if (ran) return memo; 17 | ran = true; 18 | memo = func.apply(this, arguments); 19 | func = null; 20 | return memo; 21 | }; 22 | }, 23 | isObject: function (obj) { 24 | return obj === Object(obj); 25 | }, 26 | isArray : Array.isArray || function(obj) { 27 | return toString.call(obj) == '[object Array]'; 28 | }, 29 | isString: function(obj){ 30 | return toString.call(obj) == '[object String]'; 31 | }, 32 | has: function(obj, key) { 33 | return hasOwnProperty.call(obj, key); 34 | }, 35 | keys : function (obj) { 36 | if (!_.isObject(obj)) return []; 37 | if (nativeKeys) return nativeKeys(obj); 38 | var keys = []; 39 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 40 | return keys; 41 | }, 42 | isEmpty : function (obj) { 43 | if (obj == null) return true; 44 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 45 | for (var key in obj) if (_.has(obj, key)) return false; 46 | return true; 47 | } 48 | } 49 | 50 | var Events = { 51 | 52 | // Bind an event to a `callback` function. Passing `"all"` will bind 53 | // the callback to all events fired. 54 | on: function(name, callback, context) { 55 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 56 | this._events || (this._events = {}); 57 | var events = this._events[name] || (this._events[name] = []); 58 | events.push({callback: callback, context: context, ctx: context || this}); 59 | return this; 60 | }, 61 | 62 | // Bind an event to only be triggered a single time. After the first time 63 | // the callback is invoked, it will be removed. 64 | once: function(name, callback, context) { 65 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 66 | var self = this; 67 | var once = _.once(function() { 68 | self.off(name, once); 69 | callback.apply(this, arguments); 70 | }); 71 | once._callback = callback; 72 | return this.on(name, once, context); 73 | }, 74 | 75 | // Remove one or many callbacks. If `context` is null, removes all 76 | // callbacks with that function. If `callback` is null, removes all 77 | // callbacks for the event. If `name` is null, removes all bound 78 | // callbacks for all events. 79 | off: function(name, callback, context) { 80 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 81 | 82 | // Remove all callbacks for all events. 83 | if (!name && !callback && !context) { 84 | this._events = void 0; 85 | return this; 86 | } 87 | 88 | var names = name ? [name] : _.keys(this._events); 89 | for (var i = 0, length = names.length; i < length; i++) { 90 | name = names[i]; 91 | 92 | // Bail out if there are no events stored. 93 | var events = this._events[name]; 94 | if (!events) continue; 95 | 96 | // Remove all callbacks for this event. 97 | if (!callback && !context) { 98 | delete this._events[name]; 99 | continue; 100 | } 101 | 102 | // Find any remaining events. 103 | var remaining = []; 104 | for (var j = 0, k = events.length; j < k; j++) { 105 | var event = events[j]; 106 | if ( 107 | callback && callback !== event.callback && 108 | callback !== event.callback._callback || 109 | context && context !== event.context 110 | ) { 111 | remaining.push(event); 112 | } 113 | } 114 | 115 | // Replace events if there are any remaining. Otherwise, clean up. 116 | if (remaining.length) { 117 | this._events[name] = remaining; 118 | } else { 119 | delete this._events[name]; 120 | } 121 | } 122 | 123 | return this; 124 | }, 125 | 126 | // Trigger one or many events, firing all bound callbacks. Callbacks are 127 | // passed the same arguments as `trigger` is, apart from the event name 128 | // (unless you're listening on `"all"`, which will cause your callback to 129 | // receive the true name of the event as the first argument). 130 | trigger: function(name) { 131 | if (!this._events) return this; 132 | var args = slice.call(arguments, 1); 133 | if (!eventsApi(this, 'trigger', name, args)) return this; 134 | var events = this._events[name]; 135 | var allEvents = this._events.all; 136 | if (events) triggerEvents(events, args); 137 | if (allEvents) triggerEvents(allEvents, arguments); 138 | return this; 139 | }, 140 | 141 | // Tell this object to stop listening to either specific events ... or 142 | // to every object it's currently listening to. 143 | stopListening: function(obj, name, callback) { 144 | var listeningTo = this._listeningTo; 145 | if (!listeningTo) return this; 146 | var remove = !name && !callback; 147 | if (!callback && typeof name === 'object') callback = this; 148 | if (obj) (listeningTo = {})[obj._listenId] = obj; 149 | for (var id in listeningTo) { 150 | obj = listeningTo[id]; 151 | obj.off(name, callback, this); 152 | if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; 153 | } 154 | return this; 155 | } 156 | 157 | }; 158 | 159 | // Regular expression used to split event strings. 160 | var eventSplitter = /\s+/; 161 | 162 | // Implement fancy features of the Events API such as multiple event 163 | // names `"change blur"` and jQuery-style event maps `{change: action}` 164 | // in terms of the existing API. 165 | var eventsApi = function(obj, action, name, rest) { 166 | if (!name) return true; 167 | 168 | // Handle event maps. 169 | if (typeof name === 'object') { 170 | for (var key in name) { 171 | obj[action].apply(obj, [key, name[key]].concat(rest)); 172 | } 173 | return false; 174 | } 175 | 176 | // Handle space separated event names. 177 | if (eventSplitter.test(name)) { 178 | var names = name.split(eventSplitter); 179 | for (var i = 0, length = names.length; i < length; i++) { 180 | obj[action].apply(obj, [names[i]].concat(rest)); 181 | } 182 | return false; 183 | } 184 | 185 | return true; 186 | }; 187 | 188 | // A difficult-to-believe, but optimized internal dispatch function for 189 | // triggering events. Tries to keep the usual cases speedy (most internal 190 | // Backbone events have 3 arguments). 191 | var triggerEvents = function(events, args) { 192 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 193 | switch (args.length) { 194 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 195 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 196 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 197 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 198 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 199 | } 200 | }; 201 | /* 202 | todo: 暂不迁移IOC版本 203 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 204 | 205 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 206 | // listen to an event in another object ... keeping track of what it's 207 | // listening to. 208 | _.each(listenMethods, function(implementation, method) { 209 | Events[method] = function(obj, name, callback) { 210 | var listeningTo = this._listeningTo || (this._listeningTo = {}); 211 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); 212 | listeningTo[id] = obj; 213 | if (!callback && typeof name === 'object') callback = this; 214 | obj[implementation](name, callback, this); 215 | return this; 216 | }; 217 | }); 218 | */ 219 | 220 | // Aliases for backwards compatibility. 221 | Events.bind = Events.on; 222 | Events.unbind = Events.off; 223 | 224 | return Events; 225 | })(); 226 | 227 | return Events; 228 | }) -------------------------------------------------------------------------------- /examples/chat/js/utils/WebAPIUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(require){ 5 | var ServerActionCreator = require("../actions/ChatServerActionCreator"); 6 | 7 | return { 8 | getAllMessage: function(){ 9 | var msgStr = localStorage.getItem("messages"), 10 | messages = JSON.parse(msgStr); 11 | 12 | ServerActionCreator.receiveAll(messages); 13 | }, 14 | addMessage: function(message, thread){ 15 | var rawMessages = JSON.parse(localStorage.getItem("messages")), 16 | newMessage = { 17 | id: message.id, 18 | threadID: message.threadID, 19 | threadName: thread.name, 20 | authorName: message.authorName, 21 | text: message.text, 22 | timestamp: +message.date 23 | }; 24 | rawMessages.push(newMessage); 25 | 26 | //模拟服务器异步处理并返回结果 27 | setTimeout(function(){ 28 | localStorage.setItem("messages", JSON.stringify(rawMessages)); 29 | ServerActionCreator.addMessage(newMessage); 30 | }, 1000); 31 | 32 | } 33 | } 34 | }) -------------------------------------------------------------------------------- /examples/chat/js/utils/mockdata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-4. 3 | */ 4 | define(function(require){ 5 | return { 6 | init: function() { 7 | // localStorage.clear(); 8 | var existedMessages = localStorage.getItem("messages"); 9 | if (existedMessages){ 10 | return; 11 | } 12 | localStorage.clear(); 13 | localStorage.setItem('messages', JSON.stringify([ 14 | { 15 | id: 'm_1', 16 | threadID: 't_1', 17 | threadName: 'Jing and Bill', 18 | authorName: 'Bill', 19 | text: 'Hey Jing, want to give a Flux talk at ForwardJS?', 20 | timestamp: Date.now() - 99999 21 | }, 22 | { 23 | id: 'm_2', 24 | threadID: 't_1', 25 | threadName: 'Jing and Bill', 26 | authorName: 'Bill', 27 | text: 'Seems like a pretty cool conference.', 28 | timestamp: Date.now() - 89999 29 | }, 30 | { 31 | id: 'm_3', 32 | threadID: 't_1', 33 | threadName: 'Jing and Bill', 34 | authorName: 'Jing', 35 | text: 'Sounds good. Will they be serving dessert?', 36 | timestamp: Date.now() - 79999 37 | }, 38 | { 39 | id: 'm_4', 40 | threadID: 't_2', 41 | threadName: 'Dave and Bill', 42 | authorName: 'Bill', 43 | text: 'Hey Dave, want to get a beer after the conference?', 44 | timestamp: Date.now() - 69999 45 | }, 46 | { 47 | id: 'm_5', 48 | threadID: 't_2', 49 | threadName: 'Dave and Bill', 50 | authorName: 'Dave', 51 | text: 'Totally! Meet you at the hotel bar.', 52 | timestamp: Date.now() - 59999 53 | }, 54 | { 55 | id: 'm_6', 56 | threadID: 't_3', 57 | threadName: 'Functional Heads', 58 | authorName: 'Bill', 59 | text: 'Hey Brian, are you going to be talking about functional stuff?', 60 | timestamp: Date.now() - 49999 61 | }, 62 | { 63 | id: 'm_7', 64 | threadID: 't_3', 65 | threadName: 'Bill and Brian', 66 | authorName: 'Brian', 67 | text: 'At ForwardJS? Yeah, of course. See you there!', 68 | timestamp: Date.now() - 39999 69 | } 70 | ])); 71 | } 72 | } 73 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/MessageComposer.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | 3 | var ViewActionCreator = require("../actions/ChatViewActionCreator"); 4 | 5 | var MessageStore = require("../stores/MessageStore"); 6 | 7 | var ENTER_KEY_CODE = 13; 8 | 9 | var MessageComposer = React.createClass({displayName: 'MessageComposer', 10 | getInitialState: function(){ 11 | return { 12 | text: "" 13 | } 14 | }, 15 | _onKeyDown: function(e){ 16 | if (e.keyCode === ENTER_KEY_CODE){ 17 | e.preventDefault(); 18 | var text = this.state.text.trim(); 19 | if (text){ 20 | 21 | // var message = MessageStore.getCreatedMessage(text); 22 | 23 | ViewActionCreator.createMessage(text); 24 | 25 | this.setState({text: ''}); 26 | } 27 | } 28 | }, 29 | _onChange: function(e){ 30 | this.setState({ 31 | text: e.target.value 32 | }) 33 | }, 34 | render: function(){ 35 | return ( 36 | React.createElement("textarea", { 37 | className: "message-composer", 38 | name: "message", 39 | onKeyDown: this._onKeyDown, 40 | onChange: this._onChange, 41 | value: this.state.text 42 | } 43 | ) 44 | ) 45 | } 46 | }) 47 | 48 | return MessageComposer; 49 | 50 | }); -------------------------------------------------------------------------------- /examples/chat/js/views/MessageComposer.jsx: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | 3 | var ViewActionCreator = require("../actions/ChatViewActionCreator"); 4 | 5 | var MessageStore = require("../stores/MessageStore"); 6 | 7 | var ENTER_KEY_CODE = 13; 8 | 9 | var MessageComposer = React.createClass({ 10 | getInitialState: function(){ 11 | return { 12 | text: "" 13 | } 14 | }, 15 | _onKeyDown: function(e){ 16 | if (e.keyCode === ENTER_KEY_CODE){ 17 | e.preventDefault(); 18 | var text = this.state.text.trim(); 19 | if (text){ 20 | 21 | // var message = MessageStore.getCreatedMessage(text); 22 | 23 | ViewActionCreator.createMessage(text); 24 | 25 | this.setState({text: ''}); 26 | } 27 | } 28 | }, 29 | _onChange: function(e){ 30 | this.setState({ 31 | text: e.target.value 32 | }) 33 | }, 34 | render: function(){ 35 | return ( 36 | 44 | ) 45 | } 46 | }) 47 | 48 | return MessageComposer; 49 | 50 | }); -------------------------------------------------------------------------------- /examples/chat/js/views/MessageListItem.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | 3 | 4 | return React.createClass({ 5 | render: function(){ 6 | var msg = this.props.message; 7 | 8 | var Loading; 9 | 10 | if (msg.isSending){ 11 | Loading = React.createElement("i", {className: "message-sending"}) 12 | } 13 | 14 | return ( 15 | React.createElement("li", {className: "message-list-item"}, 16 | React.createElement("h5", {className: "message-author-name"}, msg.authorName), 17 | React.createElement("div", {className: "message-time"}, msg.date.toLocaleTimeString()), 18 | React.createElement("div", {className: "message-text"}, 19 | Loading, 20 | msg.text 21 | ) 22 | ) 23 | ) 24 | } 25 | }) 26 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/MessageListItem.jsx: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | 3 | 4 | return React.createClass({ 5 | render: function(){ 6 | var msg = this.props.message; 7 | 8 | var Loading; 9 | 10 | if (msg.isSending){ 11 | Loading = 12 | } 13 | 14 | return ( 15 |
  • 16 |
    {msg.authorName}
    17 |
    {msg.date.toLocaleTimeString()}
    18 |
    19 | {Loading} 20 | {msg.text} 21 |
    22 |
  • 23 | ) 24 | } 25 | }) 26 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/MessageSection.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var MessageStore = require("../stores/MessageStore"); 3 | var ThreadStore = require("../stores/ThreadStore"); 4 | var MessageListItem = require("./MessageListItem"); 5 | var MessageComposer = require("./MessageComposer"); 6 | var Fluxify = require("../../lib/Fluxify"); 7 | 8 | return React.createClass({ 9 | 10 | // _onChange: function(){ 11 | // this.forceUpdate(); 12 | // }, 13 | // componentDidMount: function() { 14 | // MessageStore.onChange(this._onChange, this); 15 | // }, 16 | // 17 | // componentWillUnmount: function() { 18 | // MessageStore.offChange(this._onChange); 19 | // }, 20 | mixins: [Fluxify.ReactMixins], 21 | watchingStores: [MessageStore], 22 | render: function(){ 23 | var thread = ThreadStore.getCurrent(); 24 | var messages = MessageStore.getAllForCurrentThread(); 25 | 26 | var msgListItems = messages.map(function(message){ 27 | return ( 28 | React.createElement(MessageListItem, { 29 | message: message} 30 | ) 31 | ) 32 | 33 | }); 34 | 35 | return ( 36 | React.createElement("div", {className: "message-section"}, 37 | React.createElement("h3", {className: "message-thread-heading"}, 38 | thread.name 39 | ), 40 | React.createElement("ul", {className: "message-list"}, 41 | msgListItems 42 | ), 43 | React.createElement(MessageComposer, null) 44 | ) 45 | ) 46 | } 47 | }) 48 | 49 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/MessageSection.jsx: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var MessageStore = require("../stores/MessageStore"); 3 | var ThreadStore = require("../stores/ThreadStore"); 4 | var MessageListItem = require("./MessageListItem"); 5 | var MessageComposer = require("./MessageComposer"); 6 | var Fluxify = require("../../lib/Fluxify"); 7 | 8 | return React.createClass({ 9 | 10 | // _onChange: function(){ 11 | // this.forceUpdate(); 12 | // }, 13 | // componentDidMount: function() { 14 | // MessageStore.onChange(this._onChange, this); 15 | // }, 16 | // 17 | // componentWillUnmount: function() { 18 | // MessageStore.offChange(this._onChange); 19 | // }, 20 | mixins: [Fluxify.ReactMixins], 21 | watchingStores: [MessageStore], 22 | render: function(){ 23 | var thread = ThreadStore.getCurrent(); 24 | var messages = MessageStore.getAllForCurrentThread(); 25 | 26 | var msgListItems = messages.map(function(message){ 27 | return ( 28 | 31 | ) 32 | 33 | }); 34 | 35 | return ( 36 |
    37 |

    38 | {thread.name} 39 |

    40 | 43 | 44 |
    45 | ) 46 | } 47 | }) 48 | 49 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/ThreadListItem.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var ViewActionCreator = require("../actions/ChatViewActionCreator"); 3 | 4 | return React.createClass({ 5 | 6 | render: function(){ 7 | var thread = this.props.thread, 8 | lastMsg = thread.lastMessage, 9 | isCurrent = this.props.isCurrent; 10 | 11 | var className = (function(){ 12 | var base = "thread-list-item"; 13 | return isCurrent ? base + " active": base; 14 | })() 15 | 16 | var onClick = function(){ 17 | ViewActionCreator.threadClick(thread); 18 | } 19 | 20 | return ( 21 | React.createElement("li", {className: className, 22 | onClick: onClick 23 | }, 24 | React.createElement("h5", {className: "thread-name"}, thread.name), 25 | React.createElement("div", {className: "thread-time"}, 26 | new Date(lastMsg.date.toLocaleTimeString()) 27 | ), 28 | React.createElement("div", {className: "thread-last-message"}, 29 | lastMsg.text 30 | ) 31 | ) 32 | ) 33 | } 34 | }) 35 | 36 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/ThreadListItem.jsx: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var ViewActionCreator = require("../actions/ChatViewActionCreator"); 3 | 4 | return React.createClass({ 5 | 6 | render: function(){ 7 | var thread = this.props.thread, 8 | lastMsg = thread.lastMessage, 9 | isCurrent = this.props.isCurrent; 10 | 11 | var className = (function(){ 12 | var base = "thread-list-item"; 13 | return isCurrent ? base + " active": base; 14 | })() 15 | 16 | var onClick = function(){ 17 | ViewActionCreator.threadClick(thread); 18 | } 19 | 20 | return ( 21 |
  • 24 |
    {thread.name}
    25 |
    26 | {new Date(lastMsg.date.toLocaleTimeString())} 27 |
    28 |
    29 | {lastMsg.text} 30 |
    31 |
  • 32 | ) 33 | } 34 | }) 35 | 36 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/ThreadSection.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var ThreadListItem = require("./ThreadListItem"); 3 | var ThreadStore = require("../stores/ThreadStore"); 4 | var UnreadThreadStore = require("../stores/UnreadThreadStore"); 5 | 6 | var Fluxify = require("../../lib/Fluxify"); 7 | 8 | var getInitState = function(){ 9 | return { 10 | threads : ThreadStore.getAllChrono(), 11 | currentId: ThreadStore.getCurrentID(), 12 | // unreadCount: UnreadThreadStore.getUnreadThreadCount() 13 | unreadCount: ThreadStore.getUnreadCount() 14 | } 15 | } 16 | 17 | 18 | var ThreadSection = React.createClass({displayName: 'ThreadSection', 19 | getInitialState: function(){ 20 | return getInitState(); 21 | }, 22 | mixins: [Fluxify.ReactMixins], 23 | watchingStores: [ThreadStore, UnreadThreadStore], 24 | onStoreChange: function(){ 25 | this.setState(getInitState()); 26 | }, 27 | // _onChange: function(){ 28 | // this.setState(getInitState()); 29 | // }, 30 | // componentDidMount: function() { 31 | // ThreadStore.onChange(this._onChange, this); 32 | // UnreadThreadStore.onChange(this._onChange, this); 33 | // }, 34 | // 35 | // componentWillUnmount: function() { 36 | // ThreadStore.offChange(this._onChange); 37 | // UnreadThreadStore.offChange(this._onChange); 38 | // }, 39 | render: function(){ 40 | var self = this; 41 | var threadListItems = this.state.threads.map(function(thread){ 42 | return ( 43 | React.createElement(ThreadListItem, { 44 | key: thread.id, 45 | thread: thread, 46 | isCurrent: thread.id === self.state.currentId} 47 | ) 48 | ) 49 | }); 50 | 51 | return ( 52 | React.createElement("div", {className: "thread-section"}, 53 | React.createElement("div", {className: "thread-count"}, 54 | React.createElement("span", null, "Unread threads: "), 55 | React.createElement("span", null, this.state.unreadCount) 56 | ), 57 | 58 | React.createElement("ul", {className: "thread-list"}, 59 | threadListItems 60 | ) 61 | ) 62 | ) 63 | 64 | } 65 | 66 | }); 67 | 68 | return ThreadSection; 69 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/ThreadSection.jsx: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var ThreadListItem = require("./ThreadListItem"); 3 | var ThreadStore = require("../stores/ThreadStore"); 4 | var UnreadThreadStore = require("../stores/UnreadThreadStore"); 5 | 6 | var Fluxify = require("../../lib/Fluxify"); 7 | 8 | var getInitState = function(){ 9 | return { 10 | threads : ThreadStore.getAllChrono(), 11 | currentId: ThreadStore.getCurrentID(), 12 | // unreadCount: UnreadThreadStore.getUnreadThreadCount() 13 | unreadCount: ThreadStore.getUnreadCount() 14 | } 15 | } 16 | 17 | 18 | var ThreadSection = React.createClass({ 19 | getInitialState: function(){ 20 | return getInitState(); 21 | }, 22 | mixins: [Fluxify.ReactMixins], 23 | watchingStores: [ThreadStore, UnreadThreadStore], 24 | onStoreChange: function(){ 25 | this.setState(getInitState()); 26 | }, 27 | // _onChange: function(){ 28 | // this.setState(getInitState()); 29 | // }, 30 | // componentDidMount: function() { 31 | // ThreadStore.onChange(this._onChange, this); 32 | // UnreadThreadStore.onChange(this._onChange, this); 33 | // }, 34 | // 35 | // componentWillUnmount: function() { 36 | // ThreadStore.offChange(this._onChange); 37 | // UnreadThreadStore.offChange(this._onChange); 38 | // }, 39 | render: function(){ 40 | var self = this; 41 | var threadListItems = this.state.threads.map(function(thread){ 42 | return ( 43 | 48 | ) 49 | }); 50 | 51 | return ( 52 |
    53 |
    54 | Unread threads: 55 | {this.state.unreadCount} 56 |
    57 | 58 |
      59 | {threadListItems} 60 |
    61 |
    62 | ) 63 | 64 | } 65 | 66 | }); 67 | 68 | return ThreadSection; 69 | }) -------------------------------------------------------------------------------- /examples/chat/js/views/index.js: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var ThreadSection = require("./ThreadSection"); 3 | 4 | var MessageSection = require("./MessageSection"); 5 | 6 | return React.createClass({ 7 | render: function(){ 8 | return ( 9 | React.createElement("div", {className: "chatapp"}, 10 | React.createElement(ThreadSection, null), 11 | React.createElement(MessageSection, null) 12 | ) 13 | ) 14 | } 15 | }) 16 | }); -------------------------------------------------------------------------------- /examples/chat/js/views/index.jsx: -------------------------------------------------------------------------------- 1 | define(function(require){ 2 | var ThreadSection = require("./ThreadSection"); 3 | 4 | var MessageSection = require("./MessageSection"); 5 | 6 | return React.createClass({ 7 | render: function(){ 8 | return ( 9 |
    10 | 11 | 12 |
    13 | ) 14 | } 15 | }) 16 | }); -------------------------------------------------------------------------------- /examples/chat/lib/Fluxify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by cdyf on 15-2-6. 3 | */ 4 | (function(global, factory){ 5 | // CMD & CMD-like 6 | if (typeof define ==="function"){ 7 | define(function(require, exports, module){ 8 | factory(global, exports); 9 | }) 10 | }else { 11 | factory(global, {}); 12 | } 13 | })(this, function(root, Fluxify){ 14 | 15 | // var Fluxify = window.Fluxify = {}; 16 | 17 | var _OP = Object.prototype, 18 | _AP = Array.prototype, 19 | hasOwn = _OP.hasOwnProperty; 20 | 21 | 22 | var ACTION_TYPES = {}; 23 | 24 | Fluxify.utils = { 25 | /* 26 | * change {foo: null, bar: null} to {foo: "foo", bar: "bar"} 27 | * keyMirror({foo: 1}) ==> {"foo": 1}, we'll not overwrite existed value by default 28 | * keyMirror({foo: 1}, true) ==> {"foo" : "foo"} 29 | * */ 30 | keyMirror: function(object, overwrite){ 31 | overwrite = overwrite || false; 32 | 33 | var result = {}; 34 | 35 | for (var key in object){ 36 | if (!hasOwn.call(object, key)){ 37 | continue; 38 | } 39 | 40 | if (object[key]){ 41 | result[key] = overwrite ? key : object[key]; 42 | }else{ 43 | result[key] = key; 44 | } 45 | } 46 | 47 | return result; 48 | }, 49 | /* 50 | * Object.assign polyfill from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 51 | * */ 52 | objectAssign: function(target, firstSource) { 53 | "use strict"; 54 | if (target === undefined || target === null) 55 | throw new TypeError("Cannot convert first argument to object"); 56 | var to = Object(target); 57 | for (var i = 1; i < arguments.length; i++) { 58 | var nextSource = arguments[i]; 59 | if (nextSource === undefined || nextSource === null) continue; 60 | var keysArray = Object.keys(Object(nextSource)); 61 | for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { 62 | var nextKey = keysArray[nextIndex]; 63 | var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); 64 | if (desc !== undefined && desc.enumerable) to[nextKey] = nextSource[nextKey]; 65 | } 66 | } 67 | return to; 68 | }, 69 | extend : function(childProto){ 70 | var child, 71 | parent = this, 72 | proxy = function(){}; 73 | 74 | child = function(){ 75 | return parent.apply(this, arguments); 76 | }; 77 | 78 | proxy.prototype = parent.prototype; 79 | 80 | child.prototype = Fluxify.utils.objectAssign({ 81 | constructor: child 82 | }, new proxy(), childProto); 83 | 84 | child.__super__ = parent; 85 | 86 | return child; 87 | } 88 | } 89 | 90 | /* 91 | * 可以多次调用,自动merge 92 | * */ 93 | Fluxify.setActionTypes = function(types){ 94 | ACTION_TYPES = Fluxify.utils.objectAssign(ACTION_TYPES, Fluxify.utils.keyMirror(types)); 95 | 96 | return ACTION_TYPES; 97 | }; 98 | 99 | Fluxify.getActionTypes = function(){ 100 | return ACTION_TYPES; 101 | }; 102 | 103 | /* 104 | * from Backbone.js 105 | * */ 106 | Fluxify.Events = (function(){ 107 | var hasOwnProperty = _OP.hasOwnProperty, 108 | toString = _OP.toString, 109 | slice = _AP.slice; 110 | 111 | 112 | var _ = {//methods from underscore that Events in use 113 | once: function (func) { 114 | var ran = false, memo; 115 | return function() { 116 | if (ran) return memo; 117 | ran = true; 118 | memo = func.apply(this, arguments); 119 | func = null; 120 | return memo; 121 | }; 122 | }, 123 | isObject: function (obj) { 124 | return obj === Object(obj); 125 | }, 126 | isArray : Array.isArray || function(obj) { 127 | return toString.call(obj) == '[object Array]'; 128 | }, 129 | isString: function(obj){ 130 | return toString.call(obj) == '[object String]'; 131 | }, 132 | has: function(obj, key) { 133 | return hasOwnProperty.call(obj, key); 134 | }, 135 | keys : function (obj) { 136 | if (!_.isObject(obj)) return []; 137 | if (nativeKeys) return nativeKeys(obj); 138 | var keys = []; 139 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 140 | return keys; 141 | }, 142 | isEmpty : function (obj) { 143 | if (obj == null) return true; 144 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 145 | for (var key in obj) if (_.has(obj, key)) return false; 146 | return true; 147 | } 148 | } 149 | 150 | var Events = { 151 | 152 | // Bind an event to a `callback` function. Passing `"all"` will bind 153 | // the callback to all events fired. 154 | on: function(name, callback, context) { 155 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 156 | this._events || (this._events = {}); 157 | var events = this._events[name] || (this._events[name] = []); 158 | events.push({callback: callback, context: context, ctx: context || this}); 159 | return this; 160 | }, 161 | 162 | // Bind an event to only be triggered a single time. After the first time 163 | // the callback is invoked, it will be removed. 164 | once: function(name, callback, context) { 165 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 166 | var self = this; 167 | var once = _.once(function() { 168 | self.off(name, once); 169 | callback.apply(this, arguments); 170 | }); 171 | once._callback = callback; 172 | return this.on(name, once, context); 173 | }, 174 | 175 | // Remove one or many callbacks. If `context` is null, removes all 176 | // callbacks with that function. If `callback` is null, removes all 177 | // callbacks for the event. If `name` is null, removes all bound 178 | // callbacks for all events. 179 | off: function(name, callback, context) { 180 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 181 | 182 | // Remove all callbacks for all events. 183 | if (!name && !callback && !context) { 184 | this._events = void 0; 185 | return this; 186 | } 187 | 188 | var names = name ? [name] : _.keys(this._events); 189 | for (var i = 0, length = names.length; i < length; i++) { 190 | name = names[i]; 191 | 192 | // Bail out if there are no events stored. 193 | var events = this._events[name]; 194 | if (!events) continue; 195 | 196 | // Remove all callbacks for this event. 197 | if (!callback && !context) { 198 | delete this._events[name]; 199 | continue; 200 | } 201 | 202 | // Find any remaining events. 203 | var remaining = []; 204 | for (var j = 0, k = events.length; j < k; j++) { 205 | var event = events[j]; 206 | if ( 207 | callback && callback !== event.callback && 208 | callback !== event.callback._callback || 209 | context && context !== event.context 210 | ) { 211 | remaining.push(event); 212 | } 213 | } 214 | 215 | // Replace events if there are any remaining. Otherwise, clean up. 216 | if (remaining.length) { 217 | this._events[name] = remaining; 218 | } else { 219 | delete this._events[name]; 220 | } 221 | } 222 | 223 | return this; 224 | }, 225 | 226 | // Trigger one or many events, firing all bound callbacks. Callbacks are 227 | // passed the same arguments as `trigger` is, apart from the event name 228 | // (unless you're listening on `"all"`, which will cause your callback to 229 | // receive the true name of the event as the first argument). 230 | trigger: function(name) { 231 | if (!this._events) return this; 232 | var args = slice.call(arguments, 1); 233 | if (!eventsApi(this, 'trigger', name, args)) return this; 234 | var events = this._events[name]; 235 | var allEvents = this._events.all; 236 | if (events) triggerEvents(events, args); 237 | if (allEvents) triggerEvents(allEvents, arguments); 238 | return this; 239 | }, 240 | 241 | // Tell this object to stop listening to either specific events ... or 242 | // to every object it's currently listening to. 243 | stopListening: function(obj, name, callback) { 244 | var listeningTo = this._listeningTo; 245 | if (!listeningTo) return this; 246 | var remove = !name && !callback; 247 | if (!callback && typeof name === 'object') callback = this; 248 | if (obj) (listeningTo = {})[obj._listenId] = obj; 249 | for (var id in listeningTo) { 250 | obj = listeningTo[id]; 251 | obj.off(name, callback, this); 252 | if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; 253 | } 254 | return this; 255 | } 256 | 257 | }; 258 | 259 | // Regular expression used to split event strings. 260 | var eventSplitter = /\s+/; 261 | 262 | // Implement fancy features of the Events API such as multiple event 263 | // names `"change blur"` and jQuery-style event maps `{change: action}` 264 | // in terms of the existing API. 265 | var eventsApi = function(obj, action, name, rest) { 266 | if (!name) return true; 267 | 268 | // Handle event maps. 269 | if (typeof name === 'object') { 270 | for (var key in name) { 271 | obj[action].apply(obj, [key, name[key]].concat(rest)); 272 | } 273 | return false; 274 | } 275 | 276 | // Handle space separated event names. 277 | if (eventSplitter.test(name)) { 278 | var names = name.split(eventSplitter); 279 | for (var i = 0, length = names.length; i < length; i++) { 280 | obj[action].apply(obj, [names[i]].concat(rest)); 281 | } 282 | return false; 283 | } 284 | 285 | return true; 286 | }; 287 | 288 | // A difficult-to-believe, but optimized internal dispatch function for 289 | // triggering events. Tries to keep the usual cases speedy (most internal 290 | // Backbone events have 3 arguments). 291 | var triggerEvents = function(events, args) { 292 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 293 | switch (args.length) { 294 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 295 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 296 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 297 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 298 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 299 | } 300 | }; 301 | /* 302 | todo: 暂不迁移IOC版本 303 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 304 | 305 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 306 | // listen to an event in another object ... keeping track of what it's 307 | // listening to. 308 | _.each(listenMethods, function(implementation, method) { 309 | Events[method] = function(obj, name, callback) { 310 | var listeningTo = this._listeningTo || (this._listeningTo = {}); 311 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); 312 | listeningTo[id] = obj; 313 | if (!callback && typeof name === 'object') callback = this; 314 | obj[implementation](name, callback, this); 315 | return this; 316 | }; 317 | }); 318 | */ 319 | 320 | // Aliases for backwards compatibility. 321 | Events.bind = Events.on; 322 | Events.unbind = Events.off; 323 | 324 | return Events; 325 | })(); 326 | 327 | Fluxify.dispatcher = (function(){ 328 | 329 | var tokenIndex = 0, 330 | tokenPrefix = "ID_"; 331 | 332 | var Dispatcher = function(){ 333 | this._isDispatching = false; 334 | this._currentPayload = undefined; 335 | this._callbacks = {}; 336 | this._pending = {}; 337 | this._handled = {}; 338 | }; 339 | 340 | Dispatcher.prototype = { 341 | constructor : Dispatcher, 342 | register: function(callback){ 343 | var token = tokenPrefix + tokenIndex++; 344 | 345 | this._callbacks[token] = callback; 346 | 347 | return token; 348 | }, 349 | unregister: function(token){ 350 | delete this._callbacks[token]; 351 | }, 352 | dispatch : function(payload){ 353 | if (this._isDispatching){ 354 | console.error("cannot dispatch in middle of a dispatch"); 355 | return; 356 | } 357 | 358 | this._startDispatching(payload); 359 | 360 | for (var token in this._callbacks){ 361 | if (!hasOwn.call(this._callbacks, token)){ 362 | continue; 363 | } 364 | this._invokeCallback(token); 365 | 366 | } 367 | this._afterDispatching(); 368 | }, 369 | _startDispatching: function(payload){ 370 | this._currentPayload = payload; 371 | this._isDispatching = true; 372 | }, 373 | _afterDispatching: function(){ 374 | this._pending = {}; 375 | this._handled = {}; 376 | this._currentPayload = undefined; 377 | this._isDispatching = false; 378 | }, 379 | _invokeCallback: function(token){ 380 | var callback = this._callbacks[token]; 381 | 382 | this._pending[token] = true; 383 | callback.call(this, this._currentPayload); 384 | this._handled[token] = true; 385 | }, 386 | waitFor : function(tokens){ 387 | for (var i = 0, len = tokens.length; i < len; i++){ 388 | var token = tokens[i]; 389 | 390 | if (typeof token === "object"){ 391 | token = token.dispatchToken; 392 | } 393 | 394 | //check callback exist 395 | if (this._callbacks[token]){ 396 | //已经开始执行 397 | if (this._pending[token]){ 398 | //没有执行完,依赖一个执行中的callback说明成环 399 | if (!this._handled[token]){ 400 | throw Error("circle dependencies found while waitFor :" + token); 401 | } 402 | }else{//没开始执行 403 | this._invokeCallback(token); 404 | } 405 | }else{ 406 | console.error("callback not found for token: " + token); 407 | } 408 | } 409 | } 410 | } 411 | 412 | return new Dispatcher(); 413 | })(); 414 | 415 | var CHANGE_EVENT = "change"; 416 | Fluxify.createStore = function(name, methods, onDispatching){ 417 | var Store = Fluxify.utils.objectAssign({ 418 | emitChange: function(data){ 419 | this.trigger(CHANGE_EVENT, data); 420 | }, 421 | onChange: function(listener, context){ 422 | this.on(CHANGE_EVENT, listener, context); 423 | }, 424 | offChange: function(listener){ 425 | this.off(CHANGE_EVENT, listener); 426 | } 427 | }, Fluxify.Events, methods); 428 | 429 | if (typeof onDispatching === "function"){ 430 | Store.dispatchToken = Fluxify.dispatcher.register(onDispatching.bind(Store)); 431 | } 432 | 433 | return Store; 434 | } 435 | 436 | Fluxify.ReactMixins = (function(){ 437 | var storeListenerHandler = function(method){ 438 | 439 | var watchingStores = this.watchingStores, 440 | onStoreChange = this.onStoreChange || this.forceUpdate; 441 | 442 | if (!watchingStores || watchingStores.length < 0){ 443 | console.log("no valid stores watching"); 444 | } 445 | 446 | for (var i = 0, len = watchingStores.length, store = null; i < len; i++){ 447 | store = watchingStores[i]; 448 | if (store && typeof store[method] === "function"){ 449 | store[method](onStoreChange, this); 450 | } 451 | } 452 | }; 453 | 454 | return { 455 | componentDidMount : function(){ 456 | storeListenerHandler.call(this, "onChange"); 457 | }, 458 | componentWillUnmount : function(){ 459 | storeListenerHandler.call(this, "offChange"); 460 | } 461 | } 462 | })(); 463 | 464 | root.Fluxify = Fluxify; 465 | }); -------------------------------------------------------------------------------- /examples/chat/lib/sea.js: -------------------------------------------------------------------------------- 1 | /*! SeaJS 2.0.0b3 | seajs.org/LICENSE.md */ 2 | (function(u,q){function H(a){return function(c){return Object.prototype.toString.call(c)==="[object "+a+"]"}}function P(a){a=a.replace(ia,"/");for(a=a.replace(ja,"$1/");a.match(Q);)a=a.replace(Q,"/");return a}function R(a){a=P(a);ka.test(a)?a=a.slice(0,-1):la.test(a)||(a+=".js");return a.replace(":80/","/")}function S(a,c){return ma.test(a)?a:na.test(a)?(c||v).match(I)[0]+a:oa.test(a)?(v.match(pa)||["/"])[0]+a.substring(1):j.base+a}function T(a,c){if(!a)return"";var b=a,d=j.alias,b=a=d&&d.hasOwnProperty(b)? 3 | d[b]:b,d=j.paths,f;if(d&&(f=b.match(qa))&&d&&d.hasOwnProperty(f[1]))b=d[f[1]]+f[2];f=b;var x=j.vars;x&&-1 ")),n.length=0,c(!0)):(aa[a]=f,W(f,c))}function d(a){!a&&e.status=ea)return a.exports;a.status=ea;b.resolve=c;b.async=function(a,d){D(c(a),d);return b};var d=a.factory,d=y(d)?d(b,a.exports={},a):d;a.exports=d===q?a.exports:d;a.status=za;return a.exports}function Y(a){for(var c=[],b=0;b1*navigator.userAgent.replace(/.*AppleWebKit\/(\d+)\..*/,"$1"),ya=/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g, 13 | xa=/\\\\/g,l=e.cache={},F,K={},L={},G={},aa={},ta=1,E=2,J=3,ea=4,za=5;B.prototype.destroy=function(){delete l[this.uri];delete L[this.uri]};var n=[];e.use=function(a,c){fa(function(){D(r(a),c)});return e};e.resolve=T;u.define=wa;B.load=D;var N=ga,ha=N.match(/^(.+?\/)(?:seajs\/)+(?:\d[^/]+\/)?$/);ha&&(N=ha[1]);var j=M.data={base:N,charset:"utf-8",preload:[]};e.config=M;var O,h=h.search.replace(/(seajs-\w+)(&|$)/g,"$1=1$2"),h=h+(" "+p.cookie);h.replace(/seajs-(\w+)=1/g,function(a,c){(O||(O=[])).push(c)}); 14 | M({plugins:O});h=g.getAttribute("data-config");g=g.getAttribute("data-main");h&&j.preload.push(h);g&&e.use(g);if(m&&m.args){g=["define","config","use"];m=m.args;for(h=0;h