├── .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 |
50 |
58 |
59 |
60 |
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; //