Mercurial > projects > booket
comparison booket.js @ 0:c2248f662a2c version-1
Initial revision
author | Guido Berhoerster <guido+booket@berhoerster.name> |
---|---|
date | Sat, 06 Sep 2014 18:18:29 +0200 |
parents | |
children | 82c50265c8dc |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:c2248f662a2c |
---|---|
1 /* | |
2 * Copyright (C) 2014 Guido Berhoerster <guido+booket@berhoerster.name> | |
3 * | |
4 * Permission is hereby granted, free of charge, to any person obtaining | |
5 * a copy of this software and associated documentation files (the | |
6 * "Software"), to deal in the Software without restriction, including | |
7 * without limitation the rights to use, copy, modify, merge, publish, | |
8 * distribute, sublicense, and/or sell copies of the Software, and to | |
9 * permit persons to whom the Software is furnished to do so, subject to | |
10 * the following conditions: | |
11 * | |
12 * The above copyright notice and this permission notice shall be included | |
13 * in all copies or substantial portions of the Software. | |
14 * | |
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
18 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
19 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
20 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
21 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
22 */ | |
23 | |
24 (function () { | |
25 'use strict'; | |
26 | |
27 /* | |
28 * utility stuff | |
29 */ | |
30 | |
31 function isNumber(number) { | |
32 return (Object.prototype.toString.call(number) === '[object Number]'); | |
33 } | |
34 | |
35 function isString(number) { | |
36 return (Object.prototype.toString.call(number) === '[object String]'); | |
37 } | |
38 | |
39 function arrayEqual(array1, array2) { | |
40 if (!Array.isArray(array1)) { | |
41 throw new TypeError(typeof array1 + ' is not an array'); | |
42 } else if (!Array.isArray(array2)) { | |
43 throw new TypeError(typeof array2 + ' is not an array'); | |
44 } | |
45 | |
46 if (array1.length !== array2.length) { | |
47 return false; | |
48 } else if (array1.length === 0 && array2.length === 0) { | |
49 return true; | |
50 } | |
51 | |
52 return array1.slice().sort().every(function (value, i) { | |
53 return value === array2[i]; | |
54 }); | |
55 } | |
56 | |
57 function parseHash(url) { | |
58 var hashData; | |
59 var pos; | |
60 var hash; | |
61 var hashParts; | |
62 var key; | |
63 var value; | |
64 var i; | |
65 | |
66 hashData = new StringMap(); | |
67 pos = url.indexOf('#'); | |
68 hash = (pos > -1) ? url.substr(pos + 1) : ''; | |
69 // hash parts are seperated by a ';' | |
70 hashParts = hash.split(';'); | |
71 for (i = 0; i < hashParts.length; i++) { | |
72 // key and value pairs are seperated by a '=', an empty value will | |
73 // cause the key to be ignored | |
74 pos = hashParts[i].indexOf('='); | |
75 if (pos > -1) { | |
76 key = decodeURIComponent(hashParts[i].substr(0, pos)); | |
77 value = decodeURIComponent(hashParts[i].substr(pos + 1)); | |
78 hashData.set(key, value); | |
79 } | |
80 } | |
81 | |
82 return hashData; | |
83 } | |
84 | |
85 function serializeHash(url, hashData) { | |
86 var hashParts = []; | |
87 var pos; | |
88 | |
89 pos = url.indexOf('#'); | |
90 if (pos > -1) { | |
91 url = url.substr(0, pos); | |
92 } | |
93 | |
94 hashData.forEach(function (value, key) { | |
95 if (value !== '') { | |
96 hashParts.push(encodeURIComponent(key) + '=' + | |
97 encodeURIComponent(value)); | |
98 } | |
99 }); | |
100 | |
101 // only append a '#' if there are any hash parts | |
102 return url + (hashParts.length > 0 ? '#' + hashParts.join(';') : ''); | |
103 } | |
104 | |
105 function getAncestorElementDatasetItem(node, item) { | |
106 while ((node = node.parentNode) !== null) { | |
107 if (node.dataset && node.dataset[item] !== undefined) { | |
108 return node.dataset[item]; | |
109 } | |
110 } | |
111 | |
112 return undefined; | |
113 } | |
114 | |
115 // for use with Node.querySelector() and Node.querySelectorAll() | |
116 function createDatasetSelector(name, value) { | |
117 return '[data-' + name + '="' + value.replace(/["\\]/g, '\\$&') + '"]'; | |
118 } | |
119 | |
120 function extend(targetObject, sourceObject) { | |
121 var propertyName; | |
122 | |
123 for (propertyName in sourceObject.prototype) { | |
124 if (!Object.prototype.hasOwnProperty.call(targetObject.prototype, | |
125 propertyName)) { | |
126 targetObject.prototype[propertyName] = | |
127 sourceObject.prototype[propertyName]; | |
128 } | |
129 } | |
130 } | |
131 | |
132 | |
133 var ObservableMixin = function () { | |
134 this._eventsObservers = {}; | |
135 }; | |
136 | |
137 ObservableMixin.prototype.addObserver = function (eventName, observer) { | |
138 var i; | |
139 | |
140 if (!Object.prototype.hasOwnProperty.call(this._eventsObservers, | |
141 eventName)) { | |
142 this._eventsObservers[eventName] = []; | |
143 } | |
144 | |
145 // prevent observers for an event from being called more than once | |
146 for (i = 0; i < this._eventsObservers[eventName].length; i++) { | |
147 if (this._eventsObservers[eventName][i] === observer) { | |
148 return; | |
149 } | |
150 } | |
151 this._eventsObservers[eventName].push(observer); | |
152 }; | |
153 | |
154 ObservableMixin.prototype.deleteObserver = function (eventName, observer) { | |
155 var i = 0; | |
156 | |
157 if (!Object.prototype.hasOwnProperty.call(this._eventsObservers, | |
158 eventName)) { | |
159 return; | |
160 } | |
161 | |
162 while (i < this._eventsObservers[eventName].length) { | |
163 if (this._eventsObservers[eventName][i] === observer) { | |
164 this._eventsObservers[eventName].splice(i, 1); | |
165 } | |
166 } | |
167 }; | |
168 | |
169 ObservableMixin.prototype.notify = function (eventName) { | |
170 var origArguments; | |
171 | |
172 if (!Object.prototype.hasOwnProperty.call(this._eventsObservers, | |
173 eventName)) { | |
174 return; | |
175 } | |
176 | |
177 origArguments = Array.prototype.slice.call(arguments, 1); | |
178 this._eventsObservers[eventName].forEach(function (observer, i) { | |
179 // call the observer function and pass on any additional arguments | |
180 observer.apply(undefined, origArguments); | |
181 }); | |
182 }; | |
183 | |
184 | |
185 var StringMap = function (iter) { | |
186 this._stringMap = Object.create(null); | |
187 | |
188 if (iter !== undefined) { | |
189 if (Array.isArray(iter)) { | |
190 iter.forEach(function (pair) { | |
191 if (Array.isArray(pair)) { | |
192 this.set(pair[0], pair[1]); | |
193 } else { | |
194 throw new TypeError(typeof pair + ' is not an array'); | |
195 } | |
196 }, this); | |
197 } else { | |
198 throw new TypeError(typeof iter + ' is not iterable'); | |
199 } | |
200 } | |
201 }; | |
202 | |
203 Object.defineProperty(StringMap.prototype, 'size', { | |
204 get: function () { | |
205 var size = 0; | |
206 var key; | |
207 | |
208 for (key in this._stringMap) { | |
209 if (key.charAt(0) === '@') { | |
210 size++; | |
211 } | |
212 } | |
213 | |
214 return size; | |
215 } | |
216 }); | |
217 | |
218 StringMap.prototype.set = function (key, value) { | |
219 this._stringMap['@' + key] = value; | |
220 | |
221 return this; | |
222 }; | |
223 | |
224 StringMap.prototype.get = function (key) { | |
225 return this._stringMap['@' + key]; | |
226 }; | |
227 | |
228 StringMap.prototype.has = function (key) { | |
229 return (('@' + key) in this._stringMap); | |
230 }; | |
231 | |
232 StringMap.prototype.delete = function (key) { | |
233 if (this.has(key)) { | |
234 delete this._stringMap['@' + key]; | |
235 | |
236 return true; | |
237 } | |
238 | |
239 return false; | |
240 }; | |
241 | |
242 StringMap.prototype.forEach = function (callbackFn, thisArg) { | |
243 Object.keys(this._stringMap).forEach(function (key) { | |
244 if (key.charAt(0) === '@') { | |
245 key = key.substr(1); | |
246 callbackFn.call(thisArg, this.get(key), key, this); | |
247 } | |
248 }, this); | |
249 }; | |
250 | |
251 StringMap.prototype.keys = function () { | |
252 return Object.keys(this._stringMap).map(function (key) { | |
253 return key.substr(1); | |
254 }); | |
255 }; | |
256 | |
257 StringMap.prototype.toJSON = function () { | |
258 return this._stringMap; | |
259 }; | |
260 | |
261 StringMap.prototype.toString = function () { | |
262 return Object.prototype.toString.call(this._stringMap); | |
263 }; | |
264 | |
265 | |
266 var StringSet = function (iter) { | |
267 this._stringArray = []; | |
268 this._stringMap = new StringMap(); | |
269 if (iter !== undefined) { | |
270 if (Array.isArray(iter) || iter instanceof StringSet) { | |
271 iter.forEach(function (string) { | |
272 this.add(string); | |
273 }, this); | |
274 } else { | |
275 throw new TypeError(typeof iter + ' is not iterable'); | |
276 } | |
277 } | |
278 }; | |
279 | |
280 Object.defineProperty(StringSet.prototype, 'size', { | |
281 get: function () { | |
282 return this._stringArray.length; | |
283 } | |
284 }); | |
285 | |
286 StringSet.prototype.has = function (string) { | |
287 return this._stringMap.has(string); | |
288 }; | |
289 | |
290 StringSet.prototype.add = function (string) { | |
291 if (!this.has(string)) { | |
292 this._stringMap.set(string, true); | |
293 this._stringArray.push(string); | |
294 } | |
295 return this; | |
296 }; | |
297 | |
298 StringSet.prototype.delete = function (string) { | |
299 if (this.has(string)) { | |
300 this._stringMap.delete(string); | |
301 this._stringArray.splice(this._stringArray.indexOf(string), 1); | |
302 return true; | |
303 } | |
304 return false; | |
305 }; | |
306 | |
307 StringSet.prototype.forEach = function (callbackFn, thisArg) { | |
308 this._stringArray.forEach(function (key) { | |
309 callbackFn.call(thisArg, key, key, this); | |
310 }); | |
311 }; | |
312 | |
313 StringSet.prototype.keys = function () { | |
314 return this._stringArray.slice(); | |
315 }; | |
316 | |
317 StringSet.prototype.values = function () { | |
318 return this._stringArray.slice(); | |
319 }; | |
320 | |
321 StringSet.prototype.clear = function () { | |
322 this._stringMap = new StringMap(); | |
323 this._stringArray = []; | |
324 }; | |
325 | |
326 StringSet.prototype.toJSON = function () { | |
327 return this._stringArray; | |
328 }; | |
329 | |
330 StringSet.prototype.toString = function () { | |
331 return this._stringArray.toString(); | |
332 }; | |
333 | |
334 | |
335 /* | |
336 * model | |
337 */ | |
338 | |
339 var Bookmark = function (url, title, tags, ctime, mtime) { | |
340 var parsedTime; | |
341 | |
342 if (!isString(url)) { | |
343 throw new TypeError(typeof url + ' is not a string'); | |
344 } | |
345 this.url = url; | |
346 | |
347 this.title = (isString(title) && title !== '') ? title : url; | |
348 | |
349 if (Array.isArray(tags)) { | |
350 // remove duplicates, non-string or empty tags and tags containing | |
351 // commas | |
352 this.tags = new StringSet(tags.filter(function (tag) { | |
353 return (isString(tag) && tag !== '' && tag.indexOf(',') === -1); | |
354 }).sort()); | |
355 } else { | |
356 this.tags = new StringSet(); | |
357 } | |
358 | |
359 if (isNumber(ctime) || isString(ctime)) { | |
360 parsedTime = new Date(ctime); | |
361 this.ctime = !isNaN(parsedTime.getTime()) ? parsedTime : new Date(); | |
362 } else { | |
363 this.ctime = new Date(); | |
364 } | |
365 | |
366 if (isNumber(mtime) || isString(mtime)) { | |
367 parsedTime = new Date(mtime); | |
368 // modification time must be greater than creation time | |
369 this.mtime = (!isNaN(parsedTime.getTime()) || | |
370 parsedTime >= this.ctime) ? parsedTime : new Date(this.ctime); | |
371 } else { | |
372 this.mtime = new Date(this.ctime); | |
373 } | |
374 }; | |
375 | |
376 | |
377 var BookmarkModel = function () { | |
378 ObservableMixin.call(this); | |
379 | |
380 this.unsavedChanges = false; | |
381 this._bookmarks = new StringMap(); | |
382 this._tagCount = new StringMap(); | |
383 this._filterTags = new StringSet(); | |
384 this._searchTerm = ''; | |
385 this._filteredBookmarks = new StringSet(); | |
386 this._searchedBookmarks = new StringSet(); | |
387 }; | |
388 | |
389 extend(BookmarkModel, ObservableMixin); | |
390 | |
391 BookmarkModel.prototype.add = function (bookmarks) { | |
392 var addedBookmarkUrls = new StringSet(); | |
393 | |
394 // argument can be a single bookmark or a list of bookmarks | |
395 if (!Array.isArray(bookmarks)) { | |
396 bookmarks = [bookmarks]; | |
397 } | |
398 | |
399 bookmarks.forEach(function (bookmark) { | |
400 // delete any existing bookmark for the given URL before adding the new | |
401 // one in order to update views | |
402 this.delete(bookmark.url); | |
403 this._bookmarks.set(bookmark.url, bookmark); | |
404 addedBookmarkUrls.add(bookmark.url); | |
405 this.unsavedChanges = true; | |
406 this.notify('bookmark-added', bookmark); | |
407 | |
408 // update tag count | |
409 bookmark.tags.forEach(function (tag) { | |
410 var tagCount; | |
411 | |
412 if (this._tagCount.has(tag)) { | |
413 tagCount = this._tagCount.get(tag) + 1; | |
414 this._tagCount.set(tag, tagCount); | |
415 this.notify('tag-count-changed', tag, tagCount); | |
416 } else { | |
417 this._tagCount.set(tag, 1); | |
418 this.notify('tag-added', tag); | |
419 } | |
420 }, this); | |
421 }, this); | |
422 | |
423 // apply tag filter and search added bookmarks | |
424 this.updateFilteredSearchedBookmarks(addedBookmarkUrls); | |
425 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
426 this._filterTags, this._searchTerm); | |
427 }; | |
428 | |
429 BookmarkModel.prototype.has = function (url) { | |
430 return this._bookmarks.has(url); | |
431 }; | |
432 | |
433 BookmarkModel.prototype.get = function (url) { | |
434 return this._bookmarks.get(url); | |
435 }; | |
436 | |
437 BookmarkModel.prototype.delete = function (urls) { | |
438 var needUpdateFilterTags = false; | |
439 | |
440 // argument can be a single bookmark or a list of bookmarks | |
441 if (!Array.isArray(urls)) { | |
442 urls = [urls]; | |
443 } | |
444 | |
445 urls.forEach(function (url) { | |
446 var bookmark; | |
447 var tagCount; | |
448 | |
449 if (this._bookmarks.has(url)) { | |
450 bookmark = this._bookmarks.get(url); | |
451 this._bookmarks.delete(url); | |
452 this.unsavedChanges = true; | |
453 this.notify('bookmark-deleted', bookmark.url); | |
454 | |
455 // update tag count | |
456 bookmark.tags.forEach(function (tag) { | |
457 if (this._tagCount.has(tag)) { | |
458 tagCount = this._tagCount.get(tag); | |
459 if (tagCount > 1) { | |
460 tagCount--; | |
461 this._tagCount.set(tag, tagCount); | |
462 this.notify('tag-count-changed', tag, tagCount); | |
463 } else { | |
464 this._tagCount.delete(tag); | |
465 this.notify('tag-deleted', tag); | |
466 | |
467 if (this._filterTags.has(tag)) { | |
468 this._filterTags.delete(tag); | |
469 needUpdateFilterTags = true; | |
470 } | |
471 } | |
472 } | |
473 }, this); | |
474 | |
475 // update filtered and searched bookmarks | |
476 if (this._filteredBookmarks.has(url)) { | |
477 this._filteredBookmarks.delete(url); | |
478 if (this._searchedBookmarks.has(url)) { | |
479 this._searchedBookmarks.delete(url); | |
480 } | |
481 } | |
482 } | |
483 }, this); | |
484 | |
485 if (needUpdateFilterTags) { | |
486 this.updateFilteredSearchedBookmarks(); | |
487 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
488 this._filterTags, this._searchTerm); | |
489 } | |
490 }; | |
491 | |
492 BookmarkModel.prototype.forEach = function (callbackFn, thisArg) { | |
493 this._bookmarks.keys().forEach(function (key) { | |
494 callbackFn.call(thisArg, this._bookmarks.get(key), key, this); | |
495 }, this); | |
496 }; | |
497 | |
498 BookmarkModel.prototype.hasTag = function (tag) { | |
499 return this._tagCount.has(tag); | |
500 }; | |
501 | |
502 BookmarkModel.prototype.getTagCount = function (tag) { | |
503 return (this._tagCount.has(tag)) ? this._tagCount.get(tag) : undefined; | |
504 }; | |
505 | |
506 BookmarkModel.prototype.updateSearchedBookmarks = function (urlsSubset) { | |
507 var searchUrls; | |
508 | |
509 // additive search if urlsSubset is given | |
510 if (urlsSubset !== undefined) { | |
511 searchUrls = urlsSubset; | |
512 } else { | |
513 this._searchedBookmarks = new StringSet(); | |
514 | |
515 searchUrls = this._filteredBookmarks.values(); | |
516 } | |
517 | |
518 // search for the search term in title and URL | |
519 searchUrls.forEach(function (url) { | |
520 var bookmark; | |
521 | |
522 bookmark = this.get(url); | |
523 if (this._searchTerm === '' || | |
524 bookmark.title.indexOf(this._searchTerm) !== -1 || | |
525 bookmark.url.indexOf(this._searchTerm) !== -1) { | |
526 this._searchedBookmarks.add(url); | |
527 } | |
528 }, this); | |
529 }; | |
530 | |
531 BookmarkModel.prototype.updateFilteredSearchedBookmarks = | |
532 function (urlsSubset) { | |
533 var filterUrls; | |
534 var searchUrls; | |
535 | |
536 // additive filtering if urlsSubset is given | |
537 if (urlsSubset !== undefined) { | |
538 filterUrls = urlsSubset; | |
539 searchUrls = []; | |
540 } else { | |
541 this._filteredBookmarks = new StringSet(); | |
542 | |
543 filterUrls = this._bookmarks.keys(); | |
544 searchUrls = undefined; | |
545 } | |
546 | |
547 // apply tag filter | |
548 filterUrls.forEach(function (url) { | |
549 var bookmark; | |
550 var matchingTagCount = 0; | |
551 | |
552 bookmark = this.get(url); | |
553 | |
554 bookmark.tags.forEach(function (tag) { | |
555 if (this._filterTags.has(tag)) { | |
556 matchingTagCount++; | |
557 } | |
558 }, this); | |
559 | |
560 if (matchingTagCount === this._filterTags.size) { | |
561 this._filteredBookmarks.add(url); | |
562 if (urlsSubset !== undefined) { | |
563 searchUrls.push(url); | |
564 } | |
565 } | |
566 }, this); | |
567 | |
568 // search the filter results | |
569 this.updateSearchedBookmarks(searchUrls); | |
570 }; | |
571 | |
572 BookmarkModel.prototype.toggleFilterTag = function (tag) { | |
573 if (this._filterTags.has(tag)) { | |
574 this._filterTags.delete(tag); | |
575 } else { | |
576 this._filterTags.add(tag); | |
577 } | |
578 this.updateFilteredSearchedBookmarks(); | |
579 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
580 this._filterTags, this._searchTerm); | |
581 }; | |
582 | |
583 BookmarkModel.prototype.setFilterTags = function (filterTags) { | |
584 if (!arrayEqual(filterTags.values(), this._filterTags.values())) { | |
585 this._filterTags = new StringSet(filterTags); | |
586 this.updateFilteredSearchedBookmarks(); | |
587 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
588 this._filterTags, this._searchTerm); | |
589 } | |
590 }; | |
591 | |
592 BookmarkModel.prototype.setSearchTerm = function (searchTerm) { | |
593 if (searchTerm !== this._searchTerm) { | |
594 this._searchTerm = searchTerm; | |
595 this.updateSearchedBookmarks(); | |
596 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
597 this._filterTags, this._searchTerm); | |
598 } | |
599 }; | |
600 | |
601 BookmarkModel.prototype.setFilterTagsSearchTerm = function (filterTags, | |
602 searchTerm) { | |
603 if (!arrayEqual(filterTags.values(), this._filterTags.values())) { | |
604 this._filterTags = new StringSet(filterTags); | |
605 this._searchTerm = searchTerm; | |
606 this.updateFilteredSearchedBookmarks(); | |
607 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
608 this._filterTags, this._searchTerm); | |
609 } else if (searchTerm !== this._searchTerm) { | |
610 this._searchTerm = searchTerm; | |
611 this.updateSearchedBookmarks(); | |
612 this.notify('filter-tags-search-changed', this._searchedBookmarks, | |
613 this._filterTags, this._searchTerm); | |
614 } | |
615 }; | |
616 | |
617 BookmarkModel.prototype.parseLoadedBookmarks = function (data) { | |
618 var parsedData; | |
619 var bookmarks = []; | |
620 | |
621 try { | |
622 parsedData = JSON.parse(data); | |
623 } catch (e) { | |
624 this.notify('load-file-error', e.message); | |
625 return; | |
626 } | |
627 | |
628 if (!Array.isArray(parsedData.bookmarks)) { | |
629 this.notify('parse-file-error', | |
630 'This file does not contain bookmarks.'); | |
631 return; | |
632 } | |
633 | |
634 // create a temporary list of valid bookmarks | |
635 parsedData.bookmarks.forEach(function (bookmark) { | |
636 if (isString(bookmark.url) && bookmark.url !== '') { | |
637 bookmarks.push(new Bookmark(bookmark.url, bookmark.title, | |
638 bookmark.tags, bookmark.ctime, bookmark.mtime)); | |
639 } | |
640 }, this); | |
641 | |
642 // add each bookmark to the model ordered by the last modification time | |
643 this.add(bookmarks.sort(function (bookmark1, bookmark2) { | |
644 return bookmark1.ctime - bookmark2.ctime; | |
645 })); | |
646 this.unsavedChanges = false; | |
647 }; | |
648 | |
649 BookmarkModel.prototype.loadFile = function (bookmarkFile) { | |
650 var bookmarkFileReader; | |
651 | |
652 // delete all existing bookmarks first | |
653 this.delete(this._bookmarks.keys()); | |
654 this.unsavedChanges = false; | |
655 | |
656 bookmarkFileReader = new FileReader(); | |
657 bookmarkFileReader.addEventListener('error', this); | |
658 bookmarkFileReader.addEventListener('load', this); | |
659 bookmarkFileReader.readAsText(bookmarkFile); | |
660 }; | |
661 | |
662 BookmarkModel.prototype.saveFile = function () { | |
663 var jsonBlob; | |
664 var bookmarkData = { | |
665 'bookmarks': [] | |
666 }; | |
667 | |
668 this._bookmarks.forEach(function (bookmark) { | |
669 bookmarkData.bookmarks.push(bookmark); | |
670 }, this); | |
671 | |
672 jsonBlob = new Blob([JSON.stringify(bookmarkData)], {type: | |
673 'application/json'}); | |
674 this.notify('save-file', jsonBlob); | |
675 this.unsavedChanges = false; | |
676 }; | |
677 | |
678 BookmarkModel.prototype.handleEvent = function (e) { | |
679 if (e.type === 'load') { | |
680 this.parseLoadedBookmarks(e.target.result); | |
681 } else if (e.type === 'error') { | |
682 this.notify('load-file-error', e.target.error.message); | |
683 } | |
684 }; | |
685 | |
686 | |
687 /* | |
688 * view | |
689 */ | |
690 | |
691 var TagView = function () { | |
692 ObservableMixin.call(this); | |
693 | |
694 this.tagListElement = document.querySelector('#tags ul.tag-list'); | |
695 this.tagListElement.addEventListener('click', this); | |
696 | |
697 this.tagTemplate = document.querySelector('#tag-template'); | |
698 }; | |
699 | |
700 extend(TagView, ObservableMixin); | |
701 | |
702 TagView.prototype.onTagAdded = function (tag) { | |
703 var newNode; | |
704 var tagElement; | |
705 var setTagButton; | |
706 var toggleTagButton; | |
707 var tagElements; | |
708 var i; | |
709 var referenceTag = ''; | |
710 var referenceNode; | |
711 | |
712 // create new tag element from template | |
713 newNode = document.importNode(this.tagTemplate.content, true); | |
714 | |
715 tagElement = newNode.querySelector('li'); | |
716 tagElement.dataset.tag = tag; | |
717 | |
718 setTagButton = tagElement.querySelector('button[name="set-tag"]'); | |
719 setTagButton.textContent = tag; | |
720 setTagButton.title = 'Set filter to "' + tag + '"'; | |
721 | |
722 toggleTagButton = tagElement.querySelector('button[name="toggle-tag"]'); | |
723 toggleTagButton.textContent = '+'; | |
724 toggleTagButton.title = 'Add "' + tag + '" to filter'; | |
725 | |
726 // maintain alphabetical order when inserting the tag element | |
727 tagElements = this.tagListElement.querySelectorAll('li'); | |
728 for (i = 0; i < tagElements.length; i ++) { | |
729 if (tagElements[i].dataset.tag > referenceTag && | |
730 tagElements[i].dataset.tag < tag) { | |
731 referenceTag = tagElements[i].dataset.tag; | |
732 referenceNode = tagElements[i]; | |
733 } | |
734 } | |
735 this.tagListElement.insertBefore(newNode, (referenceNode !== undefined) ? | |
736 referenceNode.nextSibling : this.tagListElement.firstChild); | |
737 | |
738 // initialize tag count | |
739 this.onTagCountChanged(tag, 1); | |
740 }; | |
741 | |
742 TagView.prototype.onTagCountChanged = function (tag, tagCount) { | |
743 this.tagListElement.querySelector('li' + createDatasetSelector('tag', tag) + | |
744 ' .tag-count').textContent = '(' + tagCount + ')'; | |
745 }; | |
746 | |
747 TagView.prototype.onTagDeleted = function (tag) { | |
748 var tagElement; | |
749 | |
750 tagElement = this.tagListElement.querySelector('li' + | |
751 createDatasetSelector('tag', tag)); | |
752 if (tagElement !== null) { | |
753 tagElement.parentNode.removeChild(tagElement); | |
754 } | |
755 }; | |
756 | |
757 TagView.prototype.onFilterTagsSearchChanged = function (filteredBookmarks, | |
758 newFilterTags, newSearchTerm) { | |
759 var tagElements; | |
760 var i; | |
761 var tag; | |
762 var toggleTagButton; | |
763 | |
764 tagElements = this.tagListElement.querySelectorAll('li'); | |
765 for (i = 0; i < tagElements.length; i++) { | |
766 tag = tagElements[i].dataset.tag; | |
767 toggleTagButton = | |
768 tagElements[i].querySelector('button[name="toggle-tag"]'); | |
769 if (newFilterTags.has(tag)) { | |
770 tagElements[i].classList.add('active-filter-tag'); | |
771 toggleTagButton.textContent = '\u2212'; | |
772 toggleTagButton.title = 'Remove "' + tag + '" from filter'; | |
773 } else { | |
774 tagElements[i].classList.remove('active-filter-tag'); | |
775 toggleTagButton.textContent = '+'; | |
776 toggleTagButton.title = 'Add "' + tag + '" to filter'; | |
777 } | |
778 } | |
779 }; | |
780 | |
781 TagView.prototype.handleEvent = function (e) { | |
782 if (e.type === 'click' && (e.target.name === 'set-tag' || | |
783 e.target.name === 'toggle-tag')) { | |
784 e.target.blur(); | |
785 | |
786 this.notify(e.target.name, getAncestorElementDatasetItem(e.target, | |
787 'tag')); | |
788 } | |
789 }; | |
790 | |
791 | |
792 var ActionsView = function () { | |
793 var saveFormElement; | |
794 var loadFormElement; | |
795 var newNode; | |
796 var editorFormElement; | |
797 | |
798 ObservableMixin.call(this); | |
799 | |
800 this.tagInputTemplate = document.querySelector('#tag-input-template'); | |
801 saveFormElement = document.querySelector('form#save-form'); | |
802 saveFormElement.addEventListener('submit', this); | |
803 | |
804 this.saveLinkElement = saveFormElement.querySelector('a#save-link'); | |
805 | |
806 loadFormElement = document.querySelector('form#load-form'); | |
807 loadFormElement.addEventListener('submit', this); | |
808 | |
809 // create new editor form from template | |
810 newNode = document.importNode( | |
811 document.querySelector('#bookmark-editor-template').content, true); | |
812 | |
813 editorFormElement = newNode.querySelector('form.bookmark-editor-form'); | |
814 editorFormElement.querySelector('legend').textContent = 'Add Bookmark'; | |
815 editorFormElement.querySelector('input:not([type="hidden"])').accessKey = | |
816 'a'; | |
817 editorFormElement.addEventListener('click', this); | |
818 editorFormElement.addEventListener('submit', this); | |
819 editorFormElement.addEventListener('reset', this); | |
820 | |
821 this.editTagListElement = | |
822 editorFormElement.querySelector('ul.tag-input-list'); | |
823 this.editTagListElement.appendChild(this.createTagInputElement('')); | |
824 | |
825 saveFormElement.parentNode.insertBefore(newNode, | |
826 saveFormElement.nextSibling); | |
827 }; | |
828 | |
829 extend(ActionsView, ObservableMixin); | |
830 | |
831 ActionsView.prototype.createTagInputElement = function (tag) { | |
832 var newNode; | |
833 | |
834 newNode = document.importNode(this.tagInputTemplate.content, true); | |
835 newNode.querySelector('input[name="tag"]').value = tag; | |
836 | |
837 return newNode; | |
838 }; | |
839 | |
840 ActionsView.prototype.handleEvent = function (e) { | |
841 var tags = []; | |
842 var i; | |
843 | |
844 switch (e.type) { | |
845 case 'click': | |
846 if (e.target.name === 'more-tags') { | |
847 e.preventDefault(); | |
848 e.target.blur(); | |
849 | |
850 this.editTagListElement.appendChild(this.createTagInputElement('')); | |
851 } | |
852 break; | |
853 case 'submit': | |
854 if (e.target.id === 'save-form') { | |
855 e.preventDefault(); | |
856 e.target.blur(); | |
857 | |
858 this.notify('save-file'); | |
859 } else if (e.target.id === 'load-form') { | |
860 e.preventDefault(); | |
861 e.target.blur(); | |
862 | |
863 this.notify('load-file', e.target.file.files[0]); | |
864 e.target.reset(); | |
865 } else if (e.target.classList.contains('bookmark-editor-form')) { | |
866 e.preventDefault(); | |
867 e.target.blur(); | |
868 | |
869 if (e.target.tag.length) { | |
870 for (i = 0; i < e.target.tag.length; i++) { | |
871 tags.push(e.target.tag[i].value.trim()); | |
872 } | |
873 } else { | |
874 tags.push(e.target.tag.value.trim()); | |
875 } | |
876 | |
877 this.notify('save-bookmark', e.target.url.value, | |
878 e.target.title.value, tags); | |
879 | |
880 e.target.reset(); | |
881 } | |
882 break; | |
883 case 'reset': | |
884 if (e.target.classList.contains('bookmark-editor-form')) { | |
885 e.target.blur(); | |
886 | |
887 // remove all but one tag input element | |
888 while (this.editTagListElement.firstChild !== null) { | |
889 this.editTagListElement.removeChild( | |
890 this.editTagListElement.firstChild); | |
891 } | |
892 this.editTagListElement.appendChild(this.createTagInputElement('')); | |
893 } | |
894 break; | |
895 } | |
896 }; | |
897 | |
898 ActionsView.prototype.onSaveFile = function (jsonBlob) { | |
899 this.saveLinkElement.href = URL.createObjectURL(jsonBlob); | |
900 this.saveLinkElement.click(); | |
901 }; | |
902 | |
903 ActionsView.prototype.confirmLoadFile = function () { | |
904 return window.confirm('There are unsaved changes to your bookmarks.\n' + | |
905 'Proceed loading the bookmark file?'); | |
906 }; | |
907 | |
908 ActionsView.prototype.onLoadFileError = function (message) { | |
909 window.alert('Failed to load bookmark file:\n' + message); | |
910 }; | |
911 | |
912 ActionsView.prototype.onParseFileError = function (message) { | |
913 window.alert('Failed to parse bookmark file:\n' + message); | |
914 }; | |
915 | |
916 | |
917 var BookmarkView = function () { | |
918 var searchFormElement; | |
919 | |
920 ObservableMixin.call(this); | |
921 | |
922 this.bookmarkTemplate = document.querySelector('#bookmark-template'); | |
923 this.bookmarkTagTemplate = document.querySelector('#bookmark-tag-template'); | |
924 this.bookmarkEditorTemplate = | |
925 document.querySelector('#bookmark-editor-template'); | |
926 this.tagInputTemplate = document.querySelector('#tag-input-template'); | |
927 | |
928 this.bookmarkListElement = document.querySelector('ul#bookmark-list'); | |
929 this.bookmarkListElement.addEventListener('click', this); | |
930 this.bookmarkListElement.addEventListener('submit', this); | |
931 this.bookmarkListElement.addEventListener('reset', this); | |
932 | |
933 searchFormElement = document.querySelector('#search-form'); | |
934 searchFormElement.addEventListener('submit', this); | |
935 searchFormElement.addEventListener('reset', this); | |
936 | |
937 this.searchTermInputElement = searchFormElement['search-term']; | |
938 | |
939 this.bookmarkMessageElement = document.querySelector('#bookmark-message'); | |
940 | |
941 this.updateBookmarkMessage(); | |
942 }; | |
943 | |
944 extend(BookmarkView, ObservableMixin); | |
945 | |
946 BookmarkView.prototype.handleEvent = function (e) { | |
947 var i; | |
948 var tags = []; | |
949 var node; | |
950 | |
951 switch (e.type) { | |
952 case 'click': | |
953 switch (e.target.name) { | |
954 case 'edit-bookmark': | |
955 e.target.blur(); | |
956 // fallthrough | |
957 case 'delete-bookmark': | |
958 this.notify(e.target.name, | |
959 getAncestorElementDatasetItem(e.target, 'bookmarkUrl')); | |
960 break; | |
961 case 'more-tags': | |
962 e.target.blur(); | |
963 | |
964 e.target.form.querySelector('ul.tag-input-list').appendChild( | |
965 this.createTagInputElement('')); | |
966 break; | |
967 case 'set-tag': | |
968 case 'toggle-tag': | |
969 e.target.blur(); | |
970 | |
971 this.notify(e.target.name, | |
972 getAncestorElementDatasetItem(e.target, 'tag')); | |
973 break; | |
974 } | |
975 break; | |
976 case 'submit': | |
977 if (e.target.classList.contains('bookmark-editor-form')) { | |
978 // save bookmark-editor-form form contents | |
979 e.preventDefault(); | |
980 | |
981 if (e.target.tag.length) { | |
982 for (i = 0; i < e.target.tag.length; i++) { | |
983 tags.push(e.target.tag[i].value.trim()); | |
984 } | |
985 } else { | |
986 tags.push(e.target.tag.value.trim()); | |
987 } | |
988 | |
989 this.notify('save-bookmark', e.target.url.value, | |
990 e.target.title.value, tags, e.target['original-url'].value); | |
991 } else if (e.target.id === 'search-form') { | |
992 // search | |
993 e.preventDefault(); | |
994 e.target.blur(); | |
995 | |
996 this.notify('search', e.target['search-term'].value); | |
997 } | |
998 break; | |
999 case 'reset': | |
1000 if (e.target.classList.contains('bookmark-editor-form')) { | |
1001 // cancel bookmark-editor-form form | |
1002 e.preventDefault(); | |
1003 | |
1004 // re-enable edit button | |
1005 this.bookmarkListElement.querySelector('li' + | |
1006 createDatasetSelector('bookmark-url', | |
1007 e.target['original-url'].value) + | |
1008 ' button[name="edit-bookmark"]').disabled = false; | |
1009 | |
1010 e.target.parentNode.removeChild(e.target); | |
1011 } else if (e.target.id === 'search-form') { | |
1012 // clear search | |
1013 e.preventDefault(); | |
1014 e.target.blur(); | |
1015 | |
1016 this.notify('search', ''); | |
1017 } | |
1018 break; | |
1019 } | |
1020 }; | |
1021 | |
1022 BookmarkView.prototype.updateBookmarkMessage = function () { | |
1023 this.bookmarkMessageElement.textContent = 'Showing ' + | |
1024 this.bookmarkListElement.querySelectorAll('ul#bookmark-list > ' + | |
1025 'li:not([hidden])').length + ' of ' + | |
1026 this.bookmarkListElement.querySelectorAll('ul#bookmark-list > ' + | |
1027 'li').length + ' bookmarks.'; | |
1028 }; | |
1029 | |
1030 BookmarkView.prototype.onBookmarkAdded = function (bookmark) { | |
1031 var newNode; | |
1032 var bookmarkElement; | |
1033 var linkElement; | |
1034 var tagListElement; | |
1035 | |
1036 newNode = document.importNode(this.bookmarkTemplate.content, true); | |
1037 | |
1038 bookmarkElement = newNode.querySelector('li'); | |
1039 bookmarkElement.dataset.bookmarkUrl = bookmark.url; | |
1040 | |
1041 linkElement = bookmarkElement.querySelector('a.bookmark-link'); | |
1042 linkElement.textContent = linkElement.title = bookmark.title; | |
1043 linkElement.href = bookmark.url; | |
1044 | |
1045 tagListElement = bookmarkElement.querySelector('ul.tag-list'); | |
1046 bookmark.tags.forEach(function (tag) { | |
1047 var newNode; | |
1048 var tagElement; | |
1049 var setTagButton; | |
1050 var toggleTagButton; | |
1051 | |
1052 newNode = document.importNode(this.bookmarkTagTemplate.content, true); | |
1053 | |
1054 tagElement = newNode.querySelector('li'); | |
1055 tagElement.dataset.tag = tag; | |
1056 | |
1057 setTagButton = newNode.querySelector('button[name="set-tag"]'); | |
1058 setTagButton.textContent = tag; | |
1059 setTagButton.title = 'Set filter to "' + tag + '"'; | |
1060 | |
1061 toggleTagButton = newNode.querySelector('button[name="toggle-tag"]'); | |
1062 toggleTagButton.textContent = '+'; | |
1063 toggleTagButton.title = 'Add "' + tag + '" to filter'; | |
1064 | |
1065 tagListElement.appendChild(newNode); | |
1066 }, this); | |
1067 | |
1068 // insert new or last modified bookmark on top of the list | |
1069 this.bookmarkListElement.insertBefore(newNode, | |
1070 this.bookmarkListElement.firstChild); | |
1071 | |
1072 this.updateBookmarkMessage(); | |
1073 }; | |
1074 | |
1075 BookmarkView.prototype.onBookmarkDeleted = function (bookmarkUrl) { | |
1076 var bookmarkElement; | |
1077 | |
1078 bookmarkElement = this.bookmarkListElement.querySelector('li' + | |
1079 createDatasetSelector('bookmark-url', bookmarkUrl)); | |
1080 if (bookmarkElement !== null) { | |
1081 this.bookmarkListElement.removeChild(bookmarkElement); | |
1082 | |
1083 this.updateBookmarkMessage(); | |
1084 } | |
1085 }; | |
1086 | |
1087 BookmarkView.prototype.onFilterTagsSearchChanged = function (filteredBookmarks, | |
1088 newFilterTags, newSearchTerm) { | |
1089 var bookmarkElements; | |
1090 var i; | |
1091 var tagElements; | |
1092 var toggleTagButton; | |
1093 var j; | |
1094 var tag; | |
1095 | |
1096 this.searchTermInputElement.value = newSearchTerm; | |
1097 | |
1098 bookmarkElements = | |
1099 this.bookmarkListElement.querySelectorAll('ul#bookmark-list > li'); | |
1100 for (i = 0; i < bookmarkElements.length; i++) { | |
1101 // update visibility of bookmarks | |
1102 if (filteredBookmarks.has(bookmarkElements[i].dataset.bookmarkUrl)) { | |
1103 // update tag elements of visible bookmarks | |
1104 tagElements = | |
1105 bookmarkElements[i].querySelectorAll('ul.tag-list > li'); | |
1106 for (j = 0; j < tagElements.length; j++) { | |
1107 tag = tagElements[j].dataset.tag; | |
1108 toggleTagButton = | |
1109 tagElements[j].querySelector('button[name="toggle-tag"]'); | |
1110 if (newFilterTags.has(tag)) { | |
1111 tagElements[j].classList.add('active-filter-tag'); | |
1112 toggleTagButton.textContent = '\u2212'; | |
1113 toggleTagButton.title = 'Remove "' + tag + '" from filter'; | |
1114 } else { | |
1115 tagElements[j].classList.remove('active-filter-tag'); | |
1116 toggleTagButton.textContent = '+'; | |
1117 toggleTagButton.title = 'Add "' + tag + '" to filter'; | |
1118 } | |
1119 } | |
1120 bookmarkElements[i].hidden = false; | |
1121 } else { | |
1122 bookmarkElements[i].hidden = true; | |
1123 } | |
1124 } | |
1125 | |
1126 this.updateBookmarkMessage(); | |
1127 }; | |
1128 | |
1129 BookmarkView.prototype.createTagInputElement = function (tag) { | |
1130 var newNode; | |
1131 | |
1132 newNode = document.importNode(this.tagInputTemplate.content, true); | |
1133 newNode.querySelector('input[name="tag"]').value = tag; | |
1134 | |
1135 return newNode; | |
1136 }; | |
1137 | |
1138 BookmarkView.prototype.displayBookmarkEditor = function (bookmark) { | |
1139 var bookmarkElement; | |
1140 var newNode; | |
1141 var formElement; | |
1142 var editTagListElement; | |
1143 | |
1144 bookmarkElement = | |
1145 this.bookmarkListElement.querySelector('ul#bookmark-list > li' + | |
1146 createDatasetSelector('bookmark-url', bookmark.url)); | |
1147 | |
1148 // disable edit button while editing | |
1149 bookmarkElement.querySelector('button[name="edit-bookmark"]').disabled = | |
1150 true; | |
1151 | |
1152 // create new editor form from template | |
1153 newNode = document.importNode(this.bookmarkEditorTemplate.content, true); | |
1154 | |
1155 // fill with data of given bookmark | |
1156 formElement = newNode.querySelector('form.bookmark-editor-form'); | |
1157 formElement.querySelector('legend').textContent = 'Edit Bookmark'; | |
1158 formElement['original-url'].value = bookmark.url; | |
1159 formElement.url.value = bookmark.url; | |
1160 formElement.title.value = bookmark.title; | |
1161 | |
1162 editTagListElement = formElement.querySelector('ul.tag-input-list'); | |
1163 bookmark.tags.forEach(function (tag) { | |
1164 editTagListElement.appendChild(this.createTagInputElement(tag)); | |
1165 }, this); | |
1166 editTagListElement.appendChild(this.createTagInputElement('')); | |
1167 | |
1168 // insert editor form into bookmark item | |
1169 bookmarkElement.appendChild(newNode); | |
1170 | |
1171 // focus first input element | |
1172 formElement.querySelector('input').focus(); | |
1173 }; | |
1174 | |
1175 BookmarkView.prototype.confirmReplaceBookmark = function (bookmark) { | |
1176 return window.confirm('Replace bookmark "' + bookmark.title + '"\n[' + | |
1177 bookmark.url + ']?'); | |
1178 }; | |
1179 | |
1180 BookmarkView.prototype.confirmDeleteBookmark = function (bookmark) { | |
1181 return window.confirm('Delete bookmark "' + bookmark.title + '"\n[' + | |
1182 bookmark.url + ']?'); | |
1183 }; | |
1184 | |
1185 | |
1186 /* | |
1187 * controller | |
1188 */ | |
1189 | |
1190 var BooketController = function(bookmarkModel, actionsView, tagView, | |
1191 bookmarkView) { | |
1192 this.bookmarkModel = bookmarkModel; | |
1193 this.actionsView = actionsView; | |
1194 this.tagView = tagView; | |
1195 this.bookmarkView = bookmarkView; | |
1196 | |
1197 /* connect the views to the model */ | |
1198 this.bookmarkModel.addObserver('bookmark-added', | |
1199 this.bookmarkView.onBookmarkAdded.bind(this.bookmarkView)); | |
1200 this.bookmarkModel.addObserver('bookmark-deleted', | |
1201 this.bookmarkView.onBookmarkDeleted.bind(this.bookmarkView)); | |
1202 this.bookmarkModel.addObserver('filter-tags-search-changed', | |
1203 this.bookmarkView.onFilterTagsSearchChanged.bind(this.bookmarkView)); | |
1204 this.bookmarkModel.addObserver('load-file-error', | |
1205 this.actionsView.onLoadFileError.bind(this.actionsView)); | |
1206 this.bookmarkModel.addObserver('parse-file-error', | |
1207 this.actionsView.onParseFileError.bind(this.actionsView)); | |
1208 this.bookmarkModel.addObserver('save-file', | |
1209 this.actionsView.onSaveFile.bind(this.actionsView)); | |
1210 this.bookmarkModel.addObserver('tag-added', | |
1211 this.tagView.onTagAdded.bind(this.tagView)); | |
1212 this.bookmarkModel.addObserver('tag-count-changed', | |
1213 this.tagView.onTagCountChanged.bind(this.tagView)); | |
1214 this.bookmarkModel.addObserver('tag-deleted', | |
1215 this.tagView.onTagDeleted.bind(this.tagView)); | |
1216 this.bookmarkModel.addObserver('filter-tags-search-changed', | |
1217 this.tagView.onFilterTagsSearchChanged.bind(this.tagView)); | |
1218 this.bookmarkModel.addObserver('filter-tags-search-changed', | |
1219 this.onFilterTagsSearchChanged.bind(this)); | |
1220 | |
1221 /* handle input */ | |
1222 window.addEventListener('hashchange', this.onHashChange.bind(this)); | |
1223 window.addEventListener('beforeunload', this.onBeforeUnload.bind(this)); | |
1224 this.actionsView.addObserver('save-file', | |
1225 this.bookmarkModel.saveFile.bind(this.bookmarkModel)); | |
1226 this.actionsView.addObserver('load-file', this.onLoadFile.bind(this)); | |
1227 this.actionsView.addObserver('save-bookmark', | |
1228 this.onSaveBookmark.bind(this)); | |
1229 this.bookmarkView.addObserver('edit-bookmark', | |
1230 this.onEditBookmark.bind(this)); | |
1231 this.bookmarkView.addObserver('save-bookmark', | |
1232 this.onSaveBookmark.bind(this)); | |
1233 this.bookmarkView.addObserver('delete-bookmark', | |
1234 this.onDeleteBookmark.bind(this)); | |
1235 this.bookmarkView.addObserver('toggle-tag', | |
1236 this.onToggleFilterTag.bind(this)); | |
1237 this.bookmarkView.addObserver('set-tag', this.onSetTagFilter.bind(this)); | |
1238 this.bookmarkView.addObserver('search', this.onSearch.bind(this)); | |
1239 this.tagView.addObserver('toggle-tag', this.onToggleFilterTag.bind(this)); | |
1240 this.tagView.addObserver('set-tag', this.onSetTagFilter.bind(this)); | |
1241 }; | |
1242 | |
1243 BooketController.prototype.parseTagsParameter = function (tagsString) { | |
1244 var tags; | |
1245 | |
1246 tags = tagsString.split(',').filter(function (tag) { | |
1247 return (tag !== '') && this.bookmarkModel.hasTag(tag); | |
1248 }, this).sort(); | |
1249 | |
1250 return new StringSet(tags); | |
1251 }; | |
1252 | |
1253 BooketController.prototype.onHashChange = function (e) { | |
1254 var hashData; | |
1255 var filterTags; | |
1256 var searchTerm; | |
1257 | |
1258 hashData = parseHash(window.location.href); | |
1259 | |
1260 filterTags = hashData.has('tags') ? | |
1261 this.parseTagsParameter(hashData.get('tags')) : new StringSet(); | |
1262 | |
1263 searchTerm = hashData.has('search') ? hashData.get('search') : ''; | |
1264 | |
1265 this.bookmarkModel.setFilterTagsSearchTerm(filterTags, searchTerm); | |
1266 }; | |
1267 | |
1268 BooketController.prototype.onBeforeUnload = function (e) { | |
1269 var confirmationMessage = 'There are unsaved changes to your bookmarks.'; | |
1270 | |
1271 if (this.bookmarkModel.unsavedChanges) { | |
1272 if (e) { | |
1273 e.returnValue = confirmationMessage; | |
1274 } | |
1275 if (window.event) { | |
1276 window.event.returnValue = confirmationMessage; | |
1277 } | |
1278 return confirmationMessage; | |
1279 } | |
1280 }; | |
1281 | |
1282 BooketController.prototype.onFilterTagsSearchChanged = | |
1283 function (filteredBookmarks, newFilterTags, newSearchTerm) { | |
1284 var url = window.location.href; | |
1285 var hashData; | |
1286 | |
1287 // serialize tag filter and search term and update window.location | |
1288 hashData = parseHash(url); | |
1289 hashData.set('tags', newFilterTags.values().join(',')); | |
1290 hashData.set('search', newSearchTerm); | |
1291 history.pushState(null, null, serializeHash(url, hashData)); | |
1292 }; | |
1293 | |
1294 BooketController.prototype.onLoadFile = function (bookmarkFile) { | |
1295 if (this.bookmarkModel.unsavedChanges) { | |
1296 if (!this.actionsView.confirmLoadFile()) { | |
1297 return; | |
1298 } | |
1299 this.bookmarkModel.unsavedChanges = false; | |
1300 } | |
1301 | |
1302 this.bookmarkModel.loadFile(bookmarkFile); | |
1303 }; | |
1304 | |
1305 BooketController.prototype.onEditBookmark = function (bookmarkUrl) { | |
1306 this.bookmarkView.displayBookmarkEditor( | |
1307 this.bookmarkModel.get(bookmarkUrl)); | |
1308 }; | |
1309 | |
1310 BooketController.prototype.onSaveBookmark = function (url, title, tags, | |
1311 originalUrl) { | |
1312 var ctime; | |
1313 | |
1314 if (originalUrl === undefined) { | |
1315 // saving new bookmark, get confirmation before replacing existing one | |
1316 if (this.bookmarkModel.has(url)) { | |
1317 if (this.bookmarkView.confirmReplaceBookmark( | |
1318 this.bookmarkModel.get(url))) { | |
1319 this.bookmarkModel.delete(url); | |
1320 } else { | |
1321 return; | |
1322 } | |
1323 } | |
1324 | |
1325 ctime = new Date(); | |
1326 } else { | |
1327 // saving edited bookmark, preserve creation time of any replaced | |
1328 // bookmark | |
1329 ctime = (this.bookmarkModel.has(url)) ? | |
1330 this.bookmarkModel.get(url).ctime : new Date(); | |
1331 | |
1332 this.bookmarkModel.delete(originalUrl); | |
1333 } | |
1334 this.bookmarkModel.add(new Bookmark(url, title, tags, ctime)); | |
1335 }; | |
1336 | |
1337 BooketController.prototype.onDeleteBookmark = function (bookmarkUrl) { | |
1338 if (this.bookmarkView.confirmDeleteBookmark( | |
1339 this.bookmarkModel.get(bookmarkUrl))) { | |
1340 this.bookmarkModel.delete(bookmarkUrl); | |
1341 } | |
1342 }; | |
1343 | |
1344 BooketController.prototype.onToggleFilterTag = function (tag) { | |
1345 this.bookmarkModel.toggleFilterTag(tag); | |
1346 }; | |
1347 | |
1348 BooketController.prototype.onSetTagFilter = function (tag) { | |
1349 this.bookmarkModel.setFilterTags(new StringSet([tag])); | |
1350 }; | |
1351 | |
1352 BooketController.prototype.onSearch = function (searchTerm) { | |
1353 this.bookmarkModel.setSearchTerm(searchTerm); | |
1354 }; | |
1355 | |
1356 | |
1357 document.addEventListener('DOMContentLoaded', function (e) { | |
1358 var controller; | |
1359 var bookmarkModel; | |
1360 var actionsView; | |
1361 var tagView; | |
1362 var bookmarkView; | |
1363 var hashChangeEvent; | |
1364 | |
1365 bookmarkModel = new BookmarkModel(); | |
1366 tagView = new TagView(); | |
1367 actionsView = new ActionsView(); | |
1368 bookmarkView = new BookmarkView(); | |
1369 controller = new BooketController(bookmarkModel, actionsView, | |
1370 tagView, bookmarkView); | |
1371 | |
1372 // initialize state from the current URL | |
1373 hashChangeEvent = new Event('hashchange'); | |
1374 hashChangeEvent.oldURL = window.location.href; | |
1375 hashChangeEvent.newURL = window.location.href; | |
1376 window.dispatchEvent(hashChangeEvent); | |
1377 }); | |
1378 }()); | |
1379 |