-
53 |
-
57 | 58 | 59 | 60 | 61 |62 | 68 |
69 |
├── LICENSE ├── index.html └── app.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 LeanCloud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |85 | Logged in as {{user.username}} (Logout) 86 |
87 | 88 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | AV.init({
2 | appId: 'ozewwcwsyq92g2hommuxqrqzg6847wgl8dtrac6suxzko333',
3 | appKey: 'ni0kwg7h8hwtz6a7dw9ipr7ayk989zo5y8t0sn5gjiel6uav',
4 | serverURL: 'https://ozewwcws.lc-cn-n1-shared.com',
5 | });
6 |
7 | const Todo = AV.Object.extend('Todo');
8 |
9 | // visibility filters
10 | const filters = {
11 | all: (todos) => todos,
12 | active: (todos) => todos.filter((todo) => !todo.done),
13 | completed: (todos) => todos.filter((todo) => todo.done),
14 | };
15 |
16 | // app Vue instance
17 | const app = new Vue({
18 | // app initial state
19 | data: {
20 | todos: [],
21 | content: '',
22 | editingId: null,
23 | editingContent: '',
24 | visibility: 'all',
25 | username: '',
26 | password: '',
27 | user: null,
28 | },
29 |
30 | mounted() {
31 | onHashChange();
32 | if (AV.User.current()) {
33 | this.user = AV.User.current().toJSON();
34 | this.fetchTodos();
35 | }
36 | },
37 |
38 | beforeDestroy() {
39 | if (this.unbind) {
40 | this.unbind();
41 | }
42 | },
43 |
44 | // computed properties
45 | // https://vuejs.org/guide/computed.html
46 | computed: {
47 | filteredTodos() {
48 | return filters[this.visibility](this.todos);
49 | },
50 | remaining() {
51 | return filters.active(this.todos).length;
52 | },
53 | },
54 |
55 | filters: {
56 | pluralize(n) {
57 | return n === 1 ? 'item' : 'items';
58 | },
59 | },
60 |
61 | // methods that implement data logic.
62 | // note there's no DOM manipulation here at all.
63 | methods: {
64 | upsertTodo(todo) {
65 | for (let i = 0; i < this.todos.length; i++) {
66 | if (this.todos[i].objectId === todo.objectId) {
67 | this.$set(this.todos, i, todo);
68 | return;
69 | }
70 | }
71 | this.todos.unshift(todo);
72 | },
73 |
74 | removeTodo(todo) {
75 | for (let i = 0; i < this.todos.length; i++) {
76 | if (this.todos[i].objectId === todo.objectId) {
77 | this.$delete(this.todos, i);
78 | break;
79 | }
80 | }
81 | },
82 |
83 | handleCreateTodo() {
84 | const content = this.content.trim();
85 | if (!content) return;
86 | this.content = '';
87 |
88 | this.createTodoObject(content).then((todoObject) => {
89 | this.upsertTodo({ objectId: todoObject.id, done: false, content });
90 | });
91 | },
92 |
93 | handleEditTodo(todo) {
94 | this.editingId = todo.objectId;
95 | this.editingContent = todo.content;
96 | },
97 |
98 | handleFinishEditTodo(todo) {
99 | this.editingId = null;
100 | const content = this.editingContent.trim();
101 | if (content === todo.content) {
102 | return;
103 | }
104 | if (content) {
105 | todo.content = content;
106 | this.upsertTodo(todo);
107 | this.updateTodoObject(todo.objectId, { content: todo.content });
108 | } else {
109 | this.removeTodo(todo);
110 | this.removeTodoObject(todo.objectId);
111 | }
112 | },
113 |
114 | handleToggleDone(todo) {
115 | this.upsertTodo(todo);
116 | this.updateTodoObject(todo.objectId, { done: todo.done });
117 | },
118 |
119 | handleRemoveTodo(todo) {
120 | this.removeTodo(todo);
121 | this.removeTodoObject(todo.objectId);
122 | },
123 |
124 | handleRemoveCompleted() {
125 | const completed = filters.completed(this.todos);
126 | this.todos = filters.active(this.todos);
127 | this.removeTodoObject(completed.map((todo) => todo.objectId));
128 | },
129 |
130 | handleSignUp() {
131 | AV.User.signUp(this.username, this.password)
132 | .then((user) => {
133 | this.user = user.toJSON();
134 | this.username = '';
135 | this.password = '';
136 | })
137 | .catch(displayError);
138 | },
139 |
140 | handleLogin() {
141 | AV.User.logIn(this.username, this.password)
142 | .then((user) => {
143 | this.user = user.toJSON();
144 | this.username = '';
145 | this.password = '';
146 | this.fetchTodos();
147 | })
148 | .catch(displayError);
149 | },
150 |
151 | handleLogout() {
152 | AV.User.logOut();
153 | this.user = null;
154 | if (this.unbind) {
155 | this.unbind();
156 | }
157 | },
158 |
159 | async fetchTodos() {
160 | const query = new AV.Query(Todo)
161 | .equalTo('user', AV.User.current())
162 | .descending('createdAt');
163 | try {
164 | const todoObjects = await query.find();
165 | this.todos = todoObjects.map((todoObj) => todoObj.toJSON());
166 |
167 | if (this.unbind) {
168 | return;
169 | }
170 | const liveQuery = await query.subscribe();
171 | const upsert = (todoObject) => this.upsertTodo(todoObject.toJSON());
172 | const remove = (todoObject) => this.removeTodo(todoObject.toJSON());
173 | liveQuery.on('create', upsert);
174 | liveQuery.on('update', upsert);
175 | liveQuery.on('enter', upsert);
176 | liveQuery.on('leave', remove);
177 | liveQuery.on('delete', remove);
178 | this.unbind = () => {
179 | liveQuery.off('create', upsert);
180 | liveQuery.off('update', upsert);
181 | liveQuery.off('enter', upsert);
182 | liveQuery.off('leave', remove);
183 | liveQuery.off('delete', remove);
184 | liveQuery.unsubscribe();
185 | };
186 | } catch (error) {
187 | displayError(error);
188 | }
189 | },
190 |
191 | async createTodoObject(content) {
192 | const acl = new AV.ACL();
193 | acl.setReadAccess(AV.User.current(), true);
194 | acl.setWriteAccess(AV.User.current(), true);
195 | try {
196 | const todo = new Todo({
197 | content,
198 | done: false,
199 | user: AV.User.current(),
200 | });
201 | todo.setACL(acl);
202 | return todo.save();
203 | } catch (error) {
204 | displayError(error);
205 | }
206 | },
207 |
208 | async updateTodoObject(objectId, { content, done } = {}) {
209 | try {
210 | const todo = AV.Object.createWithoutData('Todo', objectId);
211 | await todo.save({ content, done });
212 | } catch (error) {
213 | displayError(error);
214 | }
215 | },
216 |
217 | async removeTodoObject(objectId) {
218 | try {
219 | if (Array.isArray(objectId)) {
220 | const todos = objectId.map((id) =>
221 | AV.Object.createWithoutData('Todo', id)
222 | );
223 | await AV.Object.destroyAll(todos);
224 | } else {
225 | const todo = AV.Object.createWithoutData('Todo', objectId);
226 | await todo.destroy();
227 | }
228 | } catch (error) {
229 | displayError(error);
230 | }
231 | },
232 | },
233 |
234 | // a custom directive to wait for the DOM to be updated
235 | // before focusing on the input field.
236 | // https://vuejs.org/guide/custom-directive.html
237 | directives: {
238 | 'todo-focus': function (el, value) {
239 | if (value) {
240 | el.focus();
241 | }
242 | },
243 | },
244 | });
245 |
246 | function displayError(error) {
247 | console.error(error);
248 | if (error instanceof Error) {
249 | if (error.error) {
250 | alert(error.error); // API Error
251 | } else {
252 | alert(error.message);
253 | }
254 | }
255 | }
256 |
257 | function onHashChange() {
258 | const visibility = window.location.hash.replace(/#\/?/, '');
259 | if (filters[visibility]) {
260 | app.visibility = visibility;
261 | } else {
262 | app.visibility = 'all';
263 | window.location.hash = '';
264 | }
265 | }
266 |
267 | window.addEventListener('hashchange', onHashChange);
268 |
269 | // mount
270 | app.$mount('.todoapp');
271 |
--------------------------------------------------------------------------------