163 |
164 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/src/handlers.py:
--------------------------------------------------------------------------------
1 | #coding:utf-8
2 | import copy
3 | import json
4 | import hashlib
5 | import sqlite3
6 | from datetime import datetime
7 |
8 | import web
9 | from socketio import socketio_manage
10 | from socketio.namespace import BaseNamespace
11 | from socketio.mixins import RoomsMixin, BroadcastMixin
12 |
13 | from models import Message, User, Topic
14 |
15 | session = web.config._session
16 |
17 | CACHE_USER = {}
18 |
19 |
20 | def sha1(data):
21 | return hashlib.sha1(data).hexdigest()
22 |
23 |
24 | def bad_request(message):
25 | raise web.BadRequest(message=message)
26 |
27 |
28 | # 首页
29 | class IndexHandler:
30 | def GET(self):
31 | render = web.template.render('templates/')
32 | return render.index()
33 |
34 |
35 | class UserHandler:
36 | def GET(self):
37 | # 获取当前登录的用户数据
38 | user = session.user
39 | return json.dumps(user)
40 |
41 | def POST(self):
42 | data = web.data()
43 | data = json.loads(data)
44 | username = data.get("username")
45 | password = data.get("password")
46 | password_repeat = data.get("password_repeat")
47 |
48 | if password != password_repeat:
49 | return bad_request('两次密码输入不一致')
50 |
51 | user_data = {
52 | "username": username,
53 | "password": sha1(password),
54 | "registed_time": datetime.now(),
55 | }
56 |
57 | try:
58 | user_id = User.create(**user_data)
59 | except sqlite3.IntegrityError:
60 | return bad_request('用户名已存在!')
61 |
62 | user = User.get_by_id(user_id)
63 | session.login = True
64 | session.user = user
65 |
66 | result = {
67 | 'id': user_id,
68 | 'username': username,
69 | }
70 | return json.dumps(result)
71 |
72 |
73 | class LoginHandler:
74 | def POST(self):
75 | data = web.data()
76 | data = json.loads(data)
77 | username = data.get("username")
78 | password = data.get("password")
79 | user = User.get_by_username_password(
80 | username=username,
81 | password=sha1(password)
82 | )
83 | if not user:
84 | return bad_request('用户名或密码错误!')
85 |
86 | session.login = True
87 | session.user = user
88 | result = {
89 | 'id': user.get('id'),
90 | 'username': user.get('username'),
91 | }
92 | return json.dumps(result)
93 |
94 |
95 | class LogoutHandler:
96 | def GET(self):
97 | session.login = False
98 | session.user = None
99 | session.kill()
100 | return web.tempredirect('/#login')
101 |
102 |
103 | class TopicHandler:
104 | def GET(self, pk=None):
105 | if pk:
106 | topic = Topic.get_by_id(pk)
107 | return json.dumps(topic)
108 |
109 | topics = Topic.get_all()
110 | result = []
111 | for t in topics:
112 | topic = dict(t)
113 | try:
114 | user = CACHE_USER[t.owner_id]
115 | except KeyError:
116 | user = User.get_by_id(t.owner_id)
117 | CACHE_USER[t.owner_id] = user
118 | topic['owner_name'] = user.username
119 | result.append(topic)
120 | return json.dumps(result)
121 |
122 | def POST(self):
123 | if not session.user or not session.user.id:
124 | return bad_request('请先登录!')
125 | if session.user.username != 'the5fire':
126 | return bad_request('sorry,你没有创建权限')
127 |
128 | data = web.data()
129 | data = json.loads(data)
130 |
131 | topic_data = {
132 | "title": data.get('title'),
133 | "owner_id": session.user.id,
134 | "created_time": datetime.now(),
135 | }
136 |
137 | try:
138 | topic_id = Topic.create(**topic_data)
139 | except sqlite3.IntegrityError:
140 | return bad_request('你已创建过该名称!')
141 |
142 | result = {
143 | "id": topic_id,
144 | "title": topic_data.get('title'),
145 | "owner_id": session.user.id,
146 | "owner_name": session.user.username,
147 | "created_time": str(topic_data.get('created_time')),
148 | }
149 | return json.dumps(result)
150 |
151 | def PUT(self, obj_id=None):
152 | data = web.data()
153 | print data
154 |
155 | def DELETE(self, obj_id=None):
156 | pass
157 |
158 |
159 | class MessageHandler:
160 | def GET(self):
161 | topic_id = web.input().get('topic_id')
162 | if topic_id:
163 | messages = Message.get_by_topic(topic_id) or []
164 | else:
165 | messages = Message.get_all()
166 |
167 | result = []
168 | current_user_id = session.user.id
169 | for m in messages:
170 | try:
171 | user = CACHE_USER[m.user_id]
172 | except KeyError:
173 | user = User.get_by_id(m.user_id)
174 | CACHE_USER[m.user_id] = user
175 | message = dict(m)
176 | message['user_name'] = user.username
177 | message['is_mine'] = (current_user_id == user.id)
178 | result.append(message)
179 | return json.dumps(result)
180 |
181 | def POST(self):
182 | data = web.data()
183 | data = json.loads(data)
184 | if not (session.user and session.user.id):
185 | return bad_request("请先登录!")
186 |
187 | message_data = {
188 | "content": data.get("content"),
189 | "topic_id": data.get("topic_id"),
190 | "user_id": session.user.id,
191 | "created_time": datetime.now(),
192 | }
193 | m_id = Message.create(**message_data)
194 | result = {
195 | "id": m_id,
196 | "content": message_data.get("content"),
197 | "topic_id": message_data.get("topic_id"),
198 | "user_id": session.user.id,
199 | "user_name": session.user.username,
200 | "created_time": str(message_data.get("created_time")),
201 | "is_mine": True,
202 | }
203 | return json.dumps(result)
204 |
205 |
206 | class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin):
207 | def on_go_out(self):
208 | room_num = self.socket.session.get('room')
209 | if room_num:
210 | print 'go_out', room_num
211 | self.leave(room_num)
212 |
213 | def on_topic(self, topic_id):
214 | """ 加入以某个主题id为房间
215 |
216 | 客户端进入聊天室界面先发送此请求,确定房间号
217 | """
218 | room_num = 'room_%s' % topic_id
219 | self.socket.session['room'] = room_num
220 | print 'join', room_num
221 | self.join(room_num)
222 |
223 | def on_message(self, model):
224 | user = self.environ['user']
225 | if user is None:
226 | # 手动从store中取出user
227 | session_id = self.environ['session_id']
228 | _data = session.store[session_id]
229 | user = _data['user']
230 | model.update({
231 | "user_id": user.id,
232 | "created_time": datetime.now(),
233 | })
234 | m_id = Message.create(**model)
235 | model.update({
236 | "user_name": user.username,
237 | 'id': m_id,
238 | 'created_time': str(model['created_time']),
239 | 'is_mine': True,
240 | })
241 | # 发送回客户端
242 | self.emit('message', model)
243 |
244 | # 发送给其他人
245 | model['is_mine'] = False
246 | self.emit_to_room(
247 | self.socket.session['room'],
248 | 'message',
249 | model,
250 | )
251 |
252 | def recv_disconnect(self):
253 | print 'DISCONNECT!!!!!!!!!!!!!!!!!!!!!!!'
254 | self.disconnect(silent=True)
255 |
256 |
257 | class SocketHandler:
258 | def GET(self):
259 | context = copy.copy(web.ctx.environ)
260 | context.update({
261 | "user": session.user,
262 | "session_id": session.session_id,
263 | })
264 | socketio_manage(context, {'': ChatNamespace})
265 | # 重新载入session数据,因为session在socket请求中改变了
266 | session._load()
267 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/src/static/js/chat.js:
--------------------------------------------------------------------------------
1 | /*
2 | * author: the5fire
3 | * blog: the5fire.com
4 | * date: 2014-03-16
5 | * */
6 | $(function(){
7 | WEB_SOCKET_SWF_LOCATION = "/static/WebSocketMain.swf";
8 | WEB_SOCKET_DEBUG = true;
9 |
10 | var socket = io.connect();
11 | socket.on('connect', function(){
12 | console.log('connected');
13 | });
14 |
15 | $(window).bind("beforeunload", function() {
16 | socket.disconnect();
17 | });
18 |
19 | var User = Backbone.Model.extend({
20 | urlRoot: '/user',
21 | });
22 |
23 | var Topic = Backbone.Model.extend({
24 | urlRoot: '/topic',
25 | });
26 |
27 | var Message = Backbone.Model.extend({
28 | urlRoot: '/message',
29 | sync: function(method, model, options) {
30 | if (method === 'create') {
31 | socket.emit('message', model.attributes);
32 | // 错误处理没做
33 | $('#comment').val('');
34 | } else {
35 | return Backbone.sync(method, model, options);
36 | };
37 | },
38 | });
39 |
40 | var Topics = Backbone.Collection.extend({
41 | url: '/topic',
42 | model: Topic,
43 | });
44 |
45 | var Messages = Backbone.Collection.extend({
46 | url: '/message',
47 | model: Message,
48 | });
49 |
50 | var topics = new Topics;
51 |
52 | var TopicView = Backbone.View.extend({
53 | tagName: "div class='column'",
54 | templ: _.template($('#topic-template').html()),
55 |
56 | // 渲染列表页模板
57 | render: function() {
58 | $(this.el).html(this.templ(this.model.toJSON()));
59 | return this;
60 | },
61 | });
62 |
63 | var messages = new Messages;
64 |
65 | var MessageView = Backbone.View.extend({
66 | tagName: "div class='comment'",
67 | templ: _.template($('#message-template').html()),
68 |
69 | // 渲染列表页模板
70 | render: function() {
71 | $(this.el).html(this.templ(this.model.toJSON()));
72 | return this;
73 | },
74 | });
75 |
76 |
77 | var AppView = Backbone.View.extend({
78 | el: "#main",
79 | topic_list: $("#topic_list"),
80 | topic_section: $("#topic_section"),
81 | message_section: $("#message_section"),
82 | message_list: $("#message_list"),
83 | message_head: $("#message_head"),
84 |
85 | events: {
86 | 'click .submit': 'saveMessage',
87 | 'click .submit_topic': 'saveTopic',
88 | 'keypress #comment': 'saveMessageEvent',
89 | },
90 |
91 | initialize: function() {
92 | _.bindAll(this, 'addTopic', 'addMessage');
93 |
94 | topics.bind('add', this.addTopic);
95 |
96 | // 定义消息列表池,每个topic有自己的message collection
97 | // 这样保证每个主题下得消息不冲突
98 | this.message_pool = {};
99 | this.socket = null;
100 |
101 | this.message_list_div = document.getElementById('message_list');
102 | },
103 |
104 | addTopic: function(topic) {
105 | var view = new TopicView({model: topic});
106 | this.topic_list.append(view.render().el);
107 | },
108 |
109 | addMessage: function(message) {
110 | var view = new MessageView({model: message});
111 | this.message_list.append(view.render().el);
112 | self.message_list.scrollTop(self.message_list_div.scrollHeight);
113 | },
114 |
115 | saveMessageEvent: function(evt) {
116 | if (evt.keyCode == 13) {
117 | this.saveMessage(evt);
118 | }
119 | },
120 | saveMessage: function(evt) {
121 | var comment_box = $('#comment')
122 | var content = comment_box.val();
123 | if (content == '') {
124 | alert('内容不能为空');
125 | return false;
126 | }
127 | var topic_id = comment_box.attr('topic_id');
128 | var message = new Message({
129 | content: content,
130 | topic_id: topic_id,
131 | });
132 | var messages = this.message_pool[topic_id];
133 | message.save(); // 依赖上面对sync的重载
134 | },
135 |
136 | saveTopic: function(evt) {
137 | var topic_title = $('#topic_title');
138 | if (topic_title.val() == '') {
139 | alert('主题不能为空!');
140 | return false
141 | }
142 | var topic = new Topic({
143 | title: topic_title.val(),
144 | });
145 | self = this;
146 | topic.save(null, {
147 | success: function(model, response, options){
148 | topics.add(response);
149 | topic_title.val('');
150 | },
151 | error: function(model, resp, options) {
152 | alert(resp.responseText);
153 | }
154 | });
155 | },
156 |
157 | showTopic: function(){
158 | topics.fetch();
159 | this.topic_section.show();
160 | this.message_section.hide();
161 | this.message_list.html('');
162 |
163 | this.goOut()
164 | },
165 |
166 | goOut: function(){
167 | // 退出房间
168 | socket.emit('go_out');
169 | socket.removeAllListeners('message');
170 | },
171 |
172 | initMessage: function(topic_id) {
173 | var messages = new Messages;
174 | messages.bind('add', this.addMessage);
175 | this.message_pool[topic_id] = messages;
176 | },
177 |
178 | showMessage: function(topic_id) {
179 | this.initMessage(topic_id);
180 |
181 | this.message_section.show();
182 | this.topic_section.hide();
183 |
184 | this.showMessageHead(topic_id);
185 | $('#comment').attr('topic_id', topic_id);
186 |
187 | var messages = this.message_pool[topic_id];
188 | // 进入房间
189 | socket.emit('topic', topic_id);
190 | // 监听message事件,添加对话到messages中
191 | socket.on('message', function(response) {
192 | messages.add(response);
193 | });
194 | messages.fetch({
195 | data: {topic_id: topic_id},
196 | success: function(resp) {
197 | self.message_list.scrollTop(self.message_list_div.scrollHeight)
198 | },
199 | error: function(model, resp, options) {
200 | alert(resp.responseText);
201 | }
202 | });
203 | },
204 |
205 | showMessageHead: function(topic_id) {
206 | var topic = new Topic({id: topic_id});
207 | self = this;
208 | topic.fetch({
209 | success: function(resp, model, options){
210 | self.message_head.html(model.title);
211 | },
212 | error: function(model, resp, options) {
213 | alert(resp.responseText);
214 | }
215 | });
216 | },
217 |
218 | });
219 |
220 |
221 | var LoginView = Backbone.View.extend({
222 | el: "#login",
223 | wrapper: $('#wrapper'),
224 |
225 | events: {
226 | 'keypress #login_pwd': 'loginEvent',
227 | 'click .login_submit': 'login',
228 | 'keypress #reg_pwd_repeat': 'registeEvent',
229 | 'click .registe_submit': 'registe',
230 | },
231 |
232 | hide: function() {
233 | this.wrapper.hide();
234 | },
235 |
236 | show: function() {
237 | this.wrapper.show();
238 | },
239 |
240 | loginEvent: function(evt) {
241 | if (evt.keyCode == 13) {
242 | this.login(evt);
243 | }
244 | },
245 |
246 | login: function(evt){
247 | var username_input = $('#login_username');
248 | var pwd_input = $('#login_pwd');
249 | var u = new User({
250 | username: username_input.val(),
251 | password: pwd_input.val(),
252 | });
253 | u.save(null, {
254 | url: '/login',
255 | success: function(model, resp, options){
256 | g_user = resp;
257 | // 跳转到index
258 | appRouter.navigate('index', {trigger: true});
259 | },
260 | error: function(model, resp, options) {
261 | alert(resp.responseText);
262 | }
263 | });
264 | },
265 |
266 | registeEvent: function(evt) {
267 | if (evt.keyCode == 13) {
268 | this.registe(evt);
269 | }
270 | },
271 |
272 | registe: function(evt){
273 | var reg_username_input = $('#reg_username');
274 | var reg_pwd_input = $('#reg_pwd');
275 | var reg_pwd_repeat_input = $('#reg_pwd_repeat');
276 | var u = new User({
277 | username: reg_username_input.val(),
278 | password: reg_pwd_input.val(),
279 | password_repeat: reg_pwd_repeat_input.val(),
280 | });
281 | u.save(null, {
282 | success: function(model, resp, options){
283 | g_user = resp;
284 | // 跳转到index
285 | appRouter.navigate('index', {trigger: true});
286 | },
287 | error: function(model, resp, options) {
288 | alert(resp.responseText);
289 | }
290 | });
291 | },
292 | });
293 |
294 | var UserView = Backbone.View.extend({
295 | el: "#user_info",
296 | username: $('#username'),
297 |
298 | show: function(username) {
299 | this.username.html(username);
300 | this.$el.show();
301 | },
302 | });
303 |
304 | var AppRouter = Backbone.Router.extend({
305 | routes: {
306 | "login": "login",
307 | "index": "index",
308 | "topic/:id" : "topic",
309 | },
310 |
311 | initialize: function(){
312 | // 初始化项目, 显示首页
313 | this.appView = new AppView();
314 | this.loginView = new LoginView();
315 | this.userView = new UserView();
316 | this.indexFlag = false;
317 | },
318 |
319 | login: function(){
320 | this.loginView.show();
321 | },
322 |
323 | index: function(){
324 | if (g_user && g_user.id != undefined) {
325 | this.appView.showTopic();
326 | this.userView.show(g_user.username);
327 | this.loginView.hide();
328 | this.indexFlag = true; // 标志已经到达主页了
329 | }
330 | },
331 |
332 | topic: function(topic_id) {
333 | if (g_user && g_user.id != undefined) {
334 | this.appView.showMessage(topic_id);
335 | this.userView.show(g_user.username);
336 | this.loginView.hide();
337 | this.indexFlag = true; // 标志已经到达主页了
338 | }
339 | },
340 | });
341 |
342 | var appRouter = new AppRouter();
343 | var g_user = new User;
344 | g_user.fetch({
345 | success: function(model, resp, options){
346 | g_user = resp;
347 | Backbone.history.start({pustState: true});
348 |
349 | if(g_user === null || g_user.id === undefined) {
350 | // 跳转到登录页面
351 | appRouter.navigate('login', {trigger: true});
352 | } else if (appRouter.indexFlag == false){
353 | // 跳转到首页
354 | appRouter.navigate('index', {trigger: true});
355 | }
356 | },
357 | error: function(model, resp, options) {
358 | alert(resp.responseText);
359 | }
360 | }); // 获取当前用户
361 | });
362 |
--------------------------------------------------------------------------------
/src/static/js/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | http://www.JSON.org/json2.js
3 | 2009-09-29
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, strict: false */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | if (!this.JSON) {
163 | this.JSON = {};
164 | }
165 |
166 | (function () {
167 |
168 | function f(n) {
169 | // Format integers to have at least two digits.
170 | return n < 10 ? '0' + n : n;
171 | }
172 |
173 | if (typeof Date.prototype.toJSON !== 'function') {
174 |
175 | Date.prototype.toJSON = function (key) {
176 |
177 | return isFinite(this.valueOf()) ?
178 | this.getUTCFullYear() + '-' +
179 | f(this.getUTCMonth() + 1) + '-' +
180 | f(this.getUTCDate()) + 'T' +
181 | f(this.getUTCHours()) + ':' +
182 | f(this.getUTCMinutes()) + ':' +
183 | f(this.getUTCSeconds()) + 'Z' : null;
184 | };
185 |
186 | String.prototype.toJSON =
187 | Number.prototype.toJSON =
188 | Boolean.prototype.toJSON = function (key) {
189 | return this.valueOf();
190 | };
191 | }
192 |
193 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
194 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
195 | gap,
196 | indent,
197 | meta = { // table of character substitutions
198 | '\b': '\\b',
199 | '\t': '\\t',
200 | '\n': '\\n',
201 | '\f': '\\f',
202 | '\r': '\\r',
203 | '"' : '\\"',
204 | '\\': '\\\\'
205 | },
206 | rep;
207 |
208 |
209 | function quote(string) {
210 |
211 | // If the string contains no control characters, no quote characters, and no
212 | // backslash characters, then we can safely slap some quotes around it.
213 | // Otherwise we must also replace the offending characters with safe escape
214 | // sequences.
215 |
216 | escapable.lastIndex = 0;
217 | return escapable.test(string) ?
218 | '"' + string.replace(escapable, function (a) {
219 | var c = meta[a];
220 | return typeof c === 'string' ? c :
221 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
222 | }) + '"' :
223 | '"' + string + '"';
224 | }
225 |
226 |
227 | function str(key, holder) {
228 |
229 | // Produce a string from holder[key].
230 |
231 | var i, // The loop counter.
232 | k, // The member key.
233 | v, // The member value.
234 | length,
235 | mind = gap,
236 | partial,
237 | value = holder[key];
238 |
239 | // If the value has a toJSON method, call it to obtain a replacement value.
240 |
241 | if (value && typeof value === 'object' &&
242 | typeof value.toJSON === 'function') {
243 | value = value.toJSON(key);
244 | }
245 |
246 | // If we were called with a replacer function, then call the replacer to
247 | // obtain a replacement value.
248 |
249 | if (typeof rep === 'function') {
250 | value = rep.call(holder, key, value);
251 | }
252 |
253 | // What happens next depends on the value's type.
254 |
255 | switch (typeof value) {
256 | case 'string':
257 | return quote(value);
258 |
259 | case 'number':
260 |
261 | // JSON numbers must be finite. Encode non-finite numbers as null.
262 |
263 | return isFinite(value) ? String(value) : 'null';
264 |
265 | case 'boolean':
266 | case 'null':
267 |
268 | // If the value is a boolean or null, convert it to a string. Note:
269 | // typeof null does not produce 'null'. The case is included here in
270 | // the remote chance that this gets fixed someday.
271 |
272 | return String(value);
273 |
274 | // If the type is 'object', we might be dealing with an object or an array or
275 | // null.
276 |
277 | case 'object':
278 |
279 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
280 | // so watch out for that case.
281 |
282 | if (!value) {
283 | return 'null';
284 | }
285 |
286 | // Make an array to hold the partial results of stringifying this object value.
287 |
288 | gap += indent;
289 | partial = [];
290 |
291 | // Is the value an array?
292 |
293 | if (Object.prototype.toString.apply(value) === '[object Array]') {
294 |
295 | // The value is an array. Stringify every element. Use null as a placeholder
296 | // for non-JSON values.
297 |
298 | length = value.length;
299 | for (i = 0; i < length; i += 1) {
300 | partial[i] = str(i, value) || 'null';
301 | }
302 |
303 | // Join all of the elements together, separated with commas, and wrap them in
304 | // brackets.
305 |
306 | v = partial.length === 0 ? '[]' :
307 | gap ? '[\n' + gap +
308 | partial.join(',\n' + gap) + '\n' +
309 | mind + ']' :
310 | '[' + partial.join(',') + ']';
311 | gap = mind;
312 | return v;
313 | }
314 |
315 | // If the replacer is an array, use it to select the members to be stringified.
316 |
317 | if (rep && typeof rep === 'object') {
318 | length = rep.length;
319 | for (i = 0; i < length; i += 1) {
320 | k = rep[i];
321 | if (typeof k === 'string') {
322 | v = str(k, value);
323 | if (v) {
324 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
325 | }
326 | }
327 | }
328 | } else {
329 |
330 | // Otherwise, iterate through all of the keys in the object.
331 |
332 | for (k in value) {
333 | if (Object.hasOwnProperty.call(value, k)) {
334 | v = str(k, value);
335 | if (v) {
336 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
337 | }
338 | }
339 | }
340 | }
341 |
342 | // Join all of the member texts together, separated with commas,
343 | // and wrap them in braces.
344 |
345 | v = partial.length === 0 ? '{}' :
346 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
347 | mind + '}' : '{' + partial.join(',') + '}';
348 | gap = mind;
349 | return v;
350 | }
351 | }
352 |
353 | // If the JSON object does not yet have a stringify method, give it one.
354 |
355 | if (typeof JSON.stringify !== 'function') {
356 | JSON.stringify = function (value, replacer, space) {
357 |
358 | // The stringify method takes a value and an optional replacer, and an optional
359 | // space parameter, and returns a JSON text. The replacer can be a function
360 | // that can replace values, or an array of strings that will select the keys.
361 | // A default replacer method can be provided. Use of the space parameter can
362 | // produce text that is more easily readable.
363 |
364 | var i;
365 | gap = '';
366 | indent = '';
367 |
368 | // If the space parameter is a number, make an indent string containing that
369 | // many spaces.
370 |
371 | if (typeof space === 'number') {
372 | for (i = 0; i < space; i += 1) {
373 | indent += ' ';
374 | }
375 |
376 | // If the space parameter is a string, it will be used as the indent string.
377 |
378 | } else if (typeof space === 'string') {
379 | indent = space;
380 | }
381 |
382 | // If there is a replacer, it must be a function or an array.
383 | // Otherwise, throw an error.
384 |
385 | rep = replacer;
386 | if (replacer && typeof replacer !== 'function' &&
387 | (typeof replacer !== 'object' ||
388 | typeof replacer.length !== 'number')) {
389 | throw new Error('JSON.stringify');
390 | }
391 |
392 | // Make a fake root object containing our value under the key of ''.
393 | // Return the result of stringifying the value.
394 |
395 | return str('', {'': value});
396 | };
397 | }
398 |
399 |
400 | // If the JSON object does not yet have a parse method, give it one.
401 |
402 | if (typeof JSON.parse !== 'function') {
403 | JSON.parse = function (text, reviver) {
404 |
405 | // The parse method takes a text and an optional reviver function, and returns
406 | // a JavaScript value if the text is a valid JSON text.
407 |
408 | var j;
409 |
410 | function walk(holder, key) {
411 |
412 | // The walk method is used to recursively walk the resulting structure so
413 | // that modifications can be made.
414 |
415 | var k, v, value = holder[key];
416 | if (value && typeof value === 'object') {
417 | for (k in value) {
418 | if (Object.hasOwnProperty.call(value, k)) {
419 | v = walk(value, k);
420 | if (v !== undefined) {
421 | value[k] = v;
422 | } else {
423 | delete value[k];
424 | }
425 | }
426 | }
427 | }
428 | return reviver.call(holder, key, value);
429 | }
430 |
431 |
432 | // Parsing happens in four stages. In the first stage, we replace certain
433 | // Unicode characters with escape sequences. JavaScript handles many characters
434 | // incorrectly, either silently deleting them, or treating them as line endings.
435 |
436 | cx.lastIndex = 0;
437 | if (cx.test(text)) {
438 | text = text.replace(cx, function (a) {
439 | return '\\u' +
440 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
441 | });
442 | }
443 |
444 | // In the second stage, we run the text against regular expressions that look
445 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
446 | // because they can cause invocation, and '=' because it can cause mutation.
447 | // But just to be safe, we want to reject all unexpected forms.
448 |
449 | // We split the second stage into 4 regexp operations in order to work around
450 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
451 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
452 | // replace all simple value tokens with ']' characters. Third, we delete all
453 | // open brackets that follow a colon or comma or that begin the text. Finally,
454 | // we look to see that the remaining characters are only whitespace or ']' or
455 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
456 |
457 | if (/^[\],:{}\s]*$/.
458 | test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
459 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
460 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
461 |
462 | // In the third stage we use the eval function to compile the text into a
463 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
464 | // in JavaScript: it can begin a block or an object literal. We wrap the text
465 | // in parens to eliminate the ambiguity.
466 |
467 | j = eval('(' + text + ')');
468 |
469 | // In the optional fourth stage, we recursively walk the new structure, passing
470 | // each name/value pair to a reviver function for possible transformation.
471 |
472 | return typeof reviver === 'function' ?
473 | walk({'': j}, '') : j;
474 | }
475 |
476 | // If the text is not JSON parseable, then a SyntaxError is thrown.
477 |
478 | throw new SyntaxError('JSON.parse');
479 | };
480 | }
481 | }());
--------------------------------------------------------------------------------
/src/static/js/underscore.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.4.3
2 | // http://underscorejs.org
3 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
4 | // Underscore may be freely distributed under the MIT license.
5 |
6 | (function() {
7 |
8 | // Baseline setup
9 | // --------------
10 |
11 | // Establish the root object, `window` in the browser, or `global` on the server.
12 | var root = this;
13 |
14 | // Save the previous value of the `_` variable.
15 | var previousUnderscore = root._;
16 |
17 | // Establish the object that gets returned to break out of a loop iteration.
18 | var breaker = {};
19 |
20 | // Save bytes in the minified (but not gzipped) version:
21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
22 |
23 | // Create quick reference variables for speed access to core prototypes.
24 | var push = ArrayProto.push,
25 | slice = ArrayProto.slice,
26 | concat = ArrayProto.concat,
27 | toString = ObjProto.toString,
28 | hasOwnProperty = ObjProto.hasOwnProperty;
29 |
30 | // All **ECMAScript 5** native function implementations that we hope to use
31 | // are declared here.
32 | var
33 | nativeForEach = ArrayProto.forEach,
34 | nativeMap = ArrayProto.map,
35 | nativeReduce = ArrayProto.reduce,
36 | nativeReduceRight = ArrayProto.reduceRight,
37 | nativeFilter = ArrayProto.filter,
38 | nativeEvery = ArrayProto.every,
39 | nativeSome = ArrayProto.some,
40 | nativeIndexOf = ArrayProto.indexOf,
41 | nativeLastIndexOf = ArrayProto.lastIndexOf,
42 | nativeIsArray = Array.isArray,
43 | nativeKeys = Object.keys,
44 | nativeBind = FuncProto.bind;
45 |
46 | // Create a safe reference to the Underscore object for use below.
47 | var _ = function(obj) {
48 | if (obj instanceof _) return obj;
49 | if (!(this instanceof _)) return new _(obj);
50 | this._wrapped = obj;
51 | };
52 |
53 | // Export the Underscore object for **Node.js**, with
54 | // backwards-compatibility for the old `require()` API. If we're in
55 | // the browser, add `_` as a global object via a string identifier,
56 | // for Closure Compiler "advanced" mode.
57 | if (typeof exports !== 'undefined') {
58 | if (typeof module !== 'undefined' && module.exports) {
59 | exports = module.exports = _;
60 | }
61 | exports._ = _;
62 | } else {
63 | root._ = _;
64 | }
65 |
66 | // Current version.
67 | _.VERSION = '1.4.3';
68 |
69 | // Collection Functions
70 | // --------------------
71 |
72 | // The cornerstone, an `each` implementation, aka `forEach`.
73 | // Handles objects with the built-in `forEach`, arrays, and raw objects.
74 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
75 | var each = _.each = _.forEach = function(obj, iterator, context) {
76 | if (obj == null) return;
77 | if (nativeForEach && obj.forEach === nativeForEach) {
78 | obj.forEach(iterator, context);
79 | } else if (obj.length === +obj.length) {
80 | for (var i = 0, l = obj.length; i < l; i++) {
81 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
82 | }
83 | } else {
84 | for (var key in obj) {
85 | if (_.has(obj, key)) {
86 | if (iterator.call(context, obj[key], key, obj) === breaker) return;
87 | }
88 | }
89 | }
90 | };
91 |
92 | // Return the results of applying the iterator to each element.
93 | // Delegates to **ECMAScript 5**'s native `map` if available.
94 | _.map = _.collect = function(obj, iterator, context) {
95 | var results = [];
96 | if (obj == null) return results;
97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
98 | each(obj, function(value, index, list) {
99 | results[results.length] = iterator.call(context, value, index, list);
100 | });
101 | return results;
102 | };
103 |
104 | var reduceError = 'Reduce of empty array with no initial value';
105 |
106 | // **Reduce** builds up a single result from a list of values, aka `inject`,
107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
109 | var initial = arguments.length > 2;
110 | if (obj == null) obj = [];
111 | if (nativeReduce && obj.reduce === nativeReduce) {
112 | if (context) iterator = _.bind(iterator, context);
113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
114 | }
115 | each(obj, function(value, index, list) {
116 | if (!initial) {
117 | memo = value;
118 | initial = true;
119 | } else {
120 | memo = iterator.call(context, memo, value, index, list);
121 | }
122 | });
123 | if (!initial) throw new TypeError(reduceError);
124 | return memo;
125 | };
126 |
127 | // The right-associative version of reduce, also known as `foldr`.
128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
130 | var initial = arguments.length > 2;
131 | if (obj == null) obj = [];
132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
133 | if (context) iterator = _.bind(iterator, context);
134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
135 | }
136 | var length = obj.length;
137 | if (length !== +length) {
138 | var keys = _.keys(obj);
139 | length = keys.length;
140 | }
141 | each(obj, function(value, index, list) {
142 | index = keys ? keys[--length] : --length;
143 | if (!initial) {
144 | memo = obj[index];
145 | initial = true;
146 | } else {
147 | memo = iterator.call(context, memo, obj[index], index, list);
148 | }
149 | });
150 | if (!initial) throw new TypeError(reduceError);
151 | return memo;
152 | };
153 |
154 | // Return the first value which passes a truth test. Aliased as `detect`.
155 | _.find = _.detect = function(obj, iterator, context) {
156 | var result;
157 | any(obj, function(value, index, list) {
158 | if (iterator.call(context, value, index, list)) {
159 | result = value;
160 | return true;
161 | }
162 | });
163 | return result;
164 | };
165 |
166 | // Return all the elements that pass a truth test.
167 | // Delegates to **ECMAScript 5**'s native `filter` if available.
168 | // Aliased as `select`.
169 | _.filter = _.select = function(obj, iterator, context) {
170 | var results = [];
171 | if (obj == null) return results;
172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
173 | each(obj, function(value, index, list) {
174 | if (iterator.call(context, value, index, list)) results[results.length] = value;
175 | });
176 | return results;
177 | };
178 |
179 | // Return all the elements for which a truth test fails.
180 | _.reject = function(obj, iterator, context) {
181 | return _.filter(obj, function(value, index, list) {
182 | return !iterator.call(context, value, index, list);
183 | }, context);
184 | };
185 |
186 | // Determine whether all of the elements match a truth test.
187 | // Delegates to **ECMAScript 5**'s native `every` if available.
188 | // Aliased as `all`.
189 | _.every = _.all = function(obj, iterator, context) {
190 | iterator || (iterator = _.identity);
191 | var result = true;
192 | if (obj == null) return result;
193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
194 | each(obj, function(value, index, list) {
195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker;
196 | });
197 | return !!result;
198 | };
199 |
200 | // Determine if at least one element in the object matches a truth test.
201 | // Delegates to **ECMAScript 5**'s native `some` if available.
202 | // Aliased as `any`.
203 | var any = _.some = _.any = function(obj, iterator, context) {
204 | iterator || (iterator = _.identity);
205 | var result = false;
206 | if (obj == null) return result;
207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
208 | each(obj, function(value, index, list) {
209 | if (result || (result = iterator.call(context, value, index, list))) return breaker;
210 | });
211 | return !!result;
212 | };
213 |
214 | // Determine if the array or object contains a given value (using `===`).
215 | // Aliased as `include`.
216 | _.contains = _.include = function(obj, target) {
217 | if (obj == null) return false;
218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
219 | return any(obj, function(value) {
220 | return value === target;
221 | });
222 | };
223 |
224 | // Invoke a method (with arguments) on every item in a collection.
225 | _.invoke = function(obj, method) {
226 | var args = slice.call(arguments, 2);
227 | return _.map(obj, function(value) {
228 | return (_.isFunction(method) ? method : value[method]).apply(value, args);
229 | });
230 | };
231 |
232 | // Convenience version of a common use case of `map`: fetching a property.
233 | _.pluck = function(obj, key) {
234 | return _.map(obj, function(value){ return value[key]; });
235 | };
236 |
237 | // Convenience version of a common use case of `filter`: selecting only objects
238 | // with specific `key:value` pairs.
239 | _.where = function(obj, attrs) {
240 | if (_.isEmpty(attrs)) return [];
241 | return _.filter(obj, function(value) {
242 | for (var key in attrs) {
243 | if (attrs[key] !== value[key]) return false;
244 | }
245 | return true;
246 | });
247 | };
248 |
249 | // Return the maximum element or (element-based computation).
250 | // Can't optimize arrays of integers longer than 65,535 elements.
251 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797
252 | _.max = function(obj, iterator, context) {
253 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
254 | return Math.max.apply(Math, obj);
255 | }
256 | if (!iterator && _.isEmpty(obj)) return -Infinity;
257 | var result = {computed : -Infinity, value: -Infinity};
258 | each(obj, function(value, index, list) {
259 | var computed = iterator ? iterator.call(context, value, index, list) : value;
260 | computed >= result.computed && (result = {value : value, computed : computed});
261 | });
262 | return result.value;
263 | };
264 |
265 | // Return the minimum element (or element-based computation).
266 | _.min = function(obj, iterator, context) {
267 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
268 | return Math.min.apply(Math, obj);
269 | }
270 | if (!iterator && _.isEmpty(obj)) return Infinity;
271 | var result = {computed : Infinity, value: Infinity};
272 | each(obj, function(value, index, list) {
273 | var computed = iterator ? iterator.call(context, value, index, list) : value;
274 | computed < result.computed && (result = {value : value, computed : computed});
275 | });
276 | return result.value;
277 | };
278 |
279 | // Shuffle an array.
280 | _.shuffle = function(obj) {
281 | var rand;
282 | var index = 0;
283 | var shuffled = [];
284 | each(obj, function(value) {
285 | rand = _.random(index++);
286 | shuffled[index - 1] = shuffled[rand];
287 | shuffled[rand] = value;
288 | });
289 | return shuffled;
290 | };
291 |
292 | // An internal function to generate lookup iterators.
293 | var lookupIterator = function(value) {
294 | return _.isFunction(value) ? value : function(obj){ return obj[value]; };
295 | };
296 |
297 | // Sort the object's values by a criterion produced by an iterator.
298 | _.sortBy = function(obj, value, context) {
299 | var iterator = lookupIterator(value);
300 | return _.pluck(_.map(obj, function(value, index, list) {
301 | return {
302 | value : value,
303 | index : index,
304 | criteria : iterator.call(context, value, index, list)
305 | };
306 | }).sort(function(left, right) {
307 | var a = left.criteria;
308 | var b = right.criteria;
309 | if (a !== b) {
310 | if (a > b || a === void 0) return 1;
311 | if (a < b || b === void 0) return -1;
312 | }
313 | return left.index < right.index ? -1 : 1;
314 | }), 'value');
315 | };
316 |
317 | // An internal function used for aggregate "group by" operations.
318 | var group = function(obj, value, context, behavior) {
319 | var result = {};
320 | var iterator = lookupIterator(value || _.identity);
321 | each(obj, function(value, index) {
322 | var key = iterator.call(context, value, index, obj);
323 | behavior(result, key, value);
324 | });
325 | return result;
326 | };
327 |
328 | // Groups the object's values by a criterion. Pass either a string attribute
329 | // to group by, or a function that returns the criterion.
330 | _.groupBy = function(obj, value, context) {
331 | return group(obj, value, context, function(result, key, value) {
332 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
333 | });
334 | };
335 |
336 | // Counts instances of an object that group by a certain criterion. Pass
337 | // either a string attribute to count by, or a function that returns the
338 | // criterion.
339 | _.countBy = function(obj, value, context) {
340 | return group(obj, value, context, function(result, key) {
341 | if (!_.has(result, key)) result[key] = 0;
342 | result[key]++;
343 | });
344 | };
345 |
346 | // Use a comparator function to figure out the smallest index at which
347 | // an object should be inserted so as to maintain order. Uses binary search.
348 | _.sortedIndex = function(array, obj, iterator, context) {
349 | iterator = iterator == null ? _.identity : lookupIterator(iterator);
350 | var value = iterator.call(context, obj);
351 | var low = 0, high = array.length;
352 | while (low < high) {
353 | var mid = (low + high) >>> 1;
354 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
355 | }
356 | return low;
357 | };
358 |
359 | // Safely convert anything iterable into a real, live array.
360 | _.toArray = function(obj) {
361 | if (!obj) return [];
362 | if (_.isArray(obj)) return slice.call(obj);
363 | if (obj.length === +obj.length) return _.map(obj, _.identity);
364 | return _.values(obj);
365 | };
366 |
367 | // Return the number of elements in an object.
368 | _.size = function(obj) {
369 | if (obj == null) return 0;
370 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
371 | };
372 |
373 | // Array Functions
374 | // ---------------
375 |
376 | // Get the first element of an array. Passing **n** will return the first N
377 | // values in the array. Aliased as `head` and `take`. The **guard** check
378 | // allows it to work with `_.map`.
379 | _.first = _.head = _.take = function(array, n, guard) {
380 | if (array == null) return void 0;
381 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
382 | };
383 |
384 | // Returns everything but the last entry of the array. Especially useful on
385 | // the arguments object. Passing **n** will return all the values in
386 | // the array, excluding the last N. The **guard** check allows it to work with
387 | // `_.map`.
388 | _.initial = function(array, n, guard) {
389 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
390 | };
391 |
392 | // Get the last element of an array. Passing **n** will return the last N
393 | // values in the array. The **guard** check allows it to work with `_.map`.
394 | _.last = function(array, n, guard) {
395 | if (array == null) return void 0;
396 | if ((n != null) && !guard) {
397 | return slice.call(array, Math.max(array.length - n, 0));
398 | } else {
399 | return array[array.length - 1];
400 | }
401 | };
402 |
403 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
404 | // Especially useful on the arguments object. Passing an **n** will return
405 | // the rest N values in the array. The **guard**
406 | // check allows it to work with `_.map`.
407 | _.rest = _.tail = _.drop = function(array, n, guard) {
408 | return slice.call(array, (n == null) || guard ? 1 : n);
409 | };
410 |
411 | // Trim out all falsy values from an array.
412 | _.compact = function(array) {
413 | return _.filter(array, _.identity);
414 | };
415 |
416 | // Internal implementation of a recursive `flatten` function.
417 | var flatten = function(input, shallow, output) {
418 | each(input, function(value) {
419 | if (_.isArray(value)) {
420 | shallow ? push.apply(output, value) : flatten(value, shallow, output);
421 | } else {
422 | output.push(value);
423 | }
424 | });
425 | return output;
426 | };
427 |
428 | // Return a completely flattened version of an array.
429 | _.flatten = function(array, shallow) {
430 | return flatten(array, shallow, []);
431 | };
432 |
433 | // Return a version of the array that does not contain the specified value(s).
434 | _.without = function(array) {
435 | return _.difference(array, slice.call(arguments, 1));
436 | };
437 |
438 | // Produce a duplicate-free version of the array. If the array has already
439 | // been sorted, you have the option of using a faster algorithm.
440 | // Aliased as `unique`.
441 | _.uniq = _.unique = function(array, isSorted, iterator, context) {
442 | if (_.isFunction(isSorted)) {
443 | context = iterator;
444 | iterator = isSorted;
445 | isSorted = false;
446 | }
447 | var initial = iterator ? _.map(array, iterator, context) : array;
448 | var results = [];
449 | var seen = [];
450 | each(initial, function(value, index) {
451 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
452 | seen.push(value);
453 | results.push(array[index]);
454 | }
455 | });
456 | return results;
457 | };
458 |
459 | // Produce an array that contains the union: each distinct element from all of
460 | // the passed-in arrays.
461 | _.union = function() {
462 | return _.uniq(concat.apply(ArrayProto, arguments));
463 | };
464 |
465 | // Produce an array that contains every item shared between all the
466 | // passed-in arrays.
467 | _.intersection = function(array) {
468 | var rest = slice.call(arguments, 1);
469 | return _.filter(_.uniq(array), function(item) {
470 | return _.every(rest, function(other) {
471 | return _.indexOf(other, item) >= 0;
472 | });
473 | });
474 | };
475 |
476 | // Take the difference between one array and a number of other arrays.
477 | // Only the elements present in just the first array will remain.
478 | _.difference = function(array) {
479 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
480 | return _.filter(array, function(value){ return !_.contains(rest, value); });
481 | };
482 |
483 | // Zip together multiple lists into a single array -- elements that share
484 | // an index go together.
485 | _.zip = function() {
486 | var args = slice.call(arguments);
487 | var length = _.max(_.pluck(args, 'length'));
488 | var results = new Array(length);
489 | for (var i = 0; i < length; i++) {
490 | results[i] = _.pluck(args, "" + i);
491 | }
492 | return results;
493 | };
494 |
495 | // Converts lists into objects. Pass either a single array of `[key, value]`
496 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
497 | // the corresponding values.
498 | _.object = function(list, values) {
499 | if (list == null) return {};
500 | var result = {};
501 | for (var i = 0, l = list.length; i < l; i++) {
502 | if (values) {
503 | result[list[i]] = values[i];
504 | } else {
505 | result[list[i][0]] = list[i][1];
506 | }
507 | }
508 | return result;
509 | };
510 |
511 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
512 | // we need this function. Return the position of the first occurrence of an
513 | // item in an array, or -1 if the item is not included in the array.
514 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
515 | // If the array is large and already in sort order, pass `true`
516 | // for **isSorted** to use binary search.
517 | _.indexOf = function(array, item, isSorted) {
518 | if (array == null) return -1;
519 | var i = 0, l = array.length;
520 | if (isSorted) {
521 | if (typeof isSorted == 'number') {
522 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
523 | } else {
524 | i = _.sortedIndex(array, item);
525 | return array[i] === item ? i : -1;
526 | }
527 | }
528 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
529 | for (; i < l; i++) if (array[i] === item) return i;
530 | return -1;
531 | };
532 |
533 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
534 | _.lastIndexOf = function(array, item, from) {
535 | if (array == null) return -1;
536 | var hasIndex = from != null;
537 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
538 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
539 | }
540 | var i = (hasIndex ? from : array.length);
541 | while (i--) if (array[i] === item) return i;
542 | return -1;
543 | };
544 |
545 | // Generate an integer Array containing an arithmetic progression. A port of
546 | // the native Python `range()` function. See
547 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
548 | _.range = function(start, stop, step) {
549 | if (arguments.length <= 1) {
550 | stop = start || 0;
551 | start = 0;
552 | }
553 | step = arguments[2] || 1;
554 |
555 | var len = Math.max(Math.ceil((stop - start) / step), 0);
556 | var idx = 0;
557 | var range = new Array(len);
558 |
559 | while(idx < len) {
560 | range[idx++] = start;
561 | start += step;
562 | }
563 |
564 | return range;
565 | };
566 |
567 | // Function (ahem) Functions
568 | // ------------------
569 |
570 | // Reusable constructor function for prototype setting.
571 | var ctor = function(){};
572 |
573 | // Create a function bound to a given object (assigning `this`, and arguments,
574 | // optionally). Binding with arguments is also known as `curry`.
575 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
576 | // We check for `func.bind` first, to fail fast when `func` is undefined.
577 | _.bind = function(func, context) {
578 | var args, bound;
579 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
580 | if (!_.isFunction(func)) throw new TypeError;
581 | args = slice.call(arguments, 2);
582 | return bound = function() {
583 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
584 | ctor.prototype = func.prototype;
585 | var self = new ctor;
586 | ctor.prototype = null;
587 | var result = func.apply(self, args.concat(slice.call(arguments)));
588 | if (Object(result) === result) return result;
589 | return self;
590 | };
591 | };
592 |
593 | // Bind all of an object's methods to that object. Useful for ensuring that
594 | // all callbacks defined on an object belong to it.
595 | _.bindAll = function(obj) {
596 | var funcs = slice.call(arguments, 1);
597 | if (funcs.length === 0) funcs = _.functions(obj);
598 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
599 | return obj;
600 | };
601 |
602 | // Memoize an expensive function by storing its results.
603 | _.memoize = function(func, hasher) {
604 | var memo = {};
605 | hasher || (hasher = _.identity);
606 | return function() {
607 | var key = hasher.apply(this, arguments);
608 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
609 | };
610 | };
611 |
612 | // Delays a function for the given number of milliseconds, and then calls
613 | // it with the arguments supplied.
614 | _.delay = function(func, wait) {
615 | var args = slice.call(arguments, 2);
616 | return setTimeout(function(){ return func.apply(null, args); }, wait);
617 | };
618 |
619 | // Defers a function, scheduling it to run after the current call stack has
620 | // cleared.
621 | _.defer = function(func) {
622 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
623 | };
624 |
625 | // Returns a function, that, when invoked, will only be triggered at most once
626 | // during a given window of time.
627 | _.throttle = function(func, wait) {
628 | var context, args, timeout, result;
629 | var previous = 0;
630 | var later = function() {
631 | previous = new Date;
632 | timeout = null;
633 | result = func.apply(context, args);
634 | };
635 | return function() {
636 | var now = new Date;
637 | var remaining = wait - (now - previous);
638 | context = this;
639 | args = arguments;
640 | if (remaining <= 0) {
641 | clearTimeout(timeout);
642 | timeout = null;
643 | previous = now;
644 | result = func.apply(context, args);
645 | } else if (!timeout) {
646 | timeout = setTimeout(later, remaining);
647 | }
648 | return result;
649 | };
650 | };
651 |
652 | // Returns a function, that, as long as it continues to be invoked, will not
653 | // be triggered. The function will be called after it stops being called for
654 | // N milliseconds. If `immediate` is passed, trigger the function on the
655 | // leading edge, instead of the trailing.
656 | _.debounce = function(func, wait, immediate) {
657 | var timeout, result;
658 | return function() {
659 | var context = this, args = arguments;
660 | var later = function() {
661 | timeout = null;
662 | if (!immediate) result = func.apply(context, args);
663 | };
664 | var callNow = immediate && !timeout;
665 | clearTimeout(timeout);
666 | timeout = setTimeout(later, wait);
667 | if (callNow) result = func.apply(context, args);
668 | return result;
669 | };
670 | };
671 |
672 | // Returns a function that will be executed at most one time, no matter how
673 | // often you call it. Useful for lazy initialization.
674 | _.once = function(func) {
675 | var ran = false, memo;
676 | return function() {
677 | if (ran) return memo;
678 | ran = true;
679 | memo = func.apply(this, arguments);
680 | func = null;
681 | return memo;
682 | };
683 | };
684 |
685 | // Returns the first function passed as an argument to the second,
686 | // allowing you to adjust arguments, run code before and after, and
687 | // conditionally execute the original function.
688 | _.wrap = function(func, wrapper) {
689 | return function() {
690 | var args = [func];
691 | push.apply(args, arguments);
692 | return wrapper.apply(this, args);
693 | };
694 | };
695 |
696 | // Returns a function that is the composition of a list of functions, each
697 | // consuming the return value of the function that follows.
698 | _.compose = function() {
699 | var funcs = arguments;
700 | return function() {
701 | var args = arguments;
702 | for (var i = funcs.length - 1; i >= 0; i--) {
703 | args = [funcs[i].apply(this, args)];
704 | }
705 | return args[0];
706 | };
707 | };
708 |
709 | // Returns a function that will only be executed after being called N times.
710 | _.after = function(times, func) {
711 | if (times <= 0) return func();
712 | return function() {
713 | if (--times < 1) {
714 | return func.apply(this, arguments);
715 | }
716 | };
717 | };
718 |
719 | // Object Functions
720 | // ----------------
721 |
722 | // Retrieve the names of an object's properties.
723 | // Delegates to **ECMAScript 5**'s native `Object.keys`
724 | _.keys = nativeKeys || function(obj) {
725 | if (obj !== Object(obj)) throw new TypeError('Invalid object');
726 | var keys = [];
727 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
728 | return keys;
729 | };
730 |
731 | // Retrieve the values of an object's properties.
732 | _.values = function(obj) {
733 | var values = [];
734 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
735 | return values;
736 | };
737 |
738 | // Convert an object into a list of `[key, value]` pairs.
739 | _.pairs = function(obj) {
740 | var pairs = [];
741 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
742 | return pairs;
743 | };
744 |
745 | // Invert the keys and values of an object. The values must be serializable.
746 | _.invert = function(obj) {
747 | var result = {};
748 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
749 | return result;
750 | };
751 |
752 | // Return a sorted list of the function names available on the object.
753 | // Aliased as `methods`
754 | _.functions = _.methods = function(obj) {
755 | var names = [];
756 | for (var key in obj) {
757 | if (_.isFunction(obj[key])) names.push(key);
758 | }
759 | return names.sort();
760 | };
761 |
762 | // Extend a given object with all the properties in passed-in object(s).
763 | _.extend = function(obj) {
764 | each(slice.call(arguments, 1), function(source) {
765 | if (source) {
766 | for (var prop in source) {
767 | obj[prop] = source[prop];
768 | }
769 | }
770 | });
771 | return obj;
772 | };
773 |
774 | // Return a copy of the object only containing the whitelisted properties.
775 | _.pick = function(obj) {
776 | var copy = {};
777 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
778 | each(keys, function(key) {
779 | if (key in obj) copy[key] = obj[key];
780 | });
781 | return copy;
782 | };
783 |
784 | // Return a copy of the object without the blacklisted properties.
785 | _.omit = function(obj) {
786 | var copy = {};
787 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
788 | for (var key in obj) {
789 | if (!_.contains(keys, key)) copy[key] = obj[key];
790 | }
791 | return copy;
792 | };
793 |
794 | // Fill in a given object with default properties.
795 | _.defaults = function(obj) {
796 | each(slice.call(arguments, 1), function(source) {
797 | if (source) {
798 | for (var prop in source) {
799 | if (obj[prop] == null) obj[prop] = source[prop];
800 | }
801 | }
802 | });
803 | return obj;
804 | };
805 |
806 | // Create a (shallow-cloned) duplicate of an object.
807 | _.clone = function(obj) {
808 | if (!_.isObject(obj)) return obj;
809 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
810 | };
811 |
812 | // Invokes interceptor with the obj, and then returns obj.
813 | // The primary purpose of this method is to "tap into" a method chain, in
814 | // order to perform operations on intermediate results within the chain.
815 | _.tap = function(obj, interceptor) {
816 | interceptor(obj);
817 | return obj;
818 | };
819 |
820 | // Internal recursive comparison function for `isEqual`.
821 | var eq = function(a, b, aStack, bStack) {
822 | // Identical objects are equal. `0 === -0`, but they aren't identical.
823 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
824 | if (a === b) return a !== 0 || 1 / a == 1 / b;
825 | // A strict comparison is necessary because `null == undefined`.
826 | if (a == null || b == null) return a === b;
827 | // Unwrap any wrapped objects.
828 | if (a instanceof _) a = a._wrapped;
829 | if (b instanceof _) b = b._wrapped;
830 | // Compare `[[Class]]` names.
831 | var className = toString.call(a);
832 | if (className != toString.call(b)) return false;
833 | switch (className) {
834 | // Strings, numbers, dates, and booleans are compared by value.
835 | case '[object String]':
836 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
837 | // equivalent to `new String("5")`.
838 | return a == String(b);
839 | case '[object Number]':
840 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
841 | // other numeric values.
842 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
843 | case '[object Date]':
844 | case '[object Boolean]':
845 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
846 | // millisecond representations. Note that invalid dates with millisecond representations
847 | // of `NaN` are not equivalent.
848 | return +a == +b;
849 | // RegExps are compared by their source patterns and flags.
850 | case '[object RegExp]':
851 | return a.source == b.source &&
852 | a.global == b.global &&
853 | a.multiline == b.multiline &&
854 | a.ignoreCase == b.ignoreCase;
855 | }
856 | if (typeof a != 'object' || typeof b != 'object') return false;
857 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
858 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
859 | var length = aStack.length;
860 | while (length--) {
861 | // Linear search. Performance is inversely proportional to the number of
862 | // unique nested structures.
863 | if (aStack[length] == a) return bStack[length] == b;
864 | }
865 | // Add the first object to the stack of traversed objects.
866 | aStack.push(a);
867 | bStack.push(b);
868 | var size = 0, result = true;
869 | // Recursively compare objects and arrays.
870 | if (className == '[object Array]') {
871 | // Compare array lengths to determine if a deep comparison is necessary.
872 | size = a.length;
873 | result = size == b.length;
874 | if (result) {
875 | // Deep compare the contents, ignoring non-numeric properties.
876 | while (size--) {
877 | if (!(result = eq(a[size], b[size], aStack, bStack))) break;
878 | }
879 | }
880 | } else {
881 | // Objects with different constructors are not equivalent, but `Object`s
882 | // from different frames are.
883 | var aCtor = a.constructor, bCtor = b.constructor;
884 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
885 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
886 | return false;
887 | }
888 | // Deep compare objects.
889 | for (var key in a) {
890 | if (_.has(a, key)) {
891 | // Count the expected number of properties.
892 | size++;
893 | // Deep compare each member.
894 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
895 | }
896 | }
897 | // Ensure that both objects contain the same number of properties.
898 | if (result) {
899 | for (key in b) {
900 | if (_.has(b, key) && !(size--)) break;
901 | }
902 | result = !size;
903 | }
904 | }
905 | // Remove the first object from the stack of traversed objects.
906 | aStack.pop();
907 | bStack.pop();
908 | return result;
909 | };
910 |
911 | // Perform a deep comparison to check if two objects are equal.
912 | _.isEqual = function(a, b) {
913 | return eq(a, b, [], []);
914 | };
915 |
916 | // Is a given array, string, or object empty?
917 | // An "empty" object has no enumerable own-properties.
918 | _.isEmpty = function(obj) {
919 | if (obj == null) return true;
920 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
921 | for (var key in obj) if (_.has(obj, key)) return false;
922 | return true;
923 | };
924 |
925 | // Is a given value a DOM element?
926 | _.isElement = function(obj) {
927 | return !!(obj && obj.nodeType === 1);
928 | };
929 |
930 | // Is a given value an array?
931 | // Delegates to ECMA5's native Array.isArray
932 | _.isArray = nativeIsArray || function(obj) {
933 | return toString.call(obj) == '[object Array]';
934 | };
935 |
936 | // Is a given variable an object?
937 | _.isObject = function(obj) {
938 | return obj === Object(obj);
939 | };
940 |
941 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
942 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
943 | _['is' + name] = function(obj) {
944 | return toString.call(obj) == '[object ' + name + ']';
945 | };
946 | });
947 |
948 | // Define a fallback version of the method in browsers (ahem, IE), where
949 | // there isn't any inspectable "Arguments" type.
950 | if (!_.isArguments(arguments)) {
951 | _.isArguments = function(obj) {
952 | return !!(obj && _.has(obj, 'callee'));
953 | };
954 | }
955 |
956 | // Optimize `isFunction` if appropriate.
957 | if (typeof (/./) !== 'function') {
958 | _.isFunction = function(obj) {
959 | return typeof obj === 'function';
960 | };
961 | }
962 |
963 | // Is a given object a finite number?
964 | _.isFinite = function(obj) {
965 | return isFinite(obj) && !isNaN(parseFloat(obj));
966 | };
967 |
968 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
969 | _.isNaN = function(obj) {
970 | return _.isNumber(obj) && obj != +obj;
971 | };
972 |
973 | // Is a given value a boolean?
974 | _.isBoolean = function(obj) {
975 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
976 | };
977 |
978 | // Is a given value equal to null?
979 | _.isNull = function(obj) {
980 | return obj === null;
981 | };
982 |
983 | // Is a given variable undefined?
984 | _.isUndefined = function(obj) {
985 | return obj === void 0;
986 | };
987 |
988 | // Shortcut function for checking if an object has a given property directly
989 | // on itself (in other words, not on a prototype).
990 | _.has = function(obj, key) {
991 | return hasOwnProperty.call(obj, key);
992 | };
993 |
994 | // Utility Functions
995 | // -----------------
996 |
997 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
998 | // previous owner. Returns a reference to the Underscore object.
999 | _.noConflict = function() {
1000 | root._ = previousUnderscore;
1001 | return this;
1002 | };
1003 |
1004 | // Keep the identity function around for default iterators.
1005 | _.identity = function(value) {
1006 | return value;
1007 | };
1008 |
1009 | // Run a function **n** times.
1010 | _.times = function(n, iterator, context) {
1011 | var accum = Array(n);
1012 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
1013 | return accum;
1014 | };
1015 |
1016 | // Return a random integer between min and max (inclusive).
1017 | _.random = function(min, max) {
1018 | if (max == null) {
1019 | max = min;
1020 | min = 0;
1021 | }
1022 | return min + (0 | Math.random() * (max - min + 1));
1023 | };
1024 |
1025 | // List of HTML entities for escaping.
1026 | var entityMap = {
1027 | escape: {
1028 | '&': '&',
1029 | '<': '<',
1030 | '>': '>',
1031 | '"': '"',
1032 | "'": ''',
1033 | '/': '/'
1034 | }
1035 | };
1036 | entityMap.unescape = _.invert(entityMap.escape);
1037 |
1038 | // Regexes containing the keys and values listed immediately above.
1039 | var entityRegexes = {
1040 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
1041 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
1042 | };
1043 |
1044 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1045 | _.each(['escape', 'unescape'], function(method) {
1046 | _[method] = function(string) {
1047 | if (string == null) return '';
1048 | return ('' + string).replace(entityRegexes[method], function(match) {
1049 | return entityMap[method][match];
1050 | });
1051 | };
1052 | });
1053 |
1054 | // If the value of the named property is a function then invoke it;
1055 | // otherwise, return it.
1056 | _.result = function(object, property) {
1057 | if (object == null) return null;
1058 | var value = object[property];
1059 | return _.isFunction(value) ? value.call(object) : value;
1060 | };
1061 |
1062 | // Add your own custom functions to the Underscore object.
1063 | _.mixin = function(obj) {
1064 | each(_.functions(obj), function(name){
1065 | var func = _[name] = obj[name];
1066 | _.prototype[name] = function() {
1067 | var args = [this._wrapped];
1068 | push.apply(args, arguments);
1069 | return result.call(this, func.apply(_, args));
1070 | };
1071 | });
1072 | };
1073 |
1074 | // Generate a unique integer id (unique within the entire client session).
1075 | // Useful for temporary DOM ids.
1076 | var idCounter = 0;
1077 | _.uniqueId = function(prefix) {
1078 | var id = '' + (++idCounter);
1079 | return prefix ? prefix + id : id;
1080 | };
1081 |
1082 | // By default, Underscore uses ERB-style template delimiters, change the
1083 | // following template settings to use alternative delimiters.
1084 | _.templateSettings = {
1085 | evaluate : /<%([\s\S]+?)%>/g,
1086 | interpolate : /<%=([\s\S]+?)%>/g,
1087 | escape : /<%-([\s\S]+?)%>/g
1088 | };
1089 |
1090 | // When customizing `templateSettings`, if you don't want to define an
1091 | // interpolation, evaluation or escaping regex, we need one that is
1092 | // guaranteed not to match.
1093 | var noMatch = /(.)^/;
1094 |
1095 | // Certain characters need to be escaped so that they can be put into a
1096 | // string literal.
1097 | var escapes = {
1098 | "'": "'",
1099 | '\\': '\\',
1100 | '\r': 'r',
1101 | '\n': 'n',
1102 | '\t': 't',
1103 | '\u2028': 'u2028',
1104 | '\u2029': 'u2029'
1105 | };
1106 |
1107 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
1108 |
1109 | // JavaScript micro-templating, similar to John Resig's implementation.
1110 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1111 | // and correctly escapes quotes within interpolated code.
1112 | _.template = function(text, data, settings) {
1113 | var render;
1114 | settings = _.defaults({}, settings, _.templateSettings);
1115 |
1116 | // Combine delimiters into one regular expression via alternation.
1117 | var matcher = new RegExp([
1118 | (settings.escape || noMatch).source,
1119 | (settings.interpolate || noMatch).source,
1120 | (settings.evaluate || noMatch).source
1121 | ].join('|') + '|$', 'g');
1122 |
1123 | // Compile the template source, escaping string literals appropriately.
1124 | var index = 0;
1125 | var source = "__p+='";
1126 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1127 | source += text.slice(index, offset)
1128 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
1129 |
1130 | if (escape) {
1131 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1132 | }
1133 | if (interpolate) {
1134 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1135 | }
1136 | if (evaluate) {
1137 | source += "';\n" + evaluate + "\n__p+='";
1138 | }
1139 | index = offset + match.length;
1140 | return match;
1141 | });
1142 | source += "';\n";
1143 |
1144 | // If a variable is not specified, place data values in local scope.
1145 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1146 |
1147 | source = "var __t,__p='',__j=Array.prototype.join," +
1148 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1149 | source + "return __p;\n";
1150 |
1151 | try {
1152 | render = new Function(settings.variable || 'obj', '_', source);
1153 | } catch (e) {
1154 | e.source = source;
1155 | throw e;
1156 | }
1157 |
1158 | if (data) return render(data, _);
1159 | var template = function(data) {
1160 | return render.call(this, data, _);
1161 | };
1162 |
1163 | // Provide the compiled function source as a convenience for precompilation.
1164 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
1165 |
1166 | return template;
1167 | };
1168 |
1169 | // Add a "chain" function, which will delegate to the wrapper.
1170 | _.chain = function(obj) {
1171 | return _(obj).chain();
1172 | };
1173 |
1174 | // OOP
1175 | // ---------------
1176 | // If Underscore is called as a function, it returns a wrapped object that
1177 | // can be used OO-style. This wrapper holds altered versions of all the
1178 | // underscore functions. Wrapped objects may be chained.
1179 |
1180 | // Helper function to continue chaining intermediate results.
1181 | var result = function(obj) {
1182 | return this._chain ? _(obj).chain() : obj;
1183 | };
1184 |
1185 | // Add all of the Underscore functions to the wrapper object.
1186 | _.mixin(_);
1187 |
1188 | // Add all mutator Array functions to the wrapper.
1189 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1190 | var method = ArrayProto[name];
1191 | _.prototype[name] = function() {
1192 | var obj = this._wrapped;
1193 | method.apply(obj, arguments);
1194 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
1195 | return result.call(this, obj);
1196 | };
1197 | });
1198 |
1199 | // Add all accessor Array functions to the wrapper.
1200 | each(['concat', 'join', 'slice'], function(name) {
1201 | var method = ArrayProto[name];
1202 | _.prototype[name] = function() {
1203 | return result.call(this, method.apply(this._wrapped, arguments));
1204 | };
1205 | });
1206 |
1207 | _.extend(_.prototype, {
1208 |
1209 | // Start chaining a wrapped Underscore object.
1210 | chain: function() {
1211 | this._chain = true;
1212 | return this;
1213 | },
1214 |
1215 | // Extracts the result from a wrapped and chained object.
1216 | value: function() {
1217 | return this._wrapped;
1218 | }
1219 |
1220 | });
1221 |
1222 | }).call(this);
1223 |
--------------------------------------------------------------------------------
/src/static/fonts/basic.icons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
451 |
--------------------------------------------------------------------------------