├── .idea ├── codeStyleSettings.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml ├── thin.iml └── vcs.xml ├── README.md ├── demo ├── area.html ├── binding.html ├── chess.html ├── component.html ├── controls │ └── datagrid.html ├── css │ └── tree.css ├── demo.0.1.html ├── dom.html ├── eventbus.html ├── org.html ├── partial │ └── form.html ├── simple-binding.html ├── staff.html ├── ui │ └── demo1.xml └── wizard.html ├── docs ├── 01.md ├── 02.md ├── 03.md ├── 04.md ├── 05.md ├── 06.md ├── chess.md ├── controls │ ├── datagrid01.md │ ├── datagrid02.md │ └── tree01.md ├── thin.md ├── wizard.md └── workflow.md ├── index.html ├── js ├── modules │ ├── chess │ │ ├── chessboard.js │ │ ├── chessman.js │ │ ├── config.js │ │ └── game.js │ ├── controls │ │ ├── datagrid.js │ │ ├── dialog.js │ │ ├── scrollbar.js │ │ ├── tree.js │ │ └── treegrid.js │ ├── core │ │ ├── binding.js │ │ ├── component.js │ │ ├── dom.js │ │ ├── promise.js │ │ └── remote.js │ ├── data │ │ ├── collection.js │ │ ├── object.js │ │ └── treemodel.js │ ├── decision │ │ └── rule.js │ ├── ide │ │ ├── service.js │ │ └── vm.js │ ├── role │ │ └── vm │ │ │ ├── areaViewModel.js │ │ │ ├── orgViewModel.js │ │ │ └── staffViewModel.js │ └── workflow │ │ ├── definition.js │ │ ├── designer.js │ │ └── engine.js ├── thin.js └── version │ └── thin.0.1.js └── libs ├── lodash └── lodash.min.js └── raphael-min.js /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/thin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | thin 2 | ==== 3 | 4 | a simple thin js framework 5 | 6 | 为了写一个构建前端框架的系列文章而写的一个JavaScript框架,编写的原则是: 7 | 8 | 使用方便 9 | 实现优雅 10 | 容易理解 11 | 12 | 为了达到这些目标,可能牺牲一些灵活性和性能。 13 | -------------------------------------------------------------------------------- /demo/area.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Area Management 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Area Management

19 |
20 |
21 |
22 | 23 |
24 |
25 |

Detail

26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 |
44 |
45 | 46 | 57 |
58 |
59 | 66 | 67 | -------------------------------------------------------------------------------- /demo/binding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Staff Management with binding 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Staff Management with binding

18 |
19 |
20 |
21 |
22 |
23 |

Detail

24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 | 71 |
72 | 73 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /demo/chess.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chinese Chess 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 41 | 42 | -------------------------------------------------------------------------------- /demo/component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Component Demo 5 | 6 | 7 | 8 | 9 | load component 10 |
11 | 38 | 39 | -------------------------------------------------------------------------------- /demo/controls/datagrid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DataGrid 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Staff Management

19 |
20 |
21 |
22 |
23 |
24 |

Detail

25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 | 43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 |
51 | 52 |
53 |
54 |
55 | 56 | 57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 | 77 |
78 | 79 | 317 | 318 | -------------------------------------------------------------------------------- /demo/css/tree.css: -------------------------------------------------------------------------------- 1 | ul.tree li.success > span { 2 | background-color: #dff0d8; 3 | } 4 | 5 | ul.tree li.error > span { 6 | background-color: #f2dede; 7 | } 8 | 9 | ul.tree li.warning > span { 10 | background-color: #fcf8e3; 11 | } 12 | 13 | ul.tree li.info > span { 14 | background-color: #d9edf7; 15 | } 16 | -------------------------------------------------------------------------------- /demo/demo.0.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Thin framework demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 81 | 82 | -------------------------------------------------------------------------------- /demo/dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DOM Operation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
aaaa
15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 | 42 | 43 | -------------------------------------------------------------------------------- /demo/eventbus.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/org.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Org Management with binding 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Org Management with binding

18 |
19 |
20 |
21 |
22 |
23 |

Detail

24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 | 34 |
35 |
36 |
37 | 38 | 39 |
40 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | 59 |
60 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /demo/partial/form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 7 |
8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 |
-------------------------------------------------------------------------------- /demo/simple-binding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple binding demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 62 | 63 | -------------------------------------------------------------------------------- /demo/staff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Staff Management with binding 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Staff Management with binding

18 |
19 |
20 |
21 |
22 |
23 |

Detail

24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 | 71 |
72 | 73 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /demo/ui/demo1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /demo/wizard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wizard 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Staff Management with binding

18 |
19 |
20 | 21 | 22 |
23 | 26 |
27 | 28 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /docs/01.md: -------------------------------------------------------------------------------- 1 | 从零开始编写自己的JavaScript框架(一) 2 | ==== 3 | 4 | #1. 模块的定义和加载 5 | 6 | ##1.1 模块的定义 7 | 8 | 一个框架想要能支撑较大的应用,首先要考虑怎么做模块化。有了内核和模块加载系统,外围的模块就可以一个一个增加。不同的JavaScript框架,实现模块化方式各有不同,我们来选择一种比较优雅的方式作个讲解。 9 | 10 | 先问个问题:我们做模块系统的目的是什么?如果觉得这个问题难以回答,可以从反面来考虑:假如不做模块系统,有什么样的坏处? 11 | 12 | 我们经历过比较粗放、混乱的前端开发阶段,页面里充满了全局变量,全局函数。那时候要复用js文件,就是把某些js函数放到一个文件里,然后让多个页面都来引用。 13 | 14 | 考虑到一个页面可以引用多个这样的js,这些js互相又不知道别人里面写了什么,很容易造成命名的冲突,而产生这种冲突的时候,又没有哪里能够提示出来。所以我们要有一种办法,把作用域比较好地隔开。 15 | 16 | JavaScript这种语言比较奇怪,奇怪在哪里呢,它的现有版本里没package跟class,要是有,我们也没必要来考虑什么自己做模块化了。那它是要用什么东西来隔绝作用域呢? 17 | 18 | 在很多传统高级语言里,变量作用域的边界是大括号,在{}里面定义的变量,作用域不会传到外面去,但我们的JavaScript大人不是这样的,他的边界是function。所以我们这段代码,i仍然能打出值: 19 | 20 | for (var i=0; i<5; i++) { 21 | //do something 22 | } 23 | alert(i); 24 | 25 | 那么,我们只能选用function做变量的容器,把每个模块封装到一个function里。现在问题又来了,这个function本身的作用域是全局的,怎么办?我们想不到办法,拔剑四顾心茫然。 26 | 27 | 我们有没有什么可参照的东西呢?这时候,脑海中一群语言飘过: 28 | C语言飘过:“我不是面向对象语言哦~不需要像你这么组织哦~”,“死开!” 29 | Java飘过:“我是纯面向对象语言哦,连main都要在类中哦,编译的时候通过装箱清单指定入口哦~”,“死开!” 30 | C++飘过:“我也是纯面向对象语言哦”,等等,C++是纯面向对象的语言吗?你的main是什么???main是特例,不在任何类中! 31 | 32 | 啊,我们发现了什么,既然无法避免全局的作用域,那与其让100个function都全局,不如只让一个来全局,其他的都由它管理。 33 | 34 | 本来我们打算自己当上帝的,现在只好改行先当个工商局长。你想开店吗?先来注册,不然封杀你!于是良民们纷纷来注册。店名叫什么,从哪进货,卖什么的,一一登记在案,为了方便下面的讨论,我们连进货的过程都让工商局管理起来。 35 | 36 | 店名,指的就是这里的模块名,从哪里进货,代表它依赖什么其他模块,卖什么,表示它对外提供一些什么特性。 37 | 38 | 好了,考虑到我们的这个注册管理机构是个全局作用域,我们还得把它挂在window上作为属性,然后再用一个function隔离出来,要不然,别人也定义一个同名的,就把我们覆盖掉了。 39 | 40 | (function() { 41 | window.thin = { 42 | define: function(name, dependencies, factory) { 43 | //register a module 44 | } 45 | }; 46 | })(); 47 | 48 | 在这个module方法内部,应当怎么去实现呢?我们的module应当有一个地方存储,但存储是要在工商局内部的,不是随便什么人都可以看到的,所以,这个存储结构也放在工商局同样的作用域里。 49 | 50 | 用什么结构去存储呢?工商局备案的时候,店名不能跟已有的重复,所以我们发现这是用map的很好场景,考虑到JavaScript语言层面没有map,我们弄个Object来存。 51 | 52 | (function() { 53 | var moduleMap = {}; 54 | window.thin = { 55 | define: function(name, dependencies, factory) { 56 | if (!moduleMap[name]) { 57 | var module = { 58 | name: name, 59 | dependencies: dependencies, 60 | factory: factory 61 | }; 62 | moduleMap[name] = module; 63 | } 64 | return moduleMap[name]; 65 | } 66 | }; 67 | })(); 68 | 69 | 现在,模块的存储结构就搞好了。 70 | 71 | ##1.2 模块的使用 72 | 73 | 存的部分搞好了,我们来看看怎么取。现在来了一个商家,卖木器的,他需要从一个卖钉子的那边进货,卖钉子的已经来注册过了,现在要让这个木器厂能买到钉子。现在的问题是,两个商家处于不同的作用域,也就是说,它们互相不可见,那通过什么方式,我们才能让他们产生调用关系呢? 74 | 75 | 个人解决不了的问题还是得靠政府,有困难要坚决克服,没有困难就制造困难来克服。现在困难有了,该克服了。商家说,我能不能给你我的进货名单,你帮我查一下它们在哪家店,然后告诉我?这么简单的要求当然一口答应下来,但是采用什么方式传递给你呢?这可犯难了。 76 | 77 | 我们参考AngularJS框架,写了一个类似的代码: 78 | 79 | thin.define("A", [], function() { 80 | //module A 81 | }); 82 | 83 | thin.define("B", ["A"], function(A) { 84 | //module B 85 | var a = new A(); 86 | }); 87 | 88 | 看这段代码特别在哪里呢?模块A的定义,毫无特别之处,主要看模块B。它在依赖关系里写了一个字符串的A,然后在工厂方法的形参写了一个真真切切的A类型。嗯?这个有些奇怪啊,你的A类型要怎么传递过来呢?其实是很简单的,因为我们声明了依赖项的数组,所以可以从依赖项,挨个得到对应的工厂方法,然后创建实例,传进来。 89 | 90 | use: function(name) { 91 | var module = moduleMap[name]; 92 | 93 | if (!module.entity) { 94 | var args = []; 95 | for (var i=0; i 15 | var person = { 16 | name: "Tom" 17 | }; 18 | 19 | 如果我们给name重新赋值,person.name = "Jerry",怎么才能让界面得到变更? 20 | 21 | 从直觉来说,我们需要在name发生改变的时候,触发一个事件,或者调用某个指定的方法,然后才好着手做后面的事情,比如: 22 | 23 | var person = { 24 | name: "Tom", 25 | setName: function(newName) { 26 | this.name = newName; 27 | //do something 28 | } 29 | }; 30 | 31 | 这样我们可以在setName里面去给input赋值。推而广之,为了使得实体包含的多个属性都可以运作,可以这么做: 32 | 33 | var person = { 34 | name: "Tom", 35 | gender: 5 36 | set: function(key, value) { 37 | this[key] = value; 38 | //do something 39 | } 40 | }; 41 | 42 | 或者合并两个方法,只判断是否传了参数: 43 | 44 | Person.prototype.name = function(value) { 45 | if (arguments.length == 0) { 46 | return this._name; 47 | } 48 | else { 49 | this._name = value; 50 | } 51 | } 52 | 53 | 这种情况下,赋值的时候就是person.name("Tom"),取值的时候就是var name = person.name()了。 54 | 55 | 有一些框架是通过这种方式来变通实现数据绑定的,对数据的写入只能通过方法调用。但这种方式很不直接,我们来想点别的办法。 56 | 57 | 在C#等一些语言里,有一种东西叫做存取器,比如说: 58 | 59 | class Person 60 | { 61 | private string name; 62 | 63 | public string Name 64 | { 65 | get 66 | { 67 | return name; 68 | } 69 | set 70 | { 71 | name = value; 72 | } 73 | } 74 | } 75 | 76 | 用的时候,person.Name = "Jerry",就会调用到set里,相当于是个方法。 77 | 78 | 这一点非常好,很符合我们的需要,那JavaScript里面有没有类似存取器的特性呢?老早以前是没有的,但现在有了,那就是Object.defineProperty,它的第三个参数就是可选的存取函数。比如说: 79 | 80 | var person = {}; 81 | 82 | // Add an accessor property to the object. 83 | Object.defineProperty(person, "name", { 84 | set: function (value) { 85 | this._name = value; 86 | //do something 87 | }, 88 | get: function () { 89 | return this._name; 90 | }, 91 | enumerable: true, 92 | configurable: true 93 | }); 94 | 95 | 赋值的时候,person.name = "Tom",取值的时候,var name = person.name,简直太美妙了。注意这里define的时候,是定义在实例上的,如果想要定义到类型里面,可以在构造器里面定义。 96 | 97 | 现在我们从数据到DOM的绑定可以解决掉了,至少我们能够在变量被更改的时候去做一些自己的事情,比如查找这个属性被绑定到哪些控件了,然后挨个对其赋值。框架怎么知道属性被绑定到哪些控件了呢?这个直接在第二部分的实现过程中讨论。 98 | 99 | 再看控件到数据的绑定,这个其实很好理解。无非就是给控件添加change之类的事件监听,在这里面把关联到的数据更新掉。到这里,我们在原理方面已经没有什么问题了,现在开始准备把它写出来。 100 | 101 | ##2.2 数据绑定的实现 102 | 103 | 我们的框架启动之后,要先把前面所说的这种绑定关系收集起来,这种属性会分布于DOM的各个角落,一个很现实的做法是,递归遍历界面的每个DOM节点,检测该属性,于是我们代码的结构大致如下所示。 104 | 105 | function parseElement(element) { 106 | for (var i=0; i 139 | 140 | 141 | 142 | 143 | 144 | 为了使得结构更加容易看,我们把界面的无关属性比如样式之类都去掉了,只留下不能再减少的这么一段。现在我们可以看到,在界面的顶层定义一个vm-model属性,值为实体的名称。两个输入框通过vm-value来绑定到实例属性,vm-init绑定界面的初始化方法,vm-click绑定按钮的点击事件。 145 | 146 | 好了,现在我们可以来扫描这个简单的DOM结构了。想要做这么一个绑定,首先要考虑数据从哪里来?在绑定name和code属性之前,毫无疑问,应当先实例化一个Person,我们怎么才能知道需要把Person模块实例化呢? 147 | 148 | 当扫描到一个DOM元素的时候,我们要先检测它的vm-model属性,如果有值,就取这个值来实例化,然后,把这个值一直传递下去,在扫描其他属性或者下属DOM元素的时候都带进去。这么一来,parseElement就变成一个递归了,于是它只好有两个参数,变成了这样: 149 | 150 | function parseElement(element, vm) { 151 | var model = vm; 152 | 153 | if (element.getAttribute("vm-model")) { 154 | model = bindModel(element.getAttribute("vm-model")); 155 | } 156 | 157 | for (var i=0; i 297 | 298 | 299 | Simple binding demo 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 |
308 | 309 | 310 | 311 | 312 |
313 | 314 |
315 | 316 | 317 | 318 |
319 | 340 | 341 | 342 | 343 | 或者访问这里:http://xufei.github.io/thin/demo/simple-binding.html 344 | 345 | 以刚才文章提到的内容,还不能完全解释这个例子的效果,因为没看到在哪里调用parseElement的。说来也简单,就在thin.js里面,直接写了一个thin.ready,在那边调用了这个函数,去解析了document.body,于是测试页面里面才可以只写绑定和视图模型。 346 | 347 | 我们还有一个更实际一点的例子,结合了另外一个系列里面写的简单DataGrid控件,做了一个很基础的人员管理界面:http://xufei.github.io/thin/demo/binding.html 348 | 349 | ##2.3 小结 350 | 351 | 到此为止,我们的绑定框架勉强能够运行起来了!虽然很简陋,而且要比较新的浏览器才能跑,但毕竟是跑起来了。 352 | 353 | 注意Object.defineProperty仅在Chrome等浏览器中可用,IE需要9以上才比较正常。在司徒正美的avalon框架中,巧妙使用VBScript绕过这一限制,利用vbs的property和两种语言的互通,实现了低版本IE的兼容。我们这个框架的目标不是兼容,而是为了说明原理,所以感兴趣的朋友可以去看看avalon的源码。 -------------------------------------------------------------------------------- /docs/03.md: -------------------------------------------------------------------------------- 1 | 从零开始编写自己的JavaScript框架(三) 2 | ==== 3 | 4 | #3. 视图与模型 5 | 6 | 我们第二篇文章讲述了简单的数据绑定实现,但是做得太简单了。一个真实的带绑定功能的框架,首先要能定义出各种模型,然后针对每种模型定义出与界面的关联关系。 7 | 8 | 前面的例子,只针对简单对象跟表单输入项做了绑定,在真实场景下,这显然很不够,比如说,数组怎么跟界面做绑定?树形结构如何描述? 9 | 10 | -------------------------------------------------------------------------------- /docs/04.md: -------------------------------------------------------------------------------- 1 | 从零开始编写自己的JavaScript框架(四) 2 | ==== 3 | 4 | #4. 异步编程与Promise 5 | 6 | ##4.1 异步编程有什么不同 7 | 8 | 异步编程有什么样的特点呢?比如说你有一个异步方法a,如果想要在a执行完之后,再执行一个b: 9 | 10 | a(); 11 | b(); 12 | 13 | 这样基本是得不到期待的结果的,很可能a里面的内容没有执行完,b倒先做完了。 14 | 15 | 通常我们写异步调用,都是使用回调的方式,比如这样: 16 | 17 | function a(success, fail) { 18 | //监听某个事件,检测到成功,就调用success,检测到失败,就调用fail 19 | } 20 | 21 | a(function(data) { 22 | console.log(data); 23 | }, function(reason) { 24 | console.log(reason); 25 | }); 26 | 27 | 但是回调有个缺点,如果几个操作是有依赖关系,比如b要等到a成功了再做,c要等到b成功了再做,那代码可能变成这样: 28 | 29 | a(function(data) { 30 | b(function(data1) { 31 | c(function(data2) { 32 | //c成功之后要做的事情 33 | }) 34 | }) 35 | }); 36 | 37 | 这个写法很令人头疼,才3个连环操作,代码就变成这样了,多一个操作就多一层,到最后代码基本没法看了。 38 | 39 | 现在我们的需求很明确: 40 | 41 | - 按照什么顺序写的代码,我就希望它按照什么顺序执行 42 | - 不要搞成上面那样的嵌套 43 | 44 | 再一次拔剑四顾心茫然,什么才是我们真正要的东西?如果语言本身书写和执行的顺序不能控制,那我们可以人为创造一个队列。我们联想到jQuery,它以链式表达式著称,比如:a().b().c()这样,如果我们只是这样,那这个代码还是不对,跟三个函数分开写没有任何区别,因为没有实现异步。 45 | 46 | 这好办,异步就是让后续的函数不立即执行,换言之,我只要把这些后续函数存在某个地方,然后按照在它们的先决条件执行完之后才拿出来执行,就可以了,如果这样呢?when(a).then(b).then(c); 47 | 48 | 这个看上去还可以,所以我们来打算实现它。 49 | 50 | #4.2 Promise 51 | 52 | 先看看要怎么实现方法的顺序调用,一定是要有一个地方能存放这个调用序列的,比如说是个队列。 53 | 54 | - 每次有新方法要执行的时候,如果当前正在执行其他方法,就先把这个方法放到队列尾部,否则立即执行,并且把当前状态置为执行中。 55 | - 在执行完成之后,修改执行状态为空闲,从队列头取出下一个方法,如果非空继续执行这个方法。 56 | 57 | 好,我们开始写: 58 | 59 | var queue = []; 60 | var executing = false; 61 | 62 | function then(foo) { 63 | if (executing) { 64 | queue.push(foo); 65 | } 66 | else { 67 | foo(function() { 68 | executing = false; 69 | next(); 70 | }); 71 | } 72 | } 73 | 74 | function next() { 75 | if (queue.length > 0) { 76 | var foo = queue.shift(); 77 | foo(function() { 78 | executing = false; 79 | next(); 80 | }); 81 | } 82 | } 83 | 84 | 然后看看怎么写: 85 | 86 | then(a); 87 | then(b); 88 | then(c); 89 | 90 | 哎,不对啊,我们不是要这么写,而是要形成一个链呢。再来: 91 | 92 | function ExecuteQueue() { 93 | this.queue = []; 94 | this.executing = false; 95 | } 96 | 97 | ExecuteQueue.prototype = { 98 | then: function(foo) { 99 | if (this.executing) { 100 | this.queue.push(foo); 101 | } 102 | else { 103 | var that = this; 104 | foo(function() { 105 | that.executing = false; 106 | that.next(); 107 | }); 108 | } 109 | 110 | return this; 111 | }, 112 | 113 | next: function() { 114 | if (this.queue.length > 0) { 115 | var foo = this.queue.shift(); 116 | var that = this; 117 | foo(function() { 118 | that.executing = false; 119 | that.next(); 120 | }); 121 | } 122 | } 123 | }; 124 | 125 | 用的时候,这样: 126 | 127 | var queue = new ExecuteQueue(); 128 | queue.then(a).then(b).then(c); 129 | 130 | 好像有那么回事了,但还缺一些东西,比如说,如果a出异常了,想要取消b和c的执行,怎么办?这里面的问题在什么地方呢?队列可以调度这些方法的执行过程,但是这些方法本身没法控制队列的状态。 131 | 132 | 133 | function a() { 134 | var queue = new ExecuteQueue(); 135 | 136 | 137 | return queue; 138 | } -------------------------------------------------------------------------------- /docs/05.md: -------------------------------------------------------------------------------- 1 | 从零开始编写自己的JavaScript框架(五) 2 | ==== 3 | 4 | #5. 规则 5 | 6 | 规则是一种表达式,基本单元是操作数和操作符,每个操作数本身又可以是一个规则。 7 | 8 | 规则由字符串解析生成, 9 | 10 | 操作符分为逻辑操作符和算术操作符 11 | 12 | 如果一个操作数不可再分解为其他的规则,它就是一个基础操作数。 13 | 14 | 基础操作数可能是变量,也可能是常量。 15 | 16 | 规则可以被执行。 17 | 18 | 规则是从属于某个主体的,在它执行的时候,变量需要在这个主体的上下文内执行。 19 | 20 | 解析规则的过程: 21 | 22 | 把字符串分解为基本单元:操作符,数字, -------------------------------------------------------------------------------- /docs/06.md: -------------------------------------------------------------------------------- 1 | 从零开始编写自己的JavaScript框架(三) 2 | ==== 3 | 4 | #3. DOM操作 5 | 6 | 作为一个前端框架,操作DOM是一个必不可少的功能。最广为人知的DOM操作框架是jQuery,它的选取器和链式操作,都给人们留下深刻印象。 7 | 8 | 在我们这个框架里,目标不是像它那样做一个全功能的DOM操作库,而是只做一部分很常用的功能。 9 | 10 | ##3.1. 链式表达式 11 | 12 | 在本系列的第三部分中,我们提到过链式表达式,想要有这样的效果,返回结果必须作过特殊处理。这个返回结果上要能够继续被执行方法,而且每个方法的执行结果还是自身这个实例。 13 | 14 | 注意jQuery的$方法,虽然我不赞同它的实现,但用它来说明思路还是可以的。它为什么要这么写呢,是因为想要把选取到的DOM元素再封装一层。试想一下,如果你不对这些DOM作封装,怎么能让它有on,addClass等方法呢? 15 | 16 | 所以,我们也来做这么一个封装。 17 | 18 | function DOM() { 19 | this.elements = []; 20 | } 21 | 22 | DOM.prototype = { 23 | attr: function() { 24 | this.elements.forEach(function(element) { 25 | 26 | }); 27 | return this; 28 | }, 29 | 30 | addClass: function() { 31 | 32 | }, 33 | 34 | removeClass: function() { 35 | 36 | } 37 | }; 38 | 39 | var DOMSelector = { 40 | byId: function(id) { 41 | var dom = new DOM(); 42 | dom.elements.push(document.getElementById(id)); 43 | 44 | return dom; 45 | } 46 | } -------------------------------------------------------------------------------- /docs/chess.md: -------------------------------------------------------------------------------- 1 | 使用JavaScript创建模块化的双人对战象棋程序 2 | ==== 3 | 4 | #1. 关于这篇文章 5 | 6 | 2004年,我花两天时间,用JavaScript和VML创建了一个单机双人象棋,并且作了简短的分析。在那个时代,没有AngularJS,没有BackBone,没有所有这些前端MV*框架。甚至没有jQuery,没有prototype,没有mootools,因此没有什么可借鉴的模块划分方式。我只好用很原始的办法,做了一种伪继承,实际是组合,来实现棋子和棋局之间的关系。 7 | 8 | 现在是2013年,9年过去了,Web的世界早已不是过去的样子,开发方式发生了翻天覆地的变化,我们有了Gmail,有了Google docs等等把Web技术应用到极致的优秀产品,有了asm.js、pdf.js等等让我们目瞪口呆的技术,更催生了各种MV*框架的兴起,我们有更多,更强大的方式去写Web程序。 9 | 10 | 前一段时间,我创建了一个简单的JavaScript框架叫做thin,实现了模块的定义、异步加载和使用,并且为它写了一个比较简短的Demo,但这个Demo实在太简单了,当一个应用更大、更复杂的时候,我们应该如何组织自己的程序呢? 11 | 12 | 为了说明我们这个简单框架的能力,我把之前写过的这个象棋在这个thin框架基础上重写一遍,并且作更深入的分析,以便使一些入门不久的读者得到帮助,同时也顺便检验我的新框架模块化是否是可用的。 13 | 14 | 另外一个方面,我们看到VML已经彻底衰落了,各种基于SVG和Canvas的绘图技术取代了它,因此在本例中,我们也与时俱进,改用SVG来绘制棋盘和棋子。RaphaelJS是一个很好的跨平台绘图库,它封装了SVG和VML,在能够使用SVG的浏览器中,它用SVG绘图,否则尝试使用VML,对于上层应用,操作绘图的API是毫无区别的,开发者不会感知到它的具体实现差别。 15 | 16 | 之前我写过一些文字,用于探讨软件开发模块的划分原则,感觉它们放在这篇文章里非常合适,所以略微修改之后,加了进来。 17 | 18 | #2. 模块划分的一些原则 19 | 20 | ##2.1. 面向对象 21 | 22 | 面向对象可以算是老生常谈了,在现代软件开发中,它是个主流的选择,相对于面向过程,有一些改进。 23 | 24 | 假设我们是上帝,要创造世界,因为这个过程太过复杂,无从入手,所以先从一件简单的事情看起。现在我们要设计一个方法,用于描述狼吃羊这个事情,某只狼吃了某只羊,你可以面向过程地吃,eat(狼A, 羊A),也可以面向对象地吃,狼A.eat(羊A)。差别在哪里?只是写法有点变化。 25 | 26 | 好,那么我们帮上帝模拟整个生物界,这里面很多东西可以吃,大鱼吃小鱼,小鱼吃虾米,吃不吃皮,吐不吐骨头,这个时候再来修改这个eat函数,复杂吗?eat里面要判断很多东西,假如上帝很勤劳,所有代码都自己设计,那没关系,没太大区别,判断就判断呗。 27 | 28 | 假设上帝没足够精力来管理整个东西了,雇了一群天使来协助设计,每个人都来修改这个eat函数,当然可以拆分,wolfEatSheep(), tigerEatWolf(),然后在eat里面判断参数来分别调用,把函数分下去让每个人做,可以。 29 | 30 | 动物不光要做吃这个事情,要能跑能跳,会说会叫,又多了一堆函数,每个里面都这么判断,相当相当的烦。怎么办?我们来面向对象一下。 31 | 32 | 现在开始按照动物拆分,100个天使,每个天使创造一种动物。创造哪种动物,就站在哪种动物的角度考虑问题,我吃的时候怎么吃,跑的时候怎么跑,都跟别人无关,这么一来,每个人就专注多了。每个动物只关注我要怎么才能活着,不必站在上帝的角度考虑问题。这个过程,是类的划分过程,也就是封装的过程。 33 | 34 | 这时候,上帝觉得自然界光有动物是不行的,还要有植物,刚才说的这些都是动物,植物的特点跟动物有很大区别。假设你是上帝,为每种生物安排衣食住行,那是相当复杂的。偷懒吧,上帝说,植物们,你们自己生长吧,动物们,你们吃喝玩乐吧,假如能达到这个效果,那很省事。 35 | 36 | 上帝用一个循环来遍历所有动物,让他们吃喝玩乐,用另外一个循环让植物欣欣向荣。动物跟植物为什么要区别对待?因为它们不是同样的东西,能做的事情不同。所有动物派生于动物这个基础类型,从动物这个种类下,又分出各种纲,各种目,各种属。狮子是哺乳动物,猴子也是,但是狮子是猫科动物,猴子是灵长动物,这就构成了一个倒着的树状体系,一层一层形成继承关系。哺乳动物会喂奶,那么所有继承自哺乳动物的,都自动拥有这个特征。整个这一切,构成了继承链。 37 | 38 | 假设有一天由于变异出现了新物种,不必劳烦上帝关照,只要鉴别一下它属于什么类型,就知道能做什么事了,它的一举一动,都必然拥有它所继承的种类的特征。 39 | 40 | 这样就能描述生物界了吗?不,还有那么一些怪胎的存在。你认为哺乳动物都不会飞,那就错了,因为蝙蝠会飞。蝙蝠会飞是它自身的特性,并非继承自哺乳动物,但是“飞”这个动作,却非蝙蝠独有。如果把“飞”定义成接口,那就很美好了,蝙蝠实现了它的飞行接口,虽然内部实现跟鸟类有所不同,而且这并不影响它的哺乳动物特性。 41 | 42 | 总之,是否面向对象只是思维方式的不同。做一个软件,面向对象也能做,不面向对象也能做。我的观点,如果关注可维护性和协作性,从目前的角度,面向对象是很好的选择,它很自然,很优雅,优雅得只要打一个“.”,你就能想起来什么事能做,什么事不能做。 43 | 44 | ##2.2. 模块的职责划分 45 | 46 | 面向对象的一个基本原则是分而治之(Divide and Conquer),这种方法论提倡将程序模块化,各模块实现单独的功能,在统一的管理下协同工作,构成整个系统。 47 | 48 | 在具体实施的时候,又有两种倾向:将功能高度集中于主控制模块;将功能下放到各部件。这两种做法都有很高的可行性,也分别有大量支持者。我觉得在一些程度上,后者更贴近人类的思维方式,更适合用人性化的观念来解释。 49 | 50 | 将两种类型的程序对应到生物集群,第一种相当于一个蚁群,第二个相当于人群。蚁群的特点是,个体能够完成的事务非常有限,但是因为在一个非常强有力的统治者蚁后的控制下,它们能够协同工作,统一调度,完成不可想象的事件。人群的特点是,每个人都可以独立思考,能够理解别人的指令,并且根据这些指令做到力所能及的事情。作为人群的统治者,他的智慧不需要比其他人的高太多,只需要从宏观上来把握一些事情即可。 51 | 52 | 从系统的实现来说,第一种方式难度很高。完成单个蚂蚁(小模块)的功能并不复杂,创建大量的蚂蚁也只不过是需要的时间多一点,但是,当开始设计蚁后(总控模块)的时候,噩梦开始了,整个调度算法实在是一件令人头疼的事情。对于比较复杂一点的系统,让一个人去设计这个模块简直是不可思议,但是如果由多个人共同完成这个模块,又面临着互相理解的问题,每个人的思路都不相同,在努力协作的过程中,大量的时间被浪费在交流和意见的统一上。与此同时,制作蚂蚁的程序员日益烦躁,觉得自己的工作没有难度,无聊,士气低下…… 53 | 54 | 换一种思路,从人类管理的角度来看问题。假设有一支庞大的军队(假设是一个集团军),司令官需要他的士兵列队,我们来为这个系统设计调度算法。先假设所有士兵跟蚂蚁一样笨,他们只能明白“站到司令部大门往东50米,往北100米的地方”这样的简单指令,请同情一下这位司令官,他不得不为每个士兵来指定一个位置,并且不得不研究列队的规则,他需要整天忙碌来完成这样一个庞大的任务(而且还不一定能完成)。他叹息道:哦,上帝…… 55 | 56 | 让我们设法来减轻他的烦恼吧,目标是让每个人都主动参与这个事件,不再那么被动,大家都努力完成自己力所能及的工作。于是我们授权各级指挥官让他自己的士兵列队,这样一来,司令官的工作简单多了,他发布命令:各位军长请注意,我命令你们列队,按照番号顺序,分布到司令部门口的空地上(假设这个空地足够大,姑且认为能够容纳整个集团军),各军之间保持50米间隔。 57 | 58 | 接到命令以后,军长们开始忙碌,而司令官先生已经可以搬一把椅子坐到电话机旁,等待列队完毕的报告了。同样,军长要做的事情,也就是告诉属下的各位师长,让他们按照番号顺序列队,就这样,命令被传递到最下面一级。班长大喊:伙计们,按照个头排成一列,矮的在前面,高的在后面,前后间隔一米!于是,所有人站到了他应该站的位置,望着在短时间内迅速列队的整个集团军,司令官太满意了。 59 | 60 | 我们发现了什么?很显然,下放权力的方式要省事得多,更关键的是,它使得每个人都做一定的事情,但是又不成为负担。在设计者思路清晰化的同时,负责为系统每个部分编写代码的人员也更容易享受到编程的乐趣,就算是最低层的程序员也有了发挥自己才能、用自己的思路去影响系统的机会,而且,系统集成的过程将变得更加简单。 61 | 62 | 对于一名软件设计师来说,他的思想决定了他所设计出来的软件结构,将自己的灵魂注入到冰冷的代码中,这是一种艺术。然而,不同的人有不同的风格,设计者对于世界的认知方式不同,他们对于同样的需求,可能采用的设计方式也多种多样。 63 | 64 | #3. 怎么设计我们的象棋程序 65 | 66 | ##3.1. 为象棋程序划分模块 67 | 68 | 做一个象棋程序,有哪些事情要做呢? 69 | 70 | 首先,我们要能够初始化一个棋局,把棋盘和棋子绘制出来,点击棋子的时候,能给出它可以走的地方,可以移动这个棋子,也可以吃掉对方的棋子。走完一步,要能够判断是否将军,如果吃掉了对方的将帅,能够判断棋局的终止。这些东西,除了绘图之外,我们都放在棋局模块里,我们有个第一个模块Game。绘图模块的职责比较单一,我们把它放在一个棋盘模块中,这是第二个模块ChessBoard。 71 | 72 | 天下之事,事事都是棋局,人在局中为名来,为利往,都是棋子。可见,与棋局相对的就是棋子了。按照我们在第二部分提到的思路,棋子应当是要承担一些职责的,那么,哪些事情适合交给棋子来做呢? 73 | 74 | 我们定义这么一个规则:做一件事,如果有多个参与者,其中某个参与者要付出的代价最大,这一步就由这个参与者来负责做。 75 | 76 | 我们把走棋的这个过程分解,这里有四个部分: 77 | 78 | - 判断我有没有可能出现在那个位置,比如说,象不能过河,老将和卫士不能出九宫格。 79 | - 判断目标位置有没有己方棋子,如果有,也过不去。 80 | - 判断能否直接到达目标位置,比如说,马腿是否被挡着了?象眼是否被塞着了? 81 | - 移动过去,如果有对方棋子,吃掉它。 82 | 83 | 从第一步来看,这个过程不依赖于其他任何东西,每个棋子都应当能够牢记自己能去什么地方,不能去什么地方,只要你给它一个棋盘坐标,它自己是可以知道能不能去的。比如象知道自己不能过河,如果你给的坐标就超过了,它可以知道自己不能去。所以,这个职责我们放给棋子。 84 | 85 | 再看第二步,这个我们怎么判断呢?假设我们是一个士兵,在平原上打仗,我想知道前面山顶有没有人,怎么办?看了很多电影的我们表示,很好办。“总部总部,请侦察对面山顶。”所以,这个过程我们可以看到,检索目标位置不是棋子自身的职责,他只是调用了某个别的东西(己方司令部),得到了结果。 86 | 87 | 下面是第三步,这里面有可能不需要依赖于其他模块,也可能要依赖,怎么解释呢?比如说卫士,他走路只看距离,如果是他的合法可达位置,并且和当前位置距离的平方为1+1=2,那就可以直接过去,不需要依赖任何外部模块。但是如果是马,要先看距离的平方是不是1+4=5,然后再找马腿的位置,再去看那个位置有没有棋子。所以这种情况下,就要依赖外部模块。 88 | 89 | 第四步看似很简单,过去的时候发个通知给司令部,我换地方了!但司令部那边要把当前所有人分别在哪都记录着,所以他要做的事情其实比棋子更复杂,所以这一步可以让他做。 90 | 91 | 于是,我们得出结论,棋子的职责应该是这些: 92 | 93 | - 判断自己是否可能出现在某位置 94 | - 判断自己能否到达某位置 95 | 96 | 现在我们就有了Chess模块,并且从它派生出各种棋子,同时,为这些棋子实现一个工厂模块ChessFactory,用于根据参数创建这些棋子。 97 | 98 | 现在我们来考虑,谁来提供这个查询的服务,承担司令部的这些职责,棋子的位置都保存在棋局中了,所以,很自然地,棋局承担了这个职责。使用。 99 | 100 | 简单地考虑一下,我们的棋局应当能够: 101 | 102 | - 初始化。初始化方法做的是把棋局恢复成初始状态,每次重新开局之前,我们可以这么做一下。 103 | 104 | - 走棋。走棋是把给出的棋子移动到指定位置,如果目标位置没有别的棋子,只做移动,否则还要把对方杀死。走棋之前有一些判断条件,我们也把它们列出来。 105 | 106 | * 列出某棋子的可达范围。这其实是一个辅助功能,当用户点击某棋子的时候,界面上能够标示出所有该棋子的可到达位置,便于用户选择,当用户选择其中某一个的时候,把棋子移动过去。 107 | 108 | - 判断是否终局。每一步棋走完,我们都需要看一下是否有一方获胜,如果有,本局应当终止。 109 | 110 | #3.2. 代码结构 111 | 112 | 根据上述的结论,我们建立了这么4个代码文件,用于存放不同的模块: 113 | 114 | - game,存放棋局相关的功能 115 | - chessboard,存放绘制棋盘和棋子相关的功能,点击操作也由它负责传递 116 | - config,存放各种配置信息,比如棋盘大小等等 117 | - chessman,存放各种棋子的功能和棋子生成器 118 | 119 | 注意到我在chessman.js里面,定义了多个模块,这其实就是我这个thin框架的核心理念,模块跟文件不一一对应,模块对应于Java中的class文件,而js文件对应于Java中的jar文件,是模块的集合。这么做当然也有弊端,因为无法得知模块是否有冲突,或者存在被覆盖的情况,引用也不是很方面,所以我为此还建立了一套管理和发布机制,专门用来解决这个问题。在小型项目纯手写代码的情况下,直接这样用就可以了。代码细节不一一列出,请读者自行查看。 120 | 121 | #4. 可能的改进 122 | 123 | 上面我们实现了一个可以在单机下双人对战的象棋程序,运行得还不错,但是我们想要给它一些增强,应当如何去做呢? 124 | 125 | 如果我们想要本机开多个棋局,怎么办? 126 | 127 | 在我们现有结构下,其实很简单,因为我们模块化做得还是挺好的,Game可以作为顶层模型,然后创建出对应的DOM容器,用我们上次写的Bind来扫描一遍,自动创建实例,就可以了。 128 | 129 | 另外一个很典型的增强是,既然我们都做了单机的对战了,是不是可以搞一个服务端,变成联机的对战呢?当然可以,要做这个,我们需要改动的代码是Game模块,这一步不再适合直接创建了,而是要放在新建棋局的服务端回调里面,棋局的状态也需要在服务端保存一份,然后每次下棋,把走的棋子和坐标放过去,对其他任何模块都没有改动。从这里我们也可以看到如果代码进行了合理的分层,当需要改进的时候,对原代码改动有多么容易。 130 | 131 | 再有这么一天,我们还要做棋局的撤销怎么办?虽然这个很不好,有损大丈夫的威名,但我们只从实现角度来分析一下。在设计模式中,有一种叫做命令模式,这种模式其实就很适合做undo跟redo,只要把每个事情都封装为步骤,那么,这两种操作就变成了正向和反向的两种步骤了,做起来也就非常容易。 132 | 133 | 还不满意,要添加人工智能怎么办?抛开人工智能常用的剪枝算法不谈,我们假设已有这么个算法,只需要在一方移动棋子之后,把当前局势传递给这个算法得到下一步即可。 134 | 135 | 综上所述,做大一点的Web应用,必须先做模块化,把模块按照功能划分,理清它们之间的关系,然后再用合适的框架去管理维护。作者正在编写的thin框架就是试图从模块入手,一步一步添加其他功能,把它做成一个有一定可用性的框架。 136 | 137 | 本文的Demo地址是:http://xufei.github.io/thin/demo/chess.html -------------------------------------------------------------------------------- /docs/controls/datagrid01.md: -------------------------------------------------------------------------------- 1 | 数据表格控件的基础功能 2 | ==== 3 | 4 | 数据表格是一个很常用的控件,用于把多列数据展示成表格的形状,通常有表头,表头可固定,表格内容可滚动。本文以一个数据表格控件为例,说明从构思到实现控件的整个过程。 5 | 6 | 为了使初学者更容易理解其中的原理,我们不使用任何额外的库,比如jQuery之类,仅仅使用bootstrap来控制样式。 7 | 8 | #1. 功能分析 9 | 10 | DataGrid控件主要有以下几个功能: 11 | - 加载数据并展示成表格的形状 12 | - 新增一行 13 | - 删除一行 14 | - 点击某行选中 15 | - 修改行数据并刷新 16 | 17 | DataGrid控件主要需要响应这样几个事件: 18 | - 加载完成 19 | - 选中行变更 20 | 21 | #2. 实现原理 22 | 23 | 想要实现DataGrid控件,我们有三个步骤要做: 24 | 25 | - 用什么样的DOM结构来展现 26 | - 用什么样的结构来定义数据 27 | - 数据跟DOM结构如何关联起来 28 | 29 | 下面我们来考虑如何分别实现这三个步骤。 30 | 31 | ##2.1. DOM结构 32 | 33 | 做一个控件之前,我们首先要把DOM结构确定下来,也就是用HTML能够展现控件的形态。什么结构适合展现数据表格呢?毫无疑问,是HTML中的table,语义上非常符合,为了省事,我们不考虑样式,直接用bootstrap中的表格样式。 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
#NameGenderAge
1TomMale5
2JerryFemale2
3Sun WukongMale1024
65 | 66 | 这个结构足够表达DataGrid了,样子也还可以,所以我们很满意,开始考虑数据了。单使用这个结构,是难以做到表头固定,表格体可滚动的,但为了简单,我们先用这个结构来做。 67 | 68 | ##2.2. 数据结构 69 | 70 | 在数据表格中,有两个数据是需要传入的,一是标题的列头数据,二是表格的内容,它们都很适合用数组来描述。 71 | 72 | 列头最少需要描述这些内容: 73 | 标题 74 | 数据字段 75 | 76 | 表格行需要有这些信息: 77 | 每个key的值 78 | 79 | 所以,对照上面的表格,我们可以把数据描述起来: 80 | 81 | var columns = [{ 82 | label: "#", 83 | field: "index" 84 | }, { 85 | label: "Name", 86 | field: "name" 87 | }, { 88 | label: "Gender", 89 | field: "gender" 90 | }, { 91 | label: "Age", 92 | field: "age" 93 | }]; 94 | 95 | var data = [{ 96 | index: 1, 97 | name: "Tom", 98 | gender: "Male", 99 | age: 5 100 | }, { 101 | index: 2, 102 | name: "Jerry", 103 | gender: "Female", 104 | age: 2 105 | }, { 106 | index: 3, 107 | name: "Sun Wukong", 108 | gender: "Male", 109 | age: 1024 110 | }]; 111 | 112 | ##2.3. DOM和数据的关联 113 | 114 | 这部分听起来有些复杂,我先打个比方吧。 115 | 116 | 有一个勤劳的妈妈,她有三个宝宝,每个宝宝都有不少衣物,妈妈的职责是管理这些衣物,并且用它们来装扮宝宝们。这些衣物可以分为上衣,裤子,鞋子,袜子,帽子等等。 117 | 118 | 想象一下她是怎么做的: 119 | 把第一个宝宝抱过来,选几件衣服,给他穿上,放他出去玩。 120 | 对第二个宝宝做同样的操作。 121 | 对第三个宝宝做同样的操作。 122 | 123 | 我们甚至还可以推而广之,不管她有多少宝宝,都必定是按照这个方式做的。 124 | 125 | 那么,对比我们的控件,每条数据都是一个宝宝,把数据渲染到DOM的过程就好比给宝宝穿衣服。我们的列信息就好比衣物的分类。 126 | 127 | 我们有另外的问题: 128 | 宝宝们都出去玩了,我们问妈妈:最大的宝宝穿的是些什么衣服?绿色有大嘴猴的那件T恤穿在哪个宝宝身上? 129 | 130 | 这就要求妈妈对衣服和宝宝的关联关系有所记录。拿我们这个控件来看,宝宝就相当于每行的数据,衣服相当于用来展示的DOM节点,在这两者之间是需要一些关联的。 131 | 132 | #3. 程序设计 133 | 134 | ##3.1. 实体划分 135 | 136 | 在我们这个控件中,存在两个实体:数据表格,表格行。 137 | 138 | 数据表格的职责很清楚,表格行的存在又是为了什么呢?我们完全可以把职责全部掌控在数据表格里,不下放给行。 139 | 140 | 设想我们要取选中行的name属性,我们的写法是这样的: 141 | 142 | var row = grid.selectedRow(); 143 | var name = grid.get(row, "name"); 144 | 145 | 希望这么写: 146 | 147 | var row = grid.selectedRow(); 148 | var name = row.get("name"); 149 | 150 | 甚至我们可以连着写: 151 | 152 | var name = grid.selectedRow().get("name"); 153 | 154 | 这里的问题是,selectedRow究竟要返回什么结构,才能让它有get方法? 155 | 156 | 这里有两种选择,一种是返回行的DOM结构,get方法附加在行上,行的数据也作为属性附加在DOM上,这样我们是没有单独的表格行实体的。另一种是创建表格行的实体,在其中管理DOM和数据的关联关系。这两种方式,我们应当如何选择呢? 157 | 158 | 把过多数据附加到DOM上并不是一个好的选择,尤其我们不能确定用户给控件传哪些字段,万一跟DOM自身的冲突了,会很糟糕。所以我们选择自己来管理这个关系。 159 | 160 | ##3.2. 表格行的职责 161 | 162 | 确定了这样的原则之后,我们来考虑表格行的职责。 163 | 164 | 表格行,它应当可以被选中,也可以被取消选中(这个操作不是通过点击选中状态的自身来完成,而是点击其他行,由表格控件来取消自己的选中),可以读写数据。 165 | 166 | 我们考虑一下表格行的选中要干些什么。首先,如果已经有选中的行了,要把那个的样式去掉,然后把选中的行指向当前的行,再把当前行的样式变成选中的颜色。 167 | 168 | 这些职责,我们来考虑一下,哪些属于表格,那些数据表格行。 169 | 170 | 表格行是否应当知道所在表格当前选中的行是谁?不应该,因为这跟你无关。你只要知道自己是不是被选中就行了,不要管闲事。所以,管理选中行这个事情应当给表格做,某行被点击了,他不该擅自作决定,比如先把自己颜色变掉之类的,而是应当先请示汇报:“老大,有人翻我牌子,你把我选起来吧。” 171 | 172 | 严格来说,小弟不该干涉老大的工作,比如老大这时候就应该扇他一巴掌:“扑街仔,翻你牌几啦?不把老大放眼里啦?机不机到德墨忒尔法则啦?”然后还是把他选中。问题出在哪里呢?你多嘴了。你告诉老大,有人点我就可以了,你管老大后面干什么?那是他的事,虽然你知道老大要这么干,但你这个属于知道得太多,该打。 173 | 174 | 好了,现在老大知道有人翻你牌子了,拿了个本子翻了翻,把今天的头牌改成了你,然后分别对新老两个头牌大喝一声:“浩南把你的表拿下来,山鸡戴上!”看到没有,小弟听到老大指示之后才能改变外观。 175 | 176 | 山鸡去台湾作出一番事业,从前人家叫他山鸡,回来之后,有人还想这么叫,浩南哥语重心长地纠正:“叫鸡哥!”从此大家都叫他鸡哥了。 177 | 178 | 综上所述,表格行有这几个职责: 179 | 180 | - 创建,做一些初始化的事情 181 | - 销毁,主要是行呗删除的时候把DOM和数据的引用去掉,这样浏览器可以做内存回收 182 | - 选中,改变样式为选中状态,比如山鸡戴上了三个表,从此成为了代表 183 | - 取消选中,改变样式为非选中状态,比如浩南把自己的表给了山鸡,自己就不是代表了 184 | - 设置属性,比如浩南把山鸡的称呼改成鸡哥 185 | - 获取属性,比如别人看到山鸡,打听一下就知道他是鸡哥 186 | 187 | ##3.3. 表格的职责 188 | 189 | 在上面所有功能里去掉表格行的职责,就得到了表格的职责 190 | 191 | - 加载列头数据 192 | - 根据数据加载列表 193 | - 添加行 194 | - 删除行 195 | - 选中行 196 | 197 | ##3.4. 如何实现自定义事件 198 | 199 | 什么是事件呢?本质上是一种异步的机制,打个比方说,你委托我做饭,说,做完饭给你打个电话,你先出去玩了。为什么是异步呢,因为你不在这里等我做完就走了,你也不关心我什么时候做得完,反正做好告诉你就是了。你在我这里监听了做饭完成事件,我做完之后,把这个事件派发一下,派发到你了,我的职责就完成了。 200 | 201 | 这么一看,我们的事情其实不复杂。我要对你提供什么呢: 202 | 203 | - 添加事件的监听,注意这里可能不止一个,有可能多个人同时来等着吃饭。 204 | - 移除事件的监听,这是为何?因为可能我没做完,你先给我打了电话,说别人约你吃饭,你不需要知道我是否做完了。 205 | - 当事情做完,通知所有监听方。 206 | 207 | 这些分析完,我们的代码就好写了: 208 | 209 | //事件派发机制的实现 210 | var EventDispatcher = { 211 | addEventListener: function(eventType, handler) { 212 | //事件的存储 213 | if (!this.eventMap) { 214 | this.eventMap = {}; 215 | } 216 | 217 | //对每个事件,允许添加多个监听 218 | if (!this.eventMap[eventType]) { 219 | this.eventMap[eventType] = []; 220 | } 221 | 222 | //把回调函数放入事件的执行数组 223 | this.eventMap[eventType].push(handler); 224 | }, 225 | 226 | removeEventListener: function(eventType, handler) { 227 | for (var i=0; i 0) { 276 | this.header.removeChild(this.header.rows[0]); 277 | } 278 | var tr = this.header.insertRow(0); 279 | 280 | for (var i = 0; i < columns.length; i++) { 281 | var th = tr.insertCell(i); 282 | th.innerHTML = columns[i].label; 283 | } 284 | this.columns = columns; 285 | }, 286 | 287 | loadData: function (data) { 288 | for (var i = 0; i < data.length; i++) { 289 | this.insertRow(data[i]); 290 | } 291 | 292 | //跟外面说一声,数据加载好了 293 | var event = { 294 | type: "loadCompleted", 295 | target: this 296 | }; 297 | this.dispatchEvent(event); 298 | }, 299 | 300 | insertRow: function (data) { 301 | var row = new DataRow(data, this); 302 | this.tbody.appendChild(row.dom); 303 | 304 | this.rows.push(row); 305 | 306 | var that = this; 307 | row.addEventListener("selected", function (event) { 308 | that.select(event.row); 309 | }); 310 | 311 | //已经成功添加了新行 312 | var event = { 313 | type: "rowInserted", 314 | newRow: row, 315 | target: this 316 | }; 317 | this.dispatchEvent(event); 318 | }, 319 | 320 | removeRow: function (row) { 321 | if (row === this.selectedRow) { 322 | this.selectedRow = null; 323 | } 324 | 325 | this.tbody.removeChild(row.dom); 326 | row.destroy(); 327 | 328 | for (var i = 0; i < this.rows.length; i++) { 329 | if (this.rows[i] == row) { 330 | this.rows.splice(i, 1); 331 | break; 332 | } 333 | } 334 | 335 | //已经移除 336 | var event = { 337 | type: "rowRemoved", 338 | target: this 339 | }; 340 | this.dispatchEvent(event); 341 | }, 342 | 343 | select: function (row) { 344 | var event = { 345 | type: "changed", 346 | target: this, 347 | oldRow: this.selectedRow, 348 | newRow: row 349 | }; 350 | 351 | if (this.selectedRow) { 352 | this.selectedRow.select(false); 353 | } 354 | 355 | if (row) { 356 | row.select(true); 357 | } 358 | 359 | this.selectedRow = row; 360 | 361 | this.dispatchEvent(event); 362 | } 363 | }.extend(EventDispatcher); 364 | 365 | 366 | function DataRow(data, grid) { 367 | this.data = data; 368 | this.grid = grid; 369 | 370 | this.create(); 371 | } 372 | 373 | DataRow.prototype = { 374 | create: function () { 375 | var row = document.createElement("tr"); 376 | for (var i = 0; i < this.grid.columns.length; i++) { 377 | var cell = document.createElement("td"); 378 | cell.innerHTML = this.data[this.grid.columns[i].field] || ""; 379 | row.appendChild(cell); 380 | } 381 | this.dom = row; 382 | 383 | var that = this; 384 | row.onclick = function (event) { 385 | //通知上级,我被点了 386 | var newEvent = { 387 | type: "selected", 388 | target: that, 389 | row: that 390 | }; 391 | that.dispatchEvent(newEvent); 392 | } 393 | }, 394 | 395 | destroy: function () { 396 | this.dom = null; 397 | this.data = null; 398 | this.grid = null; 399 | }, 400 | 401 | select: function (flag) { 402 | if (flag) { 403 | this.dom.className = "info"; 404 | } 405 | else { 406 | this.dom.className = ""; 407 | } 408 | }, 409 | 410 | set: function (key, value) { 411 | this.data[key] = value; 412 | 413 | for (var i = 0; i < this.grid.columns.length; i++) { 414 | if (this.grid.columns[i].field === key) { 415 | this.dom.childNodes[i].innerHTML = value; 416 | break; 417 | } 418 | } 419 | }, 420 | 421 | get: function (key) { 422 | return this.data[key]; 423 | }, 424 | 425 | refresh: function (data) { 426 | this.data = data; 427 | 428 | for (var i = 0; i < this.grid.columns.length; i++) { 429 | this.dom.childNodes[i].innerHTML = data[this.grid.columns[i].field] || ""; 430 | } 431 | } 432 | }.extend(EventDispatcher); 433 | 434 | 然后,我们为它创建一个测试页面,叫做datagrid.html,内容如下: 435 | 436 | 437 | 438 | 439 | 440 | DataGrid 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 |
451 |
452 |

Staff Management

453 |
454 |
455 |
456 |
457 |
458 |

Detail

459 |
460 |
461 |
462 |
463 | 464 |
465 | 466 |
467 |
468 |
469 | 470 |
471 | 472 |
473 |
474 |
475 |
476 |
477 | 478 |
479 | 480 |
481 |
482 |
483 | 484 |
485 | 486 |
487 |
488 |
489 |
490 |
491 | 502 |
503 | 504 | 659 | 660 | 661 | 662 | 我们可以看到,这已经可以跑一个简单的维护界面了,但我们的功能还是有限的,在后续篇幅中,我们会讲述如何实现表格的渲染器、改变列的宽度,固定列头,表格体滚动,排序等高级功能。我们的最终目标是:一个很正式的DataGrid控件。 -------------------------------------------------------------------------------- /docs/controls/datagrid02.md: -------------------------------------------------------------------------------- 1 | 数据表格控件的渲染器 2 | ==== 3 | 4 | 在第一部分中,我们讲述了如何实现一个最简单的数据表格控件,在后面的部分,我们会讨论得深入一些,探讨数据表格的渲染器、排序等功能。 5 | 6 | #5. 渲染器 7 | 8 | 什么叫做渲染器呢? 9 | 10 | 之前我们的DataGrid非常简单,每个单元格只是简单的把文本数据渲染出来,如果想要对这些文本作格式化处理,比如说,把性别的标记0和1转换成文字的“男”和“女”,要怎么去考虑? 11 | 12 | 有个笨办法,我们让用户传入数据之前,就把原始数据修改一下,比如说,原先数据格式是这样: 13 | 14 | { 15 | name: "Tom", 16 | age: 16, 17 | gender: 1 18 | } 19 | 20 | 我们给他先转换一遍,变成: 21 | 22 | { 23 | name: "Tom", 24 | age: 16, 25 | gender: 1, 26 | genderName: "Male" 27 | } 28 | 29 | 这样,不显示gender这个列,而是直接显示genderName这一列,也可以做到想要的效果。这么看上去简单方便,有没有什么弊端呢?有三种。 30 | 31 | - 破坏了原始数据 32 | - 需要对数据作预处理,这个处理过程比较集中,我们在前端常见的优化策略是把计算尽可能均摊,避免某个短时间的密集计算。 33 | - 把逻辑割裂了。为什么这么说呢,因为你不但要在批量加载数据之前做这个转换,新增、修改行的时候,是不是也同时要? 34 | 35 | 另外也有个办法,可以预定义一些规则,比如说定义了这个是性别的列,把格式化的过程内置在控件中,这种做法也不好,内置的东西总是有限的,面对不断变更的需求,需要无休止地修改。 36 | 37 | 现在讨论的只是单个列需要处理,假如有多个,那更麻烦了,有没有什么更好的办法呢? 38 | 39 | ##5.1 字段格式化 40 | 41 | 我们可以把这个格式化功能提取到外面,然后注入进来。格式化的功能,至少应当是针对列的,所以可以附加到列的初始化信息里传递过来。考虑一下格式化函数的参数,至少需要原始值,但有些情况更加复杂,所以我们多给它一些信息,比如说,本行的完整数据,还有当前列的key值。这么一来,一个典型的格式化函数就有了: 42 | 43 | function labelFunction(data, key) { 44 | var value = data[key]; 45 | if (value == 0) { 46 | return "Female"; 47 | } 48 | else if (value == 1) { 49 | return "Male"; 50 | } 51 | else { 52 | return "Unknown" 53 | } 54 | } 55 | 56 | 57 | 有了格式化,我们就可以很方便地进行一些显示的转换,比如对日需求,期、金额的实际值和显示值进行转换,或者,也可以显示一些图片和操作按钮之类。 58 | 59 | 比如说: 60 | 61 | function labelFunction(data, key) { 62 | var value = data[key]; 63 | if (value > 18) { 64 | return ""; 65 | } 66 | else { 67 | return "Hi, boy, you can do nothing."; 68 | } 69 | } 70 | 71 | 上面这段代码是一个示例,我们可以指定当年龄大于18岁的时候出来一个按钮可点,不足18的时候只出来一段文字。看上去,这段代码也满足我们需要了,但它将会遇到问题。 72 | 73 | 什么问题呢?我们来给这个按钮加个事件,点击它的时候,显示这个人的名字。考虑到我们输出的是HTML字符串,所以这个事件比较难加,除非也用字符串拼到里面,这么做是有很多弊端的,我们来考虑用一些优雅的方式解决。 74 | 75 | ##5.2 单元格渲染器 76 | 77 | 既然返回字符串不好,那我们直接一点,返回DOM结构如何? 78 | 79 | var itemRenderer = { 80 | render: function (row, key, columnIndex) { 81 | var data = row.data; 82 | 83 | if (data[key] >= 18) { 84 | var btn = document.createElement("button"); 85 | btn.innerHTML = data[key]; 86 | btn.onclick = function () { 87 | alert("I am " + data[key] + " years old, I want a bottle of wine!"); 88 | }; 89 | 90 | return btn; 91 | } 92 | else { 93 | var span = document.createElement("span"); 94 | span.innerHTML = data[key]; 95 | return span; 96 | } 97 | }, 98 | destroy: function () { 99 | 100 | } 101 | }; 102 | 103 | 这样就好多了。这个时候,我们需要考虑渲染器和格式化函数的优先级,有人会问,有了渲染器,还要格式化函数干什么?这问题其实就像有了拖拉机,为什么锄头还能卖得出去?我们把渲染器当作一个比较重量级的解决方案,格式化函数当作轻量级的,各有其使用场景。 104 | 105 | 我们来看看行的渲染方法应当怎么写: 106 | 107 | render: function (cell, data, field, index) { 108 | if (this.grid.columns[index].itemRenderer) { 109 | cell.innerHTML = ""; 110 | cell.appendChild(this.grid.columns[index].itemRenderer.render(this, field, index)); 111 | } 112 | else if (this.grid.columns[index].labelFunction) { 113 | cell.innerHTML = ""; 114 | cell.innerHTML = this.grid.columns[index].labelFunction(data, field); 115 | } 116 | else if (this.grid.itemRenderer) { 117 | cell.innerHTML = ""; 118 | cell.appendChild(this.grid.itemRenderer.render(this, field, index)); 119 | } 120 | else { 121 | cell.innerHTML = data[field]; 122 | } 123 | } 124 | 125 | 这里面有四种东西: 126 | - 针对某列的渲染器 127 | - 针对某列的格式化函数 128 | - 针对所有单元格的全局渲染器 129 | - 直接赋值 130 | 131 | 我们让它们的优先级递减。为什么会同时需要全局渲染器和列的渲染器呢?其实也可以在全局渲染器里面对行、列作判断,然后分别为每种情况渲染,但如果很多列都需要渲染,这么做不太好,需要分离成多个不同的列渲染器。 132 | 133 | 注意到我们使用的渲染器里面带有destroy方法,这个是为了减少内存泄露而设计的,使用者可以自行在这里卸载事件处理函数,隔断待回收的对象引用。 134 | 135 | 现在我们实现了单元格的渲染器机制,那么,标题的列头呢?这里可能也会需要有定制的内容,所以也需要为它设计类似的扩展机制,在此不再赘述。 136 | 137 | ##5.3 数据表格的复选功能 138 | 139 | 有了这些渲染器机制,我们可以来为数据表格添加更实用的功能。很多数据表格的使用场景需要复选,标题上有一个复选框,可以控制行的选中状态,行的选中状态也会反过来影响到标题复选框的选中状态。 140 | 141 | 所以,我们需要两个渲染器,一个是放在标题上的,一个是放在行上的。 142 | 143 | var CheckboxRenderer = { 144 | render: function(row, field, columnIndex) { 145 | var grid = row.grid; 146 | var data = row.data; 147 | 148 | var div = document.createElement("div"); 149 | var checkbox = document.createElement("input"); 150 | checkbox.type = "checkbox"; 151 | checkbox.checked = data["checked"]; 152 | 153 | checkbox.onclick = function () { 154 | data["checked"] = !data["checked"]; 155 | 156 | var checkedItems = 0; 157 | var rowLength = grid.rows.length; 158 | for (var i=0; i 28 |
  • Jiangsu 29 |
      30 |
    • Nanjing
    • 31 |
    • Suzhou 32 |
        33 |
      • Changshu
      • 34 |
      • Zhangjiagang
      • 35 |
      36 |
    • 37 |
    38 |
  • 39 |
  • Yunnan 40 |
      41 |
    • Kunming
    • 42 |
    • Lijiang
    • 43 |
    44 |
  • 45 | 46 | 47 | 效果还不错。 48 | 49 | #2.2 数据结构 50 | 51 | 在树控件中,我们应当可以接受两种类型的数据,一种是组装好的有层级关系的,一种是未组装的,包含层级信息,但是以单层存放的。对于后者,我们控件需要有这样的能力去把这样的数据转换成前面一种,然后再进行加载。 52 | 53 | 控件应当可以设置用数据的哪个字段来显示,也可以设置用那个字段做唯一键值,便于搜索。 -------------------------------------------------------------------------------- /docs/thin.md: -------------------------------------------------------------------------------- 1 | 从零开始编写自己的JavaScript框架 2 | ==== 3 | 4 | 有一定Web前端开发经验的人,很多都会有这么个想法:那些写框架的人好厉害,什么时候我才能写一个自己的框架呢?有时候看看别人的框架代码,又觉得很复杂,不知道从何看起,只有很少的人突破了这个界限,领悟到了更深层的东西。 5 | 6 | 对于这种情况,我觉得有必要改变一下。为此,打算自己写几个系列的文章来让很多人能从中领会一些前端框架的知识,带领他们走进框架开发的殿堂。 7 | 8 | 为了说明框架的一些基本原理,我写了一个简单的框架,取名为thin。thin框架的核心是模块定义和加载机制,整个框架唯一暴露的全局变量是thin,包含了模块定义,模块获取,日志等基本功能,其余一切功能都按照模块挂接在框架上。 9 | 10 | thin框架的最小发布单元是模块定义和加载机制,其他一切功能都作为可选组件。 11 | 12 | 可选组件包括: 13 | 14 | - 通用帮助类 15 | - DOM操作 16 | - 远程调用 17 | - 视图模型和数据绑定 18 | - 控件库 19 | -------------------------------------------------------------------------------- /docs/wizard.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xufei/thin/4f070d5b8e547b6e359dd8edd029d4e0c8710ddc/docs/wizard.md -------------------------------------------------------------------------------- /docs/workflow.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xufei/thin/4f070d5b8e547b6e359dd8edd029d4e0c8710ddc/docs/workflow.md -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DataGrid 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |

    Staff Management

    16 |
    17 |
    18 |
    19 |
    20 |
    21 |

    Detail

    22 |
    23 |
    24 |
    25 |
    26 | 27 |
    28 | 29 |
    30 |
    31 |
    32 | 33 |
    34 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 | 43 |
    44 |
    45 |
    46 | 47 |
    48 | 49 |
    50 |
    51 |
    52 |
    53 |
    54 | 65 |
    66 | 67 | 68 | 69 | 244 | 245 | -------------------------------------------------------------------------------- /js/modules/chess/chessboard.js: -------------------------------------------------------------------------------- 1 | thin.define("ChessBoard", ["_", "Events", "Config", "ChessText", "ChessColor"], function (_, Events, Config, ChessText, ChessColor) { 2 | var offsetX = Config.offsetX; 3 | var offsetY = Config.offsetY; 4 | var gridSize = Config.gridSize; 5 | 6 | var ChessBoard = function () { 7 | this.game = null; 8 | 9 | this.blankArr = []; 10 | this.attackArr = []; 11 | this.chesses = []; 12 | 13 | for (var i = 0; i < 9; i++) { 14 | this.chesses[i] = []; 15 | } 16 | }; 17 | 18 | ChessBoard.prototype = _.extend({ 19 | drawLine: function (x1, y1, x2, y2) { 20 | this.paper.path("M" + (offsetX + x1 * gridSize) + "," + (offsetY + y1 * gridSize) + " L" + (offsetX + x2 * gridSize) + "," + (offsetY + y2 * gridSize)); 21 | }, 22 | 23 | drawStar: function (x, y) { 24 | var distance = 1 / 10; 25 | var length = 1 / 4; 26 | 27 | var startX, startY, endX, endY; 28 | 29 | if (x != 0) { 30 | startX = x - distance; 31 | startY = y - distance - length; 32 | endX = x - distance - length; 33 | endY = y - distance; 34 | 35 | this.drawLine(startX, startY, startX, endY); 36 | this.drawLine(startX, endY, endX, endY); 37 | 38 | startY = y + distance + length; 39 | endY = y + distance; 40 | 41 | this.drawLine(startX, startY, startX, endY); 42 | this.drawLine(startX, endY, endX, endY); 43 | } 44 | 45 | if (x != 8) { 46 | startX = x + distance; 47 | startY = y - distance - length; 48 | endX = x + distance + length; 49 | endY = y - distance; 50 | 51 | this.drawLine(startX, startY, startX, endY); 52 | this.drawLine(startX, endY, endX, endY); 53 | 54 | startY = y + distance + length; 55 | endY = y + distance; 56 | 57 | this.drawLine(startX, startY, startX, endY); 58 | this.drawLine(startX, endY, endX, endY); 59 | } 60 | }, 61 | 62 | drawBoard: function (element) { 63 | var paper = Raphael(element, 2 * offsetX + 8 * gridSize, 2 * offsetY + 9 * gridSize); 64 | 65 | this.paper = paper; 66 | 67 | var bound = paper.rect(offsetX, offsetY, gridSize * 8, gridSize * 9); 68 | bound.attr({ 69 | 'stroke': 'black', 70 | 'stroke-width': 3 71 | }); 72 | 73 | for (var i = 1; i < 9; i++) { 74 | this.drawLine(0, i, 8, i); 75 | } 76 | 77 | for (var i = 1; i < 8; i++) { 78 | this.drawLine(i, 0, i, 4); 79 | this.drawLine(i, 5, i, 9); 80 | } 81 | 82 | for (var i = 0; i < 2; i++) { 83 | for (var j = 0; j < 2; j++) { 84 | this.drawLine(3 + 2 * i, 7 * j, 5 - 2 * i, 2 + 7 * j); 85 | } 86 | } 87 | 88 | for (var i = 0; i < 2; i++) { 89 | for (var j = 0; j < 2; j++) { 90 | this.drawStar(1 + i * 6, 2 + j * 5); 91 | } 92 | } 93 | 94 | for (var i = 0; i < 5; i++) { 95 | for (var j = 0; j < 2; j++) { 96 | this.drawStar(i * 2, 3 + j * 3); 97 | } 98 | } 99 | }, 100 | 101 | drawChess: function (chess) { 102 | if (chess) { 103 | var x = offsetX + gridSize * chess.x; 104 | var y = offsetY + gridSize * chess.y; 105 | 106 | var group = this.paper.set(); 107 | 108 | var bound = this.paper.circle(x, y, 0.4 * gridSize); 109 | bound.attr({ 110 | "stroke-width": 3, 111 | "fill": "#eeeeee" 112 | }); 113 | 114 | var label = this.paper.text(x, y, ChessText[chess.type + (chess.color + 1) * 7 / 2]); 115 | var color = chess.color == ChessColor.RED ? "red" : "black"; 116 | label.attr({ 117 | "font-size": 0.6 * gridSize, 118 | "font-family": "楷体, 宋体, 新宋体", 119 | "fill": color 120 | }); 121 | 122 | group.push(bound); 123 | group.push(label); 124 | group.attr({ 125 | "cursor": "pointer" 126 | }); 127 | 128 | var that = this; 129 | group.click((function (context) { 130 | return function () { 131 | var evt = { 132 | type: "chessClicked", 133 | chess: context 134 | }; 135 | that.fire(evt); 136 | }; 137 | })(chess)); 138 | 139 | this.chesses[chess.x][chess.y] = { 140 | group: group, 141 | bound: bound, 142 | label: label 143 | }; 144 | } 145 | 146 | }, 147 | 148 | drawAllChess: function () { 149 | for (var i = 0; i < 9; i++) { 150 | for (var j = 0; j < 10; j++) { 151 | this.drawChess(this.game.getChess(i, j)); 152 | } 153 | } 154 | }, 155 | 156 | moveChess: function (oldX, oldY, newX, newY) { 157 | var x = offsetX + gridSize * newX; 158 | var y = offsetY + gridSize * newY; 159 | 160 | var chess = this.chesses[oldX][oldY]; 161 | chess.group.attr({ 162 | cx: x, 163 | cy: y 164 | }); 165 | 166 | chess.label.attr({ 167 | x: x, 168 | y: y 169 | }); 170 | 171 | this.chesses[oldX][oldY] = null; 172 | this.chesses[newX][newY] = chess; 173 | }, 174 | 175 | removeChess: function (x, y) { 176 | this.chesses[x][y].label.remove(); 177 | this.chesses[x][y].group.remove(); 178 | 179 | this.chesses[x][y] = null; 180 | }, 181 | 182 | drawBlank: function (x, y) { 183 | var posX = offsetX + gridSize * x; 184 | var posY = offsetY + gridSize * y; 185 | 186 | var rect = this.paper.rect(posX - gridSize / 4, posY - gridSize / 4, gridSize / 2, gridSize / 2); 187 | rect.attr({ 188 | "stroke-width": 2, 189 | "stroke": "green", 190 | "fill": "#eeeeee" 191 | }); 192 | 193 | var that = this; 194 | rect.click(function () { 195 | var event = { 196 | type: "blankClicked", 197 | x: x, 198 | y: y 199 | }; 200 | that.fire(event); 201 | }); 202 | 203 | this.blankArr.push(rect); 204 | }, 205 | 206 | clearBlank: function () { 207 | for (var i = 0; i < this.blankArr.length; i++) { 208 | this.blankArr[i].remove(); 209 | } 210 | this.blankArr = []; 211 | }, 212 | 213 | drawAttack: function (x, y) { 214 | var posX = offsetX + gridSize * x; 215 | var posY = offsetY + gridSize * y; 216 | 217 | var rect = this.paper.rect(posX - gridSize * 0.45, posY - gridSize * 0.45, gridSize * 0.9, gridSize * 0.9); 218 | rect.attr({ 219 | "stroke-width": 2, 220 | "stroke": "green" 221 | }); 222 | 223 | this.attackArr.push(rect); 224 | }, 225 | 226 | clearAttack: function () { 227 | for (var i = 0; i < this.attackArr.length; i++) { 228 | this.attackArr[i].remove(); 229 | } 230 | this.attackArr = []; 231 | }, 232 | 233 | clearAll: function () { 234 | for (var i = 0; i < this.chesses.length; i++) { 235 | for (var j = 0; j < this.chesses[i].length; j++) { 236 | this.chesses[i][j].label.remove(); 237 | this.chesses[i][j].group.remove(); 238 | } 239 | } 240 | } 241 | }, Events); 242 | 243 | return ChessBoard; 244 | }); -------------------------------------------------------------------------------- /js/modules/chess/chessman.js: -------------------------------------------------------------------------------- 1 | thin.define("ChessMan", [], function () { 2 | function ChessMan(color, type) { 3 | this.game = null; 4 | 5 | this.color = color; 6 | this.type = type; 7 | this.x = -1; 8 | this.y = -1; 9 | 10 | this.beAttack = false; 11 | } 12 | 13 | return ChessMan; 14 | }); 15 | 16 | thin.define("General", ["ChessMan", "ChessType", "ChessColor"], function (ChessMan, ChessType, ChessColor) { 17 | function General(color) { 18 | ChessMan.call(this, color, ChessType.GENERAL); 19 | } 20 | 21 | General.prototype = { 22 | valid: function (x, y) { 23 | var result = true; 24 | switch (this.color) { 25 | case ChessColor.BLACK: 26 | if ((x < 3) || (x > 5) || ((y > 2) && (y < 7))) { 27 | result = false; 28 | } 29 | break; 30 | case ChessColor.RED: 31 | if ((x < 3) || (x > 5) || ((y > 2) && (y < 7))) { 32 | result = false; 33 | } 34 | break; 35 | default: 36 | result = true; 37 | } 38 | return result; 39 | }, 40 | 41 | canGo: function (x, y) { 42 | if (this.valid(x, y) && !this.game.isFriendly(this.color, x, y)) { 43 | if (Math.abs(this.y - y) + Math.abs(this.x - x) == 1) { 44 | return true; 45 | } 46 | else if (this.x == x) { 47 | if (this.game.getChess(x, y) && (this.game.getChess(x, y).type == ChessType.GENERAL)) { 48 | var num = 0; 49 | var minY = y > this.y ? this.y : y; 50 | var maxY = y + this.y - minY; 51 | 52 | for (var i=minY+1; i 1) 105 | || (Math.abs(this.y - y) > 1) 106 | || ((Math.abs(this.x - x) == 0) 107 | && (Math.abs(this.y - y) == 0))) { 108 | return false; 109 | } 110 | else { 111 | return true; 112 | } 113 | } 114 | return false; 115 | } 116 | }; 117 | 118 | return Guard; 119 | }); 120 | 121 | thin.define("Staff", ["ChessMan", "ChessType", "ChessColor"], function (ChessMan, ChessType, ChessColor) { 122 | function Staff(color) { 123 | ChessMan.call(this, color, ChessType.STAFF); 124 | } 125 | 126 | Staff.prototype = { 127 | valid: function (x, y) { 128 | switch (this.color) { 129 | case ChessColor.BLACK: 130 | if (((x == 0) && (y == 2)) 131 | || ((x == 2) && (y == 0)) 132 | || ((x == 2) && (y == 4)) 133 | || ((x == 4) && (y == 2)) 134 | || ((x == 6) && (y == 0)) 135 | || ((x == 6) && (y == 4)) 136 | || ((x == 8) && (y == 2))) { 137 | return true; 138 | } 139 | break; 140 | case ChessColor.RED: 141 | if (((x == 0) && (y == 7)) 142 | || ((x == 2) && (y == 5)) 143 | || ((x == 2) && (y == 9)) 144 | || ((x == 4) && (y == 7)) 145 | || ((x == 6) && (y == 5)) 146 | || ((x == 6) && (y == 9)) 147 | || ((x == 8) && (y == 7))) { 148 | return true; 149 | } 150 | break; 151 | default: 152 | return false; 153 | } 154 | }, 155 | 156 | canGo: function (x, y) { 157 | if (this.valid(x, y) && !this.game.isFriendly(this.color, x, y)) { 158 | if ((Math.abs(this.x - x) != 2) 159 | || (Math.abs(this.y - y) != 2)) { 160 | return false; 161 | } 162 | else { 163 | var i = (this.x + x) / 2; 164 | var j = (this.y + y) / 2; 165 | if (this.game.isEmpty(i, j)) { 166 | return true; 167 | } 168 | else { 169 | return false; 170 | } 171 | } 172 | } 173 | return false; 174 | } 175 | }; 176 | 177 | return Staff; 178 | }); 179 | 180 | thin.define("Horse", ["ChessMan", "ChessType"], function (ChessMan, ChessType) { 181 | function Horse(color) { 182 | ChessMan.call(this, color, ChessType.HORSE); 183 | } 184 | 185 | Horse.prototype = { 186 | valid: function (x, y) { 187 | return true; 188 | }, 189 | 190 | canGo: function (x, y) { 191 | if (this.valid(x, y) && !this.game.isFriendly(this.color, x, y)) { 192 | if (((Math.abs(this.x - x) == 1) 193 | && (Math.abs(this.y - y) == 2)) 194 | || ((Math.abs(this.x - x) == 2) 195 | && (Math.abs(this.y - y) == 1))) { 196 | var i = -1; 197 | var j = -1; 198 | if (x - this.x == 2) { 199 | i = this.x + 1; 200 | j = this.y; 201 | } 202 | else if (this.x - x == 2) { 203 | i = this.x - 1; 204 | j = this.y; 205 | } 206 | else if (y - this.y == 2) { 207 | i = this.x; 208 | j = this.y + 1; 209 | } 210 | else if (this.y - y == 2) { 211 | i = this.x; 212 | j = this.y - 1; 213 | } 214 | 215 | if (this.game.isEmpty(i, j)) { 216 | return true; 217 | } 218 | } 219 | } 220 | return false; 221 | } 222 | }; 223 | 224 | return Horse; 225 | }); 226 | 227 | thin.define("Chariot", ["ChessMan", "ChessType"], function (ChessMan, ChessType) { 228 | function Chariot(color) { 229 | ChessMan.call(this, color, ChessType.CHARIOT); 230 | } 231 | 232 | Chariot.prototype = { 233 | valid: function (x, y) { 234 | return true; 235 | }, 236 | 237 | canGo: function (x, y) { 238 | if (this.valid(x, y) && !this.game.isFriendly(this.color, x, y)) { 239 | if ((this.x != x) && (this.y != y)) { 240 | return false; 241 | } 242 | else { 243 | if (this.y == y) { 244 | if (this.x < x) { 245 | for (var i = this.i + 1; i < x; i++) { 246 | if (!this.game.isEmpty(i, this.y)) { 247 | return false; 248 | } 249 | } 250 | return true; 251 | } 252 | 253 | if (this.x > x) { 254 | for (var i = this.x - 1; i > x; i--) { 255 | if (!this.game.isEmpty(i, this.y)) { 256 | return false; 257 | } 258 | } 259 | return true; 260 | } 261 | } 262 | else { 263 | if (this.y < y) { 264 | for (var i = this.y + 1; i < y; i++) { 265 | if (!this.game.isEmpty(this.x, i)) { 266 | return false; 267 | } 268 | } 269 | return true; 270 | } 271 | 272 | if (this.y > y) { 273 | for (var i = this.y - 1; i > y; i--) { 274 | if (!this.game.isEmpty(this.x, i)) { 275 | return false; 276 | } 277 | } 278 | return true; 279 | } 280 | } 281 | } 282 | } 283 | return false; 284 | } 285 | }; 286 | 287 | return Chariot; 288 | }); 289 | 290 | thin.define("Cannon", ["ChessMan", "ChessType"], function (ChessMan, ChessType) { 291 | function Cannon(color) { 292 | ChessMan.call(this, color, ChessType.CANNON); 293 | } 294 | 295 | Cannon.prototype = { 296 | valid: function (x, y) { 297 | return true; 298 | }, 299 | 300 | canGo: function (x, y) { 301 | if (this.valid(x, y) && !this.game.isFriendly(this.color, x, y)) { 302 | if ((this.x != x) && (this.y != y)) { 303 | return false; 304 | } 305 | else { 306 | if (this.game.isEmpty(x, y)) { 307 | if (this.y == y) { 308 | if (this.x < x) { 309 | for (var i = this.x + 1; i < x; i++) { 310 | if (!this.game.isEmpty(i, this.y)) { 311 | return false; 312 | } 313 | } 314 | return true; 315 | } 316 | 317 | if (this.x > x) { 318 | for (var i = this.x - 1; i > x; i--) { 319 | if (!this.game.isEmpty(i, this.y)) { 320 | return false; 321 | } 322 | } 323 | return true; 324 | } 325 | } 326 | else { 327 | if (this.y < y) { 328 | for (var i = this.y + 1; i < y; i++) { 329 | if (!this.game.isEmpty(this.x, i)) { 330 | return false; 331 | } 332 | } 333 | return true; 334 | } 335 | 336 | if (this.y > y) { 337 | for (var i = this.y - 1; i > y; i--) { 338 | if (!this.game.isEmpty(this.x, i)) { 339 | return false; 340 | } 341 | } 342 | return true; 343 | } 344 | } 345 | } 346 | else { 347 | var count = 0; 348 | 349 | if (this.y == y) { 350 | if (this.x < x) { 351 | for (var i = this.x + 1; i < x; i++) { 352 | if (!this.game.isEmpty(i, this.y)) { 353 | count++; 354 | } 355 | } 356 | if (count == 1) { 357 | return true; 358 | } 359 | } 360 | 361 | if (this.x > x) { 362 | for (var i = this.x - 1; i > x; i--) { 363 | if (!this.game.isEmpty(i, this.y)) { 364 | count++; 365 | } 366 | } 367 | if (count == 1) { 368 | return true; 369 | } 370 | } 371 | } 372 | else { 373 | if (this.y < y) { 374 | for (var i = this.y + 1; i < y; i++) { 375 | if (!this.game.isEmpty(this.x, i)) { 376 | count++; 377 | } 378 | } 379 | if (count == 1) { 380 | return true; 381 | } 382 | } 383 | 384 | if (this.y > y) { 385 | for (var i = this.y - 1; i > y; i--) { 386 | if (!this.game.isEmpty(this.x, i)) { 387 | count++; 388 | } 389 | } 390 | if (count == 1) { 391 | return true; 392 | } 393 | } 394 | } 395 | } 396 | } 397 | } 398 | return false; 399 | } 400 | }; 401 | 402 | return Cannon; 403 | }); 404 | 405 | thin.define("Soldier", ["ChessMan", "ChessType", "ChessColor"], function (ChessMan, ChessType, ChessColor) { 406 | function Soldier(color) { 407 | ChessMan.call(this, color, ChessType.SOLDIER); 408 | } 409 | 410 | Soldier.prototype = { 411 | valid: function (x, y) { 412 | var result = true; 413 | switch (this.color) { 414 | case ChessColor.BLACK: 415 | if ((y < 3) 416 | || ((y < 5) && (x % 2 == 1))) { 417 | result = false; 418 | } 419 | break; 420 | case ChessColor.RED: 421 | if ((y > 6) 422 | || ((y > 4) && (x % 2 == 1))) { 423 | result = false; 424 | } 425 | break; 426 | default: 427 | result = true; 428 | } 429 | return result; 430 | }, 431 | 432 | canGo: function (x, y) { 433 | var result = false; 434 | if (this.valid(x, y) && !this.game.isFriendly(this.color, x, y)) { 435 | result = true; 436 | switch (this.color) { 437 | case ChessColor.BLACK: 438 | if (y < this.y) { 439 | result = false; 440 | } 441 | else { 442 | if (Math.abs(x - this.x) + y - this.y != 1) { 443 | result = false; 444 | } 445 | } 446 | break; 447 | case ChessColor.RED: 448 | if (y > this.y) { 449 | result = false; 450 | } 451 | else { 452 | if (Math.abs(x - this.x) + this.y - y != 1) { 453 | result = false; 454 | } 455 | } 456 | break; 457 | default: 458 | result = true; 459 | } 460 | } 461 | return result; 462 | } 463 | }; 464 | 465 | return Soldier; 466 | }); 467 | 468 | thin.define("ChessFactory", ["ChessType", "ChessColor", "General", "Guard", "Staff", "Horse", "Chariot", "Cannon", "Soldier"], function (ChessType, ChessColor, General) { 469 | var ChessConstructors = [].slice.call(arguments).slice(2); 470 | 471 | var ChessFactory = { 472 | createChess: function (data) { 473 | var chess; 474 | 475 | var type = data[1]; 476 | 477 | chess = new (ChessConstructors[7 - type])(data[0], data[1]); 478 | chess.x = data[2]; 479 | chess.y = data[3]; 480 | return chess; 481 | } 482 | }; 483 | 484 | return ChessFactory; 485 | }); -------------------------------------------------------------------------------- /js/modules/chess/config.js: -------------------------------------------------------------------------------- 1 | thin.define("ChessType", [], function () { 2 | return { 3 | GENERAL: 7, 4 | GUARD: 6, 5 | STAFF: 5, 6 | HORSE: 4, 7 | CHARIOT: 3, 8 | CANNON: 2, 9 | SOLDIER: 1, 10 | BLANK: 0 11 | }; 12 | }); 13 | 14 | thin.define("ChessText", [], function () { 15 | return { 16 | 14: "帅", 17 | 13: "仕", 18 | 12: "相", 19 | 11: "馬", 20 | 10: "車", 21 | 9: "砲", 22 | 8: "兵", 23 | 7: "将", 24 | 6: "士", 25 | 5: "象", 26 | 4: "马", 27 | 3: "车", 28 | 2: "炮", 29 | 1: "卒" 30 | }; 31 | }); 32 | 33 | thin.define("ChessColor", [], function () { 34 | return { 35 | RED: 1, 36 | BLACK: -1, 37 | GREY: 0 38 | } 39 | }); 40 | 41 | thin.define("Config", [], function () { 42 | return { 43 | offsetX: 40, 44 | offsetY: 40, 45 | gridSize: 60 46 | }; 47 | }); -------------------------------------------------------------------------------- /js/modules/chess/game.js: -------------------------------------------------------------------------------- 1 | thin.define("Chess.Game", ["_", "Events", "ChessColor", "ChessType", "ChessText", "ChessFactory", "ChessBoard"], function (_, Events, Color, Type, Text, Factory, ChessBoard) { 2 | //color, type, x, y 3 | var chesses = [ 4 | [1, 7, 4, 9], 5 | [1, 6, 3, 9], 6 | [1, 6, 5, 9], 7 | [1, 5, 2, 9], 8 | [1, 5, 6, 9], 9 | [1, 4, 1, 9], 10 | [1, 4, 7, 9], 11 | [1, 3, 0, 9], 12 | [1, 3, 8, 9], 13 | [1, 2, 1, 7], 14 | [1, 2, 7, 7], 15 | [1, 1, 0, 6], 16 | [1, 1, 2, 6], 17 | [1, 1, 4, 6], 18 | [1, 1, 6, 6], 19 | [1, 1, 8, 6], 20 | 21 | [-1, 7, 4, 0], 22 | [-1, 6, 3, 0], 23 | [-1, 6, 5, 0], 24 | [-1, 5, 2, 0], 25 | [-1, 5, 6, 0], 26 | [-1, 4, 1, 0], 27 | [-1, 4, 7, 0], 28 | [-1, 3, 0, 0], 29 | [-1, 3, 8, 0], 30 | [-1, 2, 1, 2], 31 | [-1, 2, 7, 2], 32 | [-1, 1, 0, 3], 33 | [-1, 1, 2, 3], 34 | [-1, 1, 4, 3], 35 | [-1, 1, 6, 3], 36 | [-1, 1, 8, 3] 37 | ]; 38 | 39 | function Game() { 40 | this.situation = []; 41 | this.currentColor = Color.RED; 42 | this.currentChess = null; 43 | this.undoList = []; 44 | this.redoList = []; 45 | this.chessUnderAttack = []; 46 | 47 | this.generals = []; 48 | } 49 | 50 | Game.prototype = _.extend({ 51 | init: function () { 52 | var element = this.chessBoardDiv; 53 | 54 | for (var i = 0; i < 9; i++) { 55 | this.situation[i] = []; 56 | } 57 | 58 | for (var i = 0; i < chesses.length; i++) { 59 | this.createChess(chesses[i]); 60 | } 61 | 62 | var chessBoard = new ChessBoard(); 63 | chessBoard.game = this; 64 | this.chessBoard = chessBoard; 65 | chessBoard.drawBoard(element); 66 | chessBoard.drawAllChess(); 67 | 68 | var game = this; 69 | chessBoard.on("chessClicked", function (event) { 70 | game.select(event.chess); 71 | }); 72 | 73 | chessBoard.on("blankClicked", function (event) { 74 | game.chessBoard.clearBlank(); 75 | game.chessBoard.clearAttack(); 76 | game.chessBoard.moveChess(game.currentChess.x, game.currentChess.y, event.x, event.y); 77 | game.moveChess(game.currentChess, event.x, event.y, false); 78 | }); 79 | }, 80 | 81 | destroy: function () { 82 | this.situation = null; 83 | this.currentChess = null; 84 | this.undoList = null; 85 | this.redoList = null; 86 | this.chessUnderAttack = null; 87 | this.chessBoard = null; 88 | this.chessContainer.parentElement.removeChild(this.chessContainer); 89 | }, 90 | 91 | createChess: function (data) { 92 | var chess = Factory.createChess(data); 93 | chess.game = this; 94 | this.situation[chess.x][chess.y] = chess; 95 | 96 | if (chess.type == Type.GENERAL) { 97 | this.generals.push(chess); 98 | } 99 | 100 | return chess; 101 | }, 102 | 103 | isFriendly: function (color, x, y) { 104 | if (this.isEmpty(x, y)) 105 | return false; 106 | 107 | return color + this.getChess(x, y).color != 0; 108 | }, 109 | 110 | isEmpty: function (x, y) { 111 | return !this.situation[x][y]; 112 | }, 113 | 114 | getChess: function (x, y) { 115 | return this.situation[x][y]; 116 | }, 117 | 118 | select: function (chess) { 119 | if (this.currentColor == Color.GREY) { 120 | this.prompt("棋局已终止!"); 121 | return; 122 | } 123 | 124 | if (this.currentChess) { 125 | if (this.currentChess.color + chess.color == 0) { 126 | var canKill = false; 127 | for (var i = 0; i < this.chessUnderAttack.length; i++) { 128 | if ((this.chessUnderAttack[i].x == chess.x) && (this.chessUnderAttack[i].y == chess.y)) { 129 | canKill = true; 130 | break; 131 | } 132 | } 133 | 134 | if (canKill) { 135 | this.chessBoard.clearBlank(); 136 | this.chessBoard.clearAttack(); 137 | this.chessBoard.removeChess(chess.x, chess.y); 138 | this.chessBoard.moveChess(this.currentChess.x, this.currentChess.y, chess.x, chess.y); 139 | this.moveChess(this.currentChess, chess.x, chess.y, false); 140 | 141 | if (chess.type == Type.GENERAL) { 142 | var winner = (chess.color == Color.RED) ? "黑" : "红"; 143 | this.prompt("结束啦," + winner + "方胜利!"); 144 | this.currentColor = Color.GREY; 145 | } 146 | return; 147 | } 148 | else { 149 | this.prompt("吃不到这个棋子!"); 150 | return; 151 | } 152 | } 153 | } 154 | else { 155 | if (chess.color != this.currentColor) { 156 | this.prompt("不该你走!"); 157 | return; 158 | } 159 | } 160 | 161 | this.currentChess = chess; 162 | var whereCanIGo = []; 163 | this.chessUnderAttack = []; 164 | for (var i = 0; i < 9; i++) { 165 | for (var j = 0; j < 10; j++) { 166 | if (chess.canGo(i, j)) { 167 | if (this.isEmpty(i, j)) { 168 | whereCanIGo.push({ 169 | x: i, 170 | y: j 171 | }); 172 | } 173 | else { 174 | this.chessUnderAttack.push({ 175 | x: i, 176 | y: j 177 | }); 178 | } 179 | } 180 | } 181 | } 182 | 183 | this.chessBoard.clearBlank(); 184 | this.chessBoard.clearAttack(); 185 | 186 | for (var i = 0; i < whereCanIGo.length; i++) { 187 | this.chessBoard.drawBlank(whereCanIGo[i].x, whereCanIGo[i].y); 188 | } 189 | 190 | for (var i = 0; i < this.chessUnderAttack.length; i++) { 191 | this.chessBoard.drawAttack(this.chessUnderAttack[i].x, this.chessUnderAttack[i].y); 192 | } 193 | }, 194 | 195 | moveChess: function (chess, newX, newY, isUndo) { 196 | var step = { 197 | chess: chess, 198 | from: { 199 | x: chess.x, 200 | y: chess.y 201 | }, 202 | dead: this.situation[newX][newY] 203 | }; 204 | 205 | if (isUndo) { 206 | this.redoList.push(step); 207 | } 208 | else { 209 | this.undoList.push(step); 210 | } 211 | 212 | this.situation[chess.x][chess.y] = null; 213 | chess.x = newX; 214 | chess.y = newY; 215 | this.situation[newX][newY] = chess; 216 | 217 | this.currentColor = Color.RED + Color.BLACK - this.currentColor; 218 | this.currentChess = null; 219 | 220 | this.log(step); 221 | this.check(); 222 | }, 223 | 224 | check: function () { 225 | for (var i = 0; i < this.generals.length; i++) { 226 | for (var j = 0; j < this.situation.length; j++) { 227 | for (var k = 0; k < this.situation[j].length; k++) { 228 | if (this.situation[j][k] && this.situation[j][k].canGo(this.generals[i].x, this.generals[i].y)) { 229 | this.prompt("将军!"); 230 | return; 231 | } 232 | } 233 | } 234 | } 235 | }, 236 | 237 | prompt: function (text) { 238 | alert(text); 239 | }, 240 | 241 | log: function (step) { 242 | var numbers = ["一", "二", "三", "四", "五", "六", "七", "八", "九"]; 243 | var directions = ["进", "平", "退"]; 244 | 245 | var chessText = Text[step.chess.type + (step.chess.color + 1) * 7 / 2]; 246 | 247 | var direction; 248 | if (step.from.y > step.chess.y) { 249 | direction = -1; 250 | } 251 | else if (step.from.y == step.chess.y) { 252 | direction = 0; 253 | } 254 | else if (step.from.y < step.chess.y) { 255 | direction = 1; 256 | } 257 | 258 | var stepLength; 259 | if (step.from.x == step.chess.x) { 260 | stepLength = Math.abs(step.from.y - step.chess.y) - 1; 261 | } 262 | else { 263 | stepLength = step.chess.x; 264 | } 265 | 266 | var text = chessText + numbers[step.from.x] + directions[direction * step.chess.color + 1] + numbers[stepLength]; 267 | thin.log(text); 268 | }, 269 | 270 | undo: function () { 271 | if (this.undoList.length > 0) { 272 | var step = this.undoList.pop(); 273 | this.chessBoard.moveChess(step.chess.x, step.chess.y, step.from.x, step.from.y); 274 | this.moveChess(step.chess, step.from.x, step.from.y, true); 275 | 276 | if (step.dead) { 277 | this.situation[step.chess.x][step.chess.y] = step.dead; 278 | this.chessBoard.drawChess(step.dead); 279 | } 280 | } 281 | }, 282 | 283 | redo: function () { 284 | if (this.redoList.length > 0) { 285 | var step = this.redoList.pop(); 286 | this.chessBoard.moveChess(step.chess.x, step.chess.y, step.from.x, step.from.y); 287 | this.moveChess(step.chess, step.from.x, step.from.y); 288 | } 289 | } 290 | }, Events); 291 | 292 | return Game; 293 | }); -------------------------------------------------------------------------------- /js/modules/controls/datagrid.js: -------------------------------------------------------------------------------- 1 | thin.define("DataGrid", ["_", "Events"], function (_, Events) { 2 | var DataGrid = function (element, config) { 3 | this.columns = []; 4 | this.rows = []; 5 | 6 | element.innerHTML = '
    '; 7 | 8 | this.header = element.firstChild.tHead; 9 | this.tbody = element.firstChild.tBodies[0]; 10 | 11 | this.selectedRow = null; 12 | 13 | this.config = config; 14 | this.variables = {}; 15 | }; 16 | 17 | DataGrid.prototype = _.extend({ 18 | loadColumns: function (columns) { 19 | if (this.header.rows.length > 0) { 20 | this.header.removeChild(this.header.rows[0]); 21 | } 22 | var tr = this.header.insertRow(0); 23 | 24 | var width = 100 / columns.length; 25 | for (var i = 0; i < columns.length; i++) { 26 | var th = tr.insertCell(i); 27 | th.width = width + "%"; 28 | } 29 | this.columns = columns; 30 | 31 | if (this.config && this.config.showCheckbox) { 32 | this.columns[0].headerRenderer = HeaderRenderer; 33 | this.columns[0].itemRenderer = CheckboxRenderer; 34 | } 35 | 36 | this.renderHeader(); 37 | }, 38 | 39 | loadData: function (data) { 40 | for (var i = 0; i < data.length; i++) { 41 | this.insertRow(data[i]); 42 | } 43 | 44 | this.renderHeader(); 45 | 46 | var event = { 47 | type: "loadCompleted", 48 | target: this 49 | }; 50 | this.fire(event); 51 | }, 52 | 53 | insertRow: function (data) { 54 | var row = new DataRow(data, this); 55 | this.tbody.appendChild(row.dom); 56 | 57 | this.rows.push(row); 58 | 59 | var that = this; 60 | row.on("selected", function (event) { 61 | that.select(event.row); 62 | }); 63 | 64 | var event = { 65 | type: "rowInserted", 66 | newRow: row, 67 | target: this 68 | }; 69 | this.fire(event); 70 | }, 71 | 72 | removeRow: function (row) { 73 | if (row === this.selectedRow) { 74 | this.selectedRow = null; 75 | } 76 | 77 | this.tbody.removeChild(row.dom); 78 | row.destroy(); 79 | 80 | for (var i = 0; i < this.rows.length; i++) { 81 | if (this.rows[i] == row) { 82 | this.rows.splice(i, 1); 83 | break; 84 | } 85 | } 86 | 87 | var event = { 88 | type: "rowRemoved", 89 | target: this 90 | }; 91 | this.fire(event); 92 | }, 93 | 94 | select: function (row) { 95 | var event = { 96 | type: "changed", 97 | target: this, 98 | oldRow: this.selectedRow, 99 | newRow: row 100 | }; 101 | 102 | if (this.selectedRow) { 103 | this.selectedRow.select(false); 104 | } 105 | 106 | if (row) { 107 | row.select(true); 108 | } 109 | 110 | this.selectedRow = row; 111 | 112 | this.fire(event); 113 | }, 114 | 115 | set: function (key, value) { 116 | this.variables[key] = value; 117 | this.renderHeader(); 118 | }, 119 | 120 | get: function (key) { 121 | return this.variables[key]; 122 | }, 123 | 124 | refresh: function () { 125 | this.renderHeader(); 126 | }, 127 | 128 | renderHeader: function () { 129 | var columns = this.columns; 130 | for (var i = 0; i < columns.length; i++) { 131 | var th = this.header.firstChild.childNodes[i]; 132 | 133 | if (columns[i].headerRenderer) { 134 | th.innerHTML = ""; 135 | th.appendChild(columns[i].headerRenderer.render(this, columns[i].field, i)); 136 | } 137 | else if (this.headerRenderer) { 138 | th.innerHTML = ""; 139 | th.appendChild(this.headerRenderer.render(this, columns[i].field, i)); 140 | } 141 | else { 142 | th.innerHTML = columns[i].label; 143 | } 144 | } 145 | } 146 | }, Events); 147 | 148 | var DataRow = function (data, grid) { 149 | this.data = data; 150 | this.grid = grid; 151 | 152 | this.create(); 153 | }; 154 | 155 | DataRow.prototype = _.extend({ 156 | create: function () { 157 | var row = document.createElement("tr"); 158 | for (var i = 0; i < this.grid.columns.length; i++) { 159 | var cell = document.createElement("td"); 160 | this.render(cell, this.data, this.grid.columns[i].field, i); 161 | row.appendChild(cell); 162 | } 163 | this.dom = row; 164 | 165 | var that = this; 166 | row.onclick = function (event) { 167 | var newEvent = { 168 | type: "selected", 169 | target: that, 170 | row: that 171 | }; 172 | that.fire(newEvent); 173 | } 174 | }, 175 | 176 | destroy: function () { 177 | //todo: 要把渲染器也destroy 178 | 179 | 180 | this.dom = null; 181 | this.data = null; 182 | this.grid = null; 183 | }, 184 | 185 | select: function (flag) { 186 | if (flag) { 187 | this.dom.className = "info"; 188 | } 189 | else { 190 | this.dom.className = ""; 191 | } 192 | }, 193 | 194 | set: function (key, value) { 195 | this.data[key] = value; 196 | 197 | this.refresh(); 198 | }, 199 | 200 | get: function (key) { 201 | return this.data[key]; 202 | }, 203 | 204 | render: function (cell, data, field, index) { 205 | if (this.grid.columns[index].itemRenderer) { 206 | cell.innerHTML = ""; 207 | cell.appendChild(this.grid.columns[index].itemRenderer.render(this, field, index)); 208 | } 209 | else if (this.grid.columns[index].labelFunction) { 210 | cell.innerHTML = ""; 211 | cell.innerHTML = this.grid.columns[index].labelFunction(data, field); 212 | } 213 | else if (this.grid.itemRenderer) { 214 | cell.innerHTML = ""; 215 | cell.appendChild(this.grid.itemRenderer.render(this, field, index)); 216 | } 217 | else { 218 | cell.innerHTML = data[field]; 219 | } 220 | }, 221 | 222 | refresh: function (data) { 223 | if (data) { 224 | this.data = data; 225 | } 226 | 227 | for (var i = 0; i < this.grid.columns.length; i++) { 228 | this.render(this.dom.childNodes[i], this.data, this.grid.columns[i].field, i); 229 | } 230 | } 231 | }, Events); 232 | 233 | var CheckboxRenderer = { 234 | render: function (row, field, columnIndex) { 235 | var grid = row.grid; 236 | var data = row.data; 237 | 238 | var div = document.createElement("div"); 239 | var checkbox = document.createElement("input"); 240 | checkbox.type = "checkbox"; 241 | checkbox.checked = data["checked"]; 242 | 243 | checkbox.onclick = function () { 244 | data["checked"] = !data["checked"]; 245 | 246 | var checkedItems = 0; 247 | var rowLength = grid.rows.length; 248 | for (var i = 0; i < rowLength; i++) { 249 | if (grid.rows[i].get("checked")) { 250 | checkedItems++; 251 | } 252 | } 253 | 254 | if (checkedItems === 0) { 255 | grid.set("checkState", "unchecked"); 256 | } 257 | else if (checkedItems === rowLength) { 258 | grid.set("checkState", "checked"); 259 | } 260 | else { 261 | grid.set("checkState", "indeterminate"); 262 | } 263 | }; 264 | div.appendChild(checkbox); 265 | 266 | var span = document.createElement("span"); 267 | span.innerHTML = data[field]; 268 | div.appendChild(span); 269 | 270 | return div; 271 | } 272 | }; 273 | 274 | var HeaderRenderer = { 275 | render: function (grid, field, columnIndex) { 276 | var rows = grid.rows; 277 | var div = document.createElement("div"); 278 | var checkbox = document.createElement("input"); 279 | checkbox.type = "checkbox"; 280 | 281 | switch (grid.get("checkState")) { 282 | case "checked": 283 | { 284 | checkbox.checked = true; 285 | break; 286 | } 287 | case "unchecked": 288 | { 289 | checkbox.checked = false; 290 | break; 291 | } 292 | case "indeterminate": 293 | { 294 | checkbox.indeterminate = true; 295 | break; 296 | } 297 | } 298 | div.appendChild(checkbox); 299 | 300 | checkbox.onclick = function () { 301 | var checked = this.checked; 302 | for (var i = 0; i < rows.length; i++) { 303 | rows[i].set("checked", checked); 304 | } 305 | }; 306 | 307 | var span = document.createElement("span"); 308 | span.innerHTML = field; 309 | div.appendChild(span); 310 | 311 | return div; 312 | }, 313 | 314 | destroy: function () { 315 | 316 | } 317 | }; 318 | 319 | return DataGrid; 320 | }); -------------------------------------------------------------------------------- /js/modules/controls/dialog.js: -------------------------------------------------------------------------------- 1 | thin.define("Dialog", [], function () { 2 | 3 | var Dialog = function () { 4 | 5 | }; 6 | 7 | Dialog.prototype = { 8 | show: function () { 9 | 10 | }, 11 | 12 | hide: function () { 13 | 14 | } 15 | }; 16 | 17 | return Dialog; 18 | }); -------------------------------------------------------------------------------- /js/modules/controls/scrollbar.js: -------------------------------------------------------------------------------- 1 | thin.define("ScrollBar", ["_", "Events"], function (_, Events) { 2 | var ScrollBar = function(element, config) { 3 | element.innerHTML = ""; 4 | }; 5 | 6 | ScrollBar.prototype = { 7 | 8 | }; 9 | 10 | return ScrollBar; 11 | }; -------------------------------------------------------------------------------- /js/modules/controls/tree.js: -------------------------------------------------------------------------------- 1 | thin.define("Tree", ["_", "Events"], function (_, Events) { 2 | var Tree = function (element) { 3 | this.allNodes = []; 4 | this.childNodes = []; 5 | 6 | this.keyField = null; 7 | this.nodeDict = {}; 8 | this.data = null; 9 | 10 | this.selectedNode = null; 11 | 12 | this.tree = this; 13 | this.labelField = null; 14 | 15 | this.childrenContainer = document.createElement("ul"); 16 | this.childrenContainer.className = "tree"; 17 | element.appendChild(this.childrenContainer); 18 | }; 19 | 20 | Tree.prototype = _.extend({ 21 | loadTreeData: function (data, keyField) { 22 | this.clear(); 23 | 24 | this.keyField = keyField; 25 | 26 | for (var i = 0; i < data.length; i++) { 27 | this.addNode(data[i]); 28 | } 29 | this.data = data; 30 | }, 31 | 32 | loadListData: function (data, selfField, parentField, topFlag) { 33 | var tree = []; 34 | var dict = {}; 35 | 36 | var length = data.length; 37 | for (var i = 0; i < length; i++) { 38 | var item = data[i]; 39 | dict[item[selfField]] = item; 40 | if (item[parentField] === topFlag) { 41 | //add root nodes 42 | tree.push(item); 43 | } 44 | } 45 | 46 | //contribute the tree data 47 | for (i = 0; i < length; i++) { 48 | var child = data[i]; 49 | if (child[parentField] === topFlag) { 50 | continue; 51 | } 52 | var parent = dict[child[parentField]]; 53 | if (parent) { 54 | child.parent = parent; 55 | if (!parent.children) { 56 | parent.children = []; 57 | } 58 | parent.children.push(child); 59 | 60 | } 61 | } 62 | 63 | this.loadTreeData(tree, selfField); 64 | }, 65 | 66 | expandAll: function () { 67 | for (var i = 0; i < this.allNodes.length; i++) { 68 | this.allNodes[i].expand(); 69 | } 70 | }, 71 | 72 | collapseAll: function () { 73 | for (var i = 0; i < this.allNodes.length; i++) { 74 | this.allNodes[i].collapse(); 75 | } 76 | }, 77 | 78 | findNode: function (key, value) { 79 | var result; 80 | for (var i = 0; i < this.allNodes.length; i++) { 81 | var node = this.allNodes[i]; 82 | if (node[key] === value) { 83 | result = node; 84 | break; 85 | } 86 | } 87 | 88 | return result; 89 | }, 90 | 91 | addNode: function (data, parent) { 92 | parent = parent || this; 93 | 94 | var node = new TreeNode(data, parent); 95 | parent.childNodes.push(node); 96 | parent.childrenContainer.appendChild(node.dom); 97 | 98 | this.allNodes.push(node); 99 | 100 | var that = this; 101 | node.on("selected", function (event) { 102 | that.selectNode(event.node); 103 | }); 104 | 105 | node.on("expanded", function (event) { 106 | if (event.node.expanded) { 107 | event.node.collapse(); 108 | } 109 | else { 110 | event.node.expand(); 111 | } 112 | }); 113 | 114 | node.on("rightClicked", function (event) { 115 | //只做转发,把主体改变一下 116 | event.target = that; 117 | that.fire(event); 118 | }); 119 | 120 | node.refreshIcon(); 121 | 122 | //已经成功添加了新节点 123 | var event = { 124 | type: "nodeAdded", 125 | newNode: node, 126 | target: this 127 | }; 128 | this.fire(event); 129 | }, 130 | 131 | removeNode: function (node) { 132 | node.clear(); 133 | 134 | if (node == this.selectedNode) { 135 | this.selectNode(null); 136 | } 137 | 138 | if (node.parent == this) { 139 | this.childrenContainer.removeChild(node.dom); 140 | 141 | for (var i = 0; i < this.nodes.length; i++) { 142 | if (this.nodes[i] == node) { 143 | this.nodes.splice(i, 1); 144 | break; 145 | } 146 | } 147 | } 148 | else { 149 | node.parent.childrenContainer.removeChild(node.dom); 150 | for (var i = 0; i < node.parent.childNodes.length; i++) { 151 | if (node.parent.childNodes[i] == node) { 152 | node.parent.childNodes.splice(i, 1); 153 | break; 154 | } 155 | } 156 | } 157 | 158 | node.destroy(); 159 | 160 | for (var i = 0; i < this.allNodes.length; i++) { 161 | if (this.allNodes[i] == node) { 162 | this.allNodes.splice(i, 1); 163 | break; 164 | } 165 | } 166 | 167 | //已经移除 168 | var event = { 169 | type: "nodeRemoved", 170 | target: this 171 | }; 172 | this.fire(event); 173 | }, 174 | 175 | swapNodes: function (node1, node2) { 176 | 177 | }, 178 | 179 | selectNode: function (node) { 180 | var event = { 181 | type: "changed", 182 | oldNode: this.selectedNode, 183 | newNode: node 184 | }; 185 | 186 | if (this.selectedNode) { 187 | this.selectedNode.select(false); 188 | } 189 | 190 | if (node) { 191 | node.select(true); 192 | } 193 | 194 | this.selectedNode = node; 195 | 196 | this.fire(event); 197 | }, 198 | 199 | clear: function () { 200 | 201 | }, 202 | 203 | destroy: function () { 204 | 205 | } 206 | }, Events); 207 | 208 | var TreeNode = function (data, parent) { 209 | this.data = data; 210 | this.parent = parent; 211 | this.tree = parent.tree; 212 | this.childNodes = []; 213 | 214 | this.create(); 215 | }; 216 | 217 | TreeNode.prototype = _.extend({ 218 | create: function () { 219 | this.dom = document.createElement("li"); 220 | this.iconContainer = document.createElement("i"); 221 | 222 | this.labelContainer = document.createElement("span"); 223 | this.labelContainer.innerHTML = this.data[this.tree.labelField || "label"]; 224 | 225 | this.dom.appendChild(this.iconContainer); 226 | this.dom.appendChild(this.labelContainer); 227 | 228 | this.childrenContainer = document.createElement("ul"); 229 | this.dom.appendChild(this.childrenContainer); 230 | 231 | if (this.data.children) { 232 | for (var i = 0; i < this.data.children.length; i++) { 233 | this.addNode(this.data.children[i]); 234 | } 235 | } 236 | 237 | this.expanded = true; 238 | 239 | bindEvent(this); 240 | 241 | function bindEvent(node) { 242 | //expand 243 | node.iconContainer.onclick = function () { 244 | var event = { 245 | type: "expanded", 246 | expanded: node.expanded, 247 | node: node, 248 | target: node 249 | }; 250 | 251 | node.fire(event); 252 | }; 253 | 254 | //select 255 | node.labelContainer.onclick = function () { 256 | var event = { 257 | type: "selected", 258 | node: node, 259 | target: node 260 | }; 261 | 262 | node.fire(event); 263 | }; 264 | 265 | //contextmenu 266 | node.dom.oncontextmenu = function (e) { 267 | var event = { 268 | type: "rightClicked", 269 | node: node, 270 | target: node 271 | }; 272 | 273 | node.fire(event); 274 | 275 | if (e && e.stopPropagation) 276 | //因此它支持W3C的stopPropagation()方法 277 | e.stopPropagation(); 278 | else 279 | //否则,我们需要使用IE的方式来取消事件冒泡 280 | window.event.cancelBubble = true; 281 | 282 | //阻止默认浏览器动作(W3C) 283 | if (e && e.preventDefault) 284 | e.preventDefault(); 285 | //IE中阻止函数器默认动作的方式 286 | else 287 | window.event.returnValue = false; 288 | return false; 289 | } 290 | } 291 | }, 292 | 293 | clear: function () { 294 | while (this.childNodes.length > 0) { 295 | this.removeNode(this.childNodes[0]); 296 | } 297 | }, 298 | 299 | destroy: function () { 300 | this.data = null; 301 | this.parent = null; 302 | this.tree = null; 303 | this.childNodes = null; 304 | 305 | this.iconContainer = null; 306 | this.labelContainer = null; 307 | this.childrenContainer = null; 308 | this.dom = null; 309 | }, 310 | 311 | addNode: function (data) { 312 | this.tree.addNode(data, this); 313 | }, 314 | 315 | removeNode: function (node) { 316 | this.childrenContainer.removeChild(node.dom); 317 | 318 | for (var i = 0; i < this.childNodes.length; i++) { 319 | if (this.childNodes[i] == node) { 320 | this.childNodes.splice(i, 1); 321 | } 322 | } 323 | 324 | for (var i = 0; i < this.tree.allNodes.length; i++) { 325 | if (this.tree.allNodes[i] == node) { 326 | this.tree.allNodes.splice(i, 1); 327 | } 328 | } 329 | }, 330 | 331 | expand: function () { 332 | this.childrenContainer.hidden = false; 333 | this.expanded = true; 334 | this.refreshIcon(); 335 | }, 336 | 337 | collapse: function () { 338 | this.childrenContainer.hidden = true; 339 | this.expanded = false; 340 | this.refreshIcon(); 341 | }, 342 | 343 | select: function (flag) { 344 | if (flag) { 345 | this.dom.className = "info"; 346 | } 347 | else { 348 | this.dom.className = ""; 349 | } 350 | }, 351 | 352 | refreshData: function (data) { 353 | this.labelContainer.innerHTML = data[this.tree.labelField || "label"]; 354 | }, 355 | 356 | refreshIcon: function () { 357 | if (this.expanded) { 358 | this.iconContainer.className = "icon-minus"; 359 | } 360 | else { 361 | this.iconContainer.className = "icon-plus"; 362 | } 363 | } 364 | }, Events); 365 | 366 | return Tree; 367 | }); -------------------------------------------------------------------------------- /js/modules/controls/treegrid.js: -------------------------------------------------------------------------------- 1 | thin.define("TreeGrid", ["_", "Events"], function (_, Events) { 2 | //作为一个控件,它的容器必须传入 3 | var TreeGrid = function (element) { 4 | this.columns = []; 5 | this.childNodes = []; 6 | this.allNodes = []; 7 | 8 | this.data = null; 9 | this.depth = -1; 10 | this.grid = this; 11 | 12 | element.innerHTML = '
    '; 13 | 14 | this.header = element.firstChild.tHead; 15 | this.tbody = element.firstChild.tBodies[0]; 16 | 17 | this.selectedNode = null; 18 | this.itemRenderer = new TreeGridItemRenderer(this); 19 | }; 20 | 21 | TreeGrid.prototype = _.extend({ 22 | loadColumns: function (columns) { 23 | if (this.header.rows.length > 0) { 24 | this.header.removeChild(this.header.rows[0]); 25 | } 26 | var tr = this.header.insertRow(0); 27 | 28 | var th = tr.insertCell(0); 29 | var checkbox = document.createElement("input"); 30 | checkbox.type = "checkbox"; 31 | th.appendChild(checkbox); 32 | this.checkbox = checkbox; 33 | 34 | var that = this; 35 | checkbox.onclick = function () { 36 | for (var i = 0; i < that.allNodes.length; i++) { 37 | that.allNodes[i].checkbox.checked = checkbox.checked; 38 | } 39 | }; 40 | 41 | for (var i = 0; i < columns.length; i++) { 42 | th = tr.insertCell(i + 1); 43 | th.innerHTML = columns[i].label; 44 | } 45 | this.columns = columns; 46 | 47 | this.initTemplate(); 48 | }, 49 | 50 | loadData: function (data) { 51 | for (var i = 0; i < data.length; i++) { 52 | this.addNode(data[i]); 53 | } 54 | 55 | //跟外面说一声,数据加载好了 56 | var event = { 57 | type: "loadCompleted", 58 | target: this 59 | }; 60 | this.fire(event); 61 | }, 62 | 63 | initTemplate: function () { 64 | var tr = document.createElement("tr"); 65 | for (var i = 0; i <= this.columns.length; i++) { 66 | var td = document.createElement("td"); 67 | tr.appendChild(td); 68 | } 69 | 70 | var checkbox = document.createElement("input"); 71 | checkbox.type = "checkbox"; 72 | tr.firstChild.appendChild(checkbox); 73 | 74 | tr.childNodes[1].appendChild(document.createElement("span")); 75 | 76 | var icon = document.createElement("i"); 77 | icon.className = "icon-folder-open"; 78 | tr.childNodes[1].appendChild(icon); 79 | tr.childNodes[1].appendChild(document.createElement("span")); 80 | 81 | this.template = tr; 82 | }, 83 | 84 | addNode: function (data, parent) { 85 | parent = parent || this; 86 | var node = new TreeNode(data, parent); 87 | parent.childNodes.push(node); 88 | this.allNodes.push(node); 89 | 90 | if (parent != this) { 91 | var loadedNodes = 0; 92 | for (var i = 0; i < parent.childNodes.length; i++) { 93 | if (parent.childNodes[i].domLoaded) { 94 | loadedNodes++; 95 | } 96 | } 97 | 98 | if (loadedNodes == 0) { 99 | parent.dom.insertAdjacentElement("afterEnd", node.dom); 100 | } 101 | else { 102 | parent.childNodes[loadedNodes - 1].dom.insertAdjacentElement("afterEnd", node.dom); 103 | } 104 | } 105 | else { 106 | this.tbody.appendChild(node.dom); 107 | } 108 | node.domLoaded = true; 109 | 110 | if (data.children) { 111 | for (var i = 0; i < data.children.length; i++) { 112 | this.addNode(data.children[i], node); 113 | } 114 | node.expanded = true; 115 | } 116 | 117 | var that = this; 118 | node.on("selected", function (event) { 119 | that.select(event.node); 120 | }); 121 | 122 | //已经成功添加了新行 123 | var event = { 124 | type: "rowInserted", 125 | newNode: node, 126 | target: this 127 | }; 128 | this.fire(event); 129 | }, 130 | 131 | removeNode: function (node) { 132 | for (var i = node.childNodes.length - 1; i >= 0; i--) { 133 | this.removeNode(node.childNodes[i]); 134 | } 135 | 136 | this.tbody.removeChild(node.dom); 137 | 138 | for (var i = 0; i < node.parent.childNodes.length; i++) { 139 | if (node.parent.childNodes[i] == node) { 140 | node.parent.childNodes.splice(i, 1); 141 | break; 142 | } 143 | } 144 | 145 | for (var i = 0; i < this.allNodes.length; i++) { 146 | if (this.allNodes[i] == node) { 147 | this.allNodes.splice(i, 1); 148 | break; 149 | } 150 | } 151 | 152 | if (this.selectedNode == node) { 153 | this.selectedNode = null; 154 | } 155 | node.destroy(); 156 | }, 157 | 158 | select: function (node) { 159 | var event = { 160 | type: "changed", 161 | target: this, 162 | oldNode: this.selectedNode, 163 | newNode: node 164 | }; 165 | 166 | if (this.selectedNode) { 167 | this.selectedNode.select(false); 168 | } 169 | 170 | if (node) { 171 | node.select(true); 172 | } 173 | 174 | this.selectedNode = node; 175 | 176 | this.fire(event); 177 | } 178 | }, Events); 179 | 180 | var TreeNode = function (data, parent) { 181 | this.data = data; 182 | this.depth = parent.depth + 1; 183 | this.parent = parent; 184 | this.grid = parent.grid; 185 | this.childNodes = []; 186 | this.domLoaded = false; 187 | 188 | this.create(); 189 | }; 190 | 191 | TreeNode.prototype = _.extend({ 192 | create: function () { 193 | var dom = this.grid.template.cloneNode(true); 194 | this.dom = dom; 195 | this.checkbox = this.dom.querySelector("tr>td>input"); 196 | 197 | var that = this; 198 | this.checkbox.onclick = function () { 199 | that.check(that.checkbox.checked); 200 | }; 201 | 202 | this.dom.querySelector("tr>td>i").onclick = function () { 203 | if (that.expanded) { 204 | that.collapse(); 205 | } 206 | else { 207 | that.expand(); 208 | } 209 | }; 210 | 211 | var that = this; 212 | this.dom.onclick = function (event) { 213 | //通知上级,我被点了 214 | var newEvent = { 215 | type: "selected", 216 | target: that, 217 | node: that 218 | }; 219 | that.fire(newEvent); 220 | }; 221 | 222 | this.blankSpan = this.dom.childNodes[1].firstChild; 223 | 224 | this.blankSpan.style.marginLeft = this.depth * 20 + "px"; 225 | this.dom.childNodes[1].lastChild.innerHTML = this.data[this.grid.columns[0].field]; 226 | 227 | for (var i = 1; i < this.grid.columns.length; i++) { 228 | this.dom.childNodes[i + 1].innerHTML = this.grid.itemRenderer.render(this, this.data, i, this.grid.columns[i].field); 229 | } 230 | }, 231 | 232 | destroy: function () { 233 | this.dom = null; 234 | this.data = null; 235 | this.grid = null; 236 | }, 237 | 238 | addNode: function (data) { 239 | this.grid.addNode(data, this); 240 | }, 241 | 242 | removeNode: function (node) { 243 | this.grid.removeNode(node); 244 | }, 245 | 246 | select: function (flag) { 247 | if (flag) { 248 | this.dom.className = "info"; 249 | } 250 | else { 251 | this.dom.className = ""; 252 | } 253 | }, 254 | 255 | set: function (key, value) { 256 | this.data[key] = value; 257 | 258 | for (var i = 0; i < this.grid.columns.length; i++) { 259 | if (this.grid.columns[i].field === key) { 260 | this.dom.childNodes[i].innerHTML = value; 261 | break; 262 | } 263 | } 264 | }, 265 | 266 | show: function () { 267 | this.dom.style.display = ""; 268 | this.expand(); 269 | }, 270 | 271 | hide: function () { 272 | this.dom.style.display = "none"; 273 | this.collapse(); 274 | }, 275 | 276 | expand: function () { 277 | for (var i = 0; i < this.childNodes.length; i++) { 278 | this.childNodes[i].show(); 279 | } 280 | this.expanded = true; 281 | }, 282 | 283 | collapse: function () { 284 | for (var i = 0; i < this.childNodes.length; i++) { 285 | this.childNodes[i].hide(); 286 | } 287 | this.expanded = false; 288 | }, 289 | 290 | check: function (flag) { 291 | this.checkbox.checked = flag; 292 | for (var i = 0; i < this.childNodes.length; i++) { 293 | this.childNodes[i].check(flag); 294 | } 295 | }, 296 | 297 | get: function (key) { 298 | return this.data[key]; 299 | }, 300 | 301 | refresh: function (data) { 302 | this.data = data; 303 | 304 | this.dom.childNodes[1].lastChild.innerHTML = this.data[this.grid.columns[0].field]; 305 | for (var i = 1; i < this.grid.columns.length; i++) { 306 | this.dom.childNodes[i + 1].innerHTML = this.grid.itemRenderer.render(this, data, i, this.grid.columns[i].field); 307 | } 308 | } 309 | }, Events); 310 | 311 | function TreeGridItemRenderer(grid) { 312 | this.grid = grid; 313 | } 314 | 315 | TreeGridItemRenderer.prototype = { 316 | render: function (node, rowData, columnIndex, key) { 317 | if (columnIndex == 0) { 318 | 319 | } 320 | else { 321 | return rowData[key] || ""; 322 | } 323 | } 324 | }; 325 | 326 | return TreeGrid; 327 | }); -------------------------------------------------------------------------------- /js/modules/core/binding.js: -------------------------------------------------------------------------------- 1 | thin.define("DOMBinding", ["_"], function (_) { 2 | var Binder = { 3 | $watch: function (key, watcher) { 4 | if (!this.$watchers[key]) { 5 | this.$watchers[key] = { 6 | value: this[key], 7 | list: [] 8 | }; 9 | 10 | Object.defineProperty(this, key, { 11 | set: function (val) { 12 | var oldValue = this.$watchers[key].value; 13 | this.$watchers[key].value = val; 14 | 15 | for (var i = 0; i < this.$watchers[key].list.length; i++) { 16 | this.$watchers[key].list[i](val, oldValue); 17 | } 18 | }, 19 | 20 | get: function () { 21 | return this.$watchers[key].value; 22 | } 23 | }); 24 | } 25 | 26 | this.$watchers[key].list.push(watcher); 27 | } 28 | }; 29 | 30 | var vmMap = {}; 31 | 32 | var changeHandlers = []; 33 | 34 | function parseElement(element, vm) { 35 | var model = vm; 36 | 37 | if (element.getAttribute("vm-model")) { 38 | model = bindModel(element.getAttribute("vm-model")); 39 | } 40 | 41 | var i; 42 | for (i = 0; i < element.attributes.length; i++) { 43 | parseAttribute(element, element.attributes[i], model); 44 | } 45 | 46 | for (i = 0; i < element.children.length; i++) { 47 | parseElement(element.children[i], model); 48 | } 49 | 50 | if (model != vm) { 51 | for (var key in model.$watchers) { 52 | model[key] = model.$watchers[key].value; 53 | } 54 | 55 | if (model.$initializer) { 56 | model.$initializer(); 57 | } 58 | } 59 | } 60 | 61 | function parseAttribute(element, attr, model) { 62 | if (attr.name.indexOf("vm-") === 0) { 63 | var type = attr.name.slice(3); 64 | 65 | switch (type) { 66 | case "init": 67 | bindInit(element, attr.value, model); 68 | break; 69 | case "value": 70 | bindValue(element, attr.value, model); 71 | break; 72 | case "list": 73 | bindList(element, attr.value, model); 74 | break; 75 | case "click": 76 | bindClick(element, attr.value, model); 77 | break; 78 | case "enable": 79 | bindEnable(element, attr.value, model, true); 80 | break; 81 | case "disable": 82 | bindEnable(element, attr.value, model, false); 83 | break; 84 | case "visible": 85 | bindVisible(element, attr.value, model, true); 86 | break; 87 | case "invisible": 88 | bindVisible(element, attr.value, model, false); 89 | break; 90 | case "element": 91 | model[attr.value] = element; 92 | break; 93 | } 94 | } 95 | } 96 | 97 | function bindModel(name) { 98 | thin.log("binding model: " + name); 99 | 100 | var model = thin.use(name); 101 | var instance = _.extend(new model(), Binder); 102 | instance.$watchers = {}; 103 | 104 | return instance; 105 | } 106 | 107 | function bindValue(element, key, vm) { 108 | thin.log("binding value: " + key); 109 | 110 | vm.$watch(key, function (value, oldValue) { 111 | element.value = value || ""; 112 | }); 113 | 114 | switch (element.tagName) { 115 | case "SELECT": { 116 | bindSelectValue(element, key, vm); 117 | break; 118 | } 119 | default: { 120 | bindTextValue(element, key, vm); 121 | break; 122 | } 123 | } 124 | 125 | function bindTextValue(el, key, model) { 126 | el.onkeyup = function () { 127 | model[key] = el.value; 128 | thin.fire({type: "vmchange"}); 129 | }; 130 | 131 | el.onpaste = function () { 132 | model[key] = el.value; 133 | thin.fire({type: "vmchange"}); 134 | }; 135 | } 136 | 137 | function bindSelectValue(el, key, model) { 138 | el.onchange = function () { 139 | vm[key] = el.value; 140 | thin.fire({type: "vmchange"}); 141 | }; 142 | } 143 | } 144 | 145 | function bindList(element, key, vm) { 146 | thin.log("binding list: " + key); 147 | 148 | vm.$watch(key, function (value, oldValue) { 149 | var selectedValue = element.value; 150 | element.innerHTML = null; 151 | 152 | for (var i = 0; i < value.length; i++) { 153 | var item = document.createElement("option"); 154 | item.innerHTML = value[i].label; 155 | item.value = value[i].key; 156 | 157 | element.appendChild(item); 158 | } 159 | element.value = selectedValue; 160 | }); 161 | } 162 | 163 | function bindInit(element, key, vm) { 164 | thin.log("binding init: " + key); 165 | 166 | vm.$initializer = (function (model) { 167 | return function () { 168 | model[key](); 169 | thin.fire({type: "vmchange"}); 170 | }; 171 | })(vm); 172 | } 173 | 174 | function bindClick(element, key, vm) { 175 | thin.log("binding click: " + key); 176 | 177 | element.onclick = function () { 178 | vm[key](); 179 | thin.fire({type: "vmchange"}); 180 | }; 181 | } 182 | 183 | function bindEnable(element, key, vm, direction) { 184 | thin.log("binding enable: " + key); 185 | 186 | if (typeof vm[key] == "function") { 187 | changeHandlers.push(function() { 188 | element.disabled = vm[key]() ^ direction ? true : false; 189 | }); 190 | } 191 | else { 192 | vm.$watch(key, function (value, oldValue) { 193 | element.disabled = value ^ direction ? true : false; 194 | }); 195 | } 196 | } 197 | 198 | function bindVisible(element, key, vm, direction) { 199 | thin.log("binding visible: " + key); 200 | 201 | if (typeof vm[key] == "function") { 202 | changeHandlers.push(function() { 203 | element.style.display = vm[key]() ^ direction ? "none" : ""; 204 | }); 205 | } 206 | else { 207 | vm.$watch(key, function (value, oldValue) { 208 | element.style.display = value ^ direction ? "none" : ""; 209 | }); 210 | } 211 | } 212 | 213 | function apply() { 214 | for (var i=0; i 0) { 17 | return this.elements[0].getAttribute(key); 18 | } 19 | } 20 | }, 21 | 22 | addClass: function (className) { 23 | this.elements.forEach(function (element) { 24 | element.classList.add(className); 25 | }); 26 | return this; 27 | }, 28 | 29 | removeClass: function (className) { 30 | this.elements.forEach(function (element) { 31 | element.classList.remove(className); 32 | }); 33 | return this; 34 | }, 35 | 36 | show: function () { 37 | this.elements.forEach(function (element) { 38 | element.style.display = ""; 39 | }); 40 | return this; 41 | }, 42 | 43 | hide: function () { 44 | this.elements.forEach(function (element) { 45 | element.style.display = "none"; 46 | }); 47 | return this; 48 | }, 49 | 50 | on: function (eventType, handler) { 51 | switch (eventType) { 52 | case "click": 53 | this.click(handler); 54 | break; 55 | } 56 | return this; 57 | }, 58 | 59 | off: function (eventType, handler) { 60 | 61 | }, 62 | 63 | click: function (handler) { 64 | this.elements.forEach(function (element) { 65 | thin.on.call(element, "click", handler); 66 | }); 67 | return this; 68 | }, 69 | 70 | querySelector: function (selector) { 71 | 72 | } 73 | }; 74 | 75 | var DOMSelector = { 76 | byId: function (id) { 77 | var dom = new DOM(); 78 | dom.elements.push(document.getElementById(id)); 79 | 80 | return dom; 81 | }, 82 | 83 | byName: function (name) { 84 | var dom = new DOMWrapper(); 85 | dom.elements = [].slice.call(document.getElementsByName(name)); 86 | return dom; 87 | }, 88 | 89 | byTagName: function (tagName) { 90 | var dom = new DOMWrapper(); 91 | dom.elements = [].slice.call(document.getElementsByTagName(tagName)); 92 | return dom; 93 | }, 94 | 95 | bySelector: function (selector) { 96 | var dom = new DOMWrapper(); 97 | dom.elements = [].slice.call(document.querySelector(selector)); 98 | return dom; 99 | }, 100 | 101 | create: function(emmet) { 102 | var tree = new ExpressionTree(emmet); 103 | } 104 | }; 105 | 106 | // todo, implement an emmet creator 107 | 108 | var EmmetOperators = { 109 | Child: ">", 110 | Sibling: "+", 111 | Climb: "^", 112 | Multiplication: "*", 113 | GroupOpen: "(", 114 | GroupClose: ")", 115 | ID: "#", 116 | Class: ".", 117 | AttributeOpen: "[", 118 | AttributeClose: "]", 119 | TextOpen: "{", 120 | TextClose: "}" 121 | }; 122 | 123 | var EmmetFunctions = { 124 | ">": function(){}, 125 | "+": function(){}, 126 | "^": function(){}, 127 | "*": function(){}, 128 | "(": function(){}, 129 | ")": function(){}, 130 | "#": function(){}, 131 | ".": function(){}, 132 | "[": function(){}, 133 | "]": function(){}, 134 | "{": function(){}, 135 | "}": function(){} 136 | }; 137 | 138 | var EmmetParser = { 139 | currentIndex: 0, 140 | groupStack: [], 141 | attrStack: [], 142 | textStack: [], 143 | 144 | isOperator: function(char) { 145 | 146 | }, 147 | 148 | readOperator: function() { 149 | 150 | }, 151 | 152 | readString: function() { 153 | 154 | }, 155 | 156 | readNumber: function() { 157 | var number = ""; 158 | var start = this.currentIndex; 159 | while (this.currentIndex < this.text.length) { 160 | var diff = this.text.charCodeAt(this.currentIndex) - 49; 161 | if (diff >= 0 && diff <= 9) { 162 | number += this.text.charAt(this.currentIndex); 163 | } 164 | else { 165 | break; 166 | } 167 | this.currentIndex++; 168 | } 169 | number = 1 * number; 170 | return number; 171 | }, 172 | 173 | parseString: function(text) { 174 | this.str = text; 175 | var tokens = []; 176 | for (var i=0; i': function (self, locals, a, b) { 64 | return a(self, locals) > b(self, locals); 65 | }, 66 | '<=': function (self, locals, a, b) { 67 | return a(self, locals) <= b(self, locals); 68 | }, 69 | '>=': function (self, locals, a, b) { 70 | return a(self, locals) >= b(self, locals); 71 | }, 72 | '&&': function (self, locals, a, b) { 73 | return a(self, locals) && b(self, locals); 74 | }, 75 | '||': function (self, locals, a, b) { 76 | return a(self, locals) || b(self, locals); 77 | }, 78 | '&': function (self, locals, a, b) { 79 | return a(self, locals) & b(self, locals); 80 | }, 81 | '|': function (self, locals, a, b) { 82 | return b(self, locals)(self, locals, a(self, locals)); 83 | }, 84 | '!': function (self, locals, a) { 85 | return !a(self, locals); 86 | } 87 | }; 88 | 89 | function Expression(text) { 90 | this.text = text; 91 | 92 | this.index = 0; 93 | } 94 | 95 | Expression.prototype = { 96 | build: function () { 97 | 98 | }, 99 | 100 | readNumber: function (index) { 101 | var number = ""; 102 | var start = index; 103 | while (index < this.text.length) { 104 | var ch = lowercase(this.text.charAt(index)); 105 | if (ch == '.' || isNumber(ch)) { 106 | number += ch; 107 | } else { 108 | var peekCh = this.peek(); 109 | if (ch == 'e' && isExpOperator(peekCh)) { 110 | number += ch; 111 | } else if (isExpOperator(ch) && 112 | peekCh && isNumber(peekCh) && 113 | number.charAt(number.length - 1) == 'e') { 114 | number += ch; 115 | } else if (isExpOperator(ch) && 116 | (!peekCh || !isNumber(peekCh)) && 117 | number.charAt(number.length - 1) == 'e') { 118 | //throwError('Invalid exponent'); 119 | } else { 120 | break; 121 | } 122 | } 123 | index++; 124 | } 125 | number = 1 * number; 126 | tokens.push({index: start, text: number, json: true, 127 | fn: function () { 128 | return number; 129 | }}); 130 | }, 131 | 132 | readString: function (quote) { 133 | var start = this.index; 134 | this.index++; 135 | var string = ""; 136 | var rawString = quote; 137 | var escape = false; 138 | while (this.index < this.text.length) { 139 | var ch = this.text.charAt(this.index); 140 | rawString += ch; 141 | if (escape) { 142 | if (ch == 'u') { 143 | var hex = this.text.substring(this.index + 1, this.index + 5); 144 | if (!hex.match(/[\da-f]{4}/i)) 145 | throwError("Invalid unicode escape [\\u" + hex + "]"); 146 | this.index += 4; 147 | string += String.fromCharCode(parseInt(hex, 16)); 148 | } else { 149 | var rep = ESCAPE[ch]; 150 | if (rep) { 151 | string += rep; 152 | } else { 153 | string += ch; 154 | } 155 | } 156 | escape = false; 157 | } else if (ch == '\\') { 158 | escape = true; 159 | } else if (ch == quote) { 160 | this.index++; 161 | tokens.push({ 162 | index: start, 163 | text: rawString, 164 | string: string, 165 | json: true, 166 | fn: function () { 167 | return string; 168 | } 169 | }); 170 | return; 171 | } else { 172 | string += ch; 173 | } 174 | this.index++; 175 | } 176 | throwError("Unterminated quote", start); 177 | }, 178 | 179 | readIdent: function () { 180 | var ident = "", 181 | start = index, 182 | lastDot, peekIndex, methodName, ch; 183 | 184 | while (index < text.length) { 185 | ch = text.charAt(index); 186 | if (ch == '.' || isIdent(ch) || isNumber(ch)) { 187 | if (ch == '.') lastDot = index; 188 | ident += ch; 189 | } else { 190 | break; 191 | } 192 | index++; 193 | } 194 | 195 | //check if this is not a method invocation and if it is back out to last dot 196 | if (lastDot) { 197 | peekIndex = index; 198 | while (peekIndex < text.length) { 199 | ch = text.charAt(peekIndex); 200 | if (ch == '(') { 201 | methodName = ident.substr(lastDot - start + 1); 202 | ident = ident.substr(0, lastDot - start); 203 | index = peekIndex; 204 | break; 205 | } 206 | if (isWhitespace(ch)) { 207 | peekIndex++; 208 | } else { 209 | break; 210 | } 211 | } 212 | } 213 | 214 | 215 | var token = { 216 | index: start, 217 | text: ident 218 | }; 219 | 220 | if (OPERATORS.hasOwnProperty(ident)) { 221 | token.fn = token.json = OPERATORS[ident]; 222 | } else { 223 | var getter = getterFn(ident, csp, text); 224 | token.fn = extend(function (self, locals) { 225 | return (getter(self, locals)); 226 | }, { 227 | assign: function (self, value) { 228 | return setter(self, ident, value, text); 229 | } 230 | }); 231 | } 232 | 233 | tokens.push(token); 234 | 235 | if (methodName) { 236 | tokens.push({ 237 | index: lastDot, 238 | text: '.', 239 | json: false 240 | }); 241 | tokens.push({ 242 | index: lastDot + 1, 243 | text: methodName, 244 | json: false 245 | }); 246 | } 247 | }, 248 | 249 | peek: function (i) { 250 | var num = i || 1; 251 | return this.index + num < this.text.length ? this.text.charAt(this.index + num) : false; 252 | } 253 | }; 254 | 255 | function isExpOperator(ch) { 256 | return ch == '-' || ch == '+' || isNumber(ch); 257 | } 258 | 259 | function isNumber(ch) { 260 | return '0' <= ch && ch <= '9'; 261 | } 262 | 263 | function isWhitespace(ch) { 264 | return ch == ' ' || ch == '\r' || ch == '\t' || 265 | ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0 266 | } 267 | 268 | function isIdent(ch) { 269 | return 'a' <= ch && ch <= 'z' || 270 | 'A' <= ch && ch <= 'Z' || 271 | '_' == ch || ch == '$'; 272 | } 273 | 274 | function unaryFn(fn, right) { 275 | return extend(function (self, locals) { 276 | return fn(self, locals, right); 277 | }, { 278 | constant: right.constant 279 | }); 280 | } 281 | 282 | function binaryFn(left, fn, right) { 283 | return extend(function (self, locals) { 284 | return fn(self, locals, left, right); 285 | }, { 286 | constant: left.constant && right.constant 287 | }); 288 | } 289 | 290 | function ternaryFn(left, middle, right) { 291 | return extend(function (self, locals) { 292 | return left(self, locals) ? middle(self, locals) : right(self, locals); 293 | }, { 294 | constant: left.constant && middle.constant && right.constant 295 | }); 296 | } 297 | 298 | 299 | function Rule(expression) { 300 | this.context = null; 301 | this.expression = expression; 302 | this.l = null; 303 | this.r = null; 304 | this.m = null; 305 | this.op = null; 306 | } 307 | 308 | Rule.prototype = { 309 | generateTree: function () { 310 | var exp = this.expression; 311 | for (var i = 0; i < exp.length; i++) { 312 | while (this.isEmpty(exp.charAt(i))) { 313 | i++; 314 | } 315 | } 316 | 317 | var char = exp.charAt(i); 318 | }, 319 | 320 | isOperator: function (str) { 321 | return OPERATORS[str] != null; 322 | }, 323 | 324 | isEmpty: function (str) { 325 | return str.trim().length > 0; 326 | }, 327 | 328 | execute: function () { 329 | switch (this.type) { 330 | case 1: 331 | { 332 | unaryFn(OPERATORS[this.op], this.r); 333 | break; 334 | } 335 | case 2: 336 | { 337 | binaryFn(OPERATORS[this.op], this.l, this.r); 338 | break; 339 | } 340 | case 3: 341 | { 342 | ternaryFn(OPERATORS[this.op], this.l, this.m, this.r); 343 | break; 344 | } 345 | } 346 | } 347 | }; 348 | 349 | return Rule; 350 | }); -------------------------------------------------------------------------------- /js/modules/ide/service.js: -------------------------------------------------------------------------------- 1 | 2 | thin.define("IDEService", ["ArrayCollection"], function() { 3 | var ToolbarModel = new ArrayCollection(); 4 | 5 | var models = { 6 | toolbar: ToolbarModel 7 | }; 8 | 9 | var IDEService = { 10 | getModel: function(modelName) { 11 | return models[modelName]; 12 | } 13 | }; 14 | 15 | return IDEService; 16 | }); -------------------------------------------------------------------------------- /js/modules/ide/vm.js: -------------------------------------------------------------------------------- 1 | 2 | thin.define("ToolbarVM", ["IDEService"], function(IDEService) { 3 | var model = IDEService.getModel("toolbar"); 4 | 5 | model.itemUpdated(); 6 | }); -------------------------------------------------------------------------------- /js/modules/role/vm/areaViewModel.js: -------------------------------------------------------------------------------- 1 | thin.define("AreaManage", ["Tree"], function (Tree) { 2 | 3 | var state = "View"; 4 | 5 | var tree = new Tree(document.getElementById("tree1")); 6 | 7 | tree.on("changed", function (event) { 8 | var data; 9 | if (event.newNode) { 10 | data = event.newNode.data; 11 | } 12 | else { 13 | data = {}; 14 | } 15 | 16 | setFormData(data); 17 | }); 18 | 19 | function init() { 20 | enableForm(false); 21 | switchButtons("Operate"); 22 | 23 | var data = [ 24 | { 25 | name: "Jiangsu", 26 | code: "js", 27 | children: [ 28 | { 29 | name: "Nanjing", 30 | code: "nj" 31 | }, 32 | { 33 | name: "Suzhou", 34 | code: "sz", 35 | children: [ 36 | { 37 | name: "Wujiang", 38 | code: "wj" 39 | }, 40 | { 41 | name: "Changshu", 42 | code: "cs" 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | name: "Yunnan", 50 | code: "yn" 51 | }, 52 | { 53 | name: "Fujian", 54 | code: "fj" 55 | } 56 | ]; 57 | 58 | tree.labelField = "name"; 59 | tree.loadTreeData(data); 60 | } 61 | 62 | document.getElementById("newBtn").onclick = function () { 63 | state = "New"; 64 | switchButtons("Confirm"); 65 | enableForm(true); 66 | 67 | setFormData({}); 68 | } 69 | 70 | document.getElementById("modifyBtn").onclick = function () { 71 | state = "Modify"; 72 | switchButtons("Confirm"); 73 | enableForm(true); 74 | } 75 | 76 | document.getElementById("deleteBtn").onclick = function () { 77 | if (confirm("Sure?")) { 78 | tree.removeNode(tree.selectedNode); 79 | } 80 | } 81 | 82 | document.getElementById("okBtn").onclick = function () { 83 | var data = getFormData(); 84 | 85 | if (state === "New") { 86 | tree.addNode(data, tree.selectedNode); 87 | } 88 | else if (state === "Modify") { 89 | tree.selectedNode.refreshData(data); 90 | } 91 | state = "View"; 92 | switchButtons("Operate"); 93 | enableForm(false); 94 | } 95 | 96 | document.getElementById("cancelBtn").onclick = function () { 97 | state = "View"; 98 | switchButtons("Operate"); 99 | enableForm(false); 100 | 101 | setFormData(tree.selectedItem.data); 102 | } 103 | 104 | function enableForm(flag) { 105 | document.getElementById("inputName").disabled = !flag; 106 | document.getElementById("inputCode").disabled = !flag; 107 | } 108 | 109 | function switchButtons(group) { 110 | if (group === "Operate") { 111 | document.getElementById("operateBtns").style.display = ""; 112 | document.getElementById("confirmBtns").style.display = "none"; 113 | } 114 | else if (group === "Confirm") { 115 | document.getElementById("operateBtns").style.display = "none"; 116 | document.getElementById("confirmBtns").style.display = ""; 117 | } 118 | } 119 | 120 | function getFormData() { 121 | return { 122 | name: document.getElementById("inputName").value, 123 | code: document.getElementById("inputCode").value 124 | }; 125 | } 126 | 127 | function setFormData(data) { 128 | document.getElementById("inputName").value = data.name || ""; 129 | document.getElementById("inputCode").value = data.code || ""; 130 | } 131 | 132 | return { 133 | init: init 134 | }; 135 | }); 136 | -------------------------------------------------------------------------------- /js/modules/role/vm/orgViewModel.js: -------------------------------------------------------------------------------- 1 | thin.define("OrgViewModel", ["TreeGrid"], function (TreeGrid) { 2 | function OrgViewModel() { 3 | this.state = "View"; 4 | this.enableForm = false; 5 | this.editing = false; 6 | 7 | this.genderArr = [ 8 | { 9 | key: "0", 10 | label: "Female" 11 | }, 12 | { 13 | key: "1", 14 | label: "Male" 15 | } 16 | ]; 17 | } 18 | 19 | OrgViewModel.prototype = { 20 | init: function () { 21 | var that = this; 22 | 23 | var grid = new TreeGrid(this.orgGrid); 24 | 25 | grid.on("loadCompleted", function (event) { 26 | if (event.target.childNodes.length > 0) { 27 | event.target.select(event.target.childNodes[0]); 28 | } 29 | }); 30 | 31 | grid.on("changed", function (event) { 32 | var data; 33 | if (event.newNode) { 34 | data = event.newNode.data; 35 | } 36 | else { 37 | data = {}; 38 | } 39 | 40 | that.setFormData(data); 41 | }); 42 | 43 | grid.on("rowInserted", function (event) { 44 | event.target.select(event.newRow); 45 | }); 46 | 47 | grid.on("rowRemoved", function (event) { 48 | if (event.target.rows.length > 0) { 49 | event.target.select(event.target.rows[0]); 50 | } 51 | }); 52 | 53 | this.grid = grid; 54 | 55 | var columns = [ 56 | { 57 | label: "Name", 58 | field: "name" 59 | }, 60 | { 61 | label: "Code", 62 | field: "code" 63 | } 64 | ]; 65 | 66 | var data = [ 67 | { 68 | name: "Jiangsu", 69 | code: "js", 70 | children: [ 71 | { 72 | name: "Nanjing", 73 | code: "nj" 74 | }, 75 | { 76 | name: "Suzhou", 77 | code: "sz", 78 | children: [ 79 | { 80 | name: "Wujiang", 81 | code: "wj" 82 | }, 83 | { 84 | name: "Changshu", 85 | code: "cs" 86 | } 87 | ] 88 | } 89 | ] 90 | }, 91 | { 92 | name: "Yunnan", 93 | code: "yn" 94 | }, 95 | { 96 | name: "Fujian", 97 | code: "fj", 98 | children: [ 99 | { 100 | name: "Fuzhou", 101 | code: "fz" 102 | }, 103 | { 104 | name: "Xiamen", 105 | code: "xm" 106 | } 107 | ] 108 | } 109 | ]; 110 | 111 | this.grid.loadColumns(columns); 112 | this.grid.loadData(data); 113 | }, 114 | 115 | newClick: function () { 116 | this.state = "New"; 117 | this.editing = true; 118 | this.enableForm = true; 119 | 120 | this.setFormData({}); 121 | }, 122 | 123 | modifyClick: function () { 124 | this.state = "Modify"; 125 | this.editing = true; 126 | this.enableForm = true; 127 | }, 128 | 129 | deleteClick: function () { 130 | if (confirm("Sure?")) { 131 | this.grid.removeNode(this.grid.selectedNode); 132 | } 133 | }, 134 | 135 | okClick: function () { 136 | var data = this.getFormData(); 137 | 138 | if (this.state === "New") { 139 | this.grid.addNode(data, this.grid.selectedNode); 140 | } 141 | else if (this.state === "Modify") { 142 | this.grid.selectedNode.refresh(data); 143 | } 144 | this.state = "View"; 145 | this.editing = false; 146 | this.enableForm = false; 147 | }, 148 | 149 | cancelClick: function () { 150 | this.state = "View"; 151 | this.editing = false; 152 | this.enableForm = false; 153 | 154 | this.setFormData(this.grid.selectedNode.data); 155 | }, 156 | 157 | getFormData: function () { 158 | return { 159 | name: this.name, 160 | code: this.code 161 | }; 162 | }, 163 | 164 | setFormData: function (data) { 165 | this.name = data.name; 166 | this.code = data.code; 167 | } 168 | }; 169 | 170 | return OrgViewModel; 171 | }); -------------------------------------------------------------------------------- /js/modules/role/vm/staffViewModel.js: -------------------------------------------------------------------------------- 1 | thin.define("StaffViewModel", ["DataGrid"], function (DataGrid) { 2 | function StaffViewModel() { 3 | this.state = "View"; 4 | this.enableForm = false; 5 | this.editing = false; 6 | 7 | this.genderArr = [ 8 | { 9 | key: "0", 10 | label: "Female" 11 | }, 12 | { 13 | key: "1", 14 | label: "Male" 15 | } 16 | ]; 17 | } 18 | 19 | StaffViewModel.prototype = { 20 | init: function () { 21 | var that = this; 22 | 23 | var grid = new DataGrid(this.staffGrid); 24 | 25 | grid.on("loadCompleted", function (event) { 26 | if (event.target.rows.length > 0) { 27 | event.target.select(event.target.rows[0]); 28 | } 29 | }); 30 | 31 | grid.on("changed", function (event) { 32 | var data; 33 | if (event.newRow) { 34 | data = event.newRow.data; 35 | } 36 | else { 37 | data = {}; 38 | } 39 | 40 | that.setFormData(data); 41 | thin.fire({type: "vmchange"}); 42 | }); 43 | 44 | grid.on("rowInserted", function (event) { 45 | event.target.select(event.newRow); 46 | }); 47 | 48 | grid.on("rowRemoved", function (event) { 49 | if (event.target.rows.length > 0) { 50 | event.target.select(event.target.rows[0]); 51 | } 52 | }); 53 | 54 | this.grid = grid; 55 | 56 | var columns = [ 57 | { 58 | label: "#", 59 | field: "index" 60 | }, 61 | { 62 | label: "Name", 63 | field: "name" 64 | }, 65 | { 66 | label: "Gender", 67 | field: "gender" 68 | }, 69 | { 70 | label: "Age", 71 | field: "age" 72 | } 73 | ]; 74 | 75 | var data = [ 76 | { 77 | index: 1, 78 | name: "Tom", 79 | gender: "1", 80 | age: 5 81 | }, 82 | { 83 | index: 2, 84 | name: "Jerry", 85 | gender: "0", 86 | age: 2 87 | }, 88 | { 89 | index: 3, 90 | name: "Sun Wukong", 91 | gender: "1", 92 | age: 1024 93 | } 94 | ]; 95 | 96 | this.grid.loadColumns(columns); 97 | this.grid.loadData(data); 98 | }, 99 | 100 | newClick: function () { 101 | this.state = "New"; 102 | this.editing = true; 103 | this.enableForm = true; 104 | 105 | this.setFormData({}); 106 | }, 107 | 108 | modifyClick: function () { 109 | this.state = "Modify"; 110 | this.editing = true; 111 | this.enableForm = true; 112 | }, 113 | 114 | deleteClick: function () { 115 | if (confirm("Sure?")) { 116 | this.grid.removeRow(this.grid.selectedRow); 117 | } 118 | }, 119 | 120 | okClick: function () { 121 | var data = this.getFormData(); 122 | 123 | if (this.state === "New") { 124 | this.grid.insertRow(data); 125 | } 126 | else if (this.state === "Modify") { 127 | this.grid.selectedRow.refresh(data); 128 | } 129 | this.state = "View"; 130 | this.editing = false; 131 | this.enableForm = false; 132 | }, 133 | 134 | cancelClick: function () { 135 | this.state = "View"; 136 | this.editing = false; 137 | this.enableForm = false; 138 | 139 | this.setFormData(this.grid.selectedRow.data); 140 | }, 141 | 142 | getFormData: function () { 143 | return { 144 | index: this.index, 145 | name: this.name, 146 | gender: this.gender, 147 | age: this.age 148 | }; 149 | }, 150 | 151 | setFormData: function (data) { 152 | this.index = data.index; 153 | this.name = data.name; 154 | this.gender = data.gender; 155 | this.age = data.age; 156 | }, 157 | 158 | okEnabled: function() { 159 | if (this.editing && (this.name) && (this.name.length >= 5)) { 160 | return true; 161 | } 162 | else { 163 | return false; 164 | } 165 | } 166 | }; 167 | 168 | return StaffViewModel; 169 | }); -------------------------------------------------------------------------------- /js/modules/workflow/definition.js: -------------------------------------------------------------------------------- 1 | thin.define("Workflow", ["WorkflowFactory"], function (WorkflowFactory) { 2 | var Workflow = function() { 3 | 4 | }; 5 | 6 | Workflow.load = function(data) { 7 | var flow = WorkflowFactory.create(data.type); 8 | flow.load(data); 9 | return flow; 10 | }; 11 | 12 | return Workflow; 13 | }); 14 | 15 | thin.define("Workflow.Type", [], function() { 16 | return { 17 | "Sequential": "sequential", 18 | "StateMachine": "stateMachine" 19 | }; 20 | }); 21 | 22 | thin.define("WorkflowFactory", ["Workflow.Type", "SequentialFlow", "StateMachine"], function(types, SequentialFlow, StateMachine) { 23 | var WorkflowFactory = { 24 | create: function(type) { 25 | var flow; 26 | switch (type) { 27 | case types.Sequential: { 28 | flow = new SequentialFlow(); 29 | break; 30 | } 31 | case types.StateMachine: { 32 | flow = new StateMachine(); 33 | break; 34 | } 35 | } 36 | return flow; 37 | } 38 | }; 39 | }); 40 | 41 | thin.define("Activity", [], function () { 42 | var Activity = function() { 43 | this.name = "Activity"; 44 | this.parent = null; 45 | this.root = null; 46 | }; 47 | 48 | return Activity; 49 | }); 50 | 51 | thin.define("CompositeActivity", ["Activity"], function(Activity) { 52 | var CompositeActivity = function() { 53 | Activity.call(this); 54 | }; 55 | 56 | CompositeActivity.prototype = { 57 | 58 | }.extend(Activity); 59 | }); 60 | 61 | thin.define("Transition", ["Condition"], function (Condition) { 62 | var Transition = function(data) { 63 | this.name = data.name || "Transition"; 64 | this.condition = new Condition(data.condition); 65 | 66 | this.parent = null; 67 | }; 68 | 69 | Transition.prototype = { 70 | evaluate: function() { 71 | if (!this.condition) { 72 | return true; 73 | } 74 | else { 75 | return this.condition.call(this.parent); 76 | } 77 | } 78 | }; 79 | 80 | return Transition; 81 | }); 82 | 83 | thin.define("Condition", [], function () { 84 | var Condition = function(expression) { 85 | this.expression = expression; 86 | }; 87 | 88 | return Condition; 89 | }); 90 | 91 | thin.define("SequentialFlow", ["CompositeActivity"], function (CompositeActivity) { 92 | function SequentialFlow() { 93 | 94 | } 95 | 96 | SequentialFlow.prototype = { 97 | clear: function () { 98 | 99 | }, 100 | 101 | addActivity: function (data) { 102 | 103 | }, 104 | 105 | addTransition: function (data, from, to) { 106 | 107 | } 108 | }.extend(Activity); 109 | 110 | return SequentialFlow; 111 | }); 112 | 113 | thin.define("StateMachine", ["Activity", "CompositeActivity", "Transition"], function (Activity, CompositeActivity, Transition) { 114 | var State = Activity; 115 | 116 | var StateMachine = function() { 117 | CompositeActivity.call(this); 118 | 119 | this.name = "State Machine"; 120 | this.states = {}; 121 | this.transitions = []; 122 | 123 | this.startState = this.addState("Start"); 124 | this.finishState = this.addState("Finish"); 125 | 126 | this.currentState = this.startState; 127 | }; 128 | 129 | StateMachine.prototype = { 130 | load: function(data) { 131 | for (var i=0; i= 0; i--) { 26 | if (readyFunctions[i]) { 27 | for (var j = 0; j < readyFunctions[i].length; j++) { 28 | readyFunctions[i][j](); 29 | } 30 | } 31 | } 32 | }, false); 33 | 34 | var Events = { 35 | on: function (eventType, handler) { 36 | if (!this.eventMap) { 37 | this.eventMap = {}; 38 | } 39 | 40 | //multiple event listener 41 | if (!this.eventMap[eventType]) { 42 | this.eventMap[eventType] = []; 43 | } 44 | this.eventMap[eventType].push(handler); 45 | }, 46 | 47 | off: function (eventType, handler) { 48 | for (var i = 0; i < this.eventMap[eventType].length; i++) { 49 | if (this.eventMap[eventType][i] === handler) { 50 | this.eventMap[eventType].splice(i, 1); 51 | break; 52 | } 53 | } 54 | }, 55 | 56 | fire: function (event) { 57 | var eventType = event.type; 58 | if (this.eventMap && this.eventMap[eventType]) { 59 | for (var i = 0; i < this.eventMap[eventType].length; i++) { 60 | this.eventMap[eventType][i](event); 61 | } 62 | } 63 | } 64 | }; 65 | 66 | _.extend(thin, { 67 | base: "../js/modules/", 68 | 69 | define: function (name, dependencies, factory) { 70 | if (!moduleMap[name]) { 71 | var module = { 72 | name: name, 73 | dependencies: dependencies, 74 | factory: factory 75 | }; 76 | 77 | moduleMap[name] = module; 78 | } 79 | 80 | return moduleMap[name]; 81 | }, 82 | 83 | use: function (name) { 84 | var module = moduleMap[name]; 85 | 86 | if (!module.entity) { 87 | var args = []; 88 | for (var i = 0; i < module.dependencies.length; i++) { 89 | if (moduleMap[module.dependencies[i]].entity) { 90 | args.push(moduleMap[module.dependencies[i]].entity); 91 | } 92 | else { 93 | args.push(this.use(module.dependencies[i])); 94 | } 95 | } 96 | 97 | module.entity = module.factory.apply(noop, args); 98 | } 99 | 100 | return module.entity; 101 | }, 102 | 103 | require: function (pathArr, callback) { 104 | var base = this.base; 105 | for (var i = 0; i < pathArr.length; i++) { 106 | loadFile(pathArr[i]); 107 | } 108 | 109 | function loadFile(file) { 110 | var head = doc.getElementsByTagName('head')[0]; 111 | var script = doc.createElement('script'); 112 | script.setAttribute('type', 'text/javascript'); 113 | script.setAttribute('src', base + file + '.js'); 114 | script.onload = script.onreadystatechange = function () { 115 | if ((!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) { 116 | fileMap[file] = true; 117 | //head.removeChild(script); 118 | checkAllFiles(); 119 | } 120 | }; 121 | head.appendChild(script); 122 | } 123 | 124 | function checkAllFiles() { 125 | var allLoaded = true; 126 | for (var i = 0; i < pathArr.length; i++) { 127 | if (!fileMap[pathArr[i]]) { 128 | allLoaded = false; 129 | break; 130 | } 131 | } 132 | 133 | if (allLoaded && callback) { 134 | callback(); 135 | } 136 | } 137 | }, 138 | 139 | ready: function (handler, priority) { 140 | priority = (priority === null) ? 1 : priority; 141 | 142 | if (!readyFunctions[priority]) { 143 | readyFunctions[priority] = []; 144 | } 145 | readyFunctions[priority].push(handler); 146 | }, 147 | 148 | error: function () { 149 | 150 | }, 151 | 152 | log: function (obj) { 153 | try { 154 | console.log(obj); 155 | } 156 | catch (ex) { 157 | 158 | } 159 | } 160 | }); 161 | 162 | _.extend(thin, Events); 163 | 164 | win.thin = thin; 165 | 166 | thin.define("_", [], function() { 167 | return _; 168 | }); 169 | 170 | //Events 171 | thin.define("Events", [], function () { 172 | return Events; 173 | }); 174 | 175 | thin.on("ready", function () { 176 | thin.require(["core/binding"], function () { 177 | var binding = thin.use("DOMBinding"); 178 | binding.parse(doc.body); 179 | }); 180 | }); 181 | })(window, document, _); 182 | -------------------------------------------------------------------------------- /js/version/thin.0.1.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var moduleMap = {}; 3 | 4 | var noop = function () { 5 | }; 6 | 7 | var thin = { 8 | define: function (name, dependencies, factory) { 9 | if (!moduleMap[name]) { 10 | var module = { 11 | name: name, 12 | dependencies: dependencies, 13 | factory: factory 14 | }; 15 | 16 | moduleMap[name] = module; 17 | } 18 | 19 | return moduleMap[name]; 20 | }, 21 | 22 | use: function (name) { 23 | var module = moduleMap[name]; 24 | 25 | if (!module.entity) { 26 | var args = []; 27 | for (var i = 0; i < module.dependencies.length; i++) { 28 | if (moduleMap[module.dependencies[i]].entity) { 29 | args.push(moduleMap[module.dependencies[i]].entity); 30 | } 31 | else { 32 | args.push(this.use(module.dependencies[i])); 33 | } 34 | } 35 | 36 | module.entity = module.factory.apply(noop, args); 37 | } 38 | 39 | return module.entity; 40 | } 41 | }; 42 | 43 | window.thin = thin; 44 | })(); --------------------------------------------------------------------------------