-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.js
executable file
·598 lines (497 loc) · 18 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
document.onmousemove = snapNote; //Snap selected note to the mouse whenever the mouse moves
document.ontouchmove = snapNoteTouch; //Logic for snapping to touch is slightly different than mouse
document.onmouseup = placeNote; //When the mouse comes up, place the selected note where the user chose
document.ontouchend = placeNote; //When the touch is released, place the note
document.onmousedown = clearMenus; //Clear menus when the mouse is clicked to the side
window.addEventListener('load', loadNotes); // When document has loaded, load any notes that are in local storage
if ('onvisibilitychange' in document)
document.addEventListener('visibilitychange', storeNotes);
else window.addEventListener('pagehide', storeNotes);
let notesCount = 0; //Used to give a unique id to each note
/**
* addNote creates a new sticky note and adds it to the document.
*/
function addNote(title = '', content = '', color = '') {
console.log('Add button pressed');
//Create note container
let note = document.createElement('div');
note.onmousedown = selectNote;
note.ontouchstart = selectNote;
note.className = 'note';
note.style.backgroundColor = color;
//Create text input for note title
let titleInput = document.createElement('textarea');
titleInput.placeholder = 'Title';
titleInput.className = 'note-title';
titleInput.onkeydown = keyDown;
titleInput.value = title;
note.appendChild(titleInput);
//Create text box for the content of the note
let textBox = document.createElement('textarea');
textBox.placeholder = 'Write your note here';
textBox.className = 'note-content';
textBox.onkeydown = keyDown;
textBox.value = content;
note.appendChild(textBox);
//Create the option button for the note
let optionButton = document.createElement('button');
optionButton.className = 'option-button';
optionButton.textContent = '. . .';
optionButton.onmousedown = noteMenu;
optionButton.ontouchstart = noteMenu;
note.appendChild(optionButton);
note.id = ++notesCount;
document.body.appendChild(note); //Add the note to the document
titleInput.focus(); //Set focus to the title of the new note
}
let selectedNote = null; //The note the user clicks on to move around
/**
* selectNote sets the selected note to the one the user clicks on.
*/
function selectNote() {
selectedNote = this;
}
let noteCopy = {}; // A copy of the note that is what will actually be moved
let mouseDidMove = false; // Whether or not the mouse has moved enough to constitute moving the note
let currentSwap = null; // The note most recently swapped with the selected note
/**
* snapNote snaps the selected note to the mouse position and swaps
* notes when the user hovers the selected note over another note.
* @param {MouseEvent} event
*/
function snapNote(event) {
if (selectedNote !== null) {
// Check that there is a selected note
let mouseMovement = Math.sqrt(event.movementX ** 2 + event.movementY ** 2); // The total distance the mouse moved
if (!mouseDidMove && mouseMovement > 4) {
// Check that the mouse has moved a reasonable distance
console.log('Mouse moved');
selectedNote.style.visibility = 'hidden'; // Hide the actual selected note
currentSwap = selectedNote;
noteCopy = copyNote(selectedNote); // Make a copy of the selected note to move around
noteCopy.style.position = 'fixed';
document.body.appendChild(noteCopy); // Add the copy to the document
//Snap the note to the mouse position
noteCopy.style.top = event.clientY - noteCopy.offsetHeight / 2 + 'px';
noteCopy.style.left = event.clientX - noteCopy.offsetWidth / 2 + 'px';
mouseDidMove = true;
} else if (mouseDidMove) {
// Snap note to the mouse position
noteCopy.style.top = event.clientY - noteCopy.offsetHeight / 2 + 'px';
noteCopy.style.left = event.clientX - noteCopy.offsetWidth / 2 + 'px';
let notes = document.getElementsByClassName('note'); // Get all the notes in the document
for (let i = 0; i < notes.length; i++) {
// Loop through the notes
let rect = notes[i].getBoundingClientRect(); // Get the bounding rectangle to know the positon of the note
// Swap the notes if appropriate
if (
currentSwap !== null &&
!noteCopy.id.includes(notes[i].id) &&
notes[i].id !== currentSwap.id
) {
// Make sure the note is a different note
if (
event.clientX > rect.left &&
event.clientX < rect.right &&
event.clientY > rect.top &&
event.clientY < rect.bottom
) {
// Check if the mouse is over this note
if (notes[i].style.position !== 'fixed') {
// Check if note is being animated
console.log('Selected: ' + noteCopy.id);
console.log('Swap with: ' + notes[i].id);
// Gather old notes positions for animating swap
let oldRects = new Map(); //Map for old note positions for animating
for (let i = 0; i < notes.length; i++) {
if (!notes[i].id.includes('copy')) {
let oldRect = notes[i].getBoundingClientRect();
oldRects.set(notes[i].id, oldRect);
}
}
currentSwap.style.visibility = 'visible'; // Make the old swap visible
checkOverflow(currentSwap.children[1]); // Resize the text box if necessary
swapNotes(selectedNote, currentSwap); //Undo previous swap
currentSwap = notes[i]; //Update currentSwap
swapNotes(selectedNote, currentSwap); //Perform new swap
currentSwap.style.visibility = 'hidden'; //Hide the new swap
animateReorder(oldRects, 300);
}
} //End if
} //End if
} //End for
} //End else
} //End if
} //End snapNote
/**
* snapNoteTouch snaps the selected note to the touch position and swaps
* notes when the user hovers the selected note over another note.
* @param {TouchEvent} event
*/
function snapNoteTouch(event) {
if (selectNote !== null) {
if (!mouseDidMove) {
// Check that the mouse has moved a reasonable distance
console.log('Mouse moved');
selectedNote.style.visibility = 'hidden'; // Hide the actual selected note
currentSwap = selectedNote;
noteCopy = copyNote(selectedNote); // Make a copy of the selected note to move around
noteCopy.style.position = 'fixed';
document.body.appendChild(noteCopy); // Add the copy to the document
//Snap the note to the mouse position
noteCopy.style.top =
event.touches[0].clientY - noteCopy.offsetHeight / 2 + 'px';
noteCopy.style.left =
event.touches[0].clientX - noteCopy.offsetWidth / 2 + 'px';
mouseDidMove = true;
} else if (mouseDidMove) {
// Snap note to the mouse position
noteCopy.style.top =
event.touches[0].clientY - noteCopy.offsetHeight / 2 + 'px';
noteCopy.style.left =
event.touches[0].clientX - noteCopy.offsetWidth / 2 + 'px';
let notes = document.getElementsByClassName('note'); // Get all the notes in the document
for (let i = 0; i < notes.length; i++) {
// Loop through the notes
let rect = notes[i].getBoundingClientRect(); // Get the bounding rectangle to know the positon of the note
// Swap the notes if appropriate
if (
currentSwap !== null &&
!noteCopy.id.includes(notes[i].id) &&
notes[i].id !== currentSwap.id
) {
// Make sure the note is a different note
if (
event.touches[0].clientX > rect.left &&
event.touches[0].clientX < rect.right &&
event.touches[0].clientY > rect.top &&
event.touches[0].clientY < rect.bottom
) {
// Check if the mouse is over this note
if (notes[i].style.position !== 'fixed') {
console.log('Selected: ' + noteCopy.id);
console.log('Swap with: ' + notes[i].id);
// Gather old notes positions for animating swap
let oldRects = new Map(); //Map for old note positions for animating
for (let i = 0; i < notes.length; i++) {
if (!notes[i].id.includes('copy')) {
let oldRect = notes[i].getBoundingClientRect();
oldRects.set(notes[i].id, oldRect);
}
}
currentSwap.style.visibility = 'visible'; // Make the old swap visible
checkOverflow(currentSwap.children[1]); // Resize the text box if necessary
swapNotes(selectedNote, currentSwap); //Undo previous swap
currentSwap = notes[i]; //Update currentSwap
swapNotes(selectedNote, currentSwap); //Perform new swap
currentSwap.style.visibility = 'hidden'; //Hide the new swap
animateReorder(oldRects, 300);
}
} //End if
} //End if
} //End for
} //End else
} //End If
} //End snapNoteTouch
/**
* placeNote places the selected note down in the proper location.
*/
function placeNote() {
if (selectedNote !== null) {
//Check if there is a note selected
selectedNote.style.visibility = 'visible';
checkOverflow(selectedNote.children[1]);
selectedNote = null;
if (mouseDidMove) {
noteCopy.remove();
mouseDidMove = false;
}
if (currentSwap !== null) {
currentSwap.style.visibility = 'visible';
checkOverflow(currentSwap.children[1]);
currentSwap = null;
}
}
}
/**
* swapNotes swaps the content and appropriate properties of each note.
* @param {HTMLDivElement} note1 The first note to swap
* @param {HTMLDivElement} note2 The second note to swap
*/
function swapNotes(note1, note2) {
//Save note1 values
let title1 = note1.children[0].value;
let content1 = note1.children[1].value;
let id1 = note1.id;
let height1 = note1.children[1].style.height;
let color1 = note1.style.backgroundColor;
//Update note1 values
note1.children[0].value = note2.children[0].value;
note1.children[1].value = note2.children[1].value;
note1.children[1].style.height = note2.children[1].style.height;
note1.id = note2.id;
note1.style.backgroundColor = note2.style.backgroundColor;
//Update note2 values
note2.children[0].value = title1;
note2.children[1].value = content1;
note2.children[1].style.height = height1;
note2.id = id1;
note2.style.backgroundColor = color1;
}
/**
* copyNote copies the content and appropriate properties of a note and returns the copy.
* @param {HTMLDivElement} originalNote
* @returns {HTMLDivElement} The copy of the original note
*/
function copyNote(originalNote) {
let noteCopy = document.createElement('div');
noteCopy.className = 'note';
noteCopy.innerHTML = originalNote.innerHTML;
noteCopy.children[0].value = originalNote.children[0].value;
noteCopy.children[1].value = originalNote.children[1].value;
noteCopy.id = originalNote.id + 'copy';
let color = originalNote.style.backgroundColor;
noteCopy.style.backgroundColor = color;
noteCopy.style.animationName = 'none'; //Remove fade-in animation
return noteCopy;
}
/**
* keyDown checks the overflow of note text boxes when a key is pressed.
*/
function keyDown() {
checkOverflow(this);
}
/**
* checkOverflow checks if a note text box needs to be resized to fit its text.
* @param {HTMLTextAreaElement} textBox
*/
function checkOverflow(textBox) {
textBox.style.height = '';
while (textBox.scrollHeight > textBox.clientHeight) {
textBox.style.height = textBox.clientHeight + 2 + 'px';
}
}
/**
* noteMenu creates the note options menu.
*/
function noteMenu() {
console.log('option button pressed');
let menus = document.getElementsByClassName('note-menu'); // Get all menus
for (let i = 0; i < menus.length; i++) {
menus[i].remove();
}
let noteMenu = document.createElement('div');
noteMenu.className = 'note-menu';
let colors = [
// Nine different note colors
'lightgoldenrodyellow',
'lightblue',
'lightgreen',
'lightpink',
'lightcoral',
'lightskyblue',
'lightsalmon',
'plum',
'lightseagreen',
];
// Create nine different color buttons
colors.forEach((color) => {
let colorOption = document.createElement('button');
colorOption.className = 'color-option';
colorOption.style.backgroundColor = color;
colorOption.onmousedown = setColor;
colorOption.ontouchstart = setColor;
noteMenu.appendChild(colorOption);
});
// Create a delete button
let deleteButton = document.createElement('div');
deleteButton.className = 'delete-note';
deleteButton.onmousedown = () => {
setTimeout(deleteNote.bind(deleteButton), 155);
}; //Add a delay to let user see button press
let deleteText = document.createElement('p');
deleteText.textContent = 'Delete';
deleteText.className = 'delete-text';
deleteButton.appendChild(deleteText);
let deleteIcon = document.createElement('img');
deleteIcon.src = 'images/delete-24px-red.svg';
deleteIcon.className = 'delete-icon';
deleteButton.appendChild(deleteIcon);
noteMenu.appendChild(deleteButton);
this.parentNode.appendChild(noteMenu); // Add the menu to the note
}
/**
* setColor sets the color of a note to the color of the button pressed.
*/
function setColor() {
console.log('color button pressed');
let note = this.parentNode.parentNode;
let newColor = this.style.backgroundColor;
note.style.backgroundColor = newColor;
}
/**
* clearMenus clears all menus that the mouse is not hovering over.
* @param {MouseEvent} event
*/
function clearMenus(event) {
console.log('Clear menus');
console.log('ClientX: ' + event.clientX);
console.log('ClientY: ' + event.clientY);
let noteMenus = document.getElementsByClassName('note-menu'); // Get all menus
for (let i = 0; i < noteMenus.length; i++) {
// Loop through the menus
let rect = noteMenus[i].getBoundingClientRect(); // Get the bounding rectangle to know the position
// If the mouse is not above the menu, then remove it
if (
event.clientX < rect.left ||
event.clientX > rect.right ||
event.clientY < rect.top ||
event.clientY > rect.bottom
) {
if (noteMenus[i].id == 'active') {
//Remove the note only on a second click to account for clicking the option button
noteMenus[i].remove();
} else {
noteMenus[i].id = 'active';
} //End else
} //End if
} //End for
} //End clearMenus
/**
* deleteNote deletes a note whose delete button was pressed and initiates the reordering animation.
*/
function deleteNote() {
let thisNote = this.parentNode.parentNode;
let notes = document.getElementsByClassName('note');
let oldRects = new Map(); // Initialize an array for the old note positions
// Collect all the current note positions
for (let i = 0; i < notes.length; i++) {
let rect = notes[i].getBoundingClientRect();
oldRects.set(notes[i].id, rect);
}
thisNote.remove();
animateReorder(oldRects, 300); // Using the old positions, animate the reording of the notes over the specified time
}
/**
* Takes the old positions of elements and animates them to their new positions
* @param {Map} oldRects dictionary of note id's and their rects
* @param {number} duration
*/
function animateReorder(oldRects, duration) {
console.log(oldRects);
let notes = document.getElementsByClassName('note'); // Get all the notes
let newRects = new Map(); // Initialize array for collecting new positions
// Collect the new positions
for (let i = 0; i < notes.length; i++) {
let newRect = notes[i].getBoundingClientRect();
newRects.set(notes[i].id, newRect);
}
// Set initial positions
let offsetX = parseFloat(window.getComputedStyle(notes[0]).marginLeft);
let offsetY = parseFloat(window.getComputedStyle(notes[0]).marginTop);
let width = parseFloat(window.getComputedStyle(notes[0]).width);
for (let i = 0; i < notes.length; i++) {
if (oldRects.has(notes[i].id) && newRects.has(notes[i].id)) {
notes[i].style.position = 'fixed';
notes[i].style.left = oldRects.get(notes[i].id).left - offsetX + 'px';
notes[i].style.top = oldRects.get(notes[i].id).top - offsetY + 'px';
notes[i].style.width = width + 'px';
}
}
let timePassed = 0; // Time passed since animation began, in ms
let lastFrame = Date.now(); //The timestamp of the previous frame
// This function animates a single frame of the animation and then passed itself to `requestAnimationFrame`.
function animateFrame() {
let deltaT = Date.now() - lastFrame; // Time difference between now and the last frame
timePassed += deltaT;
lastFrame = Date.now();
// Update the positions of the notes
for (let i = 0; i < notes.length; i++) {
if (oldRects.has(notes[i].id) && newRects.has(notes[i].id)) {
let deltaX =
((newRects.get(notes[i].id).left - oldRects.get(notes[i].id).left) *
deltaT) /
duration;
let deltaY =
((newRects.get(notes[i].id).top - oldRects.get(notes[i].id).top) *
deltaT) /
duration;
notes[i].style.left = parseFloat(notes[i].style.left) + deltaX + 'px';
notes[i].style.top = parseFloat(notes[i].style.top) + deltaY + 'px';
}
}
// Check if the proper amount of time has passed
if (timePassed < duration) {
requestAnimationFrame(animateFrame);
} else {
for (let i = 0; i < notes.length; i++) {
if (oldRects.has(notes[i].id) && newRects.has(notes[i].id)) {
notes[i].style.position = 'relative';
notes[i].style.left = '0px';
notes[i].style.top = '0px';
notes[i].style.width = '';
} //End if
} //End for
} //End else
} //End animateFrame
animateFrame();
}
/**
* @typedef Note
* @type {object}
* @property {string} title
* @property {string} content
* @property {string} color
*/
class Note {
constructor(title = '', content = '', color = '') {
this.title = title;
this.content = content;
this.color = color;
}
}
/**
* storeNotes stores any notes that are on the
* screen in local storage
*
* @returns {void}
*/
function storeNotes() {
/**
* @type {HTMLDivElement[]}
*/
const noteElements = Array.from(document.getElementsByClassName('note'));
console.log(noteElements);
/**
* @type {Note[]}
*/
const noteObjects = [];
noteElements.forEach((note) => {
noteObjects.push(
new Note(
note.children[0].value, // Title
note.children[1].value, // Content
note.style.backgroundColor
)
);
});
console.log(noteObjects);
localStorage.setItem('notes', JSON.stringify(noteObjects));
}
/**
* loadNotes gets the notes stored in localStorage,
* if there are any, and adds them to the document.
*
* @returns {void}
*/
function loadNotes() {
const data = localStorage.getItem('notes');
if (data === null) return;
/**
* @type Note[]
*/
const noteObjects = JSON.parse(data);
noteObjects.forEach((note) => {
addNote(note.title, note.content, note.color);
});
}