├── .gitignore ├── css ├── icon.png ├── next.png ├── prev.png ├── trash.png ├── throbber.gif ├── arrow_down.png ├── icon_thumb.png ├── icon_tiny.png ├── throbber-2.gif ├── normalize.css └── history.css ├── release.sh ├── .jshintrc ├── manifest.json ├── tests ├── tests.html ├── mocha.css ├── query_parser.js ├── assert.js └── expect.js ├── LICENSE.txt ├── lib ├── query_parser.js ├── util.js ├── visitsbyday.js ├── view.js ├── model.js ├── ehistory.js └── controller.js ├── README.md ├── history.html └── vendor ├── spin.js └── mustache.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE -------------------------------------------------------------------------------- /css/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/icon.png -------------------------------------------------------------------------------- /css/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/next.png -------------------------------------------------------------------------------- /css/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/prev.png -------------------------------------------------------------------------------- /css/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/trash.png -------------------------------------------------------------------------------- /css/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/throbber.gif -------------------------------------------------------------------------------- /css/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/arrow_down.png -------------------------------------------------------------------------------- /css/icon_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/icon_thumb.png -------------------------------------------------------------------------------- /css/icon_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/icon_tiny.png -------------------------------------------------------------------------------- /css/throbber-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amasad/eHistory/HEAD/css/throbber-2.gif -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | cur=`pwd` 2 | tmp=ehistory_release_tmp 3 | cd .. 4 | rm -rf $tmp 5 | cp -r $cur $tmp 6 | cd $tmp 7 | rm -rf .git 8 | rm release.sh 9 | cd .. 10 | zip -r ehistory_release.zip $tmp/* 11 | rm -rf $tmp -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { "eqeqeq": true 2 | , "latedef": true 3 | , "newcap": true 4 | , "noarg": true 5 | , "sub": true 6 | , "undef": true 7 | , "boss": true 8 | , "eqnull": true 9 | , "browser": true 10 | , "laxcomma": true 11 | , "laxbreak": true 12 | , "strict": false 13 | , "asi": true 14 | , "unused": true 15 | , "predef": { 16 | "$": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eHistory", 3 | "version": "1.14", 4 | "description": "Enhanced History", 5 | "permissions": [ 6 | "history", 7 | "chrome://favicon/" 8 | ], 9 | "chrome_url_overrides" : { 10 | "history": "history.html" 11 | }, 12 | "icons": { 13 | "128": "css/icon.png" 14 | }, 15 | "manifest_version": 2 16 | } 17 | -------------------------------------------------------------------------------- /tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) <2011> by Amjad Masad 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /lib/query_parser.js: -------------------------------------------------------------------------------- 1 | /* parseQuery: Parses the search query 2 | * @arg (String) input: The search query 3 | * @returns (Array) [settings, filters, text] 4 | */ 5 | this.parseQuery = function (query) { 6 | // Custom filters. 7 | var filters = { 8 | inurl: null, 9 | intitle: null, 10 | site: null 11 | }; 12 | 13 | // Chrome search object. 14 | var settings = { 15 | startTime: null, 16 | endTime: null 17 | }; 18 | 19 | var combined = ''; 20 | 21 | // Assumes search query is a space delimted key/value pairs. 22 | query.split(/\s/).forEach(function (pair) { 23 | if (!pair) return; 24 | // Assume key:value 25 | pair = pair.split(':'); 26 | if (settings.hasOwnProperty(pair[0])) { 27 | settings[pair[0]] = pair[1]; 28 | } else if (filters.hasOwnProperty(pair[0])) { 29 | filters[pair[0]] = pair[1] 30 | combined += ' ' + (pair[1]); 31 | } else { 32 | combined += ' ' + pair.join(':'); 33 | } 34 | }); 35 | 36 | settings.text = combined.trim(); 37 | 38 | // TODO: is this needed? 39 | // delete all empty filters 40 | for (var prop in filters) { 41 | if (filters[prop] === null) { 42 | delete filters[prop]; 43 | } 44 | } 45 | 46 | return { 47 | settings: settings, 48 | filters: filters 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | this.Util = (function () { 2 | var rProtocol = /^([a-z0-9+]+:)/i, 3 | rPort = /:[0-9]+$/, 4 | // Charecters that are not allowed in a hostname. 5 | nonHostChars = ['<', '>', '"', '`', ' ', '\r', '\n', '\t', '{', '}', '|', 6 | '\\', '^', '~', '[', ']', '`', '%', '/', '?', ';', '#'], 7 | // Protocols that don't have a hostname. 8 | hostlessProtocols = { 9 | 'javascript': true, 10 | 'file': true 11 | }; 12 | 13 | // Gets the hostname without the port. 14 | return { 15 | getHostname: function (url) { 16 | var proto = rProtocol.exec(url), 17 | rest = url, 18 | host = ''; 19 | 20 | proto = proto ? proto[0] : null; 21 | 22 | if (proto && !hostlessProtocols[proto]) { 23 | // We should have a host. 24 | rest = rest.substr(proto.length + 2); 25 | 26 | // Find the first non host character index in the url string left. 27 | var firstNonHost = -1; 28 | for (var i = 0; i < nonHostChars.length; i++) { 29 | var index = rest.indexOf(nonHostChars[i]); 30 | if (index !== -1 && 31 | (index < firstNonHost || firstNonHost === -1)) firstNonHost = index; 32 | } 33 | 34 | // Get the actual hostName. 35 | if (firstNonHost !== -1) { 36 | host = rest.substr(0, firstNonHost); 37 | // Remove port. 38 | host = host.replace(rPort, ''); 39 | } 40 | } 41 | return host; 42 | } 43 | }; 44 | 45 | })(); 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eHistory 2 | 3 | This takes the current chrome history to the next level by adding a set of very useful search operators which is inspired by google's search operators. 4 | It also works around many Google Chrome history bugs and makes it easy to use. 5 | 6 | If you are the kind of person spends a lot of time on the web, sooner or later you'll run into a situation where you want to pull up a page you've visited in the past that you can remember so little about. Maybe you remember that it was an article in the New York times. And maybe you remember you saw it two weeks ago. Normally, this kind of information won't be of any help to you. Enter eHistory: It allows you to power search your browser history with a nice and slick interface. It also adds advanced deletion features. 7 | 8 | 9 | ## Filters 10 | 11 | 1. URL. 12 | 2. Title. 13 | 3. Site. 14 | 4. Time of visit. 15 | 5. Content. 16 | 17 | ## Other features: 18 | 19 | * Search and delete individual or multiple Items. 20 | * Easily select full days to delete. 21 | * Clear history. 22 | * Clear all search results. 23 | * Works around some Chrome history bugs. 24 | 25 | ## Example: 26 | I remember reading a NY times article about coffee: 27 | site:nytimes.com intitle:coffee 28 | 29 | 30 | ## Short comings 31 | 32 | Some queries may run slower than others, and this is because all filteration is done in JavaScript. The Chrome History API only takes text field search, nonetheless eHistory is highly optimized and implements certain hacks to get the results as fast as possible. 33 | Another pain point would be that Chrome's history API doesn't expose the snippet which can reveal which part of the page your search string actually matched. 34 | 35 | -------------------------------------------------------------------------------- /tests/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | padding: 60px 50px; 6 | } 7 | 8 | #mocha ul, #mocha li { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | #mocha ul { 14 | list-style: none; 15 | } 16 | 17 | #mocha h1, #mocha h2 { 18 | margin: 0; 19 | } 20 | 21 | #mocha h1 { 22 | margin-top: 15px; 23 | font-size: 1em; 24 | font-weight: 200; 25 | } 26 | 27 | #mocha h1 a { 28 | text-decoration: none; 29 | color: inherit; 30 | } 31 | 32 | #mocha h1 a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | #mocha .suite .suite h1 { 37 | margin-top: 0; 38 | font-size: .8em; 39 | } 40 | 41 | .hidden { 42 | display: none; 43 | } 44 | 45 | #mocha h2 { 46 | font-size: 12px; 47 | font-weight: normal; 48 | cursor: pointer; 49 | } 50 | 51 | #mocha .suite { 52 | margin-left: 15px; 53 | } 54 | 55 | #mocha .test { 56 | margin-left: 15px; 57 | overflow: hidden; 58 | } 59 | 60 | #mocha .test.pending:hover h2::after { 61 | content: '(pending)'; 62 | font-family: arial; 63 | } 64 | 65 | #mocha .test.pass.medium .duration { 66 | background: #C09853; 67 | } 68 | 69 | #mocha .test.pass.slow .duration { 70 | background: #B94A48; 71 | } 72 | 73 | #mocha .test.pass::before { 74 | content: '✓'; 75 | font-size: 12px; 76 | display: block; 77 | float: left; 78 | margin-right: 5px; 79 | color: #00d6b2; 80 | } 81 | 82 | #mocha .test.pass .duration { 83 | font-size: 9px; 84 | margin-left: 5px; 85 | padding: 2px 5px; 86 | color: white; 87 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 88 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 89 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 90 | -webkit-border-radius: 5px; 91 | -moz-border-radius: 5px; 92 | -ms-border-radius: 5px; 93 | -o-border-radius: 5px; 94 | border-radius: 5px; 95 | } 96 | 97 | #mocha .test.pass.fast .duration { 98 | display: none; 99 | } 100 | 101 | #mocha .test.pending { 102 | color: #0b97c4; 103 | } 104 | 105 | #mocha .test.pending::before { 106 | content: '◦'; 107 | color: #0b97c4; 108 | } 109 | 110 | #mocha .test.fail { 111 | color: #c00; 112 | } 113 | 114 | #mocha .test.fail pre { 115 | color: black; 116 | } 117 | 118 | #mocha .test.fail::before { 119 | content: '✖'; 120 | font-size: 12px; 121 | display: block; 122 | float: left; 123 | margin-right: 5px; 124 | color: #c00; 125 | } 126 | 127 | #mocha .test pre.error { 128 | color: #c00; 129 | max-height: 300px; 130 | overflow: auto; 131 | } 132 | 133 | #mocha .test pre { 134 | display: block; 135 | float: left; 136 | clear: left; 137 | font: 12px/1.5 monaco, monospace; 138 | margin: 5px; 139 | padding: 15px; 140 | border: 1px solid #eee; 141 | border-bottom-color: #ddd; 142 | -webkit-border-radius: 3px; 143 | -webkit-box-shadow: 0 1px 3px #eee; 144 | -moz-border-radius: 3px; 145 | -moz-box-shadow: 0 1px 3px #eee; 146 | } 147 | 148 | #mocha .test h2 { 149 | position: relative; 150 | } 151 | 152 | #mocha .test a.replay { 153 | position: absolute; 154 | top: 3px; 155 | right: 0; 156 | text-decoration: none; 157 | vertical-align: middle; 158 | display: block; 159 | width: 15px; 160 | height: 15px; 161 | line-height: 15px; 162 | text-align: center; 163 | background: #eee; 164 | font-size: 15px; 165 | -moz-border-radius: 15px; 166 | border-radius: 15px; 167 | -webkit-transition: opacity 200ms; 168 | -moz-transition: opacity 200ms; 169 | transition: opacity 200ms; 170 | opacity: 0.3; 171 | color: #888; 172 | } 173 | 174 | #mocha .test:hover a.replay { 175 | opacity: 1; 176 | } 177 | 178 | #mocha-report.pass .test.fail { 179 | display: none; 180 | } 181 | 182 | #mocha-report.fail .test.pass { 183 | display: none; 184 | } 185 | 186 | #mocha-error { 187 | color: #c00; 188 | font-size: 1.5 em; 189 | font-weight: 100; 190 | letter-spacing: 1px; 191 | } 192 | 193 | #mocha-stats { 194 | position: fixed; 195 | top: 15px; 196 | right: 10px; 197 | font-size: 12px; 198 | margin: 0; 199 | color: #888; 200 | } 201 | 202 | #mocha-stats .progress { 203 | float: right; 204 | padding-top: 0; 205 | } 206 | 207 | #mocha-stats em { 208 | color: black; 209 | } 210 | 211 | #mocha-stats a { 212 | text-decoration: none; 213 | color: inherit; 214 | } 215 | 216 | #mocha-stats a:hover { 217 | border-bottom: 1px solid #eee; 218 | } 219 | 220 | #mocha-stats li { 221 | display: inline-block; 222 | margin: 0 5px; 223 | list-style: none; 224 | padding-top: 11px; 225 | } 226 | 227 | code .comment { color: #ddd } 228 | code .init { color: #2F6FAD } 229 | code .string { color: #5890AD } 230 | code .keyword { color: #8A6343 } 231 | code .number { color: #2F6FAD } 232 | 233 | @media screen and (max-device-width: 480px) { 234 | body { 235 | padding: 60px 0px; 236 | } 237 | 238 | #stats { 239 | position: absolute; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /tests/query_parser.js: -------------------------------------------------------------------------------- 1 | describe('query parser', function () { 2 | 3 | describe('plain text', function () { 4 | 5 | it('should handle plain text', function () { 6 | var options = parseQuery('foo'); 7 | expect(options).to.eql({ 8 | settings: { 9 | startTime: null 10 | , endTime: null 11 | , text: 'foo' 12 | } 13 | , filters: {} 14 | }); 15 | }); 16 | 17 | it('should handle plain text with special chars', function () { 18 | var options = parseQuery('foo + bar'); 19 | expect(options).to.eql({ 20 | settings: { 21 | startTime: null 22 | , endTime: null 23 | , text: 'foo + bar' 24 | } 25 | , filters: {} 26 | }); 27 | }); 28 | 29 | }); 30 | 31 | describe('filters', function () { 32 | 33 | describe('simple filtes', function () { 34 | 35 | it('should handle title filter', function () { 36 | var options = parseQuery('intitle:wat'); 37 | expect(options).to.eql({ 38 | settings: { 39 | startTime: null 40 | , endTime: null 41 | , text: 'wat' 42 | } 43 | , filters: { 44 | intitle: 'wat' 45 | } 46 | }); 47 | }); 48 | 49 | it('should handle site filter', function () { 50 | var options = parseQuery('site:google.com'); 51 | expect(options).to.eql({ 52 | settings: { 53 | startTime: null 54 | , endTime: null 55 | , text: 'google.com' 56 | } 57 | , filters: { 58 | site: 'google.com' 59 | } 60 | }); 61 | }); 62 | 63 | it('should handle url filter', function () { 64 | var options = parseQuery('inurl:foobar'); 65 | expect(options).to.eql({ 66 | settings: { 67 | startTime: null 68 | , endTime: null 69 | , text: 'foobar' 70 | } 71 | , filters: { 72 | inurl: 'foobar' 73 | } 74 | }); 75 | }); 76 | 77 | it('should handle startTime filter', function () { 78 | var options = parseQuery('startTime:13-10-20'); 79 | expect(options).to.eql({ 80 | settings: { 81 | startTime: '13-10-20' 82 | , endTime: null 83 | , text: '' 84 | } 85 | , filters: {} 86 | }); 87 | }); 88 | 89 | it('should handle endTime filter', function () { 90 | var options = parseQuery('endTime:13-10-20'); 91 | expect(options).to.eql({ 92 | settings: { 93 | startTime: null 94 | , endTime: '13-10-20' 95 | , text: '' 96 | } 97 | , filters: {} 98 | }); 99 | }); 100 | 101 | it('should ignore unknown filter', function () { 102 | var options = parseQuery('foo:bar'); 103 | expect(options).to.eql({ 104 | settings: { 105 | startTime: null 106 | , endTime: null 107 | , text: 'foo:bar' 108 | } 109 | , filters: {} 110 | }); 111 | }); 112 | }); 113 | 114 | describe('multi filters', function () { 115 | 116 | it('should handle time filters', function () { 117 | var options = parseQuery('endTime:13-10-20 startTime:1/1/1'); 118 | expect(options).to.eql({ 119 | settings: { 120 | startTime: '1/1/1' 121 | , endTime: '13-10-20' 122 | , text: '' 123 | } 124 | , filters: {} 125 | }); 126 | }); 127 | 128 | it('should handle time filters and site', function () { 129 | var options = parseQuery('endTime:13-10-20 startTime:1/1/1 site:wat.com'); 130 | expect(options).to.eql({ 131 | settings: { 132 | startTime: '1/1/1' 133 | , endTime: '13-10-20' 134 | , text: 'wat.com' 135 | } 136 | , filters: { 137 | site: 'wat.com' 138 | } 139 | }); 140 | }); 141 | 142 | it('should handle time filters and site', function () { 143 | var options = parseQuery('endTime:13-10-20 startTime:1/1/1 site:wat.com inurl:shitmang'); 144 | expect(options).to.eql({ 145 | settings: { 146 | startTime: '1/1/1' 147 | , endTime: '13-10-20' 148 | , text: 'wat.com shitmang' 149 | } 150 | , filters: { 151 | site: 'wat.com' 152 | , inurl: 'shitmang' 153 | } 154 | }); 155 | }); 156 | 157 | }); 158 | 159 | }); 160 | 161 | describe('plain text + filters', function () { 162 | 163 | it('should handle time filters and text', function () { 164 | var options = parseQuery('endTime:13-10-20 startTime:1/1/1 shitmang'); 165 | expect(options).to.eql({ 166 | settings: { 167 | startTime: '1/1/1' 168 | , endTime: '13-10-20' 169 | , text: 'shitmang' 170 | } 171 | , filters: { 172 | } 173 | }); 174 | }); 175 | 176 | it('should handle inurl + text', function () { 177 | var options = parseQuery('inurl:hah shitmang'); 178 | expect(options).to.eql({ 179 | settings: { 180 | startTime: null 181 | , endTime: null 182 | , text: 'hah shitmang' 183 | } 184 | , filters: { 185 | inurl: 'hah' 186 | } 187 | }); 188 | }); 189 | 190 | it('should handle inurl + ignored filter', function () { 191 | var options = parseQuery('inurl:hah shit:mang'); 192 | expect(options).to.eql({ 193 | settings: { 194 | startTime: null 195 | , endTime: null 196 | , text: 'hah shit:mang' 197 | } 198 | , filters: { 199 | inurl: 'hah' 200 | } 201 | }); 202 | 203 | }); 204 | 205 | }); 206 | 207 | }); 208 | -------------------------------------------------------------------------------- /lib/visitsbyday.js: -------------------------------------------------------------------------------- 1 | /* 2 | * eHistory Chrome Extension 3 | * https://chrome.google.com/webstore/detail/hiiknjobjfknoghbeelhfilaaikffopb 4 | * 5 | * Copyright 2011, Amjad Masad 6 | * Licensed under the MIT license 7 | * https://github.com/amasad/eHistory/blob/master/LICENSE.txt 8 | * 9 | * Date: Mon May 9 10 | */ 11 | 12 | // TODO: Implement linked list 13 | // Class DaysVisits 14 | // Holds visit items on one day 15 | 16 | // Constructor 17 | // @arg firstItem: usually when constructed the first item to put in a particular day is available. 18 | function DaysVisits (firstItem) { 19 | // create the items array 20 | this.items = [firstItem]; 21 | // a hash containing the id of the visit item and its index in the items array for faster access 22 | this.id_map = {}; 23 | this.id_map[firstItem.id] = firstItem; 24 | // The particular day timestamp 25 | this.day = firstItem.day; 26 | } 27 | 28 | DaysVisits.prototype = { 29 | // Method insert : inserts a single visit item, if the another visit 30 | // exists with the same parent history item, replace it 31 | // @arg item: visit item 32 | insert: function (item) { 33 | if (item.day != this.day) throw new Error("Invalid Day"); 34 | // pull up the index of the visit item that corresponds to the same history item if it exists 35 | var currentItem = this.id_map[item.id], 36 | index = this.items.indexOf(currentItem), 37 | mid, spliceInd; 38 | // if the index is valid and the item to be added is newer in time then replace the old one 39 | if (this.items[index]) { 40 | if (currentItem.visitTime < item.visitTime) { 41 | this.items.splice(index, 1); 42 | } else { 43 | return; 44 | } 45 | } 46 | if (!this.items.length) { 47 | this.items.push(item); 48 | } else { 49 | spliceInd = this._binsearch(item); 50 | this.items.splice(spliceInd + 1, 0, item); 51 | } 52 | /* 53 | 54 | console.log(this.binsearch(item)); 55 | } catch (e) {console.log("err",this.items.length); throw e} 56 | for (var i = 0; i < this.items.length; i++) { 57 | if (this.items[i].visitTime < item.visitTime){ 58 | this.items.splice(i, 0, item); 59 | console.log("really ", i); 60 | break; 61 | } else if (i == this.items.length - 1) { 62 | this.items.push(item); 63 | console.log("pushed ", i); 64 | break; 65 | } 66 | } 67 | if (!this.items.length) this.items.push(item);*/ 68 | this.id_map[item.id] = item; 69 | }, 70 | _binsearch: function(item) { 71 | var i = 0, 72 | j = this.items.length -1 , 73 | mid; 74 | 75 | while (true) { 76 | mid = Math.floor((j - i) / 2) + i; 77 | 78 | if (this.items[mid].visitTime < item.visitTime) { 79 | j = mid - 1; 80 | if (i > j) return j; 81 | } else { 82 | i = mid + 1; 83 | if (i > j) return mid; 84 | } 85 | } 86 | }, 87 | // Method dequeue: Gets the newest items up to a limited number, usually called after sort. 88 | // @arg length: max length of the number of items to splice off. 89 | // @return Array 90 | dequeue: function (length) { 91 | 92 | return this.items.splice(0, length); 93 | }, 94 | 95 | clear: function () { 96 | this.items = []; 97 | this.id_map = {}; 98 | } 99 | }; 100 | 101 | // Class VisitsByDay, wrapper class that holds as much DaysVisits as there is days 102 | // in the current search instance. 103 | 104 | // Constructor 105 | function VisitsByDay () { 106 | // the newest time allowable in the days this class hosts. 107 | this.latestDay = Date.now(); 108 | // hash containing all DaysVisits instances 109 | // @key: Day time stamp. 110 | // @value: DaysVisits instance. 111 | this.items_day = {}; 112 | // list of days this class items on 113 | this.days = []; 114 | } 115 | 116 | // public methods 117 | VisitsByDay.prototype = { 118 | // Method insert, inserts one single visit item into the appropriate place. 119 | // @arg item: visit Item. 120 | insert: function (item) { 121 | //array of items on one day 122 | if (item.day > this.latestDay) return; 123 | if (!this.items_day[item.day]) { 124 | this.items_day[item.day] = new DaysVisits(item); 125 | } else { 126 | this.items_day[item.day].insert(item); 127 | } 128 | }, 129 | // Method sort, sorts all DaysVisits childs 130 | // @TODO: Merge with dequeue 131 | // @chainable 132 | sort: function () { 133 | this.days = Object.keys(this.items_day); 134 | this.days.sort(function (a,b) {return parseInt(b)-parseInt(a);}); 135 | // for (var i=0; i < this.days.length; i++){ 136 | // this.items_day[this.days[i]].sort(); 137 | // } 138 | return this; 139 | }, 140 | // Method dequeue 141 | // @arg length: Maximum number of items to return 142 | dequeue: function (length) { 143 | // helper function 144 | // @arg day: A day timestamp 145 | // @arg index: The day's index in the days array. 146 | var that = this, 147 | ret = [], 148 | daysResults; 149 | 150 | var deleteDay = function(index) { 151 | delete that.items_day[that.days[index]]; 152 | that.days.splice(index, 1); 153 | } 154 | for (var i = 0; i < this.days.length && length > 0; i++){ 155 | // remove days no longer needed, i.e. garbage visits, generated from getting all visits 156 | // @TODO: is this necessary since we have this check on insertion? 157 | if (this.days[i] > this.latestDay) { 158 | deleteDay(i--); 159 | continue; 160 | } 161 | daysResults = this.items_day[this.days[i]].dequeue(length); 162 | length -= daysResults.length; 163 | ret = ret.concat(daysResults); 164 | if (!this.items_day[this.days[i]].items.length) deleteDay(i--); 165 | } 166 | // record that last date of the last visit handed over, being the latest. 167 | if (ret.length) this.latestDay = ret[ret.length - 1].day; 168 | return ret; 169 | } 170 | 171 | }; 172 | -------------------------------------------------------------------------------- /lib/view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * eHistory Chrome Extension 3 | * https://chrome.google.com/webstore/detail/hiiknjobjfknoghbeelhfilaaikffopb 4 | * 5 | * Copyright 2011, Amjad Masad 6 | * Licensed under the MIT license 7 | * https://github.com/amasad/eHistory/blob/master/LICENSE.txt 8 | * 9 | */ 10 | (function () { 11 | /* global Mustache, historyModel, EHistory, Spinner */ 12 | 'use strict'; 13 | // History View: responsible for populating results in the current page view 14 | // Holds current page state, and updates page controls accordingly 15 | // Direct communication with history model. 16 | // @exports historyView 17 | this.historyView = (function () { 18 | // Initial DOM (jQuery) variables 19 | var $table, $olderPage, $newerPage, $allNav, $throbber, $pageNo, $divMain; 20 | // Current page in the history view 21 | var currentPage = 0; 22 | 23 | var templates = { 24 | 'row': Mustache.compile($('#tmpl-entry-row').html().trim()), 25 | 'day-row': Mustache.compile($('#tmpl-day-row').html().trim()) 26 | }; 27 | 28 | var spinner = new Spinner({ 29 | lines: 13, // The number of lines to draw 30 | length: 10, // The length of each line 31 | width: 2, // The line thickness 32 | radius: 10, // The radius of the inner circle 33 | corners: 1, // Corner roundness (0..1) 34 | rotate: 0, // The rotation offset 35 | direction: 1, // 1: clockwise, -1: counterclockwise 36 | color: '#333', // #rgb or #rrggbb or array of colors 37 | speed: 1, // Rounds per second 38 | trail: 60, // Afterglow percentage 39 | shadow: false, // Whether to render a shadow 40 | hwaccel: true, // Whether to use hardware acceleration 41 | className: 'spinner', // The CSS class to assign to the spinner 42 | zIndex: 2e9, // The z-index (defaults to 2000000000) 43 | top: '0', // Top position relative to parent in px 44 | left: 'auto' // Left position relative to parent in px 45 | }); 46 | 47 | //init 48 | //On domReady get the DOM elements 49 | $(function(){ 50 | // Main table that holds results 51 | $table = $('#tbl-main'); 52 | // Button responsible for getting older results, i.e. previous page 53 | $olderPage = $('.next-page'); 54 | // Button for getting newer results, i.e. next page 55 | $newerPage = $('.prev-page'); 56 | // jQuery instance holding all the navigation controls 57 | $allNav = $olderPage.add($newerPage); 58 | $throbber = $('#throbber'); 59 | $pageNo = $('.page-no'); 60 | // Division that holds the scroll value for the overflow 61 | $divMain = $('#div-main'); 62 | // Scroll to top of results 63 | function navTop(){ 64 | $divMain.animate({ 65 | scrollTop: $divMain.position().top 66 | }, 'slow'); 67 | } 68 | // Bind buttons functionalities 69 | $olderPage.click(function () { 70 | currentPage++; 71 | $(historyModel).trigger('modelrefresh'); 72 | navTop(); 73 | }); 74 | $newerPage.click(function () { 75 | if (currentPage !== 0) currentPage--; 76 | $(historyModel).trigger('modelrefresh'); 77 | navTop(); 78 | }); 79 | // Navigation controls disabled onload 80 | $allNav.attr('disabled', true); 81 | }); 82 | // when the view is on the first page disable first, newer buttons 83 | function updateControls () { 84 | $allNav.attr('disabled', false); 85 | if (currentPage === 0){ 86 | $newerPage.attr('disabled', true); 87 | } 88 | } 89 | // Listen to history model refresh event 90 | // and get the page required from the model 91 | $(historyModel).bind('modelrefresh', function () { 92 | updateControls(); 93 | var page = this.getPage(currentPage); 94 | if (page === -1) return; 95 | $pageNo.text(currentPage + 1); 96 | // results per day hash 97 | var results_day = {}; 98 | $.each(page, function (i, visit) { 99 | if (!results_day[visit.day]) results_day[visit.day] = []; 100 | results_day[visit.day].push(visit); 101 | }); 102 | // empty the view table 103 | $table.empty(); 104 | // for each day create a day row (holds day info and a checkbox allows selection of all day items) 105 | $.each(results_day, function (day, items) { 106 | $(templates['day-row']({date: new Date(parseInt(day, 10)).toDateString()})).appendTo($table); 107 | // on each day populate the results that corresponds to that day. 108 | $.each(items, function (i, visit) { 109 | var row = $(templates['row'](visit)); 110 | // let the elem data hold info of the corrosponding visit 111 | row.data('id', visit.id); 112 | row.data('day', visit.day); 113 | // check if the current item to be populated was selected in earlier navigations 114 | if (historyModel.isSelected(visit.id, visit.day)) { 115 | row.children().children('input').attr('checked', true); 116 | } 117 | $table.append(row); 118 | }); 119 | }); 120 | }); 121 | // listens for lastPage event from the historyModel update controls accordingly 122 | $(historyModel).bind('lastPage', function () { 123 | $olderPage.attr('disabled', true); 124 | }); 125 | $(EHistory).bind('done', function () { 126 | $throbber.removeClass('active'); 127 | }); 128 | $(EHistory).bind('finished', function () { 129 | if ($table.is(':empty')) { 130 | $table.append('No results :('); 131 | } 132 | $newerPage.attr('disabled', false); 133 | }); 134 | // Public functions 135 | return { 136 | clear: function () { 137 | currentPage = 0; 138 | $table.empty(); 139 | }, 140 | disableControls: function disableControls() { 141 | $allNav.attr('disabled', true); 142 | }, 143 | displayThrobber: function () { 144 | $throbber.addClass('active'); 145 | spinner.spin($throbber[0]); 146 | } 147 | }; 148 | })(); 149 | 150 | }).call(this); 151 | -------------------------------------------------------------------------------- /history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | eHistory 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 17 | 34 | 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 |
Send Feedback
59 | 60 | 61 | 62 | 63 | 64 | 65 | 76 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /lib/model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * eHistory Chrome Extension 3 | * https://chrome.google.com/webstore/detail/hiiknjobjfknoghbeelhfilaaikffopb 4 | * 5 | * Copyright 2011, Amjad Masad 6 | * Licensed under the MIT license 7 | * https://github.com/amasad/eHistory/blob/master/LICENSE.txt 8 | * 9 | */ 10 | 11 | /* Cleanup TODO: 12 | * Why the hell was I mixing module pattern and classes? 13 | * refactor append. 14 | * remove reference to view! 15 | */ 16 | 17 | (function (){ 18 | 'use strict'; 19 | /* global EHistory, historyView, chrome, alert */ 20 | // Holds the current history search state, direct communication with the EHistory library. 21 | // Eventsource, triggers: 22 | // 'modelreferesh': when new items are appended to the model. 23 | // 'lastPage': when there is no more items. 24 | this.historyModel = (function(){ 25 | // Private variables 26 | // results, holding all the current results available 27 | var results = []; 28 | // boolean stating whether the EHistory system has delivered all of the current query results 29 | var finished = false; 30 | // hash holding all the selected items for deletion 31 | // key: Timestamp for day, value: Array of history Items 32 | // TODO: Make at an array of ids 33 | var selected = {}; 34 | var prevPage = -1; 35 | // Constructor for History Model Singleton 36 | function HistoryModel(){ 37 | // hash of items, Key: id, Value: Item 38 | this.item_map = {}; 39 | // Current results per page 40 | this.pageSize = 150; 41 | } 42 | // Receives data from the EHistory and appends the results to the current model 43 | // Used as a callback for the EHistory to be called when the search is done 44 | // @arg data: hash containing: 45 | // items: history Items 46 | // visits: visits found corresponding to history items. 47 | HistoryModel.prototype = { 48 | append: function (data) { 49 | var item_map = this.item_map; 50 | // Populate the item_map hash with items coming from EHistory 51 | for (var j = 0, item; item = data.items[j]; j++){ 52 | item_map[item.id] = item; 53 | } 54 | var visit, resultItem, timeStr, hours, which; 55 | for (var i = 0; visit = data.visits[i]; i++){ 56 | // Create a 'resultItem' that will contain all the information about the visit 57 | resultItem = Object.create(item_map[visit.id]); 58 | resultItem.visitTime = visit.visitTime || 0; 59 | resultItem.day = visit.day || 0; 60 | 61 | timeStr = new Date(visit.visitTime).toLocaleTimeString(); 62 | // If we know the format of the locale time string then we'll try to 63 | // end up with HH:MM [PERIOD] otherwise we'll just use it as is. 64 | if (/^\d{1,2}:\d{1,2}:\d{1,2}\s[AP]M$/.test(timeStr)) { 65 | var parts = timeStr.split(':'); 66 | var period = parts.pop().substr(-2); 67 | resultItem.date = parts.join(':') + ' ' + period; 68 | } else { 69 | resultItem.date = timeStr; 70 | } 71 | 72 | resultItem.domain = Util.getHostname(resultItem.url) 73 | 74 | results.push(resultItem); 75 | } 76 | // Trigger an event stating that the model has new additions 77 | $(this).trigger('modelrefresh'); 78 | }, 79 | // Gives model results according to the page number requested 80 | // if the page requested was not found, the model will make a new page request 81 | // to the EHistory system requesting a new page, and returns -1 to the caller 82 | // stating that there is no result to be found. 83 | // @arg page: # of the page requested 84 | getPage: function (page) { 85 | var pageSize = this.pageSize; 86 | // pages don't start from index 0 87 | page++; 88 | var uBound = page * pageSize; 89 | var lBound = uBound - pageSize; 90 | // check if the results requested are available and the EHistory has not finished 91 | // if not request from EHistory 92 | var ret = results.slice(lBound, lBound + pageSize); 93 | if (results.length < uBound && !finished){ 94 | if (page === prevPage) 95 | return ret; 96 | prevPage = page; 97 | // TODO combine into one function call 98 | historyView.disableControls(); 99 | historyView.displayThrobber(); 100 | EHistory.getPage(page,$.proxy(this.append, this)); 101 | return -1; 102 | } 103 | // TODO: Verify the following 104 | // if only some of the results found then make others know this is the last page. 105 | if (ret.length < pageSize) { 106 | $(this).trigger('lastPage'); 107 | } 108 | return ret; 109 | }, 110 | // called to make sure that a current item in a current day is to be deleted 111 | // @arg id: item id 112 | // @arg day: the day where the item was selected 113 | // @arg elem: the DOM elem, row corresponding to the item. 114 | // TODO: Is elem necessary? 115 | select: function (id, day, elem) { 116 | if (!selected[day]) selected[day] = []; 117 | this.item_map.elem = elem; 118 | selected[day].push(this.item_map[id]); 119 | }, 120 | // remove selection of a specific item from a specific day 121 | // @arg id: item id 122 | // @arg day: the day where the item supposed to be. 123 | unselect: function (id, day) { 124 | if (!selected[day]) return; 125 | selected[day].splice(selected[day].indexOf(this.item_map[id]), 1); 126 | }, 127 | //check to see if a specific item in a specific day is selected. 128 | isSelected: function (id, day) { 129 | if (!selected[day]) return; 130 | return selected[day].indexOf(this.item_map[id]) > -1 131 | }, 132 | // Deletes all selected items 133 | // loops over all items in all days and make a call to the EHistory 134 | // for each item to be deleted 135 | removeSelected: function () { 136 | EHistory.deleteUrls(selected, function () { 137 | //TODO: Don't reload 138 | window.location.reload(); 139 | }); 140 | }, 141 | 142 | clearHistory: function () { 143 | chrome.history.deleteAll(function () { 144 | window.location.reload(); 145 | }); 146 | }, 147 | 148 | clearResults: function () { 149 | EHistory.deleteAllresults(function () { 150 | alert('The page will reload now\nIt may take Chrome several minutes before making the history available again.'); 151 | window.location.reload(); 152 | }); 153 | }, 154 | // Clears the current model state 155 | // usually called when a new search is taking place. 156 | clear: function(){ 157 | results = []; 158 | finished = false; 159 | selected = {}; 160 | this.item_map = {}; 161 | prevPage = -1; 162 | }, 163 | 164 | getDomain: function (id) { 165 | var item = this.item_map[id]; 166 | if (item) { 167 | return Util.getHostname(item.url); 168 | } 169 | }, 170 | 171 | deleteItem: function (id) { 172 | var item = this.item_map[id]; 173 | if (item) { 174 | chrome.history.deleteUrl({url: item.url}); 175 | } 176 | } 177 | }; 178 | // an event listener for when the EHistory has got all its results 179 | $(EHistory).bind('finished', function () { 180 | finished = true; 181 | }); 182 | // Instantiate the history model 183 | var historyModel = new HistoryModel(); 184 | return historyModel; 185 | })(); 186 | 187 | }).call(this); 188 | -------------------------------------------------------------------------------- /lib/ehistory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * eHistory Chrome Extension 3 | * https://chrome.google.com/webstore/detail/hiiknjobjfknoghbeelhfilaaikffopb 4 | * 5 | * Copyright 2011, Amjad Masad 6 | * Licensed under the MIT license 7 | * https://github.com/amasad/eHistory/blob/master/LICENSE.txt 8 | * 9 | * Date: Mon May 9 10 | */ 11 | //EHistory container 12 | var EHistory = (function ($) { 13 | //CONSTANTS 14 | var MAX = 2147483647; 15 | //constructor 16 | function EHistory() {/*fdsfsd*/} 17 | // Extend date class to get some nice features 18 | (function () { 19 | // milliseconds in one day 20 | var msDay = 24 * 60 * 60 * 1000; 21 | 22 | Date.prototype.start = function () { 23 | return new Date(this.toDateString()); 24 | }; 25 | 26 | Date.prototype.next = function () { 27 | return this.start().getTime() + msDay; 28 | }; 29 | 30 | Date.prototype.prev = function () { 31 | return this.start().getTime() - msDay; 32 | }; 33 | }()); 34 | 35 | var arrayUnique = function (arr) { 36 | var ids = {}; 37 | for (var i=0; i < arr.length; i++) { 38 | if (ids[arr[i].id]) { 39 | arr.splice(i--, 1); 40 | } else { 41 | ids[arr[i].id] = true; 42 | } 43 | } 44 | return arr; 45 | } 46 | 47 | // simple memoization 48 | var memoizeKeys = (function () { 49 | var lastKeys; 50 | var lastObj; 51 | return function (obj) { 52 | if (lastObj === obj) { 53 | return lastKeys; 54 | } else { 55 | lastObj = obj; 56 | return (lastKeys = Object.keys(obj)); 57 | } 58 | } 59 | })(); 60 | 61 | //methods 62 | EHistory.prototype = { 63 | 64 | 65 | search: function (settings, filters, cb) { 66 | this.offset = 0; 67 | this.filters = filters; 68 | this.visits_day = new VisitsByDay(); 69 | this.pageSize = settings.maxResults; 70 | this.cb = cb; 71 | this.query = settings.text; 72 | this.settings = { 73 | text: "", 74 | startTime: 0, 75 | endTime: Date.now(), 76 | maxResults : 150 77 | }; 78 | $.extend(this.settings, settings); 79 | this.getPage(1); 80 | }, 81 | 82 | 83 | getPage: function (pageNo, cb, callback) { 84 | this.cb = cb || this.cb; 85 | var settings = this.settings, 86 | filtered = [], 87 | ids = {}, 88 | that = this, 89 | filter = Object.keys(this.filters).length; 90 | 91 | settings.maxResults = this.offset + this._pageLimit(pageNo); 92 | 93 | function search () { 94 | chrome.history.search(settings, function (result) { 95 | var resultItem; 96 | result = result.slice(settings.maxResults - that.pageSize, settings.maxResults); 97 | for (var i=0; i < result.length && filtered.length < that.pageSize; i++) { 98 | resultItem = result[i]; 99 | if (ids[resultItem.id] || (filter && !that.filter(resultItem))) continue; 100 | ids[resultItem.id] = true; 101 | filtered.push(resultItem); 102 | } 103 | if (result.length && filtered.length < that.pageSize) { 104 | settings.maxResults += that.pageSize; 105 | that.offset = settings.maxResults - that._pageLimit(pageNo) - i; 106 | search(); 107 | } else if (filtered.length) { 108 | if (filtered.length < that.pageSize) $(that).trigger("finished"); 109 | if (callback) callback(filtered); 110 | else that.getVisits(filtered); 111 | } else { 112 | $(that).trigger("finished"); 113 | $(that).trigger("done"); 114 | } 115 | }); 116 | } 117 | search(); 118 | }, 119 | 120 | _pageLimit: function (pageNo) { 121 | return pageNo * this.pageSize; 122 | }, 123 | 124 | getVisits: function (items) { 125 | var visits = [], 126 | that = this, 127 | items_length = items.length, 128 | days = [], 129 | visits_day = this.visits_day, 130 | visitItem, day; 131 | 132 | for (var i = 0; i < items_length; i++) { 133 | chrome.history.getVisits({url: items[i].url}, function (res_visits) { 134 | items_length--; 135 | for(var j = 0; j < res_visits.length; j++) { 136 | visitItem = res_visits[j]; 137 | if (visitItem.visitTime > that.settings.endTime || 138 | visitItem.visitTime < that.settings.startTime) continue; 139 | visitItem.day = day = new Date(visitItem.visitTime).start().getTime(); 140 | visits_day.insert(visitItem); 141 | } 142 | 143 | if (items_length === 0) { 144 | that.cb({ 145 | items: items, 146 | visits: visits_day.sort().dequeue(that.pageSize) 147 | }); 148 | $(that).trigger("done"); 149 | } 150 | }); 151 | } 152 | }, 153 | 154 | deleteUrls: function (urlsByDay, callback) { 155 | var days = memoizeKeys(urlsByDay), 156 | count = 0; 157 | 158 | (function deleteUrls() { 159 | if (count === days.length) return callback(); 160 | 161 | var urls = urlsByDay[days[count++]]; 162 | for (var i = 0; i < urls.length; i++) 163 | chrome.history.deleteUrl({url: urls[i].url}); 164 | setTimeout(deleteUrls, 100); 165 | })(); 166 | }, 167 | 168 | deleteAllresults: function (callback) { 169 | var settings = $.extend(null, this.settings), 170 | finished = false, 171 | that = this; 172 | 173 | settings.maxResults = MAX; 174 | this.offset = 0; 175 | function finish () { 176 | finished = true; 177 | $(that).unbind("finished", finish); 178 | callback(); 179 | } 180 | $(this).bind("finished", finish); 181 | (function deleteAll () { 182 | if (finished) return; 183 | that.getPage(1, undefined, function (page) { 184 | for (var i = 0; i < page.length; i++) { 185 | chrome.history.deleteUrl({url: page[i].url}); 186 | } 187 | deleteAll(); 188 | }); 189 | })(); 190 | 191 | }, 192 | /* 193 | deleteUrlOnDay: function (url, day, callback) { 194 | var nextDay = new Date(parseFloat(day)).next(); 195 | var toDelete = []; 196 | var that = this; 197 | chrome.history.getVisits({url:url}, function (visits) { 198 | var visitTime; 199 | for (var i=0, visit; visit = visits[i]; i++) { 200 | visitTime = visit.visitTime; 201 | if (visitTime >= day && visitTime <= nextDay) { 202 | toDelete.push(visitTime); 203 | } 204 | } 205 | that.removeVisits(toDelete, callback); 206 | }); 207 | }, 208 | 209 | removeVisits: function (visitTimes, callback) { 210 | var length = visitTimes.length; 211 | for (var i = 0, visitTime; visitTime = visitTimes[i]; i++) { 212 | chrome.history.deleteRange({ 213 | startTime: visitTime - 0.1, 214 | endTime: visitTime + 0.1 215 | }, function () { 216 | length--; 217 | if (length === 0) { 218 | callback(); 219 | } 220 | }); 221 | } 222 | },*/ 223 | 224 | filter: function (item) { 225 | var operators = memoizeKeys(this.filters); 226 | if (!operators.length) return true; 227 | for (var i=0; i < operators.length; i++) { 228 | var res = Filters[operators[i]](item, this.filters[operators[i]]); 229 | if (!res) return false; 230 | } 231 | return true; 232 | } 233 | }; 234 | 235 | var Filters = (function () { 236 | function isIn(prop) { 237 | return function (item, str) { 238 | return item[prop].toLowerCase().indexOf(str.toLowerCase()) > -1; 239 | } 240 | }; 241 | 242 | return { 243 | intitle: isIn('title'), 244 | inurl: isIn('url'), 245 | site: function (item, str) { 246 | // amjad.a.com a.com 247 | var hostDomains = Util.getHostname(item.url).split('.'), 248 | // handle extra dots (e.g. .jo.). 249 | siteDomains = str.replace(/^\.*|\.*$/g, '').split('.'); 250 | for (var i = siteDomains.length - 1, j = hostDomains.length - 1; j >= 0 && i >= 0; i--, j--) { 251 | if (hostDomains[j] !== siteDomains[i]) return false; 252 | } 253 | return true; 254 | } 255 | }; 256 | })(); 257 | 258 | return new EHistory; 259 | })(jQuery); -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.1.2 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /** 8 | * Correct `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | main, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9. 28 | */ 29 | 30 | audio, 31 | canvas, 32 | video { 33 | display: inline-block; 34 | } 35 | 36 | /** 37 | * Prevent modern browsers from displaying `audio` without controls. 38 | * Remove excess height in iOS 5 devices. 39 | */ 40 | 41 | audio:not([controls]) { 42 | display: none; 43 | height: 0; 44 | } 45 | 46 | /** 47 | * Address styling not present in IE 8/9. 48 | */ 49 | 50 | [hidden] { 51 | display: none; 52 | } 53 | 54 | /* ========================================================================== 55 | Base 56 | ========================================================================== */ 57 | 58 | /** 59 | * 1. Set default font family to sans-serif. 60 | * 2. Prevent iOS text size adjust after orientation change, without disabling 61 | * user zoom. 62 | */ 63 | 64 | html { 65 | font-family: sans-serif; /* 1 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | -webkit-text-size-adjust: 100%; /* 2 */ 68 | } 69 | 70 | /** 71 | * Remove default margin. 72 | */ 73 | 74 | body { 75 | margin: 0; 76 | } 77 | 78 | /* ========================================================================== 79 | Links 80 | ========================================================================== */ 81 | 82 | /** 83 | * Address `outline` inconsistency between Chrome and other browsers. 84 | */ 85 | 86 | a:focus { 87 | outline: thin dotted; 88 | } 89 | 90 | /** 91 | * Improve readability when focused and also mouse hovered in all browsers. 92 | */ 93 | 94 | a:active, 95 | a:hover { 96 | outline: 0; 97 | } 98 | 99 | /* ========================================================================== 100 | Typography 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address variable `h1` font-size and margin within `section` and `article` 105 | * contexts in Firefox 4+, Safari 5, and Chrome. 106 | */ 107 | 108 | h1 { 109 | font-size: 2em; 110 | margin: 0.67em 0; 111 | } 112 | 113 | /** 114 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 115 | */ 116 | 117 | abbr[title] { 118 | border-bottom: 1px dotted; 119 | } 120 | 121 | /** 122 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: bold; 128 | } 129 | 130 | /** 131 | * Address styling not present in Safari 5 and Chrome. 132 | */ 133 | 134 | dfn { 135 | font-style: italic; 136 | } 137 | 138 | /** 139 | * Address differences between Firefox and other browsers. 140 | */ 141 | 142 | hr { 143 | -moz-box-sizing: content-box; 144 | box-sizing: content-box; 145 | height: 0; 146 | } 147 | 148 | /** 149 | * Address styling not present in IE 8/9. 150 | */ 151 | 152 | mark { 153 | background: #ff0; 154 | color: #000; 155 | } 156 | 157 | /** 158 | * Correct font family set oddly in Safari 5 and Chrome. 159 | */ 160 | 161 | code, 162 | kbd, 163 | pre, 164 | samp { 165 | font-family: monospace, serif; 166 | font-size: 1em; 167 | } 168 | 169 | /** 170 | * Improve readability of pre-formatted text in all browsers. 171 | */ 172 | 173 | pre { 174 | white-space: pre-wrap; 175 | } 176 | 177 | /** 178 | * Set consistent quote types. 179 | */ 180 | 181 | q { 182 | quotes: "\201C" "\201D" "\2018" "\2019"; 183 | } 184 | 185 | /** 186 | * Address inconsistent and variable font size in all browsers. 187 | */ 188 | 189 | small { 190 | font-size: 80%; 191 | } 192 | 193 | /** 194 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 195 | */ 196 | 197 | sub, 198 | sup { 199 | font-size: 75%; 200 | line-height: 0; 201 | position: relative; 202 | vertical-align: baseline; 203 | } 204 | 205 | sup { 206 | top: -0.5em; 207 | } 208 | 209 | sub { 210 | bottom: -0.25em; 211 | } 212 | 213 | /* ========================================================================== 214 | Embedded content 215 | ========================================================================== */ 216 | 217 | /** 218 | * Remove border when inside `a` element in IE 8/9. 219 | */ 220 | 221 | img { 222 | border: 0; 223 | } 224 | 225 | /** 226 | * Correct overflow displayed oddly in IE 9. 227 | */ 228 | 229 | svg:not(:root) { 230 | overflow: hidden; 231 | } 232 | 233 | /* ========================================================================== 234 | Figures 235 | ========================================================================== */ 236 | 237 | /** 238 | * Address margin not present in IE 8/9 and Safari 5. 239 | */ 240 | 241 | figure { 242 | margin: 0; 243 | } 244 | 245 | /* ========================================================================== 246 | Forms 247 | ========================================================================== */ 248 | 249 | /** 250 | * Define consistent border, margin, and padding. 251 | */ 252 | 253 | fieldset { 254 | border: 1px solid #c0c0c0; 255 | margin: 0 2px; 256 | padding: 0.35em 0.625em 0.75em; 257 | } 258 | 259 | /** 260 | * 1. Correct `color` not being inherited in IE 8/9. 261 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 262 | */ 263 | 264 | legend { 265 | border: 0; /* 1 */ 266 | padding: 0; /* 2 */ 267 | } 268 | 269 | /** 270 | * 1. Correct font family not being inherited in all browsers. 271 | * 2. Correct font size not being inherited in all browsers. 272 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 273 | */ 274 | 275 | button, 276 | input, 277 | select, 278 | textarea { 279 | font-family: inherit; /* 1 */ 280 | font-size: 100%; /* 2 */ 281 | margin: 0; /* 3 */ 282 | } 283 | 284 | /** 285 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 286 | * the UA stylesheet. 287 | */ 288 | 289 | button, 290 | input { 291 | line-height: normal; 292 | } 293 | 294 | /** 295 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 296 | * All other form control elements do not inherit `text-transform` values. 297 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. 298 | * Correct `select` style inheritance in Firefox 4+ and Opera. 299 | */ 300 | 301 | button, 302 | select { 303 | text-transform: none; 304 | } 305 | 306 | /** 307 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 308 | * and `video` controls. 309 | * 2. Correct inability to style clickable `input` types in iOS. 310 | * 3. Improve usability and consistency of cursor style between image-type 311 | * `input` and others. 312 | */ 313 | 314 | button, 315 | html input[type="button"], /* 1 */ 316 | input[type="reset"], 317 | input[type="submit"] { 318 | -webkit-appearance: button; /* 2 */ 319 | cursor: pointer; /* 3 */ 320 | } 321 | 322 | /** 323 | * Re-set default cursor for disabled elements. 324 | */ 325 | 326 | button[disabled], 327 | html input[disabled] { 328 | cursor: default; 329 | } 330 | 331 | /** 332 | * 1. Address box sizing set to `content-box` in IE 8/9. 333 | * 2. Remove excess padding in IE 8/9. 334 | */ 335 | 336 | input[type="checkbox"], 337 | input[type="radio"] { 338 | box-sizing: border-box; /* 1 */ 339 | padding: 0; /* 2 */ 340 | } 341 | 342 | /** 343 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 344 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 345 | * (include `-moz` to future-proof). 346 | */ 347 | 348 | input[type="search"] { 349 | -webkit-appearance: textfield; /* 1 */ 350 | -moz-box-sizing: content-box; 351 | -webkit-box-sizing: content-box; /* 2 */ 352 | box-sizing: content-box; 353 | } 354 | 355 | /** 356 | * Remove inner padding and search cancel button in Safari 5 and Chrome 357 | * on OS X. 358 | */ 359 | 360 | input[type="search"]::-webkit-search-cancel-button, 361 | input[type="search"]::-webkit-search-decoration { 362 | -webkit-appearance: none; 363 | } 364 | 365 | /** 366 | * Remove inner padding and border in Firefox 4+. 367 | */ 368 | 369 | button::-moz-focus-inner, 370 | input::-moz-focus-inner { 371 | border: 0; 372 | padding: 0; 373 | } 374 | 375 | /** 376 | * 1. Remove default vertical scrollbar in IE 8/9. 377 | * 2. Improve readability and alignment in all browsers. 378 | */ 379 | 380 | textarea { 381 | overflow: auto; /* 1 */ 382 | vertical-align: top; /* 2 */ 383 | } 384 | 385 | /* ========================================================================== 386 | Tables 387 | ========================================================================== */ 388 | 389 | /** 390 | * Remove most spacing between table cells. 391 | */ 392 | 393 | table { 394 | border-collapse: collapse; 395 | border-spacing: 0; 396 | } -------------------------------------------------------------------------------- /lib/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * eHistory Chrome Extension 3 | * https://chrome.google.com/webstore/detail/hiiknjobjfknoghbeelhfilaaikffopb 4 | * 5 | * Copyright 2011, Amjad Masad 6 | * Licensed under the MIT license 7 | * https://github.com/amasad/eHistory/blob/master/LICENSE.txt 8 | * 9 | */ 10 | (function(){ 11 | /* global parseQuery, historyModel, historyView, EHistory, confirm */ 12 | 'use strict'; 13 | /* parseForm: Parses the html form into text format 14 | * @arg (jQueryObject) $form: jQuery object containing the form element 15 | * @returns (String) text query equivalent to the form 16 | * intitle:title inurl:url site:site startTime:startime endTime:endtime searchquery 17 | */ 18 | function parseForm ($form) { 19 | var query = '', 20 | text = ''; 21 | // loop over all input elements 22 | $form.find('input').each(function (i, elem) { 23 | elem = $(elem); 24 | if (elem.attr('id') === 'pure-text') { 25 | // just text 26 | text += elem.val(); 27 | } else { 28 | // filter/setting 29 | query += elem.val() ? ' ' + elem.data('settings-item') + ':' + elem.val() : ''; 30 | } 31 | }); 32 | // return filter/setting text format key:value followed by regular text 33 | return $.trim(query + ' ' + text); 34 | } 35 | 36 | // Check version number 37 | $(function() { 38 | $.getJSON('manifest.json', function (manifest) { 39 | var version = manifest.version; 40 | if (localStorage['version'] !== version) { 41 | localStorage.clear(); 42 | localStorage['version'] = version; 43 | /* global console */ 44 | console.log('Version Updated!'); 45 | } 46 | }); 47 | }); 48 | 49 | /*************** Controller ***************/ 50 | /* Collection of functions and event handlers 51 | * Interacts with UI, historyModel and historyView 52 | */ 53 | $(function(){ 54 | // DOM ready 55 | // search box 56 | var $query = $('#query'); 57 | // advanced search form 58 | var $pnlAdvanced = $('.advanced-search'); 59 | // history items table 60 | var $resultsTable = $('#tbl-main'); 61 | 62 | $('.open-advanced').click(function () { 63 | var config = parseQuery($query.val()); 64 | var operators = $.extend(config.settings, config.filters); 65 | $pnlAdvanced.find('input').each(function (i, elem) { 66 | elem = $(elem); 67 | if (elem.attr('id') === 'pure-text'){ 68 | elem.val(config[2]); 69 | } else { 70 | // in the elements data contains type of settings/filter 71 | elem.val(operators[elem.data('settings-item')] || ''); 72 | } 73 | }); 74 | }); 75 | 76 | // results day headers check-boxs handler 77 | $resultsTable.delegate('.chk-day', 'change', function () { 78 | // check all results until the next day header 79 | $(this).parents('tr').nextUntil('.hdr-day') 80 | .children(':nth-child(1)').children() 81 | .attr('checked', $(this).is(':checked')).trigger('change'); 82 | }); 83 | 84 | var shiftDown = false; 85 | $(document).bind('keydown keyup', function (e) { 86 | shiftDown = e.shiftKey; 87 | }); 88 | 89 | $resultsTable.delegate('.chk-entry', 'click', function () { 90 | if (shiftDown) { 91 | shiftClick($(this)); 92 | } 93 | }); 94 | 95 | var shiftClick = (function () { 96 | 97 | function getPath($firstChecked, dir, isChecked) { 98 | var $path = $(); 99 | while ($firstChecked.length) { 100 | $firstChecked = $firstChecked[dir](); 101 | if (!$firstChecked.is('.entry')) { 102 | continue; 103 | } else if ((isChecked && $firstChecked.find(':checked').length) || 104 | (!isChecked && !$firstChecked.find(':checked').length)) { 105 | break; 106 | } else { 107 | $path = $path.add($firstChecked); 108 | } 109 | } 110 | 111 | // If we reached the end and we're looking for checked boxes then 112 | // we haven't found any, however, if you we reached the end while 113 | // looking for checked boxes, we may have something. 114 | if ($firstChecked.length || !isChecked) { 115 | return $path; 116 | } else { 117 | return $(); 118 | } 119 | } 120 | 121 | return function ($input) { 122 | var $row = $input.parents('tr'); 123 | var isChecked = $input.is(':checked'); 124 | // Go up until we find the first checked input 125 | var $path = getPath($row, 'prev', isChecked); 126 | // If we couldn't find anything going up then go down. 127 | if (!$path.length) { 128 | $path = getPath($row, 'next', isChecked); 129 | } 130 | $path.each(function (i, row) { 131 | if ($(row).is('.entry')) { 132 | $(row).find('input[type=checkbox]').attr('checked', isChecked); 133 | } 134 | }); 135 | }; 136 | 137 | })(); 138 | 139 | // result item checkbox handler 140 | $resultsTable.delegate('.chk-entry', 'change', function () { 141 | var val = $(this).attr('checked'), 142 | $row = $(this).parents('tr'), 143 | // decides what function to call, select/unselect 144 | fn = val ? $.proxy(historyModel.select, historyModel) : $.proxy(historyModel.unselect, historyModel); 145 | fn($row.data('id'),$row.data('day')); 146 | }); 147 | 148 | $(document).delegate('.hdr-day, .entry', 'click', function (e) { 149 | if ($(e.target).is('input') || $(e.target).is('a')) return; 150 | var $input = $(this).find('input[type=checkbox]'); 151 | $input 152 | .attr('checked', !$input.is(':checked')) 153 | .trigger('change'); 154 | if (shiftDown) { 155 | shiftClick($input); 156 | } 157 | }); 158 | 159 | // Update the main search box whenever advanced settings are changed. 160 | var updateMainSearchBox = function () { 161 | // Delay until the keypress is handled by the browser. 162 | setTimeout(function() { 163 | $query.val(parseForm($pnlAdvanced) || ''); 164 | }, 0); 165 | }; 166 | 167 | $('input', $pnlAdvanced).change(updateMainSearchBox) 168 | .keypress(updateMainSearchBox) 169 | .keydown(updateMainSearchBox); 170 | 171 | $('body').bind('click', function (e) { 172 | if ($(e.srcElement).parents('.advanced-search').length) return; 173 | $('.open-advanced').show(); 174 | $('.advanced-search').hide(); 175 | }); 176 | $('.open-advanced').click(function () { 177 | $('.advanced-search').show(); 178 | $('.open-advanced').hide(); 179 | return false; 180 | }); 181 | 182 | // called to initiate the search 183 | function search(config) { 184 | var settings = config.settings, 185 | filters = config.filters; 186 | 187 | historyView.displayThrobber(); 188 | 189 | EHistory.search({ 190 | text: settings.text || '', 191 | startTime: new Date(settings.startTime || 0).getTime() , 192 | endTime: new Date(settings.endTime || Date.now()).getTime(), 193 | maxResults: historyModel.pageSize 194 | }, filters, function(results){ 195 | historyModel.append(results); 196 | }); 197 | 198 | // If the user isn't searching then 'delete resutls' is the same 199 | // as 'clear history'. 200 | if (!settings.text) { 201 | $('.delete-menu .results').hide(); 202 | } else { 203 | $('.delete-menu .results').show(); 204 | } 205 | } 206 | 207 | // form submit handler 208 | $('#frm-search').submit(function (e) { 209 | var text; 210 | e.preventDefault(); 211 | //clear everything 212 | historyModel.clear(); 213 | historyView.clear(); 214 | historyView.disableControls(); 215 | 216 | if ($pnlAdvanced.is(':visible')){ 217 | text = parseForm($pnlAdvanced); 218 | search(parseQuery(text)); 219 | } else { 220 | search(parseQuery($query.val())); 221 | } 222 | //return false; 223 | }); 224 | 225 | $('#btn-clear-history').click(function () { 226 | if (confirm('Delete all items from history?')) { 227 | historyModel.clearHistory(); 228 | } 229 | }); 230 | 231 | $('.delete.dropdown').click(function () { 232 | $(this).toggleClass('open'); 233 | $(this).next().toggle($(this).is('.open')); 234 | }); 235 | 236 | $('body').click(function (e) { 237 | if (!$(e.target).is('.delete.dropdown') 238 | && !$(e.target).closest('.delete.dropdown').length) { 239 | $('.delete-menu').hide(); 240 | $('.delete.dropdown').removeClass('open'); 241 | } 242 | }); 243 | 244 | $('.delete-menu .selected').click(function () { 245 | if (confirm('Delete selected items?')) { 246 | historyModel.removeSelected(); 247 | } 248 | }); 249 | 250 | $('.delete-menu .results').click(function () { 251 | if (confirm('Delete all search results?')) { 252 | historyModel.clearResults(); 253 | } 254 | }); 255 | 256 | $('.query').focus(function () { 257 | $('.query-wrapper').addClass('active'); 258 | }); 259 | $('.query').blur(function () { 260 | $('.query-wrapper').removeClass('active'); 261 | }); 262 | 263 | $resultsTable.delegate('a', 'click', function (e) { 264 | if ($(this).attr('href').match(/^file/)) { 265 | alert( 266 | 'For security concerns we cannot open local files. ' + 267 | 'You have to manually open the link by right clicking ' + 268 | 'on it and selecting "Open Link in New Tab"' 269 | ); 270 | } 271 | }); 272 | 273 | var $menu = $('.options-menu'); 274 | $menu.delegate('button', 'click', function () { 275 | var $row = $menu.data('row'); 276 | if ($row) { 277 | if ($(this).is('.delete')) { 278 | historyModel.deleteItem($row.data('id')); 279 | $row.fadeOut('fast', function () { 280 | $row.remove(); 281 | }); 282 | } else if ($(this).is('.more')) { 283 | $query.val('site:' + historyModel.getDomain($row.data('id'))); 284 | $('#frm-search').submit(); 285 | } 286 | } 287 | }); 288 | 289 | $resultsTable.delegate('.entry .options', 'click', function () { 290 | var $this = $(this); 291 | if (!$this.is('.open')) { 292 | var pos = $(this).offset(); 293 | $('.options-menu') 294 | .css({ 295 | left: pos.left, 296 | top: pos.top + $this.outerHeight() 297 | }) 298 | .show() 299 | .data('row', $this.closest('tr')); 300 | $('.entry .options.open').removeClass('open'); 301 | $this.addClass('open'); 302 | $('body').bind('click.menu', function () { 303 | $menu.hide().data('id', null); 304 | $('body').unbind('.menu'); 305 | $this.removeClass('open'); 306 | }); 307 | } else { 308 | $menu.hide().data('id', null); 309 | $(this).removeClass('open'); 310 | $menu.hide(); 311 | } 312 | return false; 313 | }); 314 | 315 | // Focus query box by default. 316 | $query.focus(); 317 | }); 318 | 319 | $(function () { 320 | 321 | $(window).resize(function () { 322 | $('#div-main').css('height', $(window).height() - 85); 323 | }).resize(); 324 | $('#frm-search').submit(); 325 | }); 326 | 327 | })(); 328 | -------------------------------------------------------------------------------- /vendor/spin.js: -------------------------------------------------------------------------------- 1 | //fgnass.github.com/spin.js#v1.3.2 2 | 3 | /** 4 | * Copyright (c) 2011-2013 Felix Gnass 5 | * Licensed under the MIT license 6 | */ 7 | (function(root, factory) { 8 | 9 | /* CommonJS */ 10 | if (typeof exports == 'object') module.exports = factory() 11 | 12 | /* AMD module */ 13 | else if (typeof define == 'function' && define.amd) define(factory) 14 | 15 | /* Browser global */ 16 | else root.Spinner = factory() 17 | } 18 | (this, function() { 19 | "use strict"; 20 | 21 | var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */ 22 | , animations = {} /* Animation rules keyed by their name */ 23 | , useCssAnimations /* Whether to use CSS animations or setTimeout */ 24 | 25 | /** 26 | * Utility function to create elements. If no tag name is given, 27 | * a DIV is created. Optionally properties can be passed. 28 | */ 29 | function createEl(tag, prop) { 30 | var el = document.createElement(tag || 'div') 31 | , n 32 | 33 | for(n in prop) el[n] = prop[n] 34 | return el 35 | } 36 | 37 | /** 38 | * Appends children and returns the parent. 39 | */ 40 | function ins(parent /* child1, child2, ...*/) { 41 | for (var i=1, n=arguments.length; i> 1) : parseInt(o.left, 10) + mid) + 'px', 194 | top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px' 195 | }) 196 | } 197 | 198 | el.setAttribute('role', 'progressbar') 199 | self.lines(el, self.opts) 200 | 201 | if (!useCssAnimations) { 202 | // No CSS animation support, use setTimeout() instead 203 | var i = 0 204 | , start = (o.lines - 1) * (1 - o.direction) / 2 205 | , alpha 206 | , fps = o.fps 207 | , f = fps/o.speed 208 | , ostep = (1-o.opacity) / (f*o.trail / 100) 209 | , astep = f/o.lines 210 | 211 | ;(function anim() { 212 | i++; 213 | for (var j = 0; j < o.lines; j++) { 214 | alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity) 215 | 216 | self.opacity(el, j * o.direction + start, alpha, o) 217 | } 218 | self.timeout = self.el && setTimeout(anim, ~~(1000/fps)) 219 | })() 220 | } 221 | return self 222 | }, 223 | 224 | /** 225 | * Stops and removes the Spinner. 226 | */ 227 | stop: function() { 228 | var el = this.el 229 | if (el) { 230 | clearTimeout(this.timeout) 231 | if (el.parentNode) el.parentNode.removeChild(el) 232 | this.el = undefined 233 | } 234 | return this 235 | }, 236 | 237 | /** 238 | * Internal method that draws the individual lines. Will be overwritten 239 | * in VML fallback mode below. 240 | */ 241 | lines: function(el, o) { 242 | var i = 0 243 | , start = (o.lines - 1) * (1 - o.direction) / 2 244 | , seg 245 | 246 | function fill(color, shadow) { 247 | return css(createEl(), { 248 | position: 'absolute', 249 | width: (o.length+o.width) + 'px', 250 | height: o.width + 'px', 251 | background: color, 252 | boxShadow: shadow, 253 | transformOrigin: 'left', 254 | transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)', 255 | borderRadius: (o.corners * o.width>>1) + 'px' 256 | }) 257 | } 258 | 259 | for (; i < o.lines; i++) { 260 | seg = css(createEl(), { 261 | position: 'absolute', 262 | top: 1+~(o.width/2) + 'px', 263 | transform: o.hwaccel ? 'translate3d(0,0,0)' : '', 264 | opacity: o.opacity, 265 | animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1/o.speed + 's linear infinite' 266 | }) 267 | 268 | if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'})) 269 | ins(el, ins(seg, fill(getColor(o.color, i), '0 0 1px rgba(0,0,0,.1)'))) 270 | } 271 | return el 272 | }, 273 | 274 | /** 275 | * Internal method that adjusts the opacity of a single line. 276 | * Will be overwritten in VML fallback mode below. 277 | */ 278 | opacity: function(el, i, val) { 279 | if (i < el.childNodes.length) el.childNodes[i].style.opacity = val 280 | } 281 | 282 | }) 283 | 284 | 285 | function initVML() { 286 | 287 | /* Utility function to create a VML tag */ 288 | function vml(tag, attr) { 289 | return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr) 290 | } 291 | 292 | // No CSS transforms but VML support, add a CSS rule for VML elements: 293 | sheet.addRule('.spin-vml', 'behavior:url(#default#VML)') 294 | 295 | Spinner.prototype.lines = function(el, o) { 296 | var r = o.length+o.width 297 | , s = 2*r 298 | 299 | function grp() { 300 | return css( 301 | vml('group', { 302 | coordsize: s + ' ' + s, 303 | coordorigin: -r + ' ' + -r 304 | }), 305 | { width: s, height: s } 306 | ) 307 | } 308 | 309 | var margin = -(o.width+o.length)*2 + 'px' 310 | , g = css(grp(), {position: 'absolute', top: margin, left: margin}) 311 | , i 312 | 313 | function seg(i, dx, filter) { 314 | ins(g, 315 | ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}), 316 | ins(css(vml('roundrect', {arcsize: o.corners}), { 317 | width: r, 318 | height: o.width, 319 | left: o.radius, 320 | top: -o.width>>1, 321 | filter: filter 322 | }), 323 | vml('fill', {color: getColor(o.color, i), opacity: o.opacity}), 324 | vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change 325 | ) 326 | ) 327 | ) 328 | } 329 | 330 | if (o.shadow) 331 | for (i = 1; i <= o.lines; i++) 332 | seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)') 333 | 334 | for (i = 1; i <= o.lines; i++) seg(i) 335 | return ins(el, g) 336 | } 337 | 338 | Spinner.prototype.opacity = function(el, i, val, o) { 339 | var c = el.firstChild 340 | o = o.shadow && o.lines || 0 341 | if (c && i+o < c.childNodes.length) { 342 | c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild 343 | if (c) c.opacity = val 344 | } 345 | } 346 | } 347 | 348 | var probe = css(createEl('group'), {behavior: 'url(#default#VML)'}) 349 | 350 | if (!vendor(probe, 'transform') && probe.adj) initVML() 351 | else useCssAnimations = vendor(probe, 'animation') 352 | 353 | return Spinner 354 | 355 | })); 356 | -------------------------------------------------------------------------------- /css/history.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 62.5%; 3 | } 4 | body { 5 | background-color: white; 6 | color: black; 7 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 8 | font-size: 1.5rem; 9 | height: 100%; 10 | overflow: hidden; 11 | -webkit-user-select: none; 12 | } 13 | 14 | * { box-sizing: border-box; } 15 | 16 | header { 17 | background: #eee; 18 | padding: 22px 20px 0px 20px; 19 | height: 85px; 20 | box-shadow: 0 2px 4px rgba(0,0,0,0.2); 21 | } 22 | 23 | .logo { 24 | -webkit-transform: scale(0.7885); 25 | margin-top: -10px; 26 | } 27 | 28 | .bttn { 29 | height: 40px; 30 | padding: 10px 20px; 31 | font-size: 1em; 32 | color: white; 33 | border: 1px solid; 34 | border-radius: 3px; 35 | } 36 | 37 | .bttn:active { 38 | background-color: #eee; 39 | opacity: 1; 40 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 41 | } 42 | .bttn.search { 43 | background: #3261c3; 44 | border: 1px solid #3b2eca; 45 | } 46 | 47 | .query-wrapper { 48 | background: #fff; 49 | display: inline-block; 50 | padding: 7px; 51 | border: 1px solid #ceced6; 52 | width: 400px; 53 | font-size: 1.13em; 54 | height: 40px; 55 | position: relative; 56 | transition: border 0.25s linear; 57 | } 58 | .query-wrapper.active { 59 | border: 1px solid #7590f5!important; 60 | outline:none; 61 | } 62 | 63 | .query { 64 | border: 0; 65 | width: -webkit-calc(100% - 16px); 66 | width: calc(100% - 16px); 67 | outline: none; 68 | } 69 | 70 | .open-advanced { 71 | width: 16px; 72 | height: 16px; 73 | position: absolute; 74 | right: 5px; 75 | top: 12px; 76 | background: url(arrow_down.png); 77 | cursor: pointer; 78 | opacity: 0.2; 79 | transition: opacity 0.25s linear; 80 | } 81 | 82 | .query-wrapper:hover { 83 | border: 1px solid rgb(147, 147, 148); 84 | } 85 | .open-advanced:hover { 86 | opacity: 1; 87 | } 88 | 89 | .advanced-search { 90 | background: #fff; 91 | width: 400px; 92 | color: grey; 93 | padding: 10px; 94 | border: 1px solid #ceced6; 95 | box-shadow: 0 2px 4px rgba(0,0,0,0.2); 96 | position: absolute; 97 | top: 60px; 98 | display: none; 99 | z-index: 1; 100 | } 101 | .advanced-search fieldset { 102 | border: none; 103 | } 104 | .advanced-search label { 105 | width: 50px; 106 | display: inline-block; 107 | } 108 | .advanced-search input { 109 | border: 1px solid #ceced6; 110 | width: calc(100% - 50px); 111 | height: 30px; 112 | } 113 | .advanced-search input:focus { 114 | border:1px solid #4d90fe!important; 115 | outline: none; 116 | } 117 | 118 | @media all and (max-width:805px) { 119 | .query-wrapper, .advanced-search { 120 | width: 300px; 121 | } 122 | } 123 | 124 | @media all and (max-width: 710px) { 125 | .query-wrapper, .advanced-search { 126 | width: 230px; 127 | } 128 | } 129 | 130 | @media all and (max-width:632px) { 131 | .top-bar { 132 | display: none; 133 | } 134 | } 135 | 136 | @media all and (max-width:460px) { 137 | .query-wrapper, .advanced-search { 138 | width: 150px; 139 | } 140 | } 141 | @media all and (max-width:365) { 142 | .query-wrapper, .advanced-search { 143 | width: 80px; 144 | } 145 | } 146 | section.content { 147 | position: relative; 148 | } 149 | 150 | header .logo { 151 | float: left; 152 | } 153 | 154 | .top-bar { 155 | margin: 0; 156 | height: 38px; 157 | padding-left: 30px; 158 | float: left; 159 | position: relative; 160 | width: 200px; 161 | } 162 | 163 | .top-bar > * { 164 | float:right; 165 | } 166 | 167 | #div-main { 168 | height: 500px; 169 | overflow-y: scroll; 170 | width: 100%; 171 | clear: both; 172 | } 173 | 174 | .page-buttons { 175 | width: 100px; 176 | height: 32px; 177 | float: right; 178 | font-weight: bold; 179 | margin-right: 5px; 180 | margin-top: 5px; 181 | display: -webkit-flex; 182 | } 183 | 184 | .prev-page,.next-page { 185 | width: 32px; 186 | height: 32px; 187 | display: inline-block; 188 | border: 1px solid #ceced6; 189 | cursor: pointer; 190 | transition: border 0.25s linear; 191 | padding: 0; 192 | position: relative; 193 | opacity: 0.9; 194 | } 195 | 196 | .bttn { 197 | background: #F3F3F3; 198 | } 199 | 200 | .bttn[disabled], .bttn[disabled]:hover { 201 | opacity: 0.4; 202 | } 203 | 204 | .bttn:focus { 205 | outline: none; 206 | -webkit-transition: border-color 200ms; 207 | border-color: rgb(77, 144, 254); 208 | } 209 | 210 | .prev-page .img, .next-page .img { 211 | position: absolute; 212 | top:0; 213 | left:0; 214 | right:0; 215 | bottom:0; 216 | transition: opacity 0.25s linear; 217 | } 218 | .prev-page .img { 219 | background: url(prev.png); 220 | } 221 | .next-page .img { 222 | background: url(next.png) 223 | } 224 | .prev-page:hover .img, .next-page:hover .img { 225 | opacity: 1; 226 | } 227 | .prev-page:hover:not([disabled]),.next-page:hover:not([disabled]) { 228 | border: 1px solid black; 229 | } 230 | .page-no { 231 | display: inline-block; 232 | height: 32px; 233 | width: 26px; 234 | padding: 0; 235 | margin: 0; 236 | display: -webkit-flex; 237 | -webkit-justify-content: center; 238 | -webkit-align-items: center; 239 | } 240 | 241 | #tbl-main { 242 | width: 100%; 243 | } 244 | .hdr-day { 245 | width: 100%; 246 | height: 35px; 247 | border-bottom: 1px #e5e5e5 solid; 248 | background-color: rgb(248, 248, 248); 249 | } 250 | .hdr-day td { 251 | margin: 0; 252 | padding: 0; 253 | border-spacing: 0; 254 | } 255 | .hdr-day .date { 256 | font-size: 1.2em; 257 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 258 | font-weight: 300; 259 | -webkit-user-select: initial; 260 | } 261 | 262 | .hdr-day, .entry { 263 | cursor: pointer; 264 | } 265 | 266 | #tbl-main td { 267 | padding: 8px 10px; 268 | } 269 | 270 | .entry .time { 271 | color: #888; 272 | min-width: 56px; 273 | white-space: nowrap; 274 | } 275 | 276 | .entry { 277 | border-bottom: 1px #e5e5e5 solid; 278 | } 279 | 280 | .entry:hover { 281 | background-color: rgb(252, 252, 252) 282 | } 283 | 284 | #tbl-main input[type="checkbox"]{ 285 | -webkit-appearance: none; 286 | height: 13px; 287 | width: 13px; 288 | border: 1px solid rgba(0, 0, 0, 0.25); 289 | border-radius: 2px; 290 | color: #444; 291 | text-shadow: 0 1px 0 rgb(240, 240, 240); 292 | } 293 | #tbl-main input[type="checkbox"]:checked { 294 | background-image:-webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAcklEQVQY02NgwA/YoJgoEA/Es4DYgJBCJSBeD8SboRinBiYg7kZS2IosyQ/Eakh8LySFq4FYHFlxGRBvBOJYqMRqJMU+yApNkSRAeC0Sux3dfSCTetE0wKyXxOWhMKhTYIr9CAUXyJMzgLgBagBBgDPGAI2LGdNt0T1AAAAAAElFTkSuQmCC') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAA60lEQVQ4y2P4//8/Ay0wA00NpgC4AXEksgA1DNYA4vVAvBnZcEoNFgLihVBDNyMbTonBLEDchWYo3HBKDM7BYSgIZ5JrsBseQxuBmIkcg5EjCx1PBmJufGHsBMQFQMxORGTB8BIglsCXKmSBeA1U8RQgliMistYBsTq+dAxy4TQ0TSBLnPFE1iYgdkAPL3SDC/BECi4cjS0i0A12RgoGYnAxEDMSYzAIyGMJDmy4B4jZcCUdXKmCnUCwzAVifnxpklA6xhY0K4BYgVBiJyaDgIJmKtTQjUBsTEwuIjbnwYLGi9jsSZ8aZEgZDACI3DQYaeTU0AAAAABJRU5ErkJggg==') 2x); 295 | } 296 | #tbl-main input[type="checkbox"]:focus { 297 | -webkit-transition: border-color 200ms; 298 | border-color: rgb(77, 144, 254); 299 | outline: none; 300 | } 301 | 302 | .entry a { 303 | background-position: 0px 1px; 304 | background-repeat: no-repeat; 305 | background-size: 16px; 306 | box-sizing: border-box; 307 | display: inline-block; 308 | max-width: 500px; 309 | overflow: hidden; 310 | padding: 1px 0px 4px 22px; 311 | text-overflow: ellipsis; 312 | white-space:nowrap; 313 | color: rgb(48, 57, 66); 314 | text-decoration: none; 315 | margin-right: 12px; 316 | -webkit-user-select: initial; 317 | } 318 | 319 | .entry a:hover { 320 | text-decoration: underline; 321 | } 322 | 323 | .entry .domain { 324 | color: rgb(151, 156, 160); 325 | font-size: 13px; 326 | -webkit-user-select: initial; 327 | } 328 | 329 | .dwn-arrow { 330 | width: 16px; 331 | height: 16px; 332 | display: inline-block; 333 | background: url(arrow_down.png); 334 | -webkit-transform: scale(0.6); 335 | margin-left: -1px; 336 | margin-top: -1px; 337 | } 338 | 339 | .entry .options { 340 | border: 1px solid rgb(192, 195, 198); 341 | opacity: 0.5; 342 | border-radius: 3px; 343 | align-items: flex-start; 344 | text-align: center; 345 | cursor: default; 346 | width: 16px; 347 | height: 16px; 348 | display: inline-block; 349 | margin-left: 10px; 350 | } 351 | 352 | .entry .options:hover { 353 | opacity: 1; 354 | } 355 | 356 | .entry .options.open { 357 | background: black; 358 | opacity: 1; 359 | } 360 | 361 | .entry .options.open .dwn-arrow { 362 | -webkit-filter: invert(100%); 363 | } 364 | 365 | .entry .item-content { 366 | display: flex; 367 | align-items: center; 368 | } 369 | 370 | menu { 371 | background: white; 372 | display: none; 373 | -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, .50); 374 | background: white; 375 | border-radius: 2px; 376 | color: black; 377 | cursor: default; 378 | margin: 0; 379 | outline: 1px solid rgba(0, 0, 0, 0.2); 380 | padding: 8px 0; 381 | position: fixed; 382 | white-space: nowrap; 383 | z-index: 99; 384 | line-height: 29px; 385 | } 386 | 387 | menu button { 388 | line-height: 29px; 389 | box-sizing: border-box; 390 | display: block; 391 | margin: 0; 392 | text-align: start; 393 | width: 100%; 394 | -webkit-appearance: none; 395 | background: transparent; 396 | border: 0; 397 | color: black; 398 | font: inherit; 399 | font-size: 13px; 400 | outline: none; 401 | overflow: hidden; 402 | padding: 0 19px; 403 | text-overflow: ellipsis; 404 | } 405 | 406 | menu button:hover { 407 | background: #d84938; 408 | color: white; 409 | } 410 | 411 | .bttn.delete { 412 | color: black; 413 | height: 32px; 414 | font-size: 0.9em; 415 | padding: 0 10px; 416 | margin-right: 5px; 417 | border: 1px solid #ceced6; 418 | transition: border 0.25s linear; 419 | margin-top: 5px; 420 | padding-bottom: 2px; 421 | } 422 | 423 | .bttn.delete:hover { 424 | border: 1px solid black; 425 | } 426 | 427 | .no-results td { 428 | padding: 50px; 429 | } 430 | 431 | #tbl-main input[type=checkbox] { 432 | margin-left: 10px; 433 | } 434 | 435 | #frm-search { 436 | -webkit-margin-start: 12px; 437 | float: left; 438 | } 439 | 440 | .feedback { 441 | position: fixed; 442 | bottom: 0; 443 | right: 30px; 444 | background: #eee; 445 | padding: 12px; 446 | border-radius: 4px; 447 | } 448 | 449 | #throbber { 450 | position: absolute; 451 | left: 0; 452 | right: 0; 453 | top: 0; 454 | bottom: 0; 455 | z-index: 2; 456 | background: #eee; 457 | display: none; 458 | } 459 | 460 | #throbber.active { 461 | display: block; 462 | } 463 | 464 | .dropdown.delete { 465 | width: 51px; 466 | opacity: 0.9; 467 | transition: opacity 0.25s linear; 468 | padding: 0; 469 | display: flex; 470 | align-items: center; 471 | vertical-align: middle; 472 | cursor: inherit; 473 | } 474 | 475 | .dropdown.delete:hover { 476 | opacity: 1; 477 | } 478 | 479 | .dropdown.delete:focus { 480 | outline: none; 481 | } 482 | 483 | .dropdown.delete .trash { 484 | background-image: url(trash.png); 485 | background-repeat: no-repeat; 486 | width: 32px; 487 | height: 32px; 488 | display: inline-block; 489 | /* Laziness */ 490 | -webkit-transform: scale(0.7); 491 | } 492 | 493 | .dropdown.delete .dwn-arrow { 494 | margin-top: 7px; 495 | align-items: flex-start; 496 | text-align: center; 497 | margin-left: -4px; 498 | } 499 | 500 | 501 | .dropdown.delete.open { 502 | background-color: #eee; 503 | opacity: 1; 504 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); 505 | } 506 | 507 | .delete-menu { 508 | display: none; 509 | position: absolute; 510 | right: 6px; 511 | top: 38px; 512 | } 513 | -------------------------------------------------------------------------------- /vendor/mustache.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mustache.js - Logic-less {{mustache}} templates with JavaScript 3 | * http://github.com/janl/mustache.js 4 | */ 5 | 6 | /*global define: false*/ 7 | 8 | (function (root, factory) { 9 | if (typeof exports === "object" && exports) { 10 | factory(exports); // CommonJS 11 | } else { 12 | var mustache = {}; 13 | factory(mustache); 14 | if (typeof define === "function" && define.amd) { 15 | define(mustache); // AMD 16 | } else { 17 | root.Mustache = mustache; //