-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmain.js
203 lines (167 loc) · 6.05 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"use strict";
//|=================================|//
// IF YOU ARE READING THIS I HOPE //
// YOU ARE HAVING A NICE DAY :) //
// PLEASE LET ME KNOW IF YOU USE //
// THIS DOCUMENT FOR WHATEVER REASON //
// mrluissan@gmail.com //
//|=================================|//
// DOM elements
const todo_input = document.getElementById('to-do');
const todos_wrapper = document.getElementById('todos');
const todos_header = document.getElementById('header');
// Buttons
const add_todo_btn = document.getElementById('add-todo');
const delete_todo_btn = document.querySelectorAll('.delete');
// TODOS container
// Retrieve the stored (localStorage) content or initialize an empty array
let todos_container = localStorage.todos !== undefined ? JSON.parse(localStorage.todos) : [];
// Let's create a beautiful and encapsulated module :)
const todos = (() => {
//|==========|//
// Properties //
//|==========|//
const addAnimDuration = 165;
const rmAnimDuration = 400;
//|=======|//
// Methods //
//|=======|//
// Checks if an input is empty (maybe this fn is not necessary?)
const isEmpty = (input) => input !== "" ? false : true;
// Clear the todo input
const clearInput = (input) => input.value = "";
// Get the id of a todo
const getTodoId = e => parseInt(e.target.parentElement.dataset.id);
// Return the index of a todo from the id of a todo
const search = id => todos_container.indexOf(todos_container.find(todo => todo.id === id));
// Syncronize the living stored todos with the offline storage
const syncStorage = () => localStorage.setItem('todos', JSON.stringify(todos_container));
// Escape html tags
const sanitize = (string) => {
return String(string)
.replace(/&/g, '&apm;')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
// Look for potential urls in the todo
const makeUrls = (string) => {
const pattern = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/;
return string.replace(pattern, '<a target="_blank" href="$&">$&</a>');
};
// Make todos old, because we don't want to end the animation party
const makeOld = () => {
todos_container.forEach(todo => {
todo.new = false;
});
syncStorage();
};
// Scroll to the end of the todos todos_wrapper
const scrollBot = () => {
todos_wrapper.scrollTop = todos_wrapper.scrollHeight;
};
// Add todo
const add = () => {
const input = makeUrls(sanitize(todo_input.value));
if (!isEmpty(input)) {
const todo = {
content: input,
id: todos_container.length,
// By being new the render method will aplly an animation to this new-brand todo
new: true
};
todos_container.push(todo);
clearInput(todo_input);
syncStorage();
render()
.then(makeOld)
.then(scrollBot);
}
};
// Updates a todo
const update = (e) => {
todos_container[search(getTodoId(e))].content = e.target.textContent;
e.target.contentEditable = false;
syncStorage();
};
// Delete todo
const remove = (e) => {
return new Promise((resolve, reject) => {
todos_container.splice(search(getTodoId(e)), 1);
e.target.parentElement.classList.add('beingRemoved');
setTimeout(resolve, rmAnimDuration);
})
.then(render)
.then(syncStorage);
};
const render = () => {
return new Promise((resolve, reject) => {
let toBeAdded = '';
todos_container.forEach(todo => {
let todoHTML = `
<div class="todo card ${todo.new ? 'new-todo' : ''}" data-id="${todo.id}">
<div class="todo-content">${todo.content}</div><button class="delete">X</button>
</div>
`;
toBeAdded += todoHTML;
});
todos_wrapper.innerHTML = toBeAdded.length > 0 ? toBeAdded : `<p id="freetime">There is nothing to do!<br>Enjoy your free time :)</p>`;
setTimeout(() => {
if (document.querySelector('.new-todo') !== null) {
document.querySelector('.new-todo').classList.remove('new-todo');
makeOld();
}
}, addAnimDuration);
resolve();
});
};
return {
add: add,
remove: remove,
update: update,
init: render,
search: search
};
})();
// Initialize the application
todos.init();
//|=====================|//
// Hook up the listeners //
//|=====================|//
// Add todo by clicking the + button
add_todo_btn.addEventListener('click', todos.add);
// Add todo by pressing Enter
todo_input.addEventListener('keyup', e => {
if (e.keyCode === 13) {
todos.add();
}
});
// Delete a todo by clicking on a dynamically created delete button
todos_wrapper.addEventListener('click', event => {
if (event.target.classList.contains('delete')) {
todos.remove(event);
}
});
// Updates a todo by double clicking inside a todo-card
todos_wrapper.addEventListener('dblclick', event => {
if (event.target.classList.contains('todo-content')) {
event.target.contentEditable = true;
// Run the update method by focusing out of the editable todo
document.querySelectorAll('.todo-content[contenteditable=true]')
.forEach(todo => todo.addEventListener('blur', todos.update));
}
});
// Add a shadow to the header if scrolled
todos_wrapper.addEventListener('scroll', () => {
if (todos_wrapper.scrollTop > 16) {
todos_header.classList.add('header-scrolled');
} else {
todos_header.classList.remove('header-scrolled');
}
});
(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/todojs/service_worker.js');
}
})();