guido+booket@0: /* guido+booket@0: * Copyright (C) 2014 Guido Berhoerster guido+booket@0: * guido+booket@0: * Permission is hereby granted, free of charge, to any person obtaining guido+booket@0: * a copy of this software and associated documentation files (the guido+booket@0: * "Software"), to deal in the Software without restriction, including guido+booket@0: * without limitation the rights to use, copy, modify, merge, publish, guido+booket@0: * distribute, sublicense, and/or sell copies of the Software, and to guido+booket@0: * permit persons to whom the Software is furnished to do so, subject to guido+booket@0: * the following conditions: guido+booket@0: * guido+booket@0: * The above copyright notice and this permission notice shall be included guido+booket@0: * in all copies or substantial portions of the Software. guido+booket@0: * guido+booket@0: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, guido+booket@0: * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF guido+booket@0: * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. guido+booket@0: * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY guido+booket@0: * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, guido+booket@0: * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE guido+booket@0: * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guido+booket@0: */ guido+booket@0: guido+booket@0: (function () { guido+booket@0: 'use strict'; guido+booket@0: guido+booket@6: var BOOKMARKLET_URI = guido+booket@7: 'javascript:(function() {' + guido+booket@7: '\'use strict\';' + guido+booket@7: '' + guido+booket@7: 'function displayBookmarkData(bookmarkData) {' + guido+booket@7: 'window.alert(\'Copy the following data and paste it into \' +' + guido+booket@7: '\'Booket:\\n\\n\' + JSON.stringify(bookmarkData));' + guido+booket@7: '}' + guido+booket@7: '' + guido+booket@7: 'var bookmarkData = {' + guido+booket@7: '\'url\': document.URL,' + guido+booket@7: '\'title\': document.title,' + guido+booket@7: '\'favicon\': undefined' + guido+booket@7: '};' + guido+booket@7: 'var faviconLinkElement;' + guido+booket@7: 'var faviconUrls = [];' + guido+booket@7: 'var aElement;' + guido+booket@7: 'var canvasElement;' + guido+booket@7: 'var canvasCtx;' + guido+booket@7: 'var imgElement;' + guido+booket@7: '' + guido+booket@7: 'aElement = document.createElement(\'a\');' + guido+booket@7: 'aElement.href = document.URL;' + guido+booket@7: '' + guido+booket@7: 'faviconUrls.push(aElement.protocol + \'//\' + aElement.host + ' + guido+booket@7: '\'/favicon.ico\');' + guido+booket@7: '' + guido+booket@7: 'faviconLinkElement = document.querySelector(' + guido+booket@7: '\'link[rel~=\\\'icon\\\']\');' + guido+booket@7: 'if (faviconLinkElement !== null) {' + guido+booket@7: 'faviconUrls.push(faviconLinkElement.href);' + guido+booket@7: '}' + guido+booket@6: '' + guido+booket@7: 'canvasElement = document.createElement(\'canvas\');' + guido+booket@7: 'canvasCtx = canvasElement.getContext(\'2d\');' + guido+booket@7: '' + guido+booket@7: 'imgElement = new Image();' + guido+booket@7: 'imgElement.addEventListener(\'load\', function(e) {' + guido+booket@7: 'var faviconUrl;' + guido+booket@7: '' + guido+booket@7: 'canvasElement.width = 16;' + guido+booket@7: 'canvasElement.height = 16;' + guido+booket@7: 'canvasCtx.clearRect(0, 0, 16, 16);' + guido+booket@7: 'try {' + guido+booket@7: 'canvasCtx.drawImage(this, 0, 0, 16, 16);' + guido+booket@7: 'bookmarkData.favicon = canvasElement.toDataURL();' + guido+booket@7: '} catch (exception) {' + guido+booket@7: 'faviconUrl = faviconUrls.pop();' + guido+booket@7: '}' + guido+booket@7: 'if (bookmarkData.favicon !== undefined || ' + guido+booket@7: 'faviconUrl === undefined) {' + guido+booket@7: 'displayBookmarkData(bookmarkData);' + guido+booket@7: '} else {' + guido+booket@7: 'imgElement.src = faviconUrl;' + guido+booket@7: '}' + guido+booket@7: '});' + guido+booket@7: 'imgElement.addEventListener(\'error\', function(e) {' + guido+booket@7: 'var faviconUrl;' + guido+booket@7: '' + guido+booket@7: 'faviconUrl = faviconUrls.pop();' + guido+booket@7: 'if (faviconUrl !== undefined) {' + guido+booket@7: 'imgElement.src = faviconUrl;' + guido+booket@7: '} else {' + guido+booket@7: 'displayBookmarkData(bookmarkData);' + guido+booket@7: '}' + guido+booket@7: '});' + guido+booket@7: 'imgElement.src = faviconUrls.pop();' + guido+booket@7: '})();'; guido+booket@6: guido+booket@6: guido+booket@0: /* guido+booket@0: * utility stuff guido+booket@0: */ guido+booket@0: guido+booket@0: function isNumber(number) { guido+booket@0: return (Object.prototype.toString.call(number) === '[object Number]'); guido+booket@0: } guido+booket@0: guido+booket@0: function isString(number) { guido+booket@0: return (Object.prototype.toString.call(number) === '[object String]'); guido+booket@0: } guido+booket@0: guido+booket@0: function arrayEqual(array1, array2) { guido+booket@0: if (!Array.isArray(array1)) { guido+booket@0: throw new TypeError(typeof array1 + ' is not an array'); guido+booket@0: } else if (!Array.isArray(array2)) { guido+booket@0: throw new TypeError(typeof array2 + ' is not an array'); guido+booket@0: } guido+booket@0: guido+booket@0: if (array1.length !== array2.length) { guido+booket@0: return false; guido+booket@0: } else if (array1.length === 0 && array2.length === 0) { guido+booket@0: return true; guido+booket@0: } guido+booket@0: guido+booket@0: return array1.slice().sort().every(function (value, i) { guido+booket@0: return value === array2[i]; guido+booket@0: }); guido+booket@0: } guido+booket@0: guido+booket@0: function parseHash(url) { guido+booket@0: var hashData; guido+booket@0: var pos; guido+booket@0: var hash; guido+booket@0: var hashParts; guido+booket@0: var key; guido+booket@0: var value; guido+booket@0: var i; guido+booket@0: guido+booket@0: hashData = new StringMap(); guido+booket@0: pos = url.indexOf('#'); guido+booket@0: hash = (pos > -1) ? url.substr(pos + 1) : ''; guido+booket@0: // hash parts are seperated by a ';' guido+booket@0: hashParts = hash.split(';'); guido+booket@0: for (i = 0; i < hashParts.length; i++) { guido+booket@0: // key and value pairs are seperated by a '=', an empty value will guido+booket@0: // cause the key to be ignored guido+booket@0: pos = hashParts[i].indexOf('='); guido+booket@0: if (pos > -1) { guido+booket@0: key = decodeURIComponent(hashParts[i].substr(0, pos)); guido+booket@0: value = decodeURIComponent(hashParts[i].substr(pos + 1)); guido+booket@0: hashData.set(key, value); guido+booket@0: } guido+booket@0: } guido+booket@0: guido+booket@0: return hashData; guido+booket@0: } guido+booket@0: guido+booket@0: function serializeHash(url, hashData) { guido+booket@0: var hashParts = []; guido+booket@0: var pos; guido+booket@0: guido+booket@0: pos = url.indexOf('#'); guido+booket@0: if (pos > -1) { guido+booket@0: url = url.substr(0, pos); guido+booket@0: } guido+booket@0: guido+booket@0: hashData.forEach(function (value, key) { guido+booket@0: if (value !== '') { guido+booket@0: hashParts.push(encodeURIComponent(key) + '=' + guido+booket@0: encodeURIComponent(value)); guido+booket@0: } guido+booket@0: }); guido+booket@0: guido+booket@0: // only append a '#' if there are any hash parts guido+booket@0: return url + (hashParts.length > 0 ? '#' + hashParts.join(';') : ''); guido+booket@0: } guido+booket@0: guido+booket@0: function getAncestorElementDatasetItem(node, item) { guido+booket@0: while ((node = node.parentNode) !== null) { guido+booket@0: if (node.dataset && node.dataset[item] !== undefined) { guido+booket@0: return node.dataset[item]; guido+booket@0: } guido+booket@0: } guido+booket@0: guido+booket@0: return undefined; guido+booket@0: } guido+booket@0: guido+booket@0: // for use with Node.querySelector() and Node.querySelectorAll() guido+booket@0: function createDatasetSelector(name, value) { guido+booket@0: return '[data-' + name + '="' + value.replace(/["\\]/g, '\\$&') + '"]'; guido+booket@0: } guido+booket@0: guido+booket@0: function extend(targetObject, sourceObject) { guido+booket@0: var propertyName; guido+booket@0: guido+booket@0: for (propertyName in sourceObject.prototype) { guido+booket@0: if (!Object.prototype.hasOwnProperty.call(targetObject.prototype, guido+booket@0: propertyName)) { guido+booket@0: targetObject.prototype[propertyName] = guido+booket@0: sourceObject.prototype[propertyName]; guido+booket@0: } guido+booket@0: } guido+booket@0: } guido+booket@0: guido+booket@0: guido+booket@0: var ObservableMixin = function () { guido+booket@0: this._eventsObservers = {}; guido+booket@0: }; guido+booket@0: guido+booket@0: ObservableMixin.prototype.addObserver = function (eventName, observer) { guido+booket@0: var i; guido+booket@0: guido+booket@0: if (!Object.prototype.hasOwnProperty.call(this._eventsObservers, guido+booket@0: eventName)) { guido+booket@0: this._eventsObservers[eventName] = []; guido+booket@0: } guido+booket@0: guido+booket@0: // prevent observers for an event from being called more than once guido+booket@0: for (i = 0; i < this._eventsObservers[eventName].length; i++) { guido+booket@0: if (this._eventsObservers[eventName][i] === observer) { guido+booket@0: return; guido+booket@0: } guido+booket@0: } guido+booket@0: this._eventsObservers[eventName].push(observer); guido+booket@0: }; guido+booket@0: guido+booket@0: ObservableMixin.prototype.deleteObserver = function (eventName, observer) { guido+booket@0: var i = 0; guido+booket@0: guido+booket@0: if (!Object.prototype.hasOwnProperty.call(this._eventsObservers, guido+booket@0: eventName)) { guido+booket@0: return; guido+booket@0: } guido+booket@0: guido+booket@0: while (i < this._eventsObservers[eventName].length) { guido+booket@0: if (this._eventsObservers[eventName][i] === observer) { guido+booket@0: this._eventsObservers[eventName].splice(i, 1); guido+booket@0: } guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: ObservableMixin.prototype.notify = function (eventName) { guido+booket@0: var origArguments; guido+booket@0: guido+booket@0: if (!Object.prototype.hasOwnProperty.call(this._eventsObservers, guido+booket@0: eventName)) { guido+booket@0: return; guido+booket@0: } guido+booket@0: guido+booket@0: origArguments = Array.prototype.slice.call(arguments, 1); guido+booket@0: this._eventsObservers[eventName].forEach(function (observer, i) { guido+booket@0: // call the observer function and pass on any additional arguments guido+booket@0: observer.apply(undefined, origArguments); guido+booket@0: }); guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: var StringMap = function (iter) { guido+booket@0: this._stringMap = Object.create(null); guido+booket@0: guido+booket@0: if (iter !== undefined) { guido+booket@0: if (Array.isArray(iter)) { guido+booket@0: iter.forEach(function (pair) { guido+booket@0: if (Array.isArray(pair)) { guido+booket@0: this.set(pair[0], pair[1]); guido+booket@0: } else { guido+booket@0: throw new TypeError(typeof pair + ' is not an array'); guido+booket@0: } guido+booket@0: }, this); guido+booket@0: } else { guido+booket@0: throw new TypeError(typeof iter + ' is not iterable'); guido+booket@0: } guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: Object.defineProperty(StringMap.prototype, 'size', { guido+booket@0: get: function () { guido+booket@0: var size = 0; guido+booket@0: var key; guido+booket@0: guido+booket@0: for (key in this._stringMap) { guido+booket@0: if (key.charAt(0) === '@') { guido+booket@0: size++; guido+booket@0: } guido+booket@0: } guido+booket@0: guido+booket@0: return size; guido+booket@0: } guido+booket@0: }); guido+booket@0: guido+booket@0: StringMap.prototype.set = function (key, value) { guido+booket@0: this._stringMap['@' + key] = value; guido+booket@0: guido+booket@0: return this; guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.get = function (key) { guido+booket@0: return this._stringMap['@' + key]; guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.has = function (key) { guido+booket@0: return (('@' + key) in this._stringMap); guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.delete = function (key) { guido+booket@0: if (this.has(key)) { guido+booket@0: delete this._stringMap['@' + key]; guido+booket@0: guido+booket@0: return true; guido+booket@0: } guido+booket@0: guido+booket@0: return false; guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.forEach = function (callbackFn, thisArg) { guido+booket@0: Object.keys(this._stringMap).forEach(function (key) { guido+booket@0: if (key.charAt(0) === '@') { guido+booket@0: key = key.substr(1); guido+booket@0: callbackFn.call(thisArg, this.get(key), key, this); guido+booket@0: } guido+booket@0: }, this); guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.keys = function () { guido+booket@0: return Object.keys(this._stringMap).map(function (key) { guido+booket@0: return key.substr(1); guido+booket@0: }); guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.toJSON = function () { guido+booket@0: return this._stringMap; guido+booket@0: }; guido+booket@0: guido+booket@0: StringMap.prototype.toString = function () { guido+booket@0: return Object.prototype.toString.call(this._stringMap); guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: var StringSet = function (iter) { guido+booket@0: this._stringArray = []; guido+booket@0: this._stringMap = new StringMap(); guido+booket@0: if (iter !== undefined) { guido+booket@0: if (Array.isArray(iter) || iter instanceof StringSet) { guido+booket@0: iter.forEach(function (string) { guido+booket@0: this.add(string); guido+booket@0: }, this); guido+booket@0: } else { guido+booket@0: throw new TypeError(typeof iter + ' is not iterable'); guido+booket@0: } guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: Object.defineProperty(StringSet.prototype, 'size', { guido+booket@0: get: function () { guido+booket@0: return this._stringArray.length; guido+booket@0: } guido+booket@0: }); guido+booket@0: guido+booket@0: StringSet.prototype.has = function (string) { guido+booket@0: return this._stringMap.has(string); guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.add = function (string) { guido+booket@0: if (!this.has(string)) { guido+booket@0: this._stringMap.set(string, true); guido+booket@0: this._stringArray.push(string); guido+booket@0: } guido+booket@0: return this; guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.delete = function (string) { guido+booket@0: if (this.has(string)) { guido+booket@0: this._stringMap.delete(string); guido+booket@0: this._stringArray.splice(this._stringArray.indexOf(string), 1); guido+booket@0: return true; guido+booket@0: } guido+booket@0: return false; guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.forEach = function (callbackFn, thisArg) { guido+booket@0: this._stringArray.forEach(function (key) { guido+booket@0: callbackFn.call(thisArg, key, key, this); guido+booket@0: }); guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.keys = function () { guido+booket@0: return this._stringArray.slice(); guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.values = function () { guido+booket@0: return this._stringArray.slice(); guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.clear = function () { guido+booket@0: this._stringMap = new StringMap(); guido+booket@0: this._stringArray = []; guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.toJSON = function () { guido+booket@0: return this._stringArray; guido+booket@0: }; guido+booket@0: guido+booket@0: StringSet.prototype.toString = function () { guido+booket@0: return this._stringArray.toString(); guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: /* guido+booket@0: * model guido+booket@0: */ guido+booket@0: guido+booket@7: var Bookmark = function (url, title, favicon, tags, ctime, mtime) { guido+booket@0: var parsedTime; guido+booket@0: guido+booket@0: if (!isString(url)) { guido+booket@0: throw new TypeError(typeof url + ' is not a string'); guido+booket@0: } guido+booket@0: this.url = url; guido+booket@0: guido+booket@0: this.title = (isString(title) && title !== '') ? title : url; guido+booket@0: guido+booket@7: if (isString(favicon) && favicon.match(/^data:image\/png;base64,/)) { guido+booket@7: this.favicon = favicon; guido+booket@7: } else { guido+booket@7: this.favicon = undefined; guido+booket@7: } guido+booket@7: guido+booket@0: if (Array.isArray(tags)) { guido+booket@0: // remove duplicates, non-string or empty tags and tags containing guido+booket@0: // commas guido+booket@0: this.tags = new StringSet(tags.filter(function (tag) { guido+booket@0: return (isString(tag) && tag !== '' && tag.indexOf(',') === -1); guido+booket@0: }).sort()); guido+booket@0: } else { guido+booket@0: this.tags = new StringSet(); guido+booket@0: } guido+booket@0: guido+booket@0: if (isNumber(ctime) || isString(ctime)) { guido+booket@0: parsedTime = new Date(ctime); guido+booket@0: this.ctime = !isNaN(parsedTime.getTime()) ? parsedTime : new Date(); guido+booket@0: } else { guido+booket@0: this.ctime = new Date(); guido+booket@0: } guido+booket@0: guido+booket@0: if (isNumber(mtime) || isString(mtime)) { guido+booket@0: parsedTime = new Date(mtime); guido+booket@0: // modification time must be greater than creation time guido+booket@0: this.mtime = (!isNaN(parsedTime.getTime()) || guido+booket@0: parsedTime >= this.ctime) ? parsedTime : new Date(this.ctime); guido+booket@0: } else { guido+booket@0: this.mtime = new Date(this.ctime); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: var BookmarkModel = function () { guido+booket@0: ObservableMixin.call(this); guido+booket@0: guido+booket@18: this._unsavedChanges = false; guido+booket@10: this.loadFileReader = null; guido+booket@10: this.importFileReader= null; guido+booket@0: this._bookmarks = new StringMap(); guido+booket@0: this._tagCount = new StringMap(); guido+booket@0: this._filterTags = new StringSet(); guido+booket@0: this._searchTerm = ''; guido+booket@0: this._filteredBookmarks = new StringSet(); guido+booket@0: this._searchedBookmarks = new StringSet(); guido+booket@0: }; guido+booket@0: guido+booket@0: extend(BookmarkModel, ObservableMixin); guido+booket@0: guido+booket@18: Object.defineProperty(BookmarkModel.prototype, 'unsavedChanges', { guido+booket@18: set: function (value) { guido+booket@18: if (this._unsavedChanges !== value) { guido+booket@18: this._unsavedChanges = value; guido+booket@18: this.notify('unsaved-changes-changed', value) guido+booket@18: } guido+booket@18: }, guido+booket@18: get: function () { guido+booket@18: return this._unsavedChanges; guido+booket@18: } guido+booket@18: }); guido+booket@18: guido+booket@0: BookmarkModel.prototype.add = function (bookmarks) { guido+booket@0: var addedBookmarkUrls = new StringSet(); guido+booket@0: guido+booket@0: // argument can be a single bookmark or a list of bookmarks guido+booket@0: if (!Array.isArray(bookmarks)) { guido+booket@0: bookmarks = [bookmarks]; guido+booket@0: } guido+booket@0: guido+booket@0: bookmarks.forEach(function (bookmark) { guido+booket@0: // delete any existing bookmark for the given URL before adding the new guido+booket@0: // one in order to update views guido+booket@0: this.delete(bookmark.url); guido+booket@0: this._bookmarks.set(bookmark.url, bookmark); guido+booket@0: addedBookmarkUrls.add(bookmark.url); guido+booket@0: this.unsavedChanges = true; guido+booket@0: this.notify('bookmark-added', bookmark); guido+booket@0: guido+booket@0: // update tag count guido+booket@0: bookmark.tags.forEach(function (tag) { guido+booket@0: var tagCount; guido+booket@0: guido+booket@0: if (this._tagCount.has(tag)) { guido+booket@0: tagCount = this._tagCount.get(tag) + 1; guido+booket@0: this._tagCount.set(tag, tagCount); guido+booket@12: this.notify('tag-count-changed', tag, this._tagCount); guido+booket@0: } else { guido+booket@0: this._tagCount.set(tag, 1); guido+booket@12: this.notify('tag-added', tag, this._tagCount); guido+booket@0: } guido+booket@0: }, this); guido+booket@0: }, this); guido+booket@0: guido+booket@0: // apply tag filter and search added bookmarks guido+booket@0: this.updateFilteredSearchedBookmarks(addedBookmarkUrls); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.has = function (url) { guido+booket@0: return this._bookmarks.has(url); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.get = function (url) { guido+booket@0: return this._bookmarks.get(url); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.delete = function (urls) { guido+booket@0: var needUpdateFilterTags = false; guido+booket@0: guido+booket@0: // argument can be a single bookmark or a list of bookmarks guido+booket@0: if (!Array.isArray(urls)) { guido+booket@0: urls = [urls]; guido+booket@0: } guido+booket@0: guido+booket@0: urls.forEach(function (url) { guido+booket@0: var bookmark; guido+booket@0: var tagCount; guido+booket@0: guido+booket@0: if (this._bookmarks.has(url)) { guido+booket@0: bookmark = this._bookmarks.get(url); guido+booket@0: this._bookmarks.delete(url); guido+booket@0: this.unsavedChanges = true; guido+booket@0: this.notify('bookmark-deleted', bookmark.url); guido+booket@0: guido+booket@0: // update tag count guido+booket@0: bookmark.tags.forEach(function (tag) { guido+booket@0: if (this._tagCount.has(tag)) { guido+booket@0: tagCount = this._tagCount.get(tag); guido+booket@0: if (tagCount > 1) { guido+booket@0: tagCount--; guido+booket@0: this._tagCount.set(tag, tagCount); guido+booket@12: this.notify('tag-count-changed', tag, this._tagCount); guido+booket@0: } else { guido+booket@0: this._tagCount.delete(tag); guido+booket@12: this.notify('tag-deleted', tag, this._tagCount); guido+booket@0: guido+booket@0: if (this._filterTags.has(tag)) { guido+booket@0: this._filterTags.delete(tag); guido+booket@0: needUpdateFilterTags = true; guido+booket@0: } guido+booket@0: } guido+booket@0: } guido+booket@0: }, this); guido+booket@0: guido+booket@0: // update filtered and searched bookmarks guido+booket@0: if (this._filteredBookmarks.has(url)) { guido+booket@0: this._filteredBookmarks.delete(url); guido+booket@0: if (this._searchedBookmarks.has(url)) { guido+booket@0: this._searchedBookmarks.delete(url); guido+booket@0: } guido+booket@0: } guido+booket@0: } guido+booket@0: }, this); guido+booket@0: guido+booket@0: if (needUpdateFilterTags) { guido+booket@0: this.updateFilteredSearchedBookmarks(); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.forEach = function (callbackFn, thisArg) { guido+booket@0: this._bookmarks.keys().forEach(function (key) { guido+booket@0: callbackFn.call(thisArg, this._bookmarks.get(key), key, this); guido+booket@0: }, this); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.hasTag = function (tag) { guido+booket@0: return this._tagCount.has(tag); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.getTagCount = function (tag) { guido+booket@0: return (this._tagCount.has(tag)) ? this._tagCount.get(tag) : undefined; guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.updateSearchedBookmarks = function (urlsSubset) { guido+booket@0: var searchUrls; guido+booket@0: guido+booket@0: // additive search if urlsSubset is given guido+booket@0: if (urlsSubset !== undefined) { guido+booket@0: searchUrls = urlsSubset; guido+booket@0: } else { guido+booket@0: this._searchedBookmarks = new StringSet(); guido+booket@0: guido+booket@0: searchUrls = this._filteredBookmarks.values(); guido+booket@0: } guido+booket@0: guido+booket@0: // search for the search term in title and URL guido+booket@0: searchUrls.forEach(function (url) { guido+booket@0: var bookmark; guido+booket@0: guido+booket@0: bookmark = this.get(url); guido+booket@0: if (this._searchTerm === '' || guido+booket@0: bookmark.title.indexOf(this._searchTerm) !== -1 || guido+booket@0: bookmark.url.indexOf(this._searchTerm) !== -1) { guido+booket@0: this._searchedBookmarks.add(url); guido+booket@0: } guido+booket@0: }, this); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.updateFilteredSearchedBookmarks = guido+booket@0: function (urlsSubset) { guido+booket@0: var filterUrls; guido+booket@0: var searchUrls; guido+booket@0: guido+booket@0: // additive filtering if urlsSubset is given guido+booket@0: if (urlsSubset !== undefined) { guido+booket@0: filterUrls = urlsSubset; guido+booket@0: searchUrls = []; guido+booket@0: } else { guido+booket@0: this._filteredBookmarks = new StringSet(); guido+booket@0: guido+booket@0: filterUrls = this._bookmarks.keys(); guido+booket@0: searchUrls = undefined; guido+booket@0: } guido+booket@0: guido+booket@0: // apply tag filter guido+booket@0: filterUrls.forEach(function (url) { guido+booket@0: var bookmark; guido+booket@0: var matchingTagCount = 0; guido+booket@0: guido+booket@0: bookmark = this.get(url); guido+booket@0: guido+booket@0: bookmark.tags.forEach(function (tag) { guido+booket@0: if (this._filterTags.has(tag)) { guido+booket@0: matchingTagCount++; guido+booket@0: } guido+booket@0: }, this); guido+booket@0: guido+booket@0: if (matchingTagCount === this._filterTags.size) { guido+booket@0: this._filteredBookmarks.add(url); guido+booket@0: if (urlsSubset !== undefined) { guido+booket@0: searchUrls.push(url); guido+booket@0: } guido+booket@0: } guido+booket@0: }, this); guido+booket@0: guido+booket@0: // search the filter results guido+booket@0: this.updateSearchedBookmarks(searchUrls); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.toggleFilterTag = function (tag) { guido+booket@0: if (this._filterTags.has(tag)) { guido+booket@0: this._filterTags.delete(tag); guido+booket@0: } else { guido+booket@0: this._filterTags.add(tag); guido+booket@0: } guido+booket@0: this.updateFilteredSearchedBookmarks(); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.setFilterTags = function (filterTags) { guido+booket@0: if (!arrayEqual(filterTags.values(), this._filterTags.values())) { guido+booket@0: this._filterTags = new StringSet(filterTags); guido+booket@0: this.updateFilteredSearchedBookmarks(); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.setSearchTerm = function (searchTerm) { guido+booket@0: if (searchTerm !== this._searchTerm) { guido+booket@0: this._searchTerm = searchTerm; guido+booket@0: this.updateSearchedBookmarks(); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.setFilterTagsSearchTerm = function (filterTags, guido+booket@0: searchTerm) { guido+booket@0: if (!arrayEqual(filterTags.values(), this._filterTags.values())) { guido+booket@0: this._filterTags = new StringSet(filterTags); guido+booket@0: this._searchTerm = searchTerm; guido+booket@0: this.updateFilteredSearchedBookmarks(); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: } else if (searchTerm !== this._searchTerm) { guido+booket@0: this._searchTerm = searchTerm; guido+booket@0: this.updateSearchedBookmarks(); guido+booket@0: this.notify('filter-tags-search-changed', this._searchedBookmarks, guido+booket@0: this._filterTags, this._searchTerm); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.parseLoadedBookmarks = function (data) { guido+booket@19: var wasEmpty = !this._bookmarks.size; guido+booket@0: var parsedData; guido+booket@0: var bookmarks = []; guido+booket@19: var bookmark; guido+booket@19: var oldBookmark; guido+booket@0: guido+booket@0: try { guido+booket@0: parsedData = JSON.parse(data); guido+booket@0: } catch (e) { guido+booket@0: this.notify('load-file-error', e.message); guido+booket@0: return; guido+booket@0: } guido+booket@0: guido+booket@0: if (!Array.isArray(parsedData.bookmarks)) { guido+booket@0: this.notify('parse-file-error', guido+booket@0: 'This file does not contain bookmarks.'); guido+booket@0: return; guido+booket@0: } guido+booket@0: guido+booket@0: // create a temporary list of valid bookmarks guido+booket@0: parsedData.bookmarks.forEach(function (bookmark) { guido+booket@0: if (isString(bookmark.url) && bookmark.url !== '') { guido+booket@19: bookmark = new Bookmark(bookmark.url, bookmark.title, guido+booket@19: bookmark.favicon, bookmark.tags, bookmark.ctime, bookmark.mtime) guido+booket@19: oldBookmark = this.get(bookmark.url); guido+booket@19: if (oldBookmark === undefined || guido+booket@19: oldBookmark.mtime < bookmark.mtime) { guido+booket@19: bookmarks.push(bookmark); guido+booket@19: } guido+booket@0: } guido+booket@0: }, this); guido+booket@0: guido+booket@0: // add each bookmark to the model ordered by the last modification time guido+booket@0: this.add(bookmarks.sort(function (bookmark1, bookmark2) { guido+booket@0: return bookmark1.ctime - bookmark2.ctime; guido+booket@0: })); guido+booket@19: if (wasEmpty) { guido+booket@19: // if there were no bookmarks before there cannot be any unsaved changes guido+booket@19: this.unsavedChanges = false; guido+booket@19: } guido+booket@0: }; guido+booket@0: guido+booket@10: BookmarkModel.prototype.parseImportedBookmarks = function (data) { guido+booket@19: var wasEmpty = (this._bookmarks.size > 0); guido+booket@10: var bookmarkDoc; guido+booket@10: var bookmarkElements; guido+booket@10: var i; guido+booket@10: var url; guido+booket@10: var title; guido+booket@10: var favicon; guido+booket@10: var tags; guido+booket@10: var ctime; guido+booket@10: var mtime; guido+booket@10: var bookmarks = []; guido+booket@19: var bookmark; guido+booket@19: var oldBookmark; guido+booket@10: guido+booket@10: bookmarkDoc = document.implementation.createHTMLDocument(); guido+booket@10: bookmarkDoc.open(); guido+booket@10: bookmarkDoc.write(data); guido+booket@10: bookmarkDoc.close(); guido+booket@10: guido+booket@10: // create a temporary list of valid bookmarks guido+booket@10: bookmarkElements = bookmarkDoc.querySelectorAll('dt > a[href]'); guido+booket@10: for (i = 0; i < bookmarkElements.length; i++) { guido+booket@10: url = bookmarkElements[i].href; guido+booket@10: if (url !== '') { guido+booket@10: title = bookmarkElements[i].textContent; guido+booket@10: favicon = bookmarkElements[i].getAttribute('icon'); guido+booket@10: tags = ((tags = bookmarkElements[i].getAttribute('tags')) !== guido+booket@10: null) ? tags.split(',') : []; guido+booket@10: ctime = !isNaN(ctime = guido+booket@10: parseInt(bookmarkElements[i].getAttribute('add_date'), 10)) ? guido+booket@10: ctime * 1000 : undefined; guido+booket@10: mtime = !isNaN(mtime = guido+booket@10: parseInt(bookmarkElements[i].getAttribute('last_modified'), guido+booket@10: 10)) ? mtime * 1000 : undefined; guido+booket@19: bookmark = new Bookmark(url, title, favicon, tags, ctime, mtime); guido+booket@19: oldBookmark = this.get(bookmark.url); guido+booket@19: if (oldBookmark === undefined || guido+booket@19: oldBookmark.mtime < bookmark.mtime) { guido+booket@19: bookmarks.push(bookmark); guido+booket@19: } guido+booket@10: } guido+booket@10: } guido+booket@10: guido+booket@10: // add each bookmark to the model ordered by the last modification time guido+booket@10: this.add(bookmarks.sort(function (bookmark1, bookmark2) { guido+booket@10: return bookmark1.ctime - bookmark2.ctime; guido+booket@10: })); guido+booket@19: if (!wasEmpty) { guido+booket@19: // if there were no bookmarks before there cannot be any unsaved changes guido+booket@19: this.unsavedChanges = false; guido+booket@19: } guido+booket@10: }; guido+booket@10: guido+booket@19: BookmarkModel.prototype.loadFile = function (bookmarkFile, merge) { guido+booket@19: if (!merge) { guido+booket@19: // delete all existing bookmarks first guido+booket@19: this.delete(this._bookmarks.keys()); guido+booket@19: this.unsavedChanges = false; guido+booket@19: } guido+booket@0: guido+booket@10: this.loadFileReader = new FileReader(); guido+booket@10: this.loadFileReader.addEventListener('error', this); guido+booket@10: this.loadFileReader.addEventListener('load', this); guido+booket@10: this.loadFileReader.readAsText(bookmarkFile); guido+booket@10: }; guido+booket@10: guido+booket@19: BookmarkModel.prototype.importFile = function (bookmarkFile, merge) { guido+booket@19: if (!merge) { guido+booket@19: // delete all existing bookmarks first guido+booket@19: this.delete(this._bookmarks.keys()); guido+booket@19: this.unsavedChanges = false; guido+booket@19: } guido+booket@10: guido+booket@10: this.importFileReader = new FileReader(); guido+booket@10: this.importFileReader.addEventListener('error', this); guido+booket@10: this.importFileReader.addEventListener('load', this); guido+booket@10: this.importFileReader.readAsText(bookmarkFile); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkModel.prototype.saveFile = function () { guido+booket@0: var jsonBlob; guido+booket@0: var bookmarkData = { guido+booket@0: 'bookmarks': [] guido+booket@0: }; guido+booket@0: guido+booket@0: this._bookmarks.forEach(function (bookmark) { guido+booket@0: bookmarkData.bookmarks.push(bookmark); guido+booket@0: }, this); guido+booket@0: guido+booket@0: jsonBlob = new Blob([JSON.stringify(bookmarkData)], {type: guido+booket@0: 'application/json'}); guido+booket@0: this.notify('save-file', jsonBlob); guido+booket@0: this.unsavedChanges = false; guido+booket@0: }; guido+booket@0: guido+booket@11: BookmarkModel.prototype.exportFile = function () { guido+booket@11: var htmlBlob; guido+booket@11: var bookmarkDoc; guido+booket@11: var commentNode; guido+booket@11: var metaElement; guido+booket@11: var titleElement; guido+booket@11: var headingElement; guido+booket@11: var bookmarkListElement; guido+booket@11: var bookmarkLinkElement; guido+booket@11: var bookmarkElement; guido+booket@11: guido+booket@11: bookmarkDoc = document.implementation.createHTMLDocument(); guido+booket@11: guido+booket@11: // construct Netscape bookmarks format within body guido+booket@11: commentNode = bookmarkDoc.createComment('This is an automatically ' + guido+booket@11: 'generated file.\nIt will be read and overwritten.\nDO NOT EDIT!'); guido+booket@11: bookmarkDoc.body.appendChild(commentNode); guido+booket@11: guido+booket@11: metaElement = bookmarkDoc.createElement('meta'); guido+booket@11: metaElement.setAttribute('http-equiv', 'Content-Type'); guido+booket@11: metaElement.setAttribute('content', 'text/html; charset=UTF-8'); guido+booket@11: bookmarkDoc.body.appendChild(metaElement); guido+booket@11: guido+booket@11: titleElement = bookmarkDoc.createElement('title'); guido+booket@11: titleElement.textContent = 'Bookmarks'; guido+booket@11: bookmarkDoc.body.appendChild(titleElement); guido+booket@11: guido+booket@11: headingElement = bookmarkDoc.createElement('h1'); guido+booket@11: headingElement.textContent = 'Bookmarks'; guido+booket@11: bookmarkDoc.body.appendChild(headingElement); guido+booket@11: guido+booket@11: bookmarkListElement = bookmarkDoc.createElement('dl'); guido+booket@11: bookmarkDoc.body.appendChild(bookmarkListElement); guido+booket@11: guido+booket@11: this._bookmarks.forEach(function (bookmark) { guido+booket@11: bookmarkElement = bookmarkDoc.createElement('dt'); guido+booket@11: guido+booket@11: bookmarkLinkElement = bookmarkDoc.createElement('a'); guido+booket@11: bookmarkLinkElement.href = bookmark.url; guido+booket@11: bookmarkLinkElement.textContent = bookmark.title; guido+booket@11: bookmarkLinkElement.setAttribute('icon', bookmark.favicon); guido+booket@11: bookmarkLinkElement.setAttribute('tags', guido+booket@11: bookmark.tags.values().join(',')); guido+booket@11: bookmarkLinkElement.setAttribute('add_date', guido+booket@11: Math.round(bookmark.ctime.getTime() / 1000)); guido+booket@11: bookmarkLinkElement.setAttribute('last_modified', guido+booket@11: Math.round(bookmark.mtime.getTime() / 1000)); guido+booket@11: guido+booket@11: bookmarkElement.appendChild(bookmarkLinkElement); guido+booket@11: guido+booket@11: bookmarkListElement.appendChild(bookmarkElement); guido+booket@11: bookmarkListElement.appendChild(bookmarkDoc.createElement('dd')); guido+booket@11: }, this); guido+booket@11: guido+booket@11: htmlBlob = new Blob(['\n' + guido+booket@11: bookmarkDoc.body.innerHTML], {type: 'text/html'}); guido+booket@11: this.notify('export-file', htmlBlob); guido+booket@11: }; guido+booket@11: guido+booket@0: BookmarkModel.prototype.handleEvent = function (e) { guido+booket@0: if (e.type === 'load') { guido+booket@10: if (e.target === this.loadFileReader) { guido+booket@10: this.parseLoadedBookmarks(e.target.result); guido+booket@10: this.loadFileReader = null; guido+booket@10: } else if (e.target === this.importFileReader) { guido+booket@10: this.parseImportedBookmarks(e.target.result); guido+booket@10: this.importFileReader = null; guido+booket@10: } guido+booket@0: } else if (e.type === 'error') { guido+booket@0: this.notify('load-file-error', e.target.error.message); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: /* guido+booket@0: * view guido+booket@0: */ guido+booket@0: guido+booket@26: var NotificationsView = function () { guido+booket@26: this.unsavedChangesElement = guido+booket@26: document.querySelector('#notifications .unsaved-changes-message'); guido+booket@22: guido+booket@22: this.shortcutKeysOverlayElement = guido+booket@22: document.querySelector('#keyboard-shortcuts'); guido+booket@26: guido+booket@26: document.addEventListener('keydown', this); guido+booket@26: document.addEventListener('keyup', this); guido+booket@22: }; guido+booket@22: guido+booket@26: NotificationsView.prototype.handleEvent = function (e) { guido+booket@22: var elements; guido+booket@22: var i; guido+booket@22: guido+booket@22: // keyboard shortcut hints are visible when the Alt key is pressed and guido+booket@22: // hidden again when the Alt key is released or another key is pressed guido+booket@22: if (this.shortcutKeysOverlayElement.dataset.overlayVisible !== undefined && guido+booket@22: (e.type === 'keyup' || e.type === 'keydown')) { guido+booket@22: delete this.shortcutKeysOverlayElement.dataset.overlayVisible; guido+booket@22: } else if (e.type === 'keydown' && e.keyCode === 18) { guido+booket@22: this.shortcutKeysOverlayElement.dataset.overlayVisible = ''; guido+booket@22: } guido+booket@22: }; guido+booket@22: guido+booket@26: NotificationsView.prototype.onUnsavedChangesChanged = function (unsavedChanges) { guido+booket@26: this.unsavedChangesElement.hidden = !unsavedChanges; guido+booket@26: }; guido+booket@26: guido+booket@22: guido+booket@0: var TagView = function () { guido+booket@12: var tagsElement; guido+booket@12: guido+booket@0: ObservableMixin.call(this); guido+booket@0: guido+booket@12: tagsElement = document.querySelector('#tags'); guido+booket@12: tagsElement.addEventListener('click', this); guido+booket@12: guido+booket@12: this.tagListElement = tagsElement.querySelector('ul.tag-list'); guido+booket@0: guido+booket@5: this.tagDatalistElement = document.querySelector('#tag-datalist'); guido+booket@5: guido+booket@0: this.tagTemplate = document.querySelector('#tag-template'); guido+booket@0: }; guido+booket@0: guido+booket@0: extend(TagView, ObservableMixin); guido+booket@0: guido+booket@12: TagView.prototype.onTagAdded = function (tag, tagCount) { guido+booket@0: var newNode; guido+booket@0: var tagElement; guido+booket@0: var setTagButton; guido+booket@0: var toggleTagButton; guido+booket@0: var tagElements; guido+booket@0: var i; guido+booket@0: var referenceTag = ''; guido+booket@0: var referenceNode; guido+booket@5: var tagOptionElement; guido+booket@5: var i; guido+booket@5: var isInDatalist = false; guido+booket@0: guido+booket@0: // create new tag element from template guido+booket@0: newNode = document.importNode(this.tagTemplate.content, true); guido+booket@0: guido+booket@0: tagElement = newNode.querySelector('li'); guido+booket@0: tagElement.dataset.tag = tag; guido+booket@0: guido+booket@0: setTagButton = tagElement.querySelector('button[name="set-tag"]'); guido+booket@0: setTagButton.textContent = tag; guido+booket@0: setTagButton.title = 'Set filter to "' + tag + '"'; guido+booket@0: guido+booket@0: toggleTagButton = tagElement.querySelector('button[name="toggle-tag"]'); guido+booket@0: toggleTagButton.textContent = '+'; guido+booket@0: toggleTagButton.title = 'Add "' + tag + '" to filter'; guido+booket@0: guido+booket@0: // maintain alphabetical order when inserting the tag element guido+booket@0: tagElements = this.tagListElement.querySelectorAll('li'); guido+booket@0: for (i = 0; i < tagElements.length; i ++) { guido+booket@0: if (tagElements[i].dataset.tag > referenceTag && guido+booket@0: tagElements[i].dataset.tag < tag) { guido+booket@0: referenceTag = tagElements[i].dataset.tag; guido+booket@0: referenceNode = tagElements[i]; guido+booket@0: } guido+booket@0: } guido+booket@0: this.tagListElement.insertBefore(newNode, (referenceNode !== undefined) ? guido+booket@0: referenceNode.nextSibling : this.tagListElement.firstChild); guido+booket@0: guido+booket@0: // initialize tag count guido+booket@12: this.onTagCountChanged(tag, tagCount); guido+booket@5: guido+booket@5: // add to datalist guido+booket@5: for (i = 0; i < this.tagDatalistElement.options.length; i++) { guido+booket@5: if (this.tagDatalistElement.options[i].value == tag) { guido+booket@5: isInDatalist = true; guido+booket@5: break; guido+booket@5: } guido+booket@5: } guido+booket@5: if (!isInDatalist) { guido+booket@5: tagOptionElement = document.createElement('option'); guido+booket@5: tagOptionElement.value = tag; guido+booket@5: this.tagDatalistElement.appendChild(tagOptionElement); guido+booket@5: } guido+booket@0: }; guido+booket@0: guido+booket@12: TagView.prototype.updateTagCloud = function (tagCount) { guido+booket@12: var tagElements; guido+booket@12: var i; guido+booket@12: var j; guido+booket@12: var tagCountMax = 1; guido+booket@12: guido+booket@12: tagCount.forEach(function (count) { guido+booket@12: if (count > tagCountMax) { guido+booket@12: tagCountMax = count; guido+booket@12: } guido+booket@12: }, this); guido+booket@12: guido+booket@12: tagElements = this.tagListElement.querySelectorAll('ul.tag-list > li'); guido+booket@12: for (i = 0; i < tagElements.length; i++) { guido+booket@12: for (j = 1; j <= 10; j++) { guido+booket@12: tagElements[i].classList.remove('tag-frequency-' + j); guido+booket@12: } guido+booket@12: tagElements[i].classList.add('tag-frequency-' + guido+booket@12: (Math.floor(tagCount.get(tagElements[i].dataset.tag) / guido+booket@12: (tagCountMax / 9)) + 1)); guido+booket@12: } guido+booket@12: }; guido+booket@12: guido+booket@0: TagView.prototype.onTagCountChanged = function (tag, tagCount) { guido+booket@0: this.tagListElement.querySelector('li' + createDatasetSelector('tag', tag) + guido+booket@12: ' .tag-count').textContent = '(' + tagCount.get(tag) + ')'; guido+booket@12: this.updateTagCloud(tagCount); guido+booket@0: }; guido+booket@0: guido+booket@12: TagView.prototype.onTagDeleted = function (tag, tagCount) { guido+booket@0: var tagElement; guido+booket@0: guido+booket@5: // remove from tag list guido+booket@0: tagElement = this.tagListElement.querySelector('li' + guido+booket@0: createDatasetSelector('tag', tag)); guido+booket@5: tagElement.parentNode.removeChild(tagElement); guido+booket@12: guido+booket@12: this.updateTagCloud(tagCount); guido+booket@0: }; guido+booket@0: guido+booket@0: TagView.prototype.onFilterTagsSearchChanged = function (filteredBookmarks, guido+booket@0: newFilterTags, newSearchTerm) { guido+booket@0: var tagElements; guido+booket@0: var i; guido+booket@0: var tag; guido+booket@0: var toggleTagButton; guido+booket@0: guido+booket@0: tagElements = this.tagListElement.querySelectorAll('li'); guido+booket@0: for (i = 0; i < tagElements.length; i++) { guido+booket@0: tag = tagElements[i].dataset.tag; guido+booket@0: toggleTagButton = guido+booket@0: tagElements[i].querySelector('button[name="toggle-tag"]'); guido+booket@0: if (newFilterTags.has(tag)) { guido+booket@0: tagElements[i].classList.add('active-filter-tag'); guido+booket@0: toggleTagButton.textContent = '\u2212'; guido+booket@0: toggleTagButton.title = 'Remove "' + tag + '" from filter'; guido+booket@0: } else { guido+booket@0: tagElements[i].classList.remove('active-filter-tag'); guido+booket@0: toggleTagButton.textContent = '+'; guido+booket@0: toggleTagButton.title = 'Add "' + tag + '" to filter'; guido+booket@0: } guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: TagView.prototype.handleEvent = function (e) { guido+booket@12: if (e.type === 'click') { guido+booket@12: if (e.target.name === 'set-tag' || guido+booket@12: e.target.name === 'toggle-tag') { guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: this.notify(e.target.name, getAncestorElementDatasetItem(e.target, guido+booket@0: 'tag')); guido+booket@12: } else if (e.target.name === 'show-tag-cloud') { guido+booket@12: this.tagListElement.classList.toggle('tag-cloud', e.target.checked); guido+booket@12: } guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: var ActionsView = function () { guido+booket@0: var saveFormElement; guido+booket@0: var newNode; guido+booket@24: var fieldsetElement; guido+booket@24: var legendElement; guido+booket@16: var i; guido+booket@0: guido+booket@0: ObservableMixin.call(this); guido+booket@0: guido+booket@24: this.actionsElement = document.querySelector('#actions'); guido+booket@24: guido+booket@0: this.tagInputTemplate = document.querySelector('#tag-input-template'); guido+booket@0: saveFormElement = document.querySelector('form#save-form'); guido+booket@0: guido+booket@0: this.saveLinkElement = saveFormElement.querySelector('a#save-link'); guido+booket@0: guido+booket@0: // create new editor form from template guido+booket@0: newNode = document.importNode( guido+booket@0: document.querySelector('#bookmark-editor-template').content, true); guido+booket@0: guido+booket@7: this.editorFormElement = newNode.querySelector('form.bookmark-editor-form'); guido+booket@22: this.editorFormElement.querySelector('label').accessKey = 'a'; guido+booket@24: guido+booket@24: fieldsetElement = this.editorFormElement.querySelector('fieldset'); guido+booket@24: fieldsetElement.classList.add('expander'); guido+booket@24: guido+booket@24: legendElement = fieldsetElement.querySelector('legend'); guido+booket@24: legendElement.textContent = 'Add Bookmark'; guido+booket@24: legendElement.tabIndex = 0; guido+booket@24: legendElement.classList.add('expander-label'); guido+booket@7: guido+booket@7: this.faviconImageElement = guido+booket@7: this.editorFormElement.querySelector('img.bookmark-favicon'); guido+booket@7: this.faviconImageElement.addEventListener('load', this); guido+booket@7: this.faviconImageElement.addEventListener('error', this); guido+booket@7: guido+booket@7: this.missingFaviconUri = this.faviconImageElement.src; guido+booket@0: guido+booket@0: this.editTagListElement = guido+booket@7: this.editorFormElement.querySelector('ul.tag-input-list'); guido+booket@16: // add four tag input elements for convenience guido+booket@16: for (i = 0; i < 4; i++) { guido+booket@16: this.editTagListElement.appendChild(this.createTagInputElement('')); guido+booket@16: } guido+booket@0: guido+booket@0: saveFormElement.parentNode.insertBefore(newNode, guido+booket@0: saveFormElement.nextSibling); guido+booket@6: guido+booket@25: this.actionsElement.addEventListener('input', this); guido+booket@24: this.actionsElement.addEventListener('click', this); guido+booket@24: this.actionsElement.addEventListener('keydown', this); guido+booket@24: this.actionsElement.addEventListener('submit', this); guido+booket@24: this.actionsElement.addEventListener('reset', this); guido+booket@24: guido+booket@6: document.querySelector('a#bookmarklet-link').href = BOOKMARKLET_URI; guido+booket@0: }; guido+booket@0: guido+booket@0: extend(ActionsView, ObservableMixin); guido+booket@0: guido+booket@0: ActionsView.prototype.createTagInputElement = function (tag) { guido+booket@0: var newNode; guido+booket@0: guido+booket@0: newNode = document.importNode(this.tagInputTemplate.content, true); guido+booket@0: newNode.querySelector('input[name="tag"]').value = tag; guido+booket@0: guido+booket@0: return newNode; guido+booket@0: }; guido+booket@0: guido+booket@0: ActionsView.prototype.handleEvent = function (e) { guido+booket@6: var bookmarkletData; guido+booket@6: var parsedData; guido+booket@0: var tags = []; guido+booket@0: var i; guido+booket@16: var tagInputElements; guido+booket@0: guido+booket@0: switch (e.type) { guido+booket@7: case 'error': guido+booket@7: if (e.target.classList.contains('bookmark-favicon')) { guido+booket@7: if (e.target.src !== this.missingFaviconUri) { guido+booket@7: e.target.src = this.missingFaviconUri; guido+booket@7: } guido+booket@7: } guido+booket@7: break; guido+booket@7: case 'load': guido+booket@7: if (e.target.classList.contains('bookmark-favicon')) { guido+booket@7: this.editorFormElement.favicon.value = guido+booket@7: (e.target.src !== this.missingFaviconUri) ? e.target.src : ''; guido+booket@7: } guido+booket@7: break; guido+booket@6: case 'input': guido+booket@6: if (e.target.name === 'bookmarklet-import') { guido+booket@6: // get rid of any preceding text guido+booket@6: bookmarkletData = e.target.value.replace(/^[^{]*/, ''); guido+booket@6: guido+booket@6: try { guido+booket@6: parsedData = JSON.parse(bookmarkletData); guido+booket@6: } catch (exception) { guido+booket@6: return; guido+booket@6: } guido+booket@6: guido+booket@6: if (isString(parsedData.url) && parsedData.url !== '') { guido+booket@6: e.target.form.elements.url.value = parsedData.url; guido+booket@6: } guido+booket@6: if (isString(parsedData.title) && parsedData.title !== '') { guido+booket@6: e.target.form.elements.title.value = parsedData.title; guido+booket@6: } guido+booket@7: if (isString(parsedData.favicon) && guido+booket@7: parsedData.favicon.match(/^data:image\/png;base64,/)) { guido+booket@7: this.faviconImageElement.src = parsedData.favicon; guido+booket@7: } guido+booket@6: } guido+booket@6: break; guido+booket@0: case 'click': guido+booket@0: if (e.target.name === 'more-tags') { guido+booket@0: e.preventDefault(); guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: this.editTagListElement.appendChild(this.createTagInputElement('')); guido+booket@24: } else if (e.target.classList.contains('expander-label')) { guido+booket@24: if (e.target.parentNode.dataset.expanderOpen !== undefined) { guido+booket@24: delete e.target.parentNode.dataset.expanderOpen; guido+booket@24: } else { guido+booket@24: e.target.parentNode.dataset.expanderOpen = ''; guido+booket@24: } guido+booket@24: } guido+booket@24: break; guido+booket@24: case 'keydown': guido+booket@24: if (e.keyCode === 32 && e.target.classList.contains('expander-label')) { guido+booket@24: if (e.target.parentNode.dataset.expanderOpen !== undefined) { guido+booket@24: delete e.target.parentNode.dataset.expanderOpen; guido+booket@24: } else { guido+booket@24: e.target.parentNode.dataset.expanderOpen = ''; guido+booket@24: } guido+booket@0: } guido+booket@0: break; guido+booket@0: case 'submit': guido+booket@0: if (e.target.id === 'save-form') { guido+booket@0: e.preventDefault(); guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: this.notify('save-file'); guido+booket@0: } else if (e.target.id === 'load-form') { guido+booket@0: e.preventDefault(); guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@19: this.notify('load-file', e.target.file.files[0], guido+booket@19: e.target.merge.checked); guido+booket@0: e.target.reset(); guido+booket@10: } else if (e.target.id === 'import-form') { guido+booket@10: e.preventDefault(); guido+booket@10: e.target.blur(); guido+booket@10: guido+booket@19: this.notify('import-file', e.target.file.files[0], guido+booket@19: e.target.merge.checked); guido+booket@10: e.target.reset(); guido+booket@11: } else if (e.target.id === 'export-form') { guido+booket@11: e.preventDefault(); guido+booket@11: e.target.blur(); guido+booket@11: guido+booket@11: this.notify('export-file'); guido+booket@0: } else if (e.target.classList.contains('bookmark-editor-form')) { guido+booket@0: e.preventDefault(); guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: if (e.target.tag.length) { guido+booket@0: for (i = 0; i < e.target.tag.length; i++) { guido+booket@0: tags.push(e.target.tag[i].value.trim()); guido+booket@0: } guido+booket@0: } else { guido+booket@0: tags.push(e.target.tag.value.trim()); guido+booket@0: } guido+booket@0: guido+booket@0: this.notify('save-bookmark', e.target.url.value, guido+booket@7: e.target.title.value, e.target.favicon.value, tags); guido+booket@0: guido+booket@0: e.target.reset(); guido+booket@0: } guido+booket@0: break; guido+booket@0: case 'reset': guido+booket@0: if (e.target.classList.contains('bookmark-editor-form')) { guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@7: e.target.querySelector('img.bookmark-favicon').src = guido+booket@7: this.missingFaviconUri; guido+booket@7: guido+booket@16: // remove all but the first four tag input elements guido+booket@16: tagInputElements = guido+booket@16: this.editTagListElement.querySelectorAll('li:nth-child(n+5)'); guido+booket@16: for (i = 0; i < tagInputElements.length; i++) { guido+booket@16: this.editTagListElement.removeChild(tagInputElements[i]); guido+booket@0: } guido+booket@0: } guido+booket@0: break; guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: ActionsView.prototype.onSaveFile = function (jsonBlob) { guido+booket@0: this.saveLinkElement.href = URL.createObjectURL(jsonBlob); guido+booket@0: this.saveLinkElement.click(); guido+booket@0: }; guido+booket@0: guido+booket@11: ActionsView.prototype.onExportFile = function (htmlBlob) { guido+booket@11: var exportLinkElement; guido+booket@11: guido+booket@11: exportLinkElement = document.querySelector('a#export-link'); guido+booket@11: exportLinkElement.href = URL.createObjectURL(htmlBlob); guido+booket@11: exportLinkElement.click(); guido+booket@11: }; guido+booket@11: guido+booket@0: ActionsView.prototype.confirmLoadFile = function () { guido+booket@0: return window.confirm('There are unsaved changes to your bookmarks.\n' + guido+booket@0: 'Proceed loading the bookmark file?'); guido+booket@0: }; guido+booket@0: guido+booket@0: ActionsView.prototype.onLoadFileError = function (message) { guido+booket@0: window.alert('Failed to load bookmark file:\n' + message); guido+booket@0: }; guido+booket@0: guido+booket@0: ActionsView.prototype.onParseFileError = function (message) { guido+booket@0: window.alert('Failed to parse bookmark file:\n' + message); guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: var BookmarkView = function () { guido+booket@0: var searchFormElement; guido+booket@0: guido+booket@0: ObservableMixin.call(this); guido+booket@0: guido+booket@0: this.bookmarkTemplate = document.querySelector('#bookmark-template'); guido+booket@0: this.bookmarkTagTemplate = document.querySelector('#bookmark-tag-template'); guido+booket@0: this.bookmarkEditorTemplate = guido+booket@0: document.querySelector('#bookmark-editor-template'); guido+booket@0: this.tagInputTemplate = document.querySelector('#tag-input-template'); guido+booket@0: guido+booket@0: this.bookmarkListElement = document.querySelector('ul#bookmark-list'); guido+booket@7: this.bookmarkListElement.addEventListener('input', this); guido+booket@0: this.bookmarkListElement.addEventListener('click', this); guido+booket@23: this.bookmarkListElement.addEventListener('keydown', this); guido+booket@0: this.bookmarkListElement.addEventListener('submit', this); guido+booket@0: this.bookmarkListElement.addEventListener('reset', this); guido+booket@0: guido+booket@0: searchFormElement = document.querySelector('#search-form'); guido+booket@0: searchFormElement.addEventListener('submit', this); guido+booket@0: searchFormElement.addEventListener('reset', this); guido+booket@0: guido+booket@0: this.searchTermInputElement = searchFormElement['search-term']; guido+booket@0: guido+booket@0: this.bookmarkMessageElement = document.querySelector('#bookmark-message'); guido+booket@0: guido+booket@7: this.missingFaviconUri = ''; guido+booket@7: guido+booket@0: this.updateBookmarkMessage(); guido+booket@0: }; guido+booket@0: guido+booket@0: extend(BookmarkView, ObservableMixin); guido+booket@0: guido+booket@23: BookmarkView.prototype.getAncestorClass = function (node, className) { guido+booket@23: while ((node = node.parentNode) !== null && guido+booket@23: (!node.classList || !node.classList.contains(className))); guido+booket@23: guido+booket@23: return node; guido+booket@23: }; guido+booket@23: guido+booket@0: BookmarkView.prototype.handleEvent = function (e) { guido+booket@7: var bookmarkletData; guido+booket@7: var parsedData; guido+booket@0: var i; guido+booket@0: var tags = []; guido+booket@0: var node; guido+booket@0: guido+booket@0: switch (e.type) { guido+booket@7: case 'error': guido+booket@7: if (e.target.classList.contains('bookmark-favicon')) { guido+booket@7: if (e.target.src !== this.missingFaviconUri) { guido+booket@7: e.target.src = this.missingFaviconUri; guido+booket@7: } guido+booket@7: } guido+booket@7: break; guido+booket@7: case 'load': guido+booket@7: if (e.target.classList.contains('bookmark-favicon')) { guido+booket@7: node = e.target; guido+booket@7: while ((node = node.parentNode) !== null) { guido+booket@7: if (node.classList.contains('bookmark-editor-form')) { guido+booket@7: node.favicon.value = guido+booket@7: (e.target.src !== this.missingFaviconUri) ? guido+booket@7: e.target.src : ''; guido+booket@7: break; guido+booket@7: } guido+booket@7: } guido+booket@7: } guido+booket@7: break; guido+booket@7: case 'input': guido+booket@7: if (e.target.name === 'bookmarklet-import') { guido+booket@7: // get rid of any preceding text guido+booket@7: bookmarkletData = e.target.value.replace(/^[^{]*/, ''); guido+booket@7: guido+booket@7: try { guido+booket@7: parsedData = JSON.parse(bookmarkletData); guido+booket@7: } catch (exception) { guido+booket@7: return; guido+booket@7: } guido+booket@7: guido+booket@7: if (isString(parsedData.url) && parsedData.url !== '') { guido+booket@7: e.target.form.elements.url.value = parsedData.url; guido+booket@7: } guido+booket@7: if (isString(parsedData.title) && parsedData.title !== '') { guido+booket@7: e.target.form.elements.title.value = parsedData.title; guido+booket@7: } guido+booket@7: if (isString(parsedData.favicon) && guido+booket@7: parsedData.favicon.match(/^data:image\/png;base64,/)) { guido+booket@7: e.target.form.querySelector('img.bookmark-favicon').src = guido+booket@7: parsedData.favicon; guido+booket@7: } guido+booket@7: } guido+booket@7: break; guido+booket@0: case 'click': guido+booket@0: switch (e.target.name) { guido+booket@0: case 'edit-bookmark': guido+booket@0: e.target.blur(); guido+booket@0: // fallthrough guido+booket@0: case 'delete-bookmark': guido+booket@0: this.notify(e.target.name, guido+booket@0: getAncestorElementDatasetItem(e.target, 'bookmarkUrl')); guido+booket@0: break; guido+booket@0: case 'more-tags': guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: e.target.form.querySelector('ul.tag-input-list').appendChild( guido+booket@0: this.createTagInputElement('')); guido+booket@0: break; guido+booket@0: case 'set-tag': guido+booket@0: case 'toggle-tag': guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: this.notify(e.target.name, guido+booket@0: getAncestorElementDatasetItem(e.target, 'tag')); guido+booket@0: break; guido+booket@23: default: guido+booket@23: if ((node = this.getAncestorClass(e.target, 'expander')) !== null) { guido+booket@23: if (node.dataset.expanderOpen !== undefined) { guido+booket@23: delete node.dataset.expanderOpen; guido+booket@23: } else { guido+booket@23: node.dataset.expanderOpen = ''; guido+booket@23: } guido+booket@23: } guido+booket@23: break; guido+booket@23: } guido+booket@23: break; guido+booket@23: case 'keydown': guido+booket@23: if (e.keyCode === 32 && guido+booket@23: (node = this.getAncestorClass(e.target, 'expander')) !== null) { guido+booket@23: if (node.dataset.expanderOpen !== undefined) { guido+booket@23: delete node.dataset.expanderOpen; guido+booket@23: } else { guido+booket@23: node.dataset.expanderOpen = ''; guido+booket@23: } guido+booket@0: } guido+booket@0: break; guido+booket@0: case 'submit': guido+booket@0: if (e.target.classList.contains('bookmark-editor-form')) { guido+booket@0: // save bookmark-editor-form form contents guido+booket@0: e.preventDefault(); guido+booket@0: guido+booket@0: if (e.target.tag.length) { guido+booket@0: for (i = 0; i < e.target.tag.length; i++) { guido+booket@0: tags.push(e.target.tag[i].value.trim()); guido+booket@0: } guido+booket@0: } else { guido+booket@0: tags.push(e.target.tag.value.trim()); guido+booket@0: } guido+booket@0: guido+booket@0: this.notify('save-bookmark', e.target.url.value, guido+booket@7: e.target.title.value, e.target.favicon.value, tags, guido+booket@7: e.target['original-url'].value); guido+booket@0: } else if (e.target.id === 'search-form') { guido+booket@0: // search guido+booket@0: e.preventDefault(); guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: this.notify('search', e.target['search-term'].value); guido+booket@0: } guido+booket@0: break; guido+booket@0: case 'reset': guido+booket@0: if (e.target.classList.contains('bookmark-editor-form')) { guido+booket@0: // cancel bookmark-editor-form form guido+booket@0: e.preventDefault(); guido+booket@0: guido+booket@0: // re-enable edit button guido+booket@0: this.bookmarkListElement.querySelector('li' + guido+booket@0: createDatasetSelector('bookmark-url', guido+booket@0: e.target['original-url'].value) + guido+booket@0: ' button[name="edit-bookmark"]').disabled = false; guido+booket@0: guido+booket@0: e.target.parentNode.removeChild(e.target); guido+booket@0: } else if (e.target.id === 'search-form') { guido+booket@0: // clear search guido+booket@0: e.preventDefault(); guido+booket@0: e.target.blur(); guido+booket@0: guido+booket@0: this.notify('search', ''); guido+booket@0: } guido+booket@0: break; guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.updateBookmarkMessage = function () { guido+booket@0: this.bookmarkMessageElement.textContent = 'Showing ' + guido+booket@0: this.bookmarkListElement.querySelectorAll('ul#bookmark-list > ' + guido+booket@0: 'li:not([hidden])').length + ' of ' + guido+booket@0: this.bookmarkListElement.querySelectorAll('ul#bookmark-list > ' + guido+booket@0: 'li').length + ' bookmarks.'; guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.onBookmarkAdded = function (bookmark) { guido+booket@0: var newNode; guido+booket@0: var bookmarkElement; guido+booket@7: var faviconElement; guido+booket@0: var linkElement; guido+booket@3: var hostnameElement; guido+booket@3: var urlElement; guido+booket@2: var ctimeElement; guido+booket@2: var mtimeElement; guido+booket@0: var tagListElement; guido+booket@0: guido+booket@0: newNode = document.importNode(this.bookmarkTemplate.content, true); guido+booket@0: guido+booket@0: bookmarkElement = newNode.querySelector('li'); guido+booket@0: bookmarkElement.dataset.bookmarkUrl = bookmark.url; guido+booket@0: guido+booket@7: faviconElement = bookmarkElement.querySelector('img.bookmark-favicon'); guido+booket@7: faviconElement.src = (bookmark.favicon) ? bookmark.favicon : guido+booket@7: this.missingFaviconUri; guido+booket@7: faviconElement.alt = ''; guido+booket@7: guido+booket@0: linkElement = bookmarkElement.querySelector('a.bookmark-link'); guido+booket@0: linkElement.textContent = linkElement.title = bookmark.title; guido+booket@0: linkElement.href = bookmark.url; guido+booket@0: guido+booket@3: hostnameElement = bookmarkElement.querySelector('.bookmark-hostname'); guido+booket@3: hostnameElement.textContent = (linkElement.hostname !== '') ? guido+booket@3: '[' + linkElement.hostname + ']' : ''; guido+booket@3: guido+booket@3: urlElement = bookmarkElement.querySelector('.bookmark-url'); guido+booket@3: urlElement.textContent = bookmark.url; guido+booket@3: guido+booket@2: ctimeElement = bookmarkElement.querySelector('.ctime'); guido+booket@2: ctimeElement.dateTime = bookmark.ctime.toISOString(); guido+booket@2: ctimeElement.textContent = bookmark.ctime.toString(); guido+booket@2: guido+booket@2: mtimeElement = bookmarkElement.querySelector('.mtime'); guido+booket@2: mtimeElement.dateTime = bookmark.mtime.toISOString(); guido+booket@2: mtimeElement.textContent = bookmark.mtime.toString(); guido+booket@2: guido+booket@0: tagListElement = bookmarkElement.querySelector('ul.tag-list'); guido+booket@0: bookmark.tags.forEach(function (tag) { guido+booket@0: var newNode; guido+booket@0: var tagElement; guido+booket@0: var setTagButton; guido+booket@0: var toggleTagButton; guido+booket@0: guido+booket@0: newNode = document.importNode(this.bookmarkTagTemplate.content, true); guido+booket@0: guido+booket@0: tagElement = newNode.querySelector('li'); guido+booket@0: tagElement.dataset.tag = tag; guido+booket@0: guido+booket@0: setTagButton = newNode.querySelector('button[name="set-tag"]'); guido+booket@0: setTagButton.textContent = tag; guido+booket@0: setTagButton.title = 'Set filter to "' + tag + '"'; guido+booket@0: guido+booket@0: toggleTagButton = newNode.querySelector('button[name="toggle-tag"]'); guido+booket@0: toggleTagButton.textContent = '+'; guido+booket@0: toggleTagButton.title = 'Add "' + tag + '" to filter'; guido+booket@0: guido+booket@0: tagListElement.appendChild(newNode); guido+booket@0: }, this); guido+booket@0: guido+booket@0: // insert new or last modified bookmark on top of the list guido+booket@0: this.bookmarkListElement.insertBefore(newNode, guido+booket@0: this.bookmarkListElement.firstChild); guido+booket@0: guido+booket@0: this.updateBookmarkMessage(); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.onBookmarkDeleted = function (bookmarkUrl) { guido+booket@0: var bookmarkElement; guido+booket@0: guido+booket@0: bookmarkElement = this.bookmarkListElement.querySelector('li' + guido+booket@0: createDatasetSelector('bookmark-url', bookmarkUrl)); guido+booket@0: if (bookmarkElement !== null) { guido+booket@0: this.bookmarkListElement.removeChild(bookmarkElement); guido+booket@0: guido+booket@0: this.updateBookmarkMessage(); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.onFilterTagsSearchChanged = function (filteredBookmarks, guido+booket@0: newFilterTags, newSearchTerm) { guido+booket@0: var bookmarkElements; guido+booket@0: var i; guido+booket@0: var tagElements; guido+booket@0: var toggleTagButton; guido+booket@0: var j; guido+booket@0: var tag; guido+booket@0: guido+booket@0: this.searchTermInputElement.value = newSearchTerm; guido+booket@0: guido+booket@0: bookmarkElements = guido+booket@0: this.bookmarkListElement.querySelectorAll('ul#bookmark-list > li'); guido+booket@0: for (i = 0; i < bookmarkElements.length; i++) { guido+booket@0: // update visibility of bookmarks guido+booket@0: if (filteredBookmarks.has(bookmarkElements[i].dataset.bookmarkUrl)) { guido+booket@0: // update tag elements of visible bookmarks guido+booket@0: tagElements = guido+booket@0: bookmarkElements[i].querySelectorAll('ul.tag-list > li'); guido+booket@0: for (j = 0; j < tagElements.length; j++) { guido+booket@0: tag = tagElements[j].dataset.tag; guido+booket@0: toggleTagButton = guido+booket@0: tagElements[j].querySelector('button[name="toggle-tag"]'); guido+booket@0: if (newFilterTags.has(tag)) { guido+booket@0: tagElements[j].classList.add('active-filter-tag'); guido+booket@0: toggleTagButton.textContent = '\u2212'; guido+booket@0: toggleTagButton.title = 'Remove "' + tag + '" from filter'; guido+booket@0: } else { guido+booket@0: tagElements[j].classList.remove('active-filter-tag'); guido+booket@0: toggleTagButton.textContent = '+'; guido+booket@0: toggleTagButton.title = 'Add "' + tag + '" to filter'; guido+booket@0: } guido+booket@0: } guido+booket@0: bookmarkElements[i].hidden = false; guido+booket@0: } else { guido+booket@0: bookmarkElements[i].hidden = true; guido+booket@0: } guido+booket@0: } guido+booket@0: guido+booket@0: this.updateBookmarkMessage(); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.createTagInputElement = function (tag) { guido+booket@0: var newNode; guido+booket@0: guido+booket@0: newNode = document.importNode(this.tagInputTemplate.content, true); guido+booket@0: newNode.querySelector('input[name="tag"]').value = tag; guido+booket@0: guido+booket@0: return newNode; guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.displayBookmarkEditor = function (bookmark) { guido+booket@0: var bookmarkElement; guido+booket@0: var newNode; guido+booket@0: var formElement; guido+booket@7: var faviconImageElement; guido+booket@0: var editTagListElement; guido+booket@0: guido+booket@0: bookmarkElement = guido+booket@0: this.bookmarkListElement.querySelector('ul#bookmark-list > li' + guido+booket@0: createDatasetSelector('bookmark-url', bookmark.url)); guido+booket@0: guido+booket@0: // disable edit button while editing guido+booket@0: bookmarkElement.querySelector('button[name="edit-bookmark"]').disabled = guido+booket@0: true; guido+booket@0: guido+booket@0: // create new editor form from template guido+booket@0: newNode = document.importNode(this.bookmarkEditorTemplate.content, true); guido+booket@0: guido+booket@0: // fill with data of given bookmark guido+booket@0: formElement = newNode.querySelector('form.bookmark-editor-form'); guido+booket@0: formElement.querySelector('legend').textContent = 'Edit Bookmark'; guido+booket@0: formElement['original-url'].value = bookmark.url; guido+booket@0: formElement.url.value = bookmark.url; guido+booket@0: formElement.title.value = bookmark.title; guido+booket@0: guido+booket@7: faviconImageElement = formElement.querySelector('img.bookmark-favicon'); guido+booket@7: faviconImageElement.addEventListener('load', this); guido+booket@7: faviconImageElement.addEventListener('error', this); guido+booket@7: this.missingFaviconUri = faviconImageElement.src; guido+booket@7: if (bookmark.favicon) { guido+booket@7: faviconImageElement.src = bookmark.favicon; guido+booket@7: } guido+booket@7: guido+booket@0: editTagListElement = formElement.querySelector('ul.tag-input-list'); guido+booket@0: bookmark.tags.forEach(function (tag) { guido+booket@0: editTagListElement.appendChild(this.createTagInputElement(tag)); guido+booket@0: }, this); guido+booket@0: editTagListElement.appendChild(this.createTagInputElement('')); guido+booket@0: guido+booket@0: // insert editor form into bookmark item guido+booket@0: bookmarkElement.appendChild(newNode); guido+booket@0: guido+booket@0: // focus first input element guido+booket@0: formElement.querySelector('input').focus(); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.confirmReplaceBookmark = function (bookmark) { guido+booket@0: return window.confirm('Replace bookmark "' + bookmark.title + '"\n[' + guido+booket@0: bookmark.url + ']?'); guido+booket@0: }; guido+booket@0: guido+booket@0: BookmarkView.prototype.confirmDeleteBookmark = function (bookmark) { guido+booket@0: return window.confirm('Delete bookmark "' + bookmark.title + '"\n[' + guido+booket@0: bookmark.url + ']?'); guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: /* guido+booket@0: * controller guido+booket@0: */ guido+booket@0: guido+booket@26: var BooketController = function(bookmarkModel, notificationsView, guido+booket@22: actionsView, tagView, bookmarkView) { guido+booket@0: this.bookmarkModel = bookmarkModel; guido+booket@26: this.notificationsView = notificationsView; guido+booket@0: this.actionsView = actionsView; guido+booket@0: this.tagView = tagView; guido+booket@0: this.bookmarkView = bookmarkView; guido+booket@0: guido+booket@0: /* connect the views to the model */ guido+booket@0: this.bookmarkModel.addObserver('bookmark-added', guido+booket@0: this.bookmarkView.onBookmarkAdded.bind(this.bookmarkView)); guido+booket@0: this.bookmarkModel.addObserver('bookmark-deleted', guido+booket@0: this.bookmarkView.onBookmarkDeleted.bind(this.bookmarkView)); guido+booket@0: this.bookmarkModel.addObserver('filter-tags-search-changed', guido+booket@0: this.bookmarkView.onFilterTagsSearchChanged.bind(this.bookmarkView)); guido+booket@0: this.bookmarkModel.addObserver('load-file-error', guido+booket@0: this.actionsView.onLoadFileError.bind(this.actionsView)); guido+booket@0: this.bookmarkModel.addObserver('parse-file-error', guido+booket@0: this.actionsView.onParseFileError.bind(this.actionsView)); guido+booket@0: this.bookmarkModel.addObserver('save-file', guido+booket@0: this.actionsView.onSaveFile.bind(this.actionsView)); guido+booket@11: this.bookmarkModel.addObserver('export-file', guido+booket@11: this.actionsView.onExportFile.bind(this.actionsView)); guido+booket@18: this.bookmarkModel.addObserver('unsaved-changes-changed', guido+booket@26: this.notificationsView.onUnsavedChangesChanged.bind(this.notificationsView)); guido+booket@0: this.bookmarkModel.addObserver('tag-added', guido+booket@0: this.tagView.onTagAdded.bind(this.tagView)); guido+booket@0: this.bookmarkModel.addObserver('tag-count-changed', guido+booket@0: this.tagView.onTagCountChanged.bind(this.tagView)); guido+booket@0: this.bookmarkModel.addObserver('tag-deleted', guido+booket@0: this.tagView.onTagDeleted.bind(this.tagView)); guido+booket@0: this.bookmarkModel.addObserver('filter-tags-search-changed', guido+booket@0: this.tagView.onFilterTagsSearchChanged.bind(this.tagView)); guido+booket@0: this.bookmarkModel.addObserver('filter-tags-search-changed', guido+booket@0: this.onFilterTagsSearchChanged.bind(this)); guido+booket@0: guido+booket@0: /* handle input */ guido+booket@0: window.addEventListener('hashchange', this.onHashChange.bind(this)); guido+booket@0: window.addEventListener('beforeunload', this.onBeforeUnload.bind(this)); guido+booket@0: this.actionsView.addObserver('save-file', guido+booket@0: this.bookmarkModel.saveFile.bind(this.bookmarkModel)); guido+booket@11: this.actionsView.addObserver('export-file', guido+booket@11: this.bookmarkModel.exportFile.bind(this.bookmarkModel)); guido+booket@0: this.actionsView.addObserver('load-file', this.onLoadFile.bind(this)); guido+booket@10: this.actionsView.addObserver('import-file', this.onImportFile.bind(this)); guido+booket@0: this.actionsView.addObserver('save-bookmark', guido+booket@0: this.onSaveBookmark.bind(this)); guido+booket@0: this.bookmarkView.addObserver('edit-bookmark', guido+booket@0: this.onEditBookmark.bind(this)); guido+booket@0: this.bookmarkView.addObserver('save-bookmark', guido+booket@0: this.onSaveBookmark.bind(this)); guido+booket@0: this.bookmarkView.addObserver('delete-bookmark', guido+booket@0: this.onDeleteBookmark.bind(this)); guido+booket@0: this.bookmarkView.addObserver('toggle-tag', guido+booket@0: this.onToggleFilterTag.bind(this)); guido+booket@0: this.bookmarkView.addObserver('set-tag', this.onSetTagFilter.bind(this)); guido+booket@0: this.bookmarkView.addObserver('search', this.onSearch.bind(this)); guido+booket@0: this.tagView.addObserver('toggle-tag', this.onToggleFilterTag.bind(this)); guido+booket@0: this.tagView.addObserver('set-tag', this.onSetTagFilter.bind(this)); guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.parseTagsParameter = function (tagsString) { guido+booket@0: var tags; guido+booket@0: guido+booket@0: tags = tagsString.split(',').filter(function (tag) { guido+booket@0: return (tag !== '') && this.bookmarkModel.hasTag(tag); guido+booket@0: }, this).sort(); guido+booket@0: guido+booket@0: return new StringSet(tags); guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onHashChange = function (e) { guido+booket@0: var hashData; guido+booket@0: var filterTags; guido+booket@0: var searchTerm; guido+booket@0: guido+booket@0: hashData = parseHash(window.location.href); guido+booket@0: guido+booket@0: filterTags = hashData.has('tags') ? guido+booket@0: this.parseTagsParameter(hashData.get('tags')) : new StringSet(); guido+booket@0: guido+booket@0: searchTerm = hashData.has('search') ? hashData.get('search') : ''; guido+booket@0: guido+booket@0: this.bookmarkModel.setFilterTagsSearchTerm(filterTags, searchTerm); guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onBeforeUnload = function (e) { guido+booket@0: var confirmationMessage = 'There are unsaved changes to your bookmarks.'; guido+booket@0: guido+booket@0: if (this.bookmarkModel.unsavedChanges) { guido+booket@0: if (e) { guido+booket@0: e.returnValue = confirmationMessage; guido+booket@0: } guido+booket@0: if (window.event) { guido+booket@0: window.event.returnValue = confirmationMessage; guido+booket@0: } guido+booket@0: return confirmationMessage; guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onFilterTagsSearchChanged = guido+booket@0: function (filteredBookmarks, newFilterTags, newSearchTerm) { guido+booket@0: var url = window.location.href; guido+booket@0: var hashData; guido+booket@0: guido+booket@0: // serialize tag filter and search term and update window.location guido+booket@0: hashData = parseHash(url); guido+booket@0: hashData.set('tags', newFilterTags.values().join(',')); guido+booket@0: hashData.set('search', newSearchTerm); guido+booket@0: history.pushState(null, null, serializeHash(url, hashData)); guido+booket@0: }; guido+booket@0: guido+booket@19: BooketController.prototype.onLoadFile = function (bookmarkFile, merge) { guido+booket@0: if (this.bookmarkModel.unsavedChanges) { guido+booket@0: if (!this.actionsView.confirmLoadFile()) { guido+booket@0: return; guido+booket@0: } guido+booket@0: this.bookmarkModel.unsavedChanges = false; guido+booket@0: } guido+booket@0: guido+booket@19: this.bookmarkModel.loadFile(bookmarkFile, merge); guido+booket@0: }; guido+booket@0: guido+booket@19: BooketController.prototype.onImportFile = function (bookmarkFile, merge) { guido+booket@10: if (this.bookmarkModel.unsavedChanges) { guido+booket@10: if (!this.actionsView.confirmLoadFile()) { guido+booket@10: return; guido+booket@10: } guido+booket@10: this.bookmarkModel.unsavedChanges = false; guido+booket@10: } guido+booket@10: guido+booket@19: this.bookmarkModel.importFile(bookmarkFile, merge); guido+booket@10: }; guido+booket@10: guido+booket@0: BooketController.prototype.onEditBookmark = function (bookmarkUrl) { guido+booket@0: this.bookmarkView.displayBookmarkEditor( guido+booket@0: this.bookmarkModel.get(bookmarkUrl)); guido+booket@0: }; guido+booket@0: guido+booket@7: BooketController.prototype.onSaveBookmark = function (url, title, guido+booket@7: favicon, tags, originalUrl) { guido+booket@0: var ctime; guido+booket@0: guido+booket@0: if (originalUrl === undefined) { guido+booket@0: // saving new bookmark, get confirmation before replacing existing one guido+booket@0: if (this.bookmarkModel.has(url)) { guido+booket@0: if (this.bookmarkView.confirmReplaceBookmark( guido+booket@0: this.bookmarkModel.get(url))) { guido+booket@0: this.bookmarkModel.delete(url); guido+booket@0: } else { guido+booket@0: return; guido+booket@0: } guido+booket@0: } guido+booket@0: guido+booket@0: ctime = new Date(); guido+booket@0: } else { guido+booket@0: // saving edited bookmark, preserve creation time of any replaced guido+booket@0: // bookmark guido+booket@0: ctime = (this.bookmarkModel.has(url)) ? guido+booket@0: this.bookmarkModel.get(url).ctime : new Date(); guido+booket@0: guido+booket@0: this.bookmarkModel.delete(originalUrl); guido+booket@0: } guido+booket@7: this.bookmarkModel.add(new Bookmark(url, title, favicon, tags, ctime)); guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onDeleteBookmark = function (bookmarkUrl) { guido+booket@0: if (this.bookmarkView.confirmDeleteBookmark( guido+booket@0: this.bookmarkModel.get(bookmarkUrl))) { guido+booket@0: this.bookmarkModel.delete(bookmarkUrl); guido+booket@0: } guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onToggleFilterTag = function (tag) { guido+booket@0: this.bookmarkModel.toggleFilterTag(tag); guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onSetTagFilter = function (tag) { guido+booket@0: this.bookmarkModel.setFilterTags(new StringSet([tag])); guido+booket@0: }; guido+booket@0: guido+booket@0: BooketController.prototype.onSearch = function (searchTerm) { guido+booket@0: this.bookmarkModel.setSearchTerm(searchTerm); guido+booket@0: }; guido+booket@0: guido+booket@0: guido+booket@0: document.addEventListener('DOMContentLoaded', function (e) { guido+booket@0: var controller; guido+booket@0: var bookmarkModel; guido+booket@26: var notificationsView; guido+booket@0: var actionsView; guido+booket@0: var tagView; guido+booket@0: var bookmarkView; guido+booket@0: var hashChangeEvent; guido+booket@0: guido+booket@0: bookmarkModel = new BookmarkModel(); guido+booket@26: notificationsView = new NotificationsView(); guido+booket@0: tagView = new TagView(); guido+booket@0: actionsView = new ActionsView(); guido+booket@0: bookmarkView = new BookmarkView(); guido+booket@26: controller = new BooketController(bookmarkModel, notificationsView, guido+booket@22: actionsView, tagView, bookmarkView); guido+booket@0: guido+booket@0: // initialize state from the current URL guido+booket@0: hashChangeEvent = new Event('hashchange'); guido+booket@0: hashChangeEvent.oldURL = window.location.href; guido+booket@0: hashChangeEvent.newURL = window.location.href; guido+booket@0: window.dispatchEvent(hashChangeEvent); guido+booket@0: }); guido+booket@0: }()); guido+booket@0: