Mercurial > projects > booket
changeset 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 | aca8d797d569 |
files | .hgignore Makefile booket.css booket.html booket.js missing-favicon.src.svg |
diffstat | 6 files changed, 332 insertions(+), 26 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Sun Sep 14 23:12:37 2014 +0200 @@ -0,0 +1,3 @@ +syntax: regexp + +(?<!\.src)\.svg$
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile Sun Sep 14 23:12:37 2014 +0200 @@ -0,0 +1,44 @@ +# +# Copyright (C) 2014 Guido Berhoerster <guido+booket@berhoerster.name> +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +SCOUR := scour +XMLLINT := xmllint + +SVGIMAGES := missing-favicon.svg + +.DEFAULT_TARGET = all + +.PHONY: all clean clobber + +all: $(SVGIMAGES) + +%.svg: %.src.svg + $(SCOUR) --quiet -i $< --create-groups --enable-id-stripping \ + --enable-comment-stripping --remove-metadata \ + --no-renderer-workaround --strip-xml-prolog --enable-viewboxing \ + --set-precision=5 | $(XMLLINT) --format --noblanks --output $@ - + +clean: + rm -f $(SVGIMAGES) + +clobber: clean
--- a/booket.css Wed Sep 10 19:45:23 2014 +0200 +++ b/booket.css Sun Sep 14 23:12:37 2014 +0200 @@ -63,6 +63,10 @@ white-space: nowrap; } +img { + border: none; +} + h1 { font-size: 2em; margin: .67em 0 @@ -130,6 +134,11 @@ margin: 1em 0 0 0; } +form.bookmark-editor-form img.bookmark-favicon { + display: block; + padding: 1px 0; +} + #bookmarklet, #keyboard-shortcuts { float: right;
--- a/booket.html Wed Sep 10 19:45:23 2014 +0200 +++ b/booket.html Sun Sep 14 23:12:37 2014 +0200 @@ -51,6 +51,9 @@ name="url" size="60" placeholder="http://example.com/"></input></label> <label>Title <input type="text" name="title" size="60" placeholder="A Title"></input></label> + <label>Favicon <img width="16" height="16" src="missing-favicon.svg" + class="bookmark-favicon" alt=""></img><input type="hidden" + name="favicon"></input></label> <div> <ul class="tag-input-list"></ul> <button type="button" name="more-tags">Add more tags</button> @@ -144,7 +147,8 @@ <template id="bookmark-template"> <li> <details> - <summary><p> + <summary><p><img width="16" height="16" + class="bookmark-favicon"></img> <a class="bookmark-link" target="_blank"></a> <span class="bookmark-hostname"></span> </p>
--- a/booket.js Wed Sep 10 19:45:23 2014 +0200 +++ b/booket.js Sun Sep 14 23:12:37 2014 +0200 @@ -25,15 +25,73 @@ 'use strict'; var BOOKMARKLET_URI = - 'javascript:(function () {' + - '\'use strict\';' + + 'javascript:(function() {' + + '\'use strict\';' + + '' + + 'function displayBookmarkData(bookmarkData) {' + + 'window.alert(\'Copy the following data and paste it into \' +' + + '\'Booket:\\n\\n\' + JSON.stringify(bookmarkData));' + + '}' + + '' + + 'var bookmarkData = {' + + '\'url\': document.URL,' + + '\'title\': document.title,' + + '\'favicon\': undefined' + + '};' + + 'var faviconLinkElement;' + + 'var faviconUrls = [];' + + 'var aElement;' + + 'var canvasElement;' + + 'var canvasCtx;' + + 'var imgElement;' + + '' + + 'aElement = document.createElement(\'a\');' + + 'aElement.href = document.URL;' + + '' + + 'faviconUrls.push(aElement.protocol + \'//\' + aElement.host + ' + + '\'/favicon.ico\');' + + '' + + 'faviconLinkElement = document.querySelector(' + + '\'link[rel~=\\\'icon\\\']\');' + + 'if (faviconLinkElement !== null) {' + + 'faviconUrls.push(faviconLinkElement.href);' + + '}' + '' + - 'window.alert(\'Copy the following data and paste it into \' +' + - '\'Booket:\\n\\n\' + JSON.stringify({' + - '\'url\': document.URL,' + - '\'title\': document.title' + - '}));' + - '}) ();'; + 'canvasElement = document.createElement(\'canvas\');' + + 'canvasCtx = canvasElement.getContext(\'2d\');' + + '' + + 'imgElement = new Image();' + + 'imgElement.addEventListener(\'load\', function(e) {' + + 'var faviconUrl;' + + '' + + 'canvasElement.width = 16;' + + 'canvasElement.height = 16;' + + 'canvasCtx.clearRect(0, 0, 16, 16);' + + 'try {' + + 'canvasCtx.drawImage(this, 0, 0, 16, 16);' + + 'bookmarkData.favicon = canvasElement.toDataURL();' + + '} catch (exception) {' + + 'faviconUrl = faviconUrls.pop();' + + '}' + + 'if (bookmarkData.favicon !== undefined || ' + + 'faviconUrl === undefined) {' + + 'displayBookmarkData(bookmarkData);' + + '} else {' + + 'imgElement.src = faviconUrl;' + + '}' + + '});' + + 'imgElement.addEventListener(\'error\', function(e) {' + + 'var faviconUrl;' + + '' + + 'faviconUrl = faviconUrls.pop();' + + 'if (faviconUrl !== undefined) {' + + 'imgElement.src = faviconUrl;' + + '} else {' + + 'displayBookmarkData(bookmarkData);' + + '}' + + '});' + + 'imgElement.src = faviconUrls.pop();' + + '})();'; /* @@ -348,7 +406,7 @@ * model */ -var Bookmark = function (url, title, tags, ctime, mtime) { +var Bookmark = function (url, title, favicon, tags, ctime, mtime) { var parsedTime; if (!isString(url)) { @@ -358,6 +416,12 @@ this.title = (isString(title) && title !== '') ? title : url; + if (isString(favicon) && favicon.match(/^data:image\/png;base64,/)) { + this.favicon = favicon; + } else { + this.favicon = undefined; + } + if (Array.isArray(tags)) { // remove duplicates, non-string or empty tags and tags containing // commas @@ -647,7 +711,8 @@ parsedData.bookmarks.forEach(function (bookmark) { if (isString(bookmark.url) && bookmark.url !== '') { bookmarks.push(new Bookmark(bookmark.url, bookmark.title, - bookmark.tags, bookmark.ctime, bookmark.mtime)); + bookmark.favicon, bookmark.tags, bookmark.ctime, + bookmark.mtime)); } }, this); @@ -822,7 +887,6 @@ var saveFormElement; var loadFormElement; var newNode; - var editorFormElement; ObservableMixin.call(this); @@ -839,17 +903,24 @@ newNode = document.importNode( document.querySelector('#bookmark-editor-template').content, true); - editorFormElement = newNode.querySelector('form.bookmark-editor-form'); - editorFormElement.querySelector('legend').textContent = 'Add Bookmark'; - editorFormElement.querySelector('input:not([type="hidden"])').accessKey = - 'a'; - editorFormElement.addEventListener('input', this); - editorFormElement.addEventListener('click', this); - editorFormElement.addEventListener('submit', this); - editorFormElement.addEventListener('reset', this); + this.editorFormElement = newNode.querySelector('form.bookmark-editor-form'); + this.editorFormElement.querySelector('legend').textContent = 'Add Bookmark'; + this.editorFormElement.querySelector( + 'input:not([type="hidden"])').accessKey = 'a'; + this.editorFormElement.addEventListener('input', this); + this.editorFormElement.addEventListener('click', this); + this.editorFormElement.addEventListener('submit', this); + this.editorFormElement.addEventListener('reset', this); + + this.faviconImageElement = + this.editorFormElement.querySelector('img.bookmark-favicon'); + this.faviconImageElement.addEventListener('load', this); + this.faviconImageElement.addEventListener('error', this); + + this.missingFaviconUri = this.faviconImageElement.src; this.editTagListElement = - editorFormElement.querySelector('ul.tag-input-list'); + this.editorFormElement.querySelector('ul.tag-input-list'); this.editTagListElement.appendChild(this.createTagInputElement('')); saveFormElement.parentNode.insertBefore(newNode, @@ -876,6 +947,19 @@ var i; switch (e.type) { + case 'error': + if (e.target.classList.contains('bookmark-favicon')) { + if (e.target.src !== this.missingFaviconUri) { + e.target.src = this.missingFaviconUri; + } + } + break; + case 'load': + if (e.target.classList.contains('bookmark-favicon')) { + this.editorFormElement.favicon.value = + (e.target.src !== this.missingFaviconUri) ? e.target.src : ''; + } + break; case 'input': if (e.target.name === 'bookmarklet-import') { // get rid of any preceding text @@ -893,6 +977,10 @@ if (isString(parsedData.title) && parsedData.title !== '') { e.target.form.elements.title.value = parsedData.title; } + if (isString(parsedData.favicon) && + parsedData.favicon.match(/^data:image\/png;base64,/)) { + this.faviconImageElement.src = parsedData.favicon; + } } break; case 'click': @@ -928,7 +1016,7 @@ } this.notify('save-bookmark', e.target.url.value, - e.target.title.value, tags); + e.target.title.value, e.target.favicon.value, tags); e.target.reset(); } @@ -937,6 +1025,9 @@ if (e.target.classList.contains('bookmark-editor-form')) { e.target.blur(); + e.target.querySelector('img.bookmark-favicon').src = + this.missingFaviconUri; + // remove all but one tag input element while (this.editTagListElement.firstChild !== null) { this.editTagListElement.removeChild( @@ -979,6 +1070,7 @@ this.tagInputTemplate = document.querySelector('#tag-input-template'); this.bookmarkListElement = document.querySelector('ul#bookmark-list'); + this.bookmarkListElement.addEventListener('input', this); this.bookmarkListElement.addEventListener('click', this); this.bookmarkListElement.addEventListener('submit', this); this.bookmarkListElement.addEventListener('reset', this); @@ -991,17 +1083,65 @@ this.bookmarkMessageElement = document.querySelector('#bookmark-message'); + this.missingFaviconUri = ''; + this.updateBookmarkMessage(); }; extend(BookmarkView, ObservableMixin); BookmarkView.prototype.handleEvent = function (e) { + var bookmarkletData; + var parsedData; var i; var tags = []; var node; switch (e.type) { + case 'error': + if (e.target.classList.contains('bookmark-favicon')) { + if (e.target.src !== this.missingFaviconUri) { + e.target.src = this.missingFaviconUri; + } + } + break; + case 'load': + if (e.target.classList.contains('bookmark-favicon')) { + node = e.target; + while ((node = node.parentNode) !== null) { + if (node.classList.contains('bookmark-editor-form')) { + node.favicon.value = + (e.target.src !== this.missingFaviconUri) ? + e.target.src : ''; + break; + } + } + } + break; + case 'input': + if (e.target.name === 'bookmarklet-import') { + // get rid of any preceding text + bookmarkletData = e.target.value.replace(/^[^{]*/, ''); + + try { + parsedData = JSON.parse(bookmarkletData); + } catch (exception) { + return; + } + + if (isString(parsedData.url) && parsedData.url !== '') { + e.target.form.elements.url.value = parsedData.url; + } + if (isString(parsedData.title) && parsedData.title !== '') { + e.target.form.elements.title.value = parsedData.title; + } + if (isString(parsedData.favicon) && + parsedData.favicon.match(/^data:image\/png;base64,/)) { + e.target.form.querySelector('img.bookmark-favicon').src = + parsedData.favicon; + } + } + break; case 'click': switch (e.target.name) { case 'edit-bookmark': @@ -1040,7 +1180,8 @@ } this.notify('save-bookmark', e.target.url.value, - e.target.title.value, tags, e.target['original-url'].value); + e.target.title.value, e.target.favicon.value, tags, + e.target['original-url'].value); } else if (e.target.id === 'search-form') { // search e.preventDefault(); @@ -1083,6 +1224,7 @@ BookmarkView.prototype.onBookmarkAdded = function (bookmark) { var newNode; var bookmarkElement; + var faviconElement; var linkElement; var hostnameElement; var urlElement; @@ -1095,6 +1237,11 @@ bookmarkElement = newNode.querySelector('li'); bookmarkElement.dataset.bookmarkUrl = bookmark.url; + faviconElement = bookmarkElement.querySelector('img.bookmark-favicon'); + faviconElement.src = (bookmark.favicon) ? bookmark.favicon : + this.missingFaviconUri; + faviconElement.alt = ''; + linkElement = bookmarkElement.querySelector('a.bookmark-link'); linkElement.textContent = linkElement.title = bookmark.title; linkElement.href = bookmark.url; @@ -1211,6 +1358,7 @@ var bookmarkElement; var newNode; var formElement; + var faviconImageElement; var editTagListElement; bookmarkElement = @@ -1231,6 +1379,14 @@ formElement.url.value = bookmark.url; formElement.title.value = bookmark.title; + faviconImageElement = formElement.querySelector('img.bookmark-favicon'); + faviconImageElement.addEventListener('load', this); + faviconImageElement.addEventListener('error', this); + this.missingFaviconUri = faviconImageElement.src; + if (bookmark.favicon) { + faviconImageElement.src = bookmark.favicon; + } + editTagListElement = formElement.querySelector('ul.tag-input-list'); bookmark.tags.forEach(function (tag) { editTagListElement.appendChild(this.createTagInputElement(tag)); @@ -1379,8 +1535,8 @@ this.bookmarkModel.get(bookmarkUrl)); }; -BooketController.prototype.onSaveBookmark = function (url, title, tags, - originalUrl) { +BooketController.prototype.onSaveBookmark = function (url, title, + favicon, tags, originalUrl) { var ctime; if (originalUrl === undefined) { @@ -1403,7 +1559,7 @@ this.bookmarkModel.delete(originalUrl); } - this.bookmarkModel.add(new Bookmark(url, title, tags, ctime)); + this.bookmarkModel.add(new Bookmark(url, title, favicon, tags, ctime)); }; BooketController.prototype.onDeleteBookmark = function (bookmarkUrl) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/missing-favicon.src.svg Sun Sep 14 23:12:37 2014 +0200 @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="16" + height="16" + id="svg2" + version="1.1" + inkscape:version="0.48.4 r9939" + sodipodi:docname="missing-favicon.svg"> + <title + id="title2985">Missing Favicon</title> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="43.9375" + inkscape:cx="7.4651494" + inkscape:cy="8" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="true" + inkscape:window-width="1542" + inkscape:window-height="947" + inkscape:window-x="54" + inkscape:window-y="0" + inkscape:window-maximized="0"> + <inkscape:grid + type="xygrid" + id="grid3006" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>Missing Favicon</dc:title> + <dc:creator> + <cc:Agent> + <dc:title>Guido Berhoerster</dc:title> + </cc:Agent> + </dc:creator> + <dc:date>2014-09-14</dc:date> + <cc:license + rdf:resource="http://creativecommons.org/licenses/publicdomain/" /> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/publicdomain/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1036.3622)"> + <rect + style="fill:none;stroke:#555753;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.00000029;stroke-opacity:1;stroke-dasharray:1,1;stroke-dashoffset:0.5" + id="rect3008" + width="15" + height="15" + x="0.5" + y="1036.8622" /> + </g> +</svg>