Mercurial > projects > booket
comparison booket.js @ 7:a1a934adff8d version-2
Add support for favicons
author | Guido Berhoerster <guido+booket@berhoerster.name> |
---|---|
date | Sun, 14 Sep 2014 23:12:37 +0200 |
parents | e9ad4c625b7a |
children | 20902b548d9f |
comparison
equal
deleted
inserted
replaced
6:e9ad4c625b7a | 7:a1a934adff8d |
---|---|
23 | 23 |
24 (function () { | 24 (function () { |
25 'use strict'; | 25 'use strict'; |
26 | 26 |
27 var BOOKMARKLET_URI = | 27 var BOOKMARKLET_URI = |
28 'javascript:(function () {' + | 28 'javascript:(function() {' + |
29 '\'use strict\';' + | 29 '\'use strict\';' + |
30 '' + | |
31 'function displayBookmarkData(bookmarkData) {' + | |
32 'window.alert(\'Copy the following data and paste it into \' +' + | |
33 '\'Booket:\\n\\n\' + JSON.stringify(bookmarkData));' + | |
34 '}' + | |
30 '' + | 35 '' + |
31 'window.alert(\'Copy the following data and paste it into \' +' + | 36 'var bookmarkData = {' + |
32 '\'Booket:\\n\\n\' + JSON.stringify({' + | 37 '\'url\': document.URL,' + |
33 '\'url\': document.URL,' + | 38 '\'title\': document.title,' + |
34 '\'title\': document.title' + | 39 '\'favicon\': undefined' + |
35 '}));' + | 40 '};' + |
36 '}) ();'; | 41 'var faviconLinkElement;' + |
42 'var faviconUrls = [];' + | |
43 'var aElement;' + | |
44 'var canvasElement;' + | |
45 'var canvasCtx;' + | |
46 'var imgElement;' + | |
47 '' + | |
48 'aElement = document.createElement(\'a\');' + | |
49 'aElement.href = document.URL;' + | |
50 '' + | |
51 'faviconUrls.push(aElement.protocol + \'//\' + aElement.host + ' + | |
52 '\'/favicon.ico\');' + | |
53 '' + | |
54 'faviconLinkElement = document.querySelector(' + | |
55 '\'link[rel~=\\\'icon\\\']\');' + | |
56 'if (faviconLinkElement !== null) {' + | |
57 'faviconUrls.push(faviconLinkElement.href);' + | |
58 '}' + | |
59 '' + | |
60 'canvasElement = document.createElement(\'canvas\');' + | |
61 'canvasCtx = canvasElement.getContext(\'2d\');' + | |
62 '' + | |
63 'imgElement = new Image();' + | |
64 'imgElement.addEventListener(\'load\', function(e) {' + | |
65 'var faviconUrl;' + | |
66 '' + | |
67 'canvasElement.width = 16;' + | |
68 'canvasElement.height = 16;' + | |
69 'canvasCtx.clearRect(0, 0, 16, 16);' + | |
70 'try {' + | |
71 'canvasCtx.drawImage(this, 0, 0, 16, 16);' + | |
72 'bookmarkData.favicon = canvasElement.toDataURL();' + | |
73 '} catch (exception) {' + | |
74 'faviconUrl = faviconUrls.pop();' + | |
75 '}' + | |
76 'if (bookmarkData.favicon !== undefined || ' + | |
77 'faviconUrl === undefined) {' + | |
78 'displayBookmarkData(bookmarkData);' + | |
79 '} else {' + | |
80 'imgElement.src = faviconUrl;' + | |
81 '}' + | |
82 '});' + | |
83 'imgElement.addEventListener(\'error\', function(e) {' + | |
84 'var faviconUrl;' + | |
85 '' + | |
86 'faviconUrl = faviconUrls.pop();' + | |
87 'if (faviconUrl !== undefined) {' + | |
88 'imgElement.src = faviconUrl;' + | |
89 '} else {' + | |
90 'displayBookmarkData(bookmarkData);' + | |
91 '}' + | |
92 '});' + | |
93 'imgElement.src = faviconUrls.pop();' + | |
94 '})();'; | |
37 | 95 |
38 | 96 |
39 /* | 97 /* |
40 * utility stuff | 98 * utility stuff |
41 */ | 99 */ |
346 | 404 |
347 /* | 405 /* |
348 * model | 406 * model |
349 */ | 407 */ |
350 | 408 |
351 var Bookmark = function (url, title, tags, ctime, mtime) { | 409 var Bookmark = function (url, title, favicon, tags, ctime, mtime) { |
352 var parsedTime; | 410 var parsedTime; |
353 | 411 |
354 if (!isString(url)) { | 412 if (!isString(url)) { |
355 throw new TypeError(typeof url + ' is not a string'); | 413 throw new TypeError(typeof url + ' is not a string'); |
356 } | 414 } |
357 this.url = url; | 415 this.url = url; |
358 | 416 |
359 this.title = (isString(title) && title !== '') ? title : url; | 417 this.title = (isString(title) && title !== '') ? title : url; |
418 | |
419 if (isString(favicon) && favicon.match(/^data:image\/png;base64,/)) { | |
420 this.favicon = favicon; | |
421 } else { | |
422 this.favicon = undefined; | |
423 } | |
360 | 424 |
361 if (Array.isArray(tags)) { | 425 if (Array.isArray(tags)) { |
362 // remove duplicates, non-string or empty tags and tags containing | 426 // remove duplicates, non-string or empty tags and tags containing |
363 // commas | 427 // commas |
364 this.tags = new StringSet(tags.filter(function (tag) { | 428 this.tags = new StringSet(tags.filter(function (tag) { |
645 | 709 |
646 // create a temporary list of valid bookmarks | 710 // create a temporary list of valid bookmarks |
647 parsedData.bookmarks.forEach(function (bookmark) { | 711 parsedData.bookmarks.forEach(function (bookmark) { |
648 if (isString(bookmark.url) && bookmark.url !== '') { | 712 if (isString(bookmark.url) && bookmark.url !== '') { |
649 bookmarks.push(new Bookmark(bookmark.url, bookmark.title, | 713 bookmarks.push(new Bookmark(bookmark.url, bookmark.title, |
650 bookmark.tags, bookmark.ctime, bookmark.mtime)); | 714 bookmark.favicon, bookmark.tags, bookmark.ctime, |
715 bookmark.mtime)); | |
651 } | 716 } |
652 }, this); | 717 }, this); |
653 | 718 |
654 // add each bookmark to the model ordered by the last modification time | 719 // add each bookmark to the model ordered by the last modification time |
655 this.add(bookmarks.sort(function (bookmark1, bookmark2) { | 720 this.add(bookmarks.sort(function (bookmark1, bookmark2) { |
820 | 885 |
821 var ActionsView = function () { | 886 var ActionsView = function () { |
822 var saveFormElement; | 887 var saveFormElement; |
823 var loadFormElement; | 888 var loadFormElement; |
824 var newNode; | 889 var newNode; |
825 var editorFormElement; | |
826 | 890 |
827 ObservableMixin.call(this); | 891 ObservableMixin.call(this); |
828 | 892 |
829 this.tagInputTemplate = document.querySelector('#tag-input-template'); | 893 this.tagInputTemplate = document.querySelector('#tag-input-template'); |
830 saveFormElement = document.querySelector('form#save-form'); | 894 saveFormElement = document.querySelector('form#save-form'); |
837 | 901 |
838 // create new editor form from template | 902 // create new editor form from template |
839 newNode = document.importNode( | 903 newNode = document.importNode( |
840 document.querySelector('#bookmark-editor-template').content, true); | 904 document.querySelector('#bookmark-editor-template').content, true); |
841 | 905 |
842 editorFormElement = newNode.querySelector('form.bookmark-editor-form'); | 906 this.editorFormElement = newNode.querySelector('form.bookmark-editor-form'); |
843 editorFormElement.querySelector('legend').textContent = 'Add Bookmark'; | 907 this.editorFormElement.querySelector('legend').textContent = 'Add Bookmark'; |
844 editorFormElement.querySelector('input:not([type="hidden"])').accessKey = | 908 this.editorFormElement.querySelector( |
845 'a'; | 909 'input:not([type="hidden"])').accessKey = 'a'; |
846 editorFormElement.addEventListener('input', this); | 910 this.editorFormElement.addEventListener('input', this); |
847 editorFormElement.addEventListener('click', this); | 911 this.editorFormElement.addEventListener('click', this); |
848 editorFormElement.addEventListener('submit', this); | 912 this.editorFormElement.addEventListener('submit', this); |
849 editorFormElement.addEventListener('reset', this); | 913 this.editorFormElement.addEventListener('reset', this); |
914 | |
915 this.faviconImageElement = | |
916 this.editorFormElement.querySelector('img.bookmark-favicon'); | |
917 this.faviconImageElement.addEventListener('load', this); | |
918 this.faviconImageElement.addEventListener('error', this); | |
919 | |
920 this.missingFaviconUri = this.faviconImageElement.src; | |
850 | 921 |
851 this.editTagListElement = | 922 this.editTagListElement = |
852 editorFormElement.querySelector('ul.tag-input-list'); | 923 this.editorFormElement.querySelector('ul.tag-input-list'); |
853 this.editTagListElement.appendChild(this.createTagInputElement('')); | 924 this.editTagListElement.appendChild(this.createTagInputElement('')); |
854 | 925 |
855 saveFormElement.parentNode.insertBefore(newNode, | 926 saveFormElement.parentNode.insertBefore(newNode, |
856 saveFormElement.nextSibling); | 927 saveFormElement.nextSibling); |
857 | 928 |
874 var parsedData; | 945 var parsedData; |
875 var tags = []; | 946 var tags = []; |
876 var i; | 947 var i; |
877 | 948 |
878 switch (e.type) { | 949 switch (e.type) { |
950 case 'error': | |
951 if (e.target.classList.contains('bookmark-favicon')) { | |
952 if (e.target.src !== this.missingFaviconUri) { | |
953 e.target.src = this.missingFaviconUri; | |
954 } | |
955 } | |
956 break; | |
957 case 'load': | |
958 if (e.target.classList.contains('bookmark-favicon')) { | |
959 this.editorFormElement.favicon.value = | |
960 (e.target.src !== this.missingFaviconUri) ? e.target.src : ''; | |
961 } | |
962 break; | |
879 case 'input': | 963 case 'input': |
880 if (e.target.name === 'bookmarklet-import') { | 964 if (e.target.name === 'bookmarklet-import') { |
881 // get rid of any preceding text | 965 // get rid of any preceding text |
882 bookmarkletData = e.target.value.replace(/^[^{]*/, ''); | 966 bookmarkletData = e.target.value.replace(/^[^{]*/, ''); |
883 | 967 |
890 if (isString(parsedData.url) && parsedData.url !== '') { | 974 if (isString(parsedData.url) && parsedData.url !== '') { |
891 e.target.form.elements.url.value = parsedData.url; | 975 e.target.form.elements.url.value = parsedData.url; |
892 } | 976 } |
893 if (isString(parsedData.title) && parsedData.title !== '') { | 977 if (isString(parsedData.title) && parsedData.title !== '') { |
894 e.target.form.elements.title.value = parsedData.title; | 978 e.target.form.elements.title.value = parsedData.title; |
979 } | |
980 if (isString(parsedData.favicon) && | |
981 parsedData.favicon.match(/^data:image\/png;base64,/)) { | |
982 this.faviconImageElement.src = parsedData.favicon; | |
895 } | 983 } |
896 } | 984 } |
897 break; | 985 break; |
898 case 'click': | 986 case 'click': |
899 if (e.target.name === 'more-tags') { | 987 if (e.target.name === 'more-tags') { |
926 } else { | 1014 } else { |
927 tags.push(e.target.tag.value.trim()); | 1015 tags.push(e.target.tag.value.trim()); |
928 } | 1016 } |
929 | 1017 |
930 this.notify('save-bookmark', e.target.url.value, | 1018 this.notify('save-bookmark', e.target.url.value, |
931 e.target.title.value, tags); | 1019 e.target.title.value, e.target.favicon.value, tags); |
932 | 1020 |
933 e.target.reset(); | 1021 e.target.reset(); |
934 } | 1022 } |
935 break; | 1023 break; |
936 case 'reset': | 1024 case 'reset': |
937 if (e.target.classList.contains('bookmark-editor-form')) { | 1025 if (e.target.classList.contains('bookmark-editor-form')) { |
938 e.target.blur(); | 1026 e.target.blur(); |
1027 | |
1028 e.target.querySelector('img.bookmark-favicon').src = | |
1029 this.missingFaviconUri; | |
939 | 1030 |
940 // remove all but one tag input element | 1031 // remove all but one tag input element |
941 while (this.editTagListElement.firstChild !== null) { | 1032 while (this.editTagListElement.firstChild !== null) { |
942 this.editTagListElement.removeChild( | 1033 this.editTagListElement.removeChild( |
943 this.editTagListElement.firstChild); | 1034 this.editTagListElement.firstChild); |
977 this.bookmarkEditorTemplate = | 1068 this.bookmarkEditorTemplate = |
978 document.querySelector('#bookmark-editor-template'); | 1069 document.querySelector('#bookmark-editor-template'); |
979 this.tagInputTemplate = document.querySelector('#tag-input-template'); | 1070 this.tagInputTemplate = document.querySelector('#tag-input-template'); |
980 | 1071 |
981 this.bookmarkListElement = document.querySelector('ul#bookmark-list'); | 1072 this.bookmarkListElement = document.querySelector('ul#bookmark-list'); |
1073 this.bookmarkListElement.addEventListener('input', this); | |
982 this.bookmarkListElement.addEventListener('click', this); | 1074 this.bookmarkListElement.addEventListener('click', this); |
983 this.bookmarkListElement.addEventListener('submit', this); | 1075 this.bookmarkListElement.addEventListener('submit', this); |
984 this.bookmarkListElement.addEventListener('reset', this); | 1076 this.bookmarkListElement.addEventListener('reset', this); |
985 | 1077 |
986 searchFormElement = document.querySelector('#search-form'); | 1078 searchFormElement = document.querySelector('#search-form'); |
989 | 1081 |
990 this.searchTermInputElement = searchFormElement['search-term']; | 1082 this.searchTermInputElement = searchFormElement['search-term']; |
991 | 1083 |
992 this.bookmarkMessageElement = document.querySelector('#bookmark-message'); | 1084 this.bookmarkMessageElement = document.querySelector('#bookmark-message'); |
993 | 1085 |
1086 this.missingFaviconUri = ''; | |
1087 | |
994 this.updateBookmarkMessage(); | 1088 this.updateBookmarkMessage(); |
995 }; | 1089 }; |
996 | 1090 |
997 extend(BookmarkView, ObservableMixin); | 1091 extend(BookmarkView, ObservableMixin); |
998 | 1092 |
999 BookmarkView.prototype.handleEvent = function (e) { | 1093 BookmarkView.prototype.handleEvent = function (e) { |
1094 var bookmarkletData; | |
1095 var parsedData; | |
1000 var i; | 1096 var i; |
1001 var tags = []; | 1097 var tags = []; |
1002 var node; | 1098 var node; |
1003 | 1099 |
1004 switch (e.type) { | 1100 switch (e.type) { |
1101 case 'error': | |
1102 if (e.target.classList.contains('bookmark-favicon')) { | |
1103 if (e.target.src !== this.missingFaviconUri) { | |
1104 e.target.src = this.missingFaviconUri; | |
1105 } | |
1106 } | |
1107 break; | |
1108 case 'load': | |
1109 if (e.target.classList.contains('bookmark-favicon')) { | |
1110 node = e.target; | |
1111 while ((node = node.parentNode) !== null) { | |
1112 if (node.classList.contains('bookmark-editor-form')) { | |
1113 node.favicon.value = | |
1114 (e.target.src !== this.missingFaviconUri) ? | |
1115 e.target.src : ''; | |
1116 break; | |
1117 } | |
1118 } | |
1119 } | |
1120 break; | |
1121 case 'input': | |
1122 if (e.target.name === 'bookmarklet-import') { | |
1123 // get rid of any preceding text | |
1124 bookmarkletData = e.target.value.replace(/^[^{]*/, ''); | |
1125 | |
1126 try { | |
1127 parsedData = JSON.parse(bookmarkletData); | |
1128 } catch (exception) { | |
1129 return; | |
1130 } | |
1131 | |
1132 if (isString(parsedData.url) && parsedData.url !== '') { | |
1133 e.target.form.elements.url.value = parsedData.url; | |
1134 } | |
1135 if (isString(parsedData.title) && parsedData.title !== '') { | |
1136 e.target.form.elements.title.value = parsedData.title; | |
1137 } | |
1138 if (isString(parsedData.favicon) && | |
1139 parsedData.favicon.match(/^data:image\/png;base64,/)) { | |
1140 e.target.form.querySelector('img.bookmark-favicon').src = | |
1141 parsedData.favicon; | |
1142 } | |
1143 } | |
1144 break; | |
1005 case 'click': | 1145 case 'click': |
1006 switch (e.target.name) { | 1146 switch (e.target.name) { |
1007 case 'edit-bookmark': | 1147 case 'edit-bookmark': |
1008 e.target.blur(); | 1148 e.target.blur(); |
1009 // fallthrough | 1149 // fallthrough |
1038 } else { | 1178 } else { |
1039 tags.push(e.target.tag.value.trim()); | 1179 tags.push(e.target.tag.value.trim()); |
1040 } | 1180 } |
1041 | 1181 |
1042 this.notify('save-bookmark', e.target.url.value, | 1182 this.notify('save-bookmark', e.target.url.value, |
1043 e.target.title.value, tags, e.target['original-url'].value); | 1183 e.target.title.value, e.target.favicon.value, tags, |
1184 e.target['original-url'].value); | |
1044 } else if (e.target.id === 'search-form') { | 1185 } else if (e.target.id === 'search-form') { |
1045 // search | 1186 // search |
1046 e.preventDefault(); | 1187 e.preventDefault(); |
1047 e.target.blur(); | 1188 e.target.blur(); |
1048 | 1189 |
1081 }; | 1222 }; |
1082 | 1223 |
1083 BookmarkView.prototype.onBookmarkAdded = function (bookmark) { | 1224 BookmarkView.prototype.onBookmarkAdded = function (bookmark) { |
1084 var newNode; | 1225 var newNode; |
1085 var bookmarkElement; | 1226 var bookmarkElement; |
1227 var faviconElement; | |
1086 var linkElement; | 1228 var linkElement; |
1087 var hostnameElement; | 1229 var hostnameElement; |
1088 var urlElement; | 1230 var urlElement; |
1089 var ctimeElement; | 1231 var ctimeElement; |
1090 var mtimeElement; | 1232 var mtimeElement; |
1092 | 1234 |
1093 newNode = document.importNode(this.bookmarkTemplate.content, true); | 1235 newNode = document.importNode(this.bookmarkTemplate.content, true); |
1094 | 1236 |
1095 bookmarkElement = newNode.querySelector('li'); | 1237 bookmarkElement = newNode.querySelector('li'); |
1096 bookmarkElement.dataset.bookmarkUrl = bookmark.url; | 1238 bookmarkElement.dataset.bookmarkUrl = bookmark.url; |
1239 | |
1240 faviconElement = bookmarkElement.querySelector('img.bookmark-favicon'); | |
1241 faviconElement.src = (bookmark.favicon) ? bookmark.favicon : | |
1242 this.missingFaviconUri; | |
1243 faviconElement.alt = ''; | |
1097 | 1244 |
1098 linkElement = bookmarkElement.querySelector('a.bookmark-link'); | 1245 linkElement = bookmarkElement.querySelector('a.bookmark-link'); |
1099 linkElement.textContent = linkElement.title = bookmark.title; | 1246 linkElement.textContent = linkElement.title = bookmark.title; |
1100 linkElement.href = bookmark.url; | 1247 linkElement.href = bookmark.url; |
1101 | 1248 |
1209 | 1356 |
1210 BookmarkView.prototype.displayBookmarkEditor = function (bookmark) { | 1357 BookmarkView.prototype.displayBookmarkEditor = function (bookmark) { |
1211 var bookmarkElement; | 1358 var bookmarkElement; |
1212 var newNode; | 1359 var newNode; |
1213 var formElement; | 1360 var formElement; |
1361 var faviconImageElement; | |
1214 var editTagListElement; | 1362 var editTagListElement; |
1215 | 1363 |
1216 bookmarkElement = | 1364 bookmarkElement = |
1217 this.bookmarkListElement.querySelector('ul#bookmark-list > li' + | 1365 this.bookmarkListElement.querySelector('ul#bookmark-list > li' + |
1218 createDatasetSelector('bookmark-url', bookmark.url)); | 1366 createDatasetSelector('bookmark-url', bookmark.url)); |
1228 formElement = newNode.querySelector('form.bookmark-editor-form'); | 1376 formElement = newNode.querySelector('form.bookmark-editor-form'); |
1229 formElement.querySelector('legend').textContent = 'Edit Bookmark'; | 1377 formElement.querySelector('legend').textContent = 'Edit Bookmark'; |
1230 formElement['original-url'].value = bookmark.url; | 1378 formElement['original-url'].value = bookmark.url; |
1231 formElement.url.value = bookmark.url; | 1379 formElement.url.value = bookmark.url; |
1232 formElement.title.value = bookmark.title; | 1380 formElement.title.value = bookmark.title; |
1381 | |
1382 faviconImageElement = formElement.querySelector('img.bookmark-favicon'); | |
1383 faviconImageElement.addEventListener('load', this); | |
1384 faviconImageElement.addEventListener('error', this); | |
1385 this.missingFaviconUri = faviconImageElement.src; | |
1386 if (bookmark.favicon) { | |
1387 faviconImageElement.src = bookmark.favicon; | |
1388 } | |
1233 | 1389 |
1234 editTagListElement = formElement.querySelector('ul.tag-input-list'); | 1390 editTagListElement = formElement.querySelector('ul.tag-input-list'); |
1235 bookmark.tags.forEach(function (tag) { | 1391 bookmark.tags.forEach(function (tag) { |
1236 editTagListElement.appendChild(this.createTagInputElement(tag)); | 1392 editTagListElement.appendChild(this.createTagInputElement(tag)); |
1237 }, this); | 1393 }, this); |
1377 BooketController.prototype.onEditBookmark = function (bookmarkUrl) { | 1533 BooketController.prototype.onEditBookmark = function (bookmarkUrl) { |
1378 this.bookmarkView.displayBookmarkEditor( | 1534 this.bookmarkView.displayBookmarkEditor( |
1379 this.bookmarkModel.get(bookmarkUrl)); | 1535 this.bookmarkModel.get(bookmarkUrl)); |
1380 }; | 1536 }; |
1381 | 1537 |
1382 BooketController.prototype.onSaveBookmark = function (url, title, tags, | 1538 BooketController.prototype.onSaveBookmark = function (url, title, |
1383 originalUrl) { | 1539 favicon, tags, originalUrl) { |
1384 var ctime; | 1540 var ctime; |
1385 | 1541 |
1386 if (originalUrl === undefined) { | 1542 if (originalUrl === undefined) { |
1387 // saving new bookmark, get confirmation before replacing existing one | 1543 // saving new bookmark, get confirmation before replacing existing one |
1388 if (this.bookmarkModel.has(url)) { | 1544 if (this.bookmarkModel.has(url)) { |
1401 ctime = (this.bookmarkModel.has(url)) ? | 1557 ctime = (this.bookmarkModel.has(url)) ? |
1402 this.bookmarkModel.get(url).ctime : new Date(); | 1558 this.bookmarkModel.get(url).ctime : new Date(); |
1403 | 1559 |
1404 this.bookmarkModel.delete(originalUrl); | 1560 this.bookmarkModel.delete(originalUrl); |
1405 } | 1561 } |
1406 this.bookmarkModel.add(new Bookmark(url, title, tags, ctime)); | 1562 this.bookmarkModel.add(new Bookmark(url, title, favicon, tags, ctime)); |
1407 }; | 1563 }; |
1408 | 1564 |
1409 BooketController.prototype.onDeleteBookmark = function (bookmarkUrl) { | 1565 BooketController.prototype.onDeleteBookmark = function (bookmarkUrl) { |
1410 if (this.bookmarkView.confirmDeleteBookmark( | 1566 if (this.bookmarkView.confirmDeleteBookmark( |
1411 this.bookmarkModel.get(bookmarkUrl))) { | 1567 this.bookmarkModel.get(bookmarkUrl))) { |