├── README.md ├── chrome ├── background.js ├── github-time-travel.js ├── manifest.json └── pikaday │ ├── pikaday.css │ └── pikaday.js └── firefox ├── data ├── github-time-travel.js └── pikaday │ ├── pikaday.css │ └── pikaday.js ├── index.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Github Time Travel 4 | 5 | ![](http://i.imgur.com/nL9FpdH.png) 6 | 7 | Browser extension for browsing commits by date on Github. Only works for public repos. 8 | 9 | - Firefox: https://addons.mozilla.org/en-US/firefox/addon/github-time-travel/ 10 | - Chrome: https://chrome.google.com/webstore/detail/github-time-travel/gdaikofkgknonbdhgomechckcjlcpgpg 11 | -------------------------------------------------------------------------------- /chrome/background.js: -------------------------------------------------------------------------------- 1 | 2 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 3 | var re = /.*github\.com\/(.*)\/(.*)\/commits.*/, 4 | m = re.exec(changeInfo.url); 5 | 6 | if (m === null) return; 7 | 8 | chrome.tabs.executeScript({ 9 | code: 'setDomChangeTimeout(attachDateButton, "commit-group-title", 2000);' 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /chrome/github-time-travel.js: -------------------------------------------------------------------------------- 1 | 2 | function setDomChangeTimeout (callback, klass, timeout) { 3 | // A function like setTimeout that watches a DOM element (by class name) 4 | // for change. The callback is called as soon as the DOM element 5 | // changes. If it doesn't change, the callback is called at timeout. 6 | 7 | // This is the fastest way to get a callback when the new DOM settles. 8 | // Right now, it naively uses innerHTML, but for this use, it's good enough. 9 | 10 | var start = performance.now(), 11 | startContent = '', 12 | startElements = document.getElementsByClassName(klass); 13 | 14 | if (startElements.length > 0) { 15 | startContent = startElements[0].innerHTML; 16 | } 17 | 18 | var interval = setInterval(function () { 19 | if (performance.now() - start > timeout) { 20 | clearInterval(interval); 21 | callback(); 22 | } else { 23 | var endElements = document.getElementsByClassName(klass); 24 | 25 | if (endElements.length > 0) { 26 | if (endElements[0].innerHTML !== startContent) { 27 | clearInterval(interval); 28 | callback(); 29 | } 30 | } 31 | } 32 | }, 50); 33 | } 34 | 35 | function attachDateButton () { 36 | // Extract the Github username and repo from a commit log URL. 37 | // If the DOM element does not already exist, create an element to hold 38 | // the datepicker and append it to the .file-navigation DOM element 39 | // after styling it like a Github button. 40 | 41 | // Then, create the datepicker with a callback to make an API call on select. 42 | // Attach the listener to the API call to handle the response. 43 | 44 | var re = /.*github\.com\/(.*)\/(.*)\/commits.*/, 45 | m = re.exec(document.location.href); 46 | 47 | if (m === null) return; 48 | if (document.getElementsByClassName('datepicker-button').length > 0) return; 49 | 50 | var username = m[1], 51 | repo = m[2], 52 | api = 'https://api.github.com/repos/' + username + '/' + repo + '/commits', 53 | date = getCommitDate(), 54 | el = document.createElement('span'); 55 | 56 | el.className = 'btn btn-sm select-menu-button datepicker-button'; 57 | el.innerHTML = 'Date: ' + date + ' '; 58 | 59 | document.getElementsByClassName('file-navigation')[0].appendChild(el); 60 | 61 | new Pikaday({ 62 | field: el, 63 | defaultDate: new Date(date), 64 | setDefaultDate: true, 65 | onSelect: function (date) { 66 | var req = new XMLHttpRequest(), 67 | url = api + '?per_page=1&until=' + date.toISOString().split('T')[0]; 68 | 69 | req.addEventListener('load', reqListener); 70 | req.open('get', url, true); 71 | req.send(); 72 | } 73 | }); 74 | } 75 | 76 | function getCommitDate () { 77 | // Find first .commit-group-title in page, extract the date from the text. 78 | // Return 'unknown' if no commit group titles are found. 79 | 80 | var commitGroups = document.getElementsByClassName('commit-group-title'); 81 | 82 | if (commitGroups.length > 0) { 83 | var re = /Commits on (.*)/, 84 | m = re.exec(commitGroups[0].innerHTML), 85 | date = m[1]; 86 | 87 | return date; 88 | } else { 89 | return 'unknown'; 90 | } 91 | } 92 | 93 | function reqListener () { 94 | // Listen for a response from the Github Commits API 95 | // https://developer.github.com/v3/repos/commits/ 96 | // Parse the JSON response and extract the HTML link from the first commit 97 | // if commits were returned. 98 | // Change 'commit' to 'commits' in the URL to browse the commit log. 99 | 100 | var parsed = JSON.parse(this.responseText); 101 | 102 | if (parsed.length > 0) { 103 | var tree_url = parsed[0].html_url.replace('/commit/', '/commits/'); 104 | document.location.href = tree_url; 105 | } 106 | } 107 | 108 | attachDateButton(); 109 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Github Time Travel", 4 | "version": "1.0.1", 5 | "description": "Browse commits by date on Github", 6 | "author": "Nathan Cahill", 7 | "permissions": ["https://*.github.com/", "tabs"], 8 | "background": { 9 | "persistent": false, 10 | "scripts": ["background.js"] 11 | }, 12 | "content_scripts": [{ 13 | "matches": ["https://*.github.com/*/*"], 14 | "css": ["pikaday/pikaday.css"], 15 | "js": ["pikaday/pikaday.js", "github-time-travel.js"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /chrome/pikaday/pikaday.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /*! 4 | * Pikaday 5 | * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/ 6 | */ 7 | 8 | .pika-single { 9 | z-index: 9999; 10 | display: block; 11 | position: relative; 12 | color: #333; 13 | background: #fff; 14 | border: 1px solid #ccc; 15 | border-bottom-color: #bbb; 16 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 17 | } 18 | 19 | /* 20 | clear child float (pika-lendar), using the famous micro clearfix hack 21 | http://nicolasgallagher.com/micro-clearfix-hack/ 22 | */ 23 | .pika-single:before, 24 | .pika-single:after { 25 | content: " "; 26 | display: table; 27 | } 28 | .pika-single:after { clear: both } 29 | .pika-single { *zoom: 1 } 30 | 31 | .pika-single.is-hidden { 32 | display: none; 33 | } 34 | 35 | .pika-single.is-bound { 36 | position: absolute; 37 | box-shadow: 0 5px 15px -5px rgba(0,0,0,.5); 38 | } 39 | 40 | .pika-lendar { 41 | float: left; 42 | width: 240px; 43 | margin: 8px; 44 | } 45 | 46 | .pika-title { 47 | position: relative; 48 | text-align: center; 49 | } 50 | 51 | .pika-label { 52 | display: inline-block; 53 | *display: inline; 54 | position: relative; 55 | z-index: 9999; 56 | overflow: hidden; 57 | margin: 0; 58 | padding: 5px 3px; 59 | font-size: 14px; 60 | line-height: 20px; 61 | font-weight: bold; 62 | background-color: #fff; 63 | } 64 | .pika-title select { 65 | cursor: pointer; 66 | position: absolute; 67 | z-index: 9998; 68 | margin: 0; 69 | left: 0; 70 | top: 5px; 71 | filter: alpha(opacity=0); 72 | opacity: 0; 73 | } 74 | 75 | .pika-prev, 76 | .pika-next { 77 | display: block; 78 | cursor: pointer; 79 | position: relative; 80 | outline: none; 81 | border: 0; 82 | padding: 0; 83 | width: 20px; 84 | height: 30px; 85 | /* hide text using text-indent trick, using width value (it's enough) */ 86 | text-indent: 20px; 87 | white-space: nowrap; 88 | overflow: hidden; 89 | background-color: transparent; 90 | background-position: center center; 91 | background-repeat: no-repeat; 92 | background-size: 75% 75%; 93 | opacity: .5; 94 | *position: absolute; 95 | *top: 0; 96 | } 97 | 98 | .pika-prev:hover, 99 | .pika-next:hover { 100 | opacity: 1; 101 | } 102 | 103 | .pika-prev, 104 | .is-rtl .pika-next { 105 | float: left; 106 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=='); 107 | *left: 0; 108 | } 109 | 110 | .pika-next, 111 | .is-rtl .pika-prev { 112 | float: right; 113 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII='); 114 | *right: 0; 115 | } 116 | 117 | .pika-prev.is-disabled, 118 | .pika-next.is-disabled { 119 | cursor: default; 120 | opacity: .2; 121 | } 122 | 123 | .pika-select { 124 | display: inline-block; 125 | *display: inline; 126 | } 127 | 128 | .pika-table { 129 | width: 100%; 130 | border-collapse: collapse; 131 | border-spacing: 0; 132 | border: 0; 133 | } 134 | 135 | .pika-table th, 136 | .pika-table td { 137 | width: 14.285714285714286%; 138 | padding: 0; 139 | } 140 | 141 | .pika-table th { 142 | color: #999; 143 | font-size: 12px; 144 | line-height: 25px; 145 | font-weight: bold; 146 | text-align: center; 147 | } 148 | 149 | .pika-button { 150 | cursor: pointer; 151 | display: block; 152 | box-sizing: border-box; 153 | -moz-box-sizing: border-box; 154 | outline: none; 155 | border: 0; 156 | margin: 0; 157 | width: 100%; 158 | padding: 5px; 159 | color: #666; 160 | font-size: 12px; 161 | line-height: 15px; 162 | text-align: right; 163 | background: #f5f5f5; 164 | } 165 | 166 | .pika-week { 167 | font-size: 11px; 168 | color: #999; 169 | } 170 | 171 | .is-today .pika-button { 172 | color: #33aaff; 173 | font-weight: bold; 174 | } 175 | 176 | .is-selected .pika-button { 177 | color: #fff; 178 | font-weight: bold; 179 | background: #33aaff; 180 | box-shadow: inset 0 1px 3px #178fe5; 181 | border-radius: 3px; 182 | } 183 | 184 | .is-inrange .pika-button { 185 | background: #D5E9F7; 186 | } 187 | 188 | .is-startrange .pika-button { 189 | color: #fff; 190 | background: #6CB31D; 191 | box-shadow: none; 192 | border-radius: 3px; 193 | } 194 | 195 | .is-endrange .pika-button { 196 | color: #fff; 197 | background: #33aaff; 198 | box-shadow: none; 199 | border-radius: 3px; 200 | } 201 | 202 | .is-disabled .pika-button { 203 | pointer-events: none; 204 | cursor: default; 205 | color: #999; 206 | opacity: .3; 207 | } 208 | 209 | .pika-button:hover { 210 | color: #fff; 211 | background: #ff8000; 212 | box-shadow: none; 213 | border-radius: 3px; 214 | } 215 | 216 | /* styling for abbr */ 217 | .pika-table abbr { 218 | border-bottom: none; 219 | cursor: help; 220 | } 221 | 222 | -------------------------------------------------------------------------------- /chrome/pikaday/pikaday.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Pikaday 3 | * 4 | * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday 5 | */ 6 | 7 | (function (root, factory) 8 | { 9 | 'use strict'; 10 | 11 | var moment; 12 | if (typeof exports === 'object') { 13 | // CommonJS module 14 | // Load moment.js as an optional dependency 15 | try { moment = require('moment'); } catch (e) {} 16 | module.exports = factory(moment); 17 | } else if (typeof define === 'function' && define.amd) { 18 | // AMD. Register as an anonymous module. 19 | define(function (req) 20 | { 21 | // Load moment.js as an optional dependency 22 | var id = 'moment'; 23 | try { moment = req(id); } catch (e) {} 24 | return factory(moment); 25 | }); 26 | } else { 27 | root.Pikaday = factory(root.moment); 28 | } 29 | }(this, function (moment) 30 | { 31 | 'use strict'; 32 | 33 | /** 34 | * feature detection and helper functions 35 | */ 36 | var hasMoment = typeof moment === 'function', 37 | 38 | hasEventListeners = !!window.addEventListener, 39 | 40 | document = window.document, 41 | 42 | sto = window.setTimeout, 43 | 44 | addEvent = function(el, e, callback, capture) 45 | { 46 | if (hasEventListeners) { 47 | el.addEventListener(e, callback, !!capture); 48 | } else { 49 | el.attachEvent('on' + e, callback); 50 | } 51 | }, 52 | 53 | removeEvent = function(el, e, callback, capture) 54 | { 55 | if (hasEventListeners) { 56 | el.removeEventListener(e, callback, !!capture); 57 | } else { 58 | el.detachEvent('on' + e, callback); 59 | } 60 | }, 61 | 62 | fireEvent = function(el, eventName, data) 63 | { 64 | var ev; 65 | 66 | if (document.createEvent) { 67 | ev = document.createEvent('HTMLEvents'); 68 | ev.initEvent(eventName, true, false); 69 | ev = extend(ev, data); 70 | el.dispatchEvent(ev); 71 | } else if (document.createEventObject) { 72 | ev = document.createEventObject(); 73 | ev = extend(ev, data); 74 | el.fireEvent('on' + eventName, ev); 75 | } 76 | }, 77 | 78 | trim = function(str) 79 | { 80 | return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,''); 81 | }, 82 | 83 | hasClass = function(el, cn) 84 | { 85 | return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1; 86 | }, 87 | 88 | addClass = function(el, cn) 89 | { 90 | if (!hasClass(el, cn)) { 91 | el.className = (el.className === '') ? cn : el.className + ' ' + cn; 92 | } 93 | }, 94 | 95 | removeClass = function(el, cn) 96 | { 97 | el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' ')); 98 | }, 99 | 100 | isArray = function(obj) 101 | { 102 | return (/Array/).test(Object.prototype.toString.call(obj)); 103 | }, 104 | 105 | isDate = function(obj) 106 | { 107 | return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); 108 | }, 109 | 110 | isWeekend = function(date) 111 | { 112 | var day = date.getDay(); 113 | return day === 0 || day === 6; 114 | }, 115 | 116 | isLeapYear = function(year) 117 | { 118 | // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951 119 | return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; 120 | }, 121 | 122 | getDaysInMonth = function(year, month) 123 | { 124 | return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 125 | }, 126 | 127 | setToStartOfDay = function(date) 128 | { 129 | if (isDate(date)) date.setHours(0,0,0,0); 130 | }, 131 | 132 | compareDates = function(a,b) 133 | { 134 | // weak date comparison (use setToStartOfDay(date) to ensure correct result) 135 | return a.getTime() === b.getTime(); 136 | }, 137 | 138 | extend = function(to, from, overwrite) 139 | { 140 | var prop, hasProp; 141 | for (prop in from) { 142 | hasProp = to[prop] !== undefined; 143 | if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) { 144 | if (isDate(from[prop])) { 145 | if (overwrite) { 146 | to[prop] = new Date(from[prop].getTime()); 147 | } 148 | } 149 | else if (isArray(from[prop])) { 150 | if (overwrite) { 151 | to[prop] = from[prop].slice(0); 152 | } 153 | } else { 154 | to[prop] = extend({}, from[prop], overwrite); 155 | } 156 | } else if (overwrite || !hasProp) { 157 | to[prop] = from[prop]; 158 | } 159 | } 160 | return to; 161 | }, 162 | 163 | adjustCalendar = function(calendar) { 164 | if (calendar.month < 0) { 165 | calendar.year -= Math.ceil(Math.abs(calendar.month)/12); 166 | calendar.month += 12; 167 | } 168 | if (calendar.month > 11) { 169 | calendar.year += Math.floor(Math.abs(calendar.month)/12); 170 | calendar.month -= 12; 171 | } 172 | return calendar; 173 | }, 174 | 175 | /** 176 | * defaults and localisation 177 | */ 178 | defaults = { 179 | 180 | // bind the picker to a form field 181 | field: null, 182 | 183 | // automatically show/hide the picker on `field` focus (default `true` if `field` is set) 184 | bound: undefined, 185 | 186 | // position of the datepicker, relative to the field (default to bottom & left) 187 | // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position) 188 | position: 'bottom left', 189 | 190 | // automatically fit in the viewport even if it means repositioning from the position option 191 | reposition: true, 192 | 193 | // the default output format for `.toString()` and `field` value 194 | format: 'YYYY-MM-DD', 195 | 196 | // the initial date to view when first opened 197 | defaultDate: null, 198 | 199 | // make the `defaultDate` the initial selected value 200 | setDefaultDate: false, 201 | 202 | // first day of week (0: Sunday, 1: Monday etc) 203 | firstDay: 0, 204 | 205 | // the minimum/earliest date that can be selected 206 | minDate: null, 207 | // the maximum/latest date that can be selected 208 | maxDate: null, 209 | 210 | // number of years either side, or array of upper/lower range 211 | yearRange: 10, 212 | 213 | // show week numbers at head of row 214 | showWeekNumber: false, 215 | 216 | // used internally (don't config outside) 217 | minYear: 0, 218 | maxYear: 9999, 219 | minMonth: undefined, 220 | maxMonth: undefined, 221 | 222 | startRange: null, 223 | endRange: null, 224 | 225 | isRTL: false, 226 | 227 | // Additional text to append to the year in the calendar title 228 | yearSuffix: '', 229 | 230 | // Render the month after year in the calendar title 231 | showMonthAfterYear: false, 232 | 233 | // how many months are visible 234 | numberOfMonths: 1, 235 | 236 | // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`) 237 | // only used for the first display or when a selected date is not visible 238 | mainCalendar: 'left', 239 | 240 | // Specify a DOM element to render the calendar in 241 | container: undefined, 242 | 243 | // internationalization 244 | i18n: { 245 | previousMonth : 'Previous Month', 246 | nextMonth : 'Next Month', 247 | months : ['January','February','March','April','May','June','July','August','September','October','November','December'], 248 | weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], 249 | weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] 250 | }, 251 | 252 | // Theme Classname 253 | theme: null, 254 | 255 | // callback function 256 | onSelect: null, 257 | onOpen: null, 258 | onClose: null, 259 | onDraw: null 260 | }, 261 | 262 | 263 | /** 264 | * templating functions to abstract HTML rendering 265 | */ 266 | renderDayName = function(opts, day, abbr) 267 | { 268 | day += opts.firstDay; 269 | while (day >= 7) { 270 | day -= 7; 271 | } 272 | return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day]; 273 | }, 274 | 275 | renderDay = function(opts) 276 | { 277 | if (opts.isEmpty) { 278 | return ''; 279 | } 280 | var arr = []; 281 | if (opts.isDisabled) { 282 | arr.push('is-disabled'); 283 | } 284 | if (opts.isToday) { 285 | arr.push('is-today'); 286 | } 287 | if (opts.isSelected) { 288 | arr.push('is-selected'); 289 | } 290 | if (opts.isInRange) { 291 | arr.push('is-inrange'); 292 | } 293 | if (opts.isStartRange) { 294 | arr.push('is-startrange'); 295 | } 296 | if (opts.isEndRange) { 297 | arr.push('is-endrange'); 298 | } 299 | return '' + 300 | '' + 304 | ''; 305 | }, 306 | 307 | renderWeek = function (d, m, y) { 308 | // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified. 309 | var onejan = new Date(y, 0, 1), 310 | weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7); 311 | return '' + weekNum + ''; 312 | }, 313 | 314 | renderRow = function(days, isRTL) 315 | { 316 | return '' + (isRTL ? days.reverse() : days).join('') + ''; 317 | }, 318 | 319 | renderBody = function(rows) 320 | { 321 | return '' + rows.join('') + ''; 322 | }, 323 | 324 | renderHead = function(opts) 325 | { 326 | var i, arr = []; 327 | if (opts.showWeekNumber) { 328 | arr.push(''); 329 | } 330 | for (i = 0; i < 7; i++) { 331 | arr.push('' + renderDayName(opts, i, true) + ''); 332 | } 333 | return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; 334 | }, 335 | 336 | renderTitle = function(instance, c, year, month, refYear) 337 | { 338 | var i, j, arr, 339 | opts = instance._o, 340 | isMinYear = year === opts.minYear, 341 | isMaxYear = year === opts.maxYear, 342 | html = '
', 343 | monthHtml, 344 | yearHtml, 345 | prev = true, 346 | next = true; 347 | 348 | for (arr = [], i = 0; i < 12; i++) { 349 | arr.push(''); 353 | } 354 | monthHtml = '
' + opts.i18n.months[month] + '
'; 355 | 356 | if (isArray(opts.yearRange)) { 357 | i = opts.yearRange[0]; 358 | j = opts.yearRange[1] + 1; 359 | } else { 360 | i = year - opts.yearRange; 361 | j = 1 + year + opts.yearRange; 362 | } 363 | 364 | for (arr = []; i < j && i <= opts.maxYear; i++) { 365 | if (i >= opts.minYear) { 366 | arr.push(''); 367 | } 368 | } 369 | yearHtml = '
' + year + opts.yearSuffix + '
'; 370 | 371 | if (opts.showMonthAfterYear) { 372 | html += yearHtml + monthHtml; 373 | } else { 374 | html += monthHtml + yearHtml; 375 | } 376 | 377 | if (isMinYear && (month === 0 || opts.minMonth >= month)) { 378 | prev = false; 379 | } 380 | 381 | if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { 382 | next = false; 383 | } 384 | 385 | if (c === 0) { 386 | html += ''; 387 | } 388 | if (c === (instance._o.numberOfMonths - 1) ) { 389 | html += ''; 390 | } 391 | 392 | return html += '
'; 393 | }, 394 | 395 | renderTable = function(opts, data) 396 | { 397 | return '' + renderHead(opts) + renderBody(data) + '
'; 398 | }, 399 | 400 | 401 | /** 402 | * Pikaday constructor 403 | */ 404 | Pikaday = function(options) 405 | { 406 | var self = this, 407 | opts = self.config(options); 408 | 409 | self._onMouseDown = function(e) 410 | { 411 | if (!self._v) { 412 | return; 413 | } 414 | e = e || window.event; 415 | var target = e.target || e.srcElement; 416 | if (!target) { 417 | return; 418 | } 419 | 420 | if (!hasClass(target.parentNode, 'is-disabled')) { 421 | if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty')) { 422 | self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day'))); 423 | if (opts.bound) { 424 | sto(function() { 425 | self.hide(); 426 | if (opts.field) { 427 | opts.field.blur(); 428 | } 429 | }, 100); 430 | } 431 | return; 432 | } 433 | else if (hasClass(target, 'pika-prev')) { 434 | self.prevMonth(); 435 | } 436 | else if (hasClass(target, 'pika-next')) { 437 | self.nextMonth(); 438 | } 439 | } 440 | if (!hasClass(target, 'pika-select')) { 441 | if (e.preventDefault) { 442 | e.preventDefault(); 443 | } else { 444 | e.returnValue = false; 445 | return false; 446 | } 447 | } else { 448 | self._c = true; 449 | } 450 | }; 451 | 452 | self._onChange = function(e) 453 | { 454 | e = e || window.event; 455 | var target = e.target || e.srcElement; 456 | if (!target) { 457 | return; 458 | } 459 | if (hasClass(target, 'pika-select-month')) { 460 | self.gotoMonth(target.value); 461 | } 462 | else if (hasClass(target, 'pika-select-year')) { 463 | self.gotoYear(target.value); 464 | } 465 | }; 466 | 467 | self._onInputChange = function(e) 468 | { 469 | var date; 470 | 471 | if (e.firedBy === self) { 472 | return; 473 | } 474 | if (hasMoment) { 475 | date = moment(opts.field.value, opts.format); 476 | date = (date && date.isValid()) ? date.toDate() : null; 477 | } 478 | else { 479 | date = new Date(Date.parse(opts.field.value)); 480 | } 481 | if (isDate(date)) { 482 | self.setDate(date); 483 | } 484 | if (!self._v) { 485 | self.show(); 486 | } 487 | }; 488 | 489 | self._onInputFocus = function() 490 | { 491 | self.show(); 492 | }; 493 | 494 | self._onInputClick = function() 495 | { 496 | self.show(); 497 | }; 498 | 499 | self._onInputBlur = function() 500 | { 501 | // IE allows pika div to gain focus; catch blur the input field 502 | var pEl = document.activeElement; 503 | do { 504 | if (hasClass(pEl, 'pika-single')) { 505 | return; 506 | } 507 | } 508 | while ((pEl = pEl.parentNode)); 509 | 510 | if (!self._c) { 511 | self._b = sto(function() { 512 | self.hide(); 513 | }, 50); 514 | } 515 | self._c = false; 516 | }; 517 | 518 | self._onClick = function(e) 519 | { 520 | e = e || window.event; 521 | var target = e.target || e.srcElement, 522 | pEl = target; 523 | if (!target) { 524 | return; 525 | } 526 | if (!hasEventListeners && hasClass(target, 'pika-select')) { 527 | if (!target.onchange) { 528 | target.setAttribute('onchange', 'return;'); 529 | addEvent(target, 'change', self._onChange); 530 | } 531 | } 532 | do { 533 | if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) { 534 | return; 535 | } 536 | } 537 | while ((pEl = pEl.parentNode)); 538 | if (self._v && target !== opts.trigger && pEl !== opts.trigger) { 539 | self.hide(); 540 | } 541 | }; 542 | 543 | self.el = document.createElement('div'); 544 | self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : ''); 545 | 546 | addEvent(self.el, 'ontouchend' in document ? 'touchend' : 'mousedown', self._onMouseDown, true); 547 | addEvent(self.el, 'change', self._onChange); 548 | 549 | if (opts.field) { 550 | if (opts.container) { 551 | opts.container.appendChild(self.el); 552 | } else if (opts.bound) { 553 | document.body.appendChild(self.el); 554 | } else { 555 | opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling); 556 | } 557 | addEvent(opts.field, 'change', self._onInputChange); 558 | 559 | if (!opts.defaultDate) { 560 | if (hasMoment && opts.field.value) { 561 | opts.defaultDate = moment(opts.field.value, opts.format).toDate(); 562 | } else { 563 | opts.defaultDate = new Date(Date.parse(opts.field.value)); 564 | } 565 | opts.setDefaultDate = true; 566 | } 567 | } 568 | 569 | var defDate = opts.defaultDate; 570 | 571 | if (isDate(defDate)) { 572 | if (opts.setDefaultDate) { 573 | self.setDate(defDate, true); 574 | } else { 575 | self.gotoDate(defDate); 576 | } 577 | } else { 578 | self.gotoDate(new Date()); 579 | } 580 | 581 | if (opts.bound) { 582 | this.hide(); 583 | self.el.className += ' is-bound'; 584 | addEvent(opts.trigger, 'click', self._onInputClick); 585 | addEvent(opts.trigger, 'focus', self._onInputFocus); 586 | addEvent(opts.trigger, 'blur', self._onInputBlur); 587 | } else { 588 | this.show(); 589 | } 590 | }; 591 | 592 | 593 | /** 594 | * public Pikaday API 595 | */ 596 | Pikaday.prototype = { 597 | 598 | 599 | /** 600 | * configure functionality 601 | */ 602 | config: function(options) 603 | { 604 | if (!this._o) { 605 | this._o = extend({}, defaults, true); 606 | } 607 | 608 | var opts = extend(this._o, options, true); 609 | 610 | opts.isRTL = !!opts.isRTL; 611 | 612 | opts.field = (opts.field && opts.field.nodeName) ? opts.field : null; 613 | 614 | opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null; 615 | 616 | opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field); 617 | 618 | opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field; 619 | 620 | opts.disableWeekends = !!opts.disableWeekends; 621 | 622 | opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null; 623 | 624 | var nom = parseInt(opts.numberOfMonths, 10) || 1; 625 | opts.numberOfMonths = nom > 4 ? 4 : nom; 626 | 627 | if (!isDate(opts.minDate)) { 628 | opts.minDate = false; 629 | } 630 | if (!isDate(opts.maxDate)) { 631 | opts.maxDate = false; 632 | } 633 | if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) { 634 | opts.maxDate = opts.minDate = false; 635 | } 636 | if (opts.minDate) { 637 | this.setMinDate(opts.minDate); 638 | } 639 | if (opts.maxDate) { 640 | setToStartOfDay(opts.maxDate); 641 | opts.maxYear = opts.maxDate.getFullYear(); 642 | opts.maxMonth = opts.maxDate.getMonth(); 643 | } 644 | 645 | if (isArray(opts.yearRange)) { 646 | var fallback = new Date().getFullYear() - 10; 647 | opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback; 648 | opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback; 649 | } else { 650 | opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange; 651 | if (opts.yearRange > 100) { 652 | opts.yearRange = 100; 653 | } 654 | } 655 | 656 | return opts; 657 | }, 658 | 659 | /** 660 | * return a formatted string of the current selection (using Moment.js if available) 661 | */ 662 | toString: function(format) 663 | { 664 | return !isDate(this._d) ? '' : hasMoment ? moment(this._d).format(format || this._o.format) : this._d.toDateString(); 665 | }, 666 | 667 | /** 668 | * return a Moment.js object of the current selection (if available) 669 | */ 670 | getMoment: function() 671 | { 672 | return hasMoment ? moment(this._d) : null; 673 | }, 674 | 675 | /** 676 | * set the current selection from a Moment.js object (if available) 677 | */ 678 | setMoment: function(date, preventOnSelect) 679 | { 680 | if (hasMoment && moment.isMoment(date)) { 681 | this.setDate(date.toDate(), preventOnSelect); 682 | } 683 | }, 684 | 685 | /** 686 | * return a Date object of the current selection 687 | */ 688 | getDate: function() 689 | { 690 | return isDate(this._d) ? new Date(this._d.getTime()) : null; 691 | }, 692 | 693 | /** 694 | * set the current selection 695 | */ 696 | setDate: function(date, preventOnSelect) 697 | { 698 | if (!date) { 699 | this._d = null; 700 | 701 | if (this._o.field) { 702 | this._o.field.value = ''; 703 | fireEvent(this._o.field, 'change', { firedBy: this }); 704 | } 705 | 706 | return this.draw(); 707 | } 708 | if (typeof date === 'string') { 709 | date = new Date(Date.parse(date)); 710 | } 711 | if (!isDate(date)) { 712 | return; 713 | } 714 | 715 | var min = this._o.minDate, 716 | max = this._o.maxDate; 717 | 718 | if (isDate(min) && date < min) { 719 | date = min; 720 | } else if (isDate(max) && date > max) { 721 | date = max; 722 | } 723 | 724 | this._d = new Date(date.getTime()); 725 | setToStartOfDay(this._d); 726 | this.gotoDate(this._d); 727 | 728 | if (this._o.field) { 729 | this._o.field.value = this.toString(); 730 | fireEvent(this._o.field, 'change', { firedBy: this }); 731 | } 732 | if (!preventOnSelect && typeof this._o.onSelect === 'function') { 733 | this._o.onSelect.call(this, this.getDate()); 734 | } 735 | }, 736 | 737 | /** 738 | * change view to a specific date 739 | */ 740 | gotoDate: function(date) 741 | { 742 | var newCalendar = true; 743 | 744 | if (!isDate(date)) { 745 | return; 746 | } 747 | 748 | if (this.calendars) { 749 | var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1), 750 | lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1), 751 | visibleDate = date.getTime(); 752 | // get the end of the month 753 | lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1); 754 | lastVisibleDate.setDate(lastVisibleDate.getDate()-1); 755 | newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate); 756 | } 757 | 758 | if (newCalendar) { 759 | this.calendars = [{ 760 | month: date.getMonth(), 761 | year: date.getFullYear() 762 | }]; 763 | if (this._o.mainCalendar === 'right') { 764 | this.calendars[0].month += 1 - this._o.numberOfMonths; 765 | } 766 | } 767 | 768 | this.adjustCalendars(); 769 | }, 770 | 771 | adjustCalendars: function() { 772 | this.calendars[0] = adjustCalendar(this.calendars[0]); 773 | for (var c = 1; c < this._o.numberOfMonths; c++) { 774 | this.calendars[c] = adjustCalendar({ 775 | month: this.calendars[0].month + c, 776 | year: this.calendars[0].year 777 | }); 778 | } 779 | this.draw(); 780 | }, 781 | 782 | gotoToday: function() 783 | { 784 | this.gotoDate(new Date()); 785 | }, 786 | 787 | /** 788 | * change view to a specific month (zero-index, e.g. 0: January) 789 | */ 790 | gotoMonth: function(month) 791 | { 792 | if (!isNaN(month)) { 793 | this.calendars[0].month = parseInt(month, 10); 794 | this.adjustCalendars(); 795 | } 796 | }, 797 | 798 | nextMonth: function() 799 | { 800 | this.calendars[0].month++; 801 | this.adjustCalendars(); 802 | }, 803 | 804 | prevMonth: function() 805 | { 806 | this.calendars[0].month--; 807 | this.adjustCalendars(); 808 | }, 809 | 810 | /** 811 | * change view to a specific full year (e.g. "2012") 812 | */ 813 | gotoYear: function(year) 814 | { 815 | if (!isNaN(year)) { 816 | this.calendars[0].year = parseInt(year, 10); 817 | this.adjustCalendars(); 818 | } 819 | }, 820 | 821 | /** 822 | * change the minDate 823 | */ 824 | setMinDate: function(value) 825 | { 826 | setToStartOfDay(value); 827 | this._o.minDate = value; 828 | this._o.minYear = value.getFullYear(); 829 | this._o.minMonth = value.getMonth(); 830 | }, 831 | 832 | /** 833 | * change the maxDate 834 | */ 835 | setMaxDate: function(value) 836 | { 837 | this._o.maxDate = value; 838 | }, 839 | 840 | setStartRange: function(value) 841 | { 842 | this._o.startRange = value; 843 | }, 844 | 845 | setEndRange: function(value) 846 | { 847 | this._o.endRange = value; 848 | }, 849 | 850 | /** 851 | * refresh the HTML 852 | */ 853 | draw: function(force) 854 | { 855 | if (!this._v && !force) { 856 | return; 857 | } 858 | var opts = this._o, 859 | minYear = opts.minYear, 860 | maxYear = opts.maxYear, 861 | minMonth = opts.minMonth, 862 | maxMonth = opts.maxMonth, 863 | html = ''; 864 | 865 | if (this._y <= minYear) { 866 | this._y = minYear; 867 | if (!isNaN(minMonth) && this._m < minMonth) { 868 | this._m = minMonth; 869 | } 870 | } 871 | if (this._y >= maxYear) { 872 | this._y = maxYear; 873 | if (!isNaN(maxMonth) && this._m > maxMonth) { 874 | this._m = maxMonth; 875 | } 876 | } 877 | 878 | for (var c = 0; c < opts.numberOfMonths; c++) { 879 | html += '
' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year) + this.render(this.calendars[c].year, this.calendars[c].month) + '
'; 880 | } 881 | 882 | this.el.innerHTML = html; 883 | 884 | if (opts.bound) { 885 | if(opts.field.type !== 'hidden') { 886 | sto(function() { 887 | opts.trigger.focus(); 888 | }, 1); 889 | } 890 | } 891 | 892 | if (typeof this._o.onDraw === 'function') { 893 | var self = this; 894 | sto(function() { 895 | self._o.onDraw.call(self); 896 | }, 0); 897 | } 898 | }, 899 | 900 | adjustPosition: function() 901 | { 902 | var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect; 903 | 904 | if (this._o.container) return; 905 | 906 | this.el.style.position = 'absolute'; 907 | 908 | field = this._o.trigger; 909 | pEl = field; 910 | width = this.el.offsetWidth; 911 | height = this.el.offsetHeight; 912 | viewportWidth = window.innerWidth || document.documentElement.clientWidth; 913 | viewportHeight = window.innerHeight || document.documentElement.clientHeight; 914 | scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; 915 | 916 | if (typeof field.getBoundingClientRect === 'function') { 917 | clientRect = field.getBoundingClientRect(); 918 | left = clientRect.left + window.pageXOffset; 919 | top = clientRect.bottom + window.pageYOffset; 920 | } else { 921 | left = pEl.offsetLeft; 922 | top = pEl.offsetTop + pEl.offsetHeight; 923 | while((pEl = pEl.offsetParent)) { 924 | left += pEl.offsetLeft; 925 | top += pEl.offsetTop; 926 | } 927 | } 928 | 929 | // default position is bottom & left 930 | if ((this._o.reposition && left + width > viewportWidth) || 931 | ( 932 | this._o.position.indexOf('right') > -1 && 933 | left - width + field.offsetWidth > 0 934 | ) 935 | ) { 936 | left = left - width + field.offsetWidth; 937 | } 938 | if ((this._o.reposition && top + height > viewportHeight + scrollTop) || 939 | ( 940 | this._o.position.indexOf('top') > -1 && 941 | top - height - field.offsetHeight > 0 942 | ) 943 | ) { 944 | top = top - height - field.offsetHeight; 945 | } 946 | 947 | this.el.style.left = left + 'px'; 948 | this.el.style.top = top + 'px'; 949 | }, 950 | 951 | /** 952 | * render HTML for a particular month 953 | */ 954 | render: function(year, month) 955 | { 956 | var opts = this._o, 957 | now = new Date(), 958 | days = getDaysInMonth(year, month), 959 | before = new Date(year, month, 1).getDay(), 960 | data = [], 961 | row = []; 962 | setToStartOfDay(now); 963 | if (opts.firstDay > 0) { 964 | before -= opts.firstDay; 965 | if (before < 0) { 966 | before += 7; 967 | } 968 | } 969 | var cells = days + before, 970 | after = cells; 971 | while(after > 7) { 972 | after -= 7; 973 | } 974 | cells += 7 - after; 975 | for (var i = 0, r = 0; i < cells; i++) 976 | { 977 | var dayConfig, 978 | day = new Date(year, month, 1 + (i - before)), 979 | isSelected = isDate(this._d) ? compareDates(day, this._d) : false, 980 | isToday = compareDates(day, now), 981 | isEmpty = i < before || i >= (days + before), 982 | isStartRange = opts.startRange && compareDates(opts.startRange, day), 983 | isEndRange = opts.endRange && compareDates(opts.endRange, day), 984 | isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange, 985 | isDisabled = (opts.minDate && day < opts.minDate) || 986 | (opts.maxDate && day > opts.maxDate) || 987 | (opts.disableWeekends && isWeekend(day)) || 988 | (opts.disableDayFn && opts.disableDayFn(day)), 989 | dayConfig = { 990 | day: 1 + (i - before), 991 | month: month, 992 | year: year, 993 | isSelected: isSelected, 994 | isToday: isToday, 995 | isDisabled: isDisabled, 996 | isEmpty: isEmpty, 997 | isStartRange: isStartRange, 998 | isEndRange: isEndRange, 999 | isInRange: isInRange 1000 | }; 1001 | 1002 | row.push(renderDay(dayConfig)); 1003 | 1004 | if (++r === 7) { 1005 | if (opts.showWeekNumber) { 1006 | row.unshift(renderWeek(i - before, month, year)); 1007 | } 1008 | data.push(renderRow(row, opts.isRTL)); 1009 | row = []; 1010 | r = 0; 1011 | } 1012 | } 1013 | return renderTable(opts, data); 1014 | }, 1015 | 1016 | isVisible: function() 1017 | { 1018 | return this._v; 1019 | }, 1020 | 1021 | show: function() 1022 | { 1023 | if (!this._v) { 1024 | removeClass(this.el, 'is-hidden'); 1025 | this._v = true; 1026 | this.draw(); 1027 | if (this._o.bound) { 1028 | addEvent(document, 'click', this._onClick); 1029 | this.adjustPosition(); 1030 | } 1031 | if (typeof this._o.onOpen === 'function') { 1032 | this._o.onOpen.call(this); 1033 | } 1034 | } 1035 | }, 1036 | 1037 | hide: function() 1038 | { 1039 | var v = this._v; 1040 | if (v !== false) { 1041 | if (this._o.bound) { 1042 | removeEvent(document, 'click', this._onClick); 1043 | } 1044 | this.el.style.position = 'static'; // reset 1045 | this.el.style.left = 'auto'; 1046 | this.el.style.top = 'auto'; 1047 | addClass(this.el, 'is-hidden'); 1048 | this._v = false; 1049 | if (v !== undefined && typeof this._o.onClose === 'function') { 1050 | this._o.onClose.call(this); 1051 | } 1052 | } 1053 | }, 1054 | 1055 | /** 1056 | * GAME OVER 1057 | */ 1058 | destroy: function() 1059 | { 1060 | this.hide(); 1061 | removeEvent(this.el, 'mousedown', this._onMouseDown, true); 1062 | removeEvent(this.el, 'change', this._onChange); 1063 | if (this._o.field) { 1064 | removeEvent(this._o.field, 'change', this._onInputChange); 1065 | if (this._o.bound) { 1066 | removeEvent(this._o.trigger, 'click', this._onInputClick); 1067 | removeEvent(this._o.trigger, 'focus', this._onInputFocus); 1068 | removeEvent(this._o.trigger, 'blur', this._onInputBlur); 1069 | } 1070 | } 1071 | if (this.el.parentNode) { 1072 | this.el.parentNode.removeChild(this.el); 1073 | } 1074 | } 1075 | 1076 | }; 1077 | 1078 | return Pikaday; 1079 | 1080 | })); 1081 | -------------------------------------------------------------------------------- /firefox/data/github-time-travel.js: -------------------------------------------------------------------------------- 1 | 2 | var pushState = history.pushState; 3 | 4 | function pushStatePatch (state) { 5 | // Github uses pushState for navigation within a repo (actually pjax, but 6 | // pushState is the underlying API). We have to add an onpushstate listener 7 | // to add our functionality to the DOM when the page changes. 8 | // This requires monkey-patching the window.history.pushState function. 9 | 10 | if (typeof history.onpushstate == "function") { 11 | history.onpushstate({state: state}); 12 | } 13 | 14 | return pushState.apply(history, arguments); 15 | } 16 | 17 | history.onpushstate = function (state) { 18 | // When navigation happens, we have to wait for the DOM to change before 19 | // attaching the date button. 20 | 21 | setDomChangeTimeout(attachDateButton, 'commit-group-title', 2000); 22 | } 23 | 24 | function setDomChangeTimeout (callback, klass, timeout) { 25 | // A function like setTimeout that watches a DOM element (by class name) 26 | // for change. The callback is called as soon as the DOM element 27 | // changes. If it doesn't change, the callback is called at timeout. 28 | 29 | // This is the fastest way to get a callback when the new DOM settles. 30 | // Right now, it naively uses innerHTML, but for this use, it's good enough. 31 | 32 | var start = performance.now(), 33 | startContent = '', 34 | startElements = document.getElementsByClassName(klass); 35 | 36 | if (startElements.length > 0) { 37 | startContent = startElements[0].innerHTML; 38 | } 39 | 40 | var interval = setInterval(function () { 41 | if (performance.now() - start > timeout) { 42 | clearInterval(interval); 43 | callback(); 44 | } else { 45 | var endElements = document.getElementsByClassName(klass); 46 | 47 | if (endElements.length > 0) { 48 | if (endElements[0].innerHTML !== startContent) { 49 | clearInterval(interval); 50 | callback(); 51 | } 52 | } 53 | } 54 | }, 50); 55 | } 56 | 57 | function attachDateButton () { 58 | // Extract the Github username and repo from a commit log URL. 59 | // If the DOM element does not already exist, create an element to hold 60 | // the datepicker and append it to the .file-navigation DOM element 61 | // after styling it like a Github button. 62 | 63 | // Then, create the datepicker with a callback to make an API call on select. 64 | // Attach the listener to the API call to handle the response. 65 | 66 | var re = /.*github\.com\/(.*)\/(.*)\/commits.*/, 67 | m = re.exec(document.location.href); 68 | 69 | if (m === null) return; 70 | if (document.getElementsByClassName('datepicker-button').length > 0) return; 71 | 72 | var username = m[1], 73 | repo = m[2], 74 | api = 'https://api.github.com/repos/' + username + '/' + repo + '/commits', 75 | date = getCommitDate(), 76 | el = document.createElement('span'); 77 | 78 | el.className = 'btn btn-sm select-menu-button datepicker-button'; 79 | el.innerHTML = 'Date: ' + date + ' '; 80 | 81 | document.getElementsByClassName('file-navigation')[0].appendChild(el); 82 | 83 | new Pikaday({ 84 | field: el, 85 | defaultDate: new Date(date), 86 | setDefaultDate: true, 87 | onSelect: function (date) { 88 | var req = new XMLHttpRequest(), 89 | url = api + '?per_page=1&until=' + date.toISOString().split('T')[0]; 90 | 91 | req.addEventListener('load', reqListener); 92 | req.open('get', url, true); 93 | req.send(); 94 | } 95 | }); 96 | } 97 | 98 | function getCommitDate () { 99 | // Find first .commit-group-title in page, extract the date from the text. 100 | // Return 'unknown' if no commit group titles are found. 101 | 102 | var commitGroups = document.getElementsByClassName('commit-group-title'); 103 | 104 | if (commitGroups.length > 0) { 105 | var re = /Commits on (.*)/, 106 | m = re.exec(commitGroups[0].innerHTML), 107 | date = m[1]; 108 | 109 | return date; 110 | } else { 111 | return 'unknown'; 112 | } 113 | } 114 | 115 | function reqListener () { 116 | // Listen for a response from the Github Commits API 117 | // https://developer.github.com/v3/repos/commits/ 118 | // Parse the JSON response and extract the HTML link from the first commit 119 | // if commits were returned. 120 | // Change 'commit' to 'commits' in the URL to browse the commit log. 121 | 122 | var parsed = JSON.parse(this.responseText); 123 | 124 | if (parsed.length > 0) { 125 | var tree_url = parsed[0].html_url.replace('/commit/', '/commits/'); 126 | document.location.href = tree_url; 127 | } 128 | } 129 | 130 | attachDateButton(); 131 | exportFunction(pushStatePatch, unsafeWindow.history, {defineAs: 'pushState', allowCallbacks: true}); 132 | -------------------------------------------------------------------------------- /firefox/data/pikaday/pikaday.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /*! 4 | * Pikaday 5 | * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/ 6 | */ 7 | 8 | .pika-single { 9 | z-index: 9999; 10 | display: block; 11 | position: relative; 12 | color: #333; 13 | background: #fff; 14 | border: 1px solid #ccc; 15 | border-bottom-color: #bbb; 16 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 17 | } 18 | 19 | /* 20 | clear child float (pika-lendar), using the famous micro clearfix hack 21 | http://nicolasgallagher.com/micro-clearfix-hack/ 22 | */ 23 | .pika-single:before, 24 | .pika-single:after { 25 | content: " "; 26 | display: table; 27 | } 28 | .pika-single:after { clear: both } 29 | .pika-single { *zoom: 1 } 30 | 31 | .pika-single.is-hidden { 32 | display: none; 33 | } 34 | 35 | .pika-single.is-bound { 36 | position: absolute; 37 | box-shadow: 0 5px 15px -5px rgba(0,0,0,.5); 38 | } 39 | 40 | .pika-lendar { 41 | float: left; 42 | width: 240px; 43 | margin: 8px; 44 | } 45 | 46 | .pika-title { 47 | position: relative; 48 | text-align: center; 49 | } 50 | 51 | .pika-label { 52 | display: inline-block; 53 | *display: inline; 54 | position: relative; 55 | z-index: 9999; 56 | overflow: hidden; 57 | margin: 0; 58 | padding: 5px 3px; 59 | font-size: 14px; 60 | line-height: 20px; 61 | font-weight: bold; 62 | background-color: #fff; 63 | } 64 | .pika-title select { 65 | cursor: pointer; 66 | position: absolute; 67 | z-index: 9998; 68 | margin: 0; 69 | left: 0; 70 | top: 5px; 71 | filter: alpha(opacity=0); 72 | opacity: 0; 73 | } 74 | 75 | .pika-prev, 76 | .pika-next { 77 | display: block; 78 | cursor: pointer; 79 | position: relative; 80 | outline: none; 81 | border: 0; 82 | padding: 0; 83 | width: 20px; 84 | height: 30px; 85 | /* hide text using text-indent trick, using width value (it's enough) */ 86 | text-indent: 20px; 87 | white-space: nowrap; 88 | overflow: hidden; 89 | background-color: transparent; 90 | background-position: center center; 91 | background-repeat: no-repeat; 92 | background-size: 75% 75%; 93 | opacity: .5; 94 | *position: absolute; 95 | *top: 0; 96 | } 97 | 98 | .pika-prev:hover, 99 | .pika-next:hover { 100 | opacity: 1; 101 | } 102 | 103 | .pika-prev, 104 | .is-rtl .pika-next { 105 | float: left; 106 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=='); 107 | *left: 0; 108 | } 109 | 110 | .pika-next, 111 | .is-rtl .pika-prev { 112 | float: right; 113 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII='); 114 | *right: 0; 115 | } 116 | 117 | .pika-prev.is-disabled, 118 | .pika-next.is-disabled { 119 | cursor: default; 120 | opacity: .2; 121 | } 122 | 123 | .pika-select { 124 | display: inline-block; 125 | *display: inline; 126 | } 127 | 128 | .pika-table { 129 | width: 100%; 130 | border-collapse: collapse; 131 | border-spacing: 0; 132 | border: 0; 133 | } 134 | 135 | .pika-table th, 136 | .pika-table td { 137 | width: 14.285714285714286%; 138 | padding: 0; 139 | } 140 | 141 | .pika-table th { 142 | color: #999; 143 | font-size: 12px; 144 | line-height: 25px; 145 | font-weight: bold; 146 | text-align: center; 147 | } 148 | 149 | .pika-button { 150 | cursor: pointer; 151 | display: block; 152 | box-sizing: border-box; 153 | -moz-box-sizing: border-box; 154 | outline: none; 155 | border: 0; 156 | margin: 0; 157 | width: 100%; 158 | padding: 5px; 159 | color: #666; 160 | font-size: 12px; 161 | line-height: 15px; 162 | text-align: right; 163 | background: #f5f5f5; 164 | } 165 | 166 | .pika-week { 167 | font-size: 11px; 168 | color: #999; 169 | } 170 | 171 | .is-today .pika-button { 172 | color: #33aaff; 173 | font-weight: bold; 174 | } 175 | 176 | .is-selected .pika-button { 177 | color: #fff; 178 | font-weight: bold; 179 | background: #33aaff; 180 | box-shadow: inset 0 1px 3px #178fe5; 181 | border-radius: 3px; 182 | } 183 | 184 | .is-inrange .pika-button { 185 | background: #D5E9F7; 186 | } 187 | 188 | .is-startrange .pika-button { 189 | color: #fff; 190 | background: #6CB31D; 191 | box-shadow: none; 192 | border-radius: 3px; 193 | } 194 | 195 | .is-endrange .pika-button { 196 | color: #fff; 197 | background: #33aaff; 198 | box-shadow: none; 199 | border-radius: 3px; 200 | } 201 | 202 | .is-disabled .pika-button { 203 | pointer-events: none; 204 | cursor: default; 205 | color: #999; 206 | opacity: .3; 207 | } 208 | 209 | .pika-button:hover { 210 | color: #fff; 211 | background: #ff8000; 212 | box-shadow: none; 213 | border-radius: 3px; 214 | } 215 | 216 | /* styling for abbr */ 217 | .pika-table abbr { 218 | border-bottom: none; 219 | cursor: help; 220 | } 221 | 222 | -------------------------------------------------------------------------------- /firefox/data/pikaday/pikaday.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Pikaday 3 | * 4 | * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday 5 | */ 6 | 7 | (function (root, factory) 8 | { 9 | 'use strict'; 10 | 11 | var moment; 12 | if (typeof exports === 'object') { 13 | // CommonJS module 14 | // Load moment.js as an optional dependency 15 | try { moment = require('moment'); } catch (e) {} 16 | module.exports = factory(moment); 17 | } else if (typeof define === 'function' && define.amd) { 18 | // AMD. Register as an anonymous module. 19 | define(function (req) 20 | { 21 | // Load moment.js as an optional dependency 22 | var id = 'moment'; 23 | try { moment = req(id); } catch (e) {} 24 | return factory(moment); 25 | }); 26 | } else { 27 | root.Pikaday = factory(root.moment); 28 | } 29 | }(this, function (moment) 30 | { 31 | 'use strict'; 32 | 33 | /** 34 | * feature detection and helper functions 35 | */ 36 | var hasMoment = typeof moment === 'function', 37 | 38 | hasEventListeners = !!window.addEventListener, 39 | 40 | document = window.document, 41 | 42 | sto = window.setTimeout, 43 | 44 | addEvent = function(el, e, callback, capture) 45 | { 46 | if (hasEventListeners) { 47 | el.addEventListener(e, callback, !!capture); 48 | } else { 49 | el.attachEvent('on' + e, callback); 50 | } 51 | }, 52 | 53 | removeEvent = function(el, e, callback, capture) 54 | { 55 | if (hasEventListeners) { 56 | el.removeEventListener(e, callback, !!capture); 57 | } else { 58 | el.detachEvent('on' + e, callback); 59 | } 60 | }, 61 | 62 | fireEvent = function(el, eventName, data) 63 | { 64 | var ev; 65 | 66 | if (document.createEvent) { 67 | ev = document.createEvent('HTMLEvents'); 68 | ev.initEvent(eventName, true, false); 69 | ev = extend(ev, data); 70 | el.dispatchEvent(ev); 71 | } else if (document.createEventObject) { 72 | ev = document.createEventObject(); 73 | ev = extend(ev, data); 74 | el.fireEvent('on' + eventName, ev); 75 | } 76 | }, 77 | 78 | trim = function(str) 79 | { 80 | return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,''); 81 | }, 82 | 83 | hasClass = function(el, cn) 84 | { 85 | return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1; 86 | }, 87 | 88 | addClass = function(el, cn) 89 | { 90 | if (!hasClass(el, cn)) { 91 | el.className = (el.className === '') ? cn : el.className + ' ' + cn; 92 | } 93 | }, 94 | 95 | removeClass = function(el, cn) 96 | { 97 | el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' ')); 98 | }, 99 | 100 | isArray = function(obj) 101 | { 102 | return (/Array/).test(Object.prototype.toString.call(obj)); 103 | }, 104 | 105 | isDate = function(obj) 106 | { 107 | return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); 108 | }, 109 | 110 | isWeekend = function(date) 111 | { 112 | var day = date.getDay(); 113 | return day === 0 || day === 6; 114 | }, 115 | 116 | isLeapYear = function(year) 117 | { 118 | // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951 119 | return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; 120 | }, 121 | 122 | getDaysInMonth = function(year, month) 123 | { 124 | return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 125 | }, 126 | 127 | setToStartOfDay = function(date) 128 | { 129 | if (isDate(date)) date.setHours(0,0,0,0); 130 | }, 131 | 132 | compareDates = function(a,b) 133 | { 134 | // weak date comparison (use setToStartOfDay(date) to ensure correct result) 135 | return a.getTime() === b.getTime(); 136 | }, 137 | 138 | extend = function(to, from, overwrite) 139 | { 140 | var prop, hasProp; 141 | for (prop in from) { 142 | hasProp = to[prop] !== undefined; 143 | if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) { 144 | if (isDate(from[prop])) { 145 | if (overwrite) { 146 | to[prop] = new Date(from[prop].getTime()); 147 | } 148 | } 149 | else if (isArray(from[prop])) { 150 | if (overwrite) { 151 | to[prop] = from[prop].slice(0); 152 | } 153 | } else { 154 | to[prop] = extend({}, from[prop], overwrite); 155 | } 156 | } else if (overwrite || !hasProp) { 157 | to[prop] = from[prop]; 158 | } 159 | } 160 | return to; 161 | }, 162 | 163 | adjustCalendar = function(calendar) { 164 | if (calendar.month < 0) { 165 | calendar.year -= Math.ceil(Math.abs(calendar.month)/12); 166 | calendar.month += 12; 167 | } 168 | if (calendar.month > 11) { 169 | calendar.year += Math.floor(Math.abs(calendar.month)/12); 170 | calendar.month -= 12; 171 | } 172 | return calendar; 173 | }, 174 | 175 | /** 176 | * defaults and localisation 177 | */ 178 | defaults = { 179 | 180 | // bind the picker to a form field 181 | field: null, 182 | 183 | // automatically show/hide the picker on `field` focus (default `true` if `field` is set) 184 | bound: undefined, 185 | 186 | // position of the datepicker, relative to the field (default to bottom & left) 187 | // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position) 188 | position: 'bottom left', 189 | 190 | // automatically fit in the viewport even if it means repositioning from the position option 191 | reposition: true, 192 | 193 | // the default output format for `.toString()` and `field` value 194 | format: 'YYYY-MM-DD', 195 | 196 | // the initial date to view when first opened 197 | defaultDate: null, 198 | 199 | // make the `defaultDate` the initial selected value 200 | setDefaultDate: false, 201 | 202 | // first day of week (0: Sunday, 1: Monday etc) 203 | firstDay: 0, 204 | 205 | // the minimum/earliest date that can be selected 206 | minDate: null, 207 | // the maximum/latest date that can be selected 208 | maxDate: null, 209 | 210 | // number of years either side, or array of upper/lower range 211 | yearRange: 10, 212 | 213 | // show week numbers at head of row 214 | showWeekNumber: false, 215 | 216 | // used internally (don't config outside) 217 | minYear: 0, 218 | maxYear: 9999, 219 | minMonth: undefined, 220 | maxMonth: undefined, 221 | 222 | startRange: null, 223 | endRange: null, 224 | 225 | isRTL: false, 226 | 227 | // Additional text to append to the year in the calendar title 228 | yearSuffix: '', 229 | 230 | // Render the month after year in the calendar title 231 | showMonthAfterYear: false, 232 | 233 | // how many months are visible 234 | numberOfMonths: 1, 235 | 236 | // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`) 237 | // only used for the first display or when a selected date is not visible 238 | mainCalendar: 'left', 239 | 240 | // Specify a DOM element to render the calendar in 241 | container: undefined, 242 | 243 | // internationalization 244 | i18n: { 245 | previousMonth : 'Previous Month', 246 | nextMonth : 'Next Month', 247 | months : ['January','February','March','April','May','June','July','August','September','October','November','December'], 248 | weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], 249 | weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] 250 | }, 251 | 252 | // Theme Classname 253 | theme: null, 254 | 255 | // callback function 256 | onSelect: null, 257 | onOpen: null, 258 | onClose: null, 259 | onDraw: null 260 | }, 261 | 262 | 263 | /** 264 | * templating functions to abstract HTML rendering 265 | */ 266 | renderDayName = function(opts, day, abbr) 267 | { 268 | day += opts.firstDay; 269 | while (day >= 7) { 270 | day -= 7; 271 | } 272 | return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day]; 273 | }, 274 | 275 | renderDay = function(opts) 276 | { 277 | if (opts.isEmpty) { 278 | return ''; 279 | } 280 | var arr = []; 281 | if (opts.isDisabled) { 282 | arr.push('is-disabled'); 283 | } 284 | if (opts.isToday) { 285 | arr.push('is-today'); 286 | } 287 | if (opts.isSelected) { 288 | arr.push('is-selected'); 289 | } 290 | if (opts.isInRange) { 291 | arr.push('is-inrange'); 292 | } 293 | if (opts.isStartRange) { 294 | arr.push('is-startrange'); 295 | } 296 | if (opts.isEndRange) { 297 | arr.push('is-endrange'); 298 | } 299 | return '' + 300 | '' + 304 | ''; 305 | }, 306 | 307 | renderWeek = function (d, m, y) { 308 | // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified. 309 | var onejan = new Date(y, 0, 1), 310 | weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7); 311 | return '' + weekNum + ''; 312 | }, 313 | 314 | renderRow = function(days, isRTL) 315 | { 316 | return '' + (isRTL ? days.reverse() : days).join('') + ''; 317 | }, 318 | 319 | renderBody = function(rows) 320 | { 321 | return '' + rows.join('') + ''; 322 | }, 323 | 324 | renderHead = function(opts) 325 | { 326 | var i, arr = []; 327 | if (opts.showWeekNumber) { 328 | arr.push(''); 329 | } 330 | for (i = 0; i < 7; i++) { 331 | arr.push('' + renderDayName(opts, i, true) + ''); 332 | } 333 | return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; 334 | }, 335 | 336 | renderTitle = function(instance, c, year, month, refYear) 337 | { 338 | var i, j, arr, 339 | opts = instance._o, 340 | isMinYear = year === opts.minYear, 341 | isMaxYear = year === opts.maxYear, 342 | html = '
', 343 | monthHtml, 344 | yearHtml, 345 | prev = true, 346 | next = true; 347 | 348 | for (arr = [], i = 0; i < 12; i++) { 349 | arr.push(''); 353 | } 354 | monthHtml = '
' + opts.i18n.months[month] + '
'; 355 | 356 | if (isArray(opts.yearRange)) { 357 | i = opts.yearRange[0]; 358 | j = opts.yearRange[1] + 1; 359 | } else { 360 | i = year - opts.yearRange; 361 | j = 1 + year + opts.yearRange; 362 | } 363 | 364 | for (arr = []; i < j && i <= opts.maxYear; i++) { 365 | if (i >= opts.minYear) { 366 | arr.push(''); 367 | } 368 | } 369 | yearHtml = '
' + year + opts.yearSuffix + '
'; 370 | 371 | if (opts.showMonthAfterYear) { 372 | html += yearHtml + monthHtml; 373 | } else { 374 | html += monthHtml + yearHtml; 375 | } 376 | 377 | if (isMinYear && (month === 0 || opts.minMonth >= month)) { 378 | prev = false; 379 | } 380 | 381 | if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { 382 | next = false; 383 | } 384 | 385 | if (c === 0) { 386 | html += ''; 387 | } 388 | if (c === (instance._o.numberOfMonths - 1) ) { 389 | html += ''; 390 | } 391 | 392 | return html += '
'; 393 | }, 394 | 395 | renderTable = function(opts, data) 396 | { 397 | return '' + renderHead(opts) + renderBody(data) + '
'; 398 | }, 399 | 400 | 401 | /** 402 | * Pikaday constructor 403 | */ 404 | Pikaday = function(options) 405 | { 406 | var self = this, 407 | opts = self.config(options); 408 | 409 | self._onMouseDown = function(e) 410 | { 411 | if (!self._v) { 412 | return; 413 | } 414 | e = e || window.event; 415 | var target = e.target || e.srcElement; 416 | if (!target) { 417 | return; 418 | } 419 | 420 | if (!hasClass(target.parentNode, 'is-disabled')) { 421 | if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty')) { 422 | self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day'))); 423 | if (opts.bound) { 424 | sto(function() { 425 | self.hide(); 426 | if (opts.field) { 427 | opts.field.blur(); 428 | } 429 | }, 100); 430 | } 431 | return; 432 | } 433 | else if (hasClass(target, 'pika-prev')) { 434 | self.prevMonth(); 435 | } 436 | else if (hasClass(target, 'pika-next')) { 437 | self.nextMonth(); 438 | } 439 | } 440 | if (!hasClass(target, 'pika-select')) { 441 | if (e.preventDefault) { 442 | e.preventDefault(); 443 | } else { 444 | e.returnValue = false; 445 | return false; 446 | } 447 | } else { 448 | self._c = true; 449 | } 450 | }; 451 | 452 | self._onChange = function(e) 453 | { 454 | e = e || window.event; 455 | var target = e.target || e.srcElement; 456 | if (!target) { 457 | return; 458 | } 459 | if (hasClass(target, 'pika-select-month')) { 460 | self.gotoMonth(target.value); 461 | } 462 | else if (hasClass(target, 'pika-select-year')) { 463 | self.gotoYear(target.value); 464 | } 465 | }; 466 | 467 | self._onInputChange = function(e) 468 | { 469 | var date; 470 | 471 | if (e.firedBy === self) { 472 | return; 473 | } 474 | if (hasMoment) { 475 | date = moment(opts.field.value, opts.format); 476 | date = (date && date.isValid()) ? date.toDate() : null; 477 | } 478 | else { 479 | date = new Date(Date.parse(opts.field.value)); 480 | } 481 | if (isDate(date)) { 482 | self.setDate(date); 483 | } 484 | if (!self._v) { 485 | self.show(); 486 | } 487 | }; 488 | 489 | self._onInputFocus = function() 490 | { 491 | self.show(); 492 | }; 493 | 494 | self._onInputClick = function() 495 | { 496 | self.show(); 497 | }; 498 | 499 | self._onInputBlur = function() 500 | { 501 | // IE allows pika div to gain focus; catch blur the input field 502 | var pEl = document.activeElement; 503 | do { 504 | if (hasClass(pEl, 'pika-single')) { 505 | return; 506 | } 507 | } 508 | while ((pEl = pEl.parentNode)); 509 | 510 | if (!self._c) { 511 | self._b = sto(function() { 512 | self.hide(); 513 | }, 50); 514 | } 515 | self._c = false; 516 | }; 517 | 518 | self._onClick = function(e) 519 | { 520 | e = e || window.event; 521 | var target = e.target || e.srcElement, 522 | pEl = target; 523 | if (!target) { 524 | return; 525 | } 526 | if (!hasEventListeners && hasClass(target, 'pika-select')) { 527 | if (!target.onchange) { 528 | target.setAttribute('onchange', 'return;'); 529 | addEvent(target, 'change', self._onChange); 530 | } 531 | } 532 | do { 533 | if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) { 534 | return; 535 | } 536 | } 537 | while ((pEl = pEl.parentNode)); 538 | if (self._v && target !== opts.trigger && pEl !== opts.trigger) { 539 | self.hide(); 540 | } 541 | }; 542 | 543 | self.el = document.createElement('div'); 544 | self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : ''); 545 | 546 | addEvent(self.el, 'ontouchend' in document ? 'touchend' : 'mousedown', self._onMouseDown, true); 547 | addEvent(self.el, 'change', self._onChange); 548 | 549 | if (opts.field) { 550 | if (opts.container) { 551 | opts.container.appendChild(self.el); 552 | } else if (opts.bound) { 553 | document.body.appendChild(self.el); 554 | } else { 555 | opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling); 556 | } 557 | addEvent(opts.field, 'change', self._onInputChange); 558 | 559 | if (!opts.defaultDate) { 560 | if (hasMoment && opts.field.value) { 561 | opts.defaultDate = moment(opts.field.value, opts.format).toDate(); 562 | } else { 563 | opts.defaultDate = new Date(Date.parse(opts.field.value)); 564 | } 565 | opts.setDefaultDate = true; 566 | } 567 | } 568 | 569 | var defDate = opts.defaultDate; 570 | 571 | if (isDate(defDate)) { 572 | if (opts.setDefaultDate) { 573 | self.setDate(defDate, true); 574 | } else { 575 | self.gotoDate(defDate); 576 | } 577 | } else { 578 | self.gotoDate(new Date()); 579 | } 580 | 581 | if (opts.bound) { 582 | this.hide(); 583 | self.el.className += ' is-bound'; 584 | addEvent(opts.trigger, 'click', self._onInputClick); 585 | addEvent(opts.trigger, 'focus', self._onInputFocus); 586 | addEvent(opts.trigger, 'blur', self._onInputBlur); 587 | } else { 588 | this.show(); 589 | } 590 | }; 591 | 592 | 593 | /** 594 | * public Pikaday API 595 | */ 596 | Pikaday.prototype = { 597 | 598 | 599 | /** 600 | * configure functionality 601 | */ 602 | config: function(options) 603 | { 604 | if (!this._o) { 605 | this._o = extend({}, defaults, true); 606 | } 607 | 608 | var opts = extend(this._o, options, true); 609 | 610 | opts.isRTL = !!opts.isRTL; 611 | 612 | opts.field = (opts.field && opts.field.nodeName) ? opts.field : null; 613 | 614 | opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null; 615 | 616 | opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field); 617 | 618 | opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field; 619 | 620 | opts.disableWeekends = !!opts.disableWeekends; 621 | 622 | opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null; 623 | 624 | var nom = parseInt(opts.numberOfMonths, 10) || 1; 625 | opts.numberOfMonths = nom > 4 ? 4 : nom; 626 | 627 | if (!isDate(opts.minDate)) { 628 | opts.minDate = false; 629 | } 630 | if (!isDate(opts.maxDate)) { 631 | opts.maxDate = false; 632 | } 633 | if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) { 634 | opts.maxDate = opts.minDate = false; 635 | } 636 | if (opts.minDate) { 637 | this.setMinDate(opts.minDate); 638 | } 639 | if (opts.maxDate) { 640 | setToStartOfDay(opts.maxDate); 641 | opts.maxYear = opts.maxDate.getFullYear(); 642 | opts.maxMonth = opts.maxDate.getMonth(); 643 | } 644 | 645 | if (isArray(opts.yearRange)) { 646 | var fallback = new Date().getFullYear() - 10; 647 | opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback; 648 | opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback; 649 | } else { 650 | opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange; 651 | if (opts.yearRange > 100) { 652 | opts.yearRange = 100; 653 | } 654 | } 655 | 656 | return opts; 657 | }, 658 | 659 | /** 660 | * return a formatted string of the current selection (using Moment.js if available) 661 | */ 662 | toString: function(format) 663 | { 664 | return !isDate(this._d) ? '' : hasMoment ? moment(this._d).format(format || this._o.format) : this._d.toDateString(); 665 | }, 666 | 667 | /** 668 | * return a Moment.js object of the current selection (if available) 669 | */ 670 | getMoment: function() 671 | { 672 | return hasMoment ? moment(this._d) : null; 673 | }, 674 | 675 | /** 676 | * set the current selection from a Moment.js object (if available) 677 | */ 678 | setMoment: function(date, preventOnSelect) 679 | { 680 | if (hasMoment && moment.isMoment(date)) { 681 | this.setDate(date.toDate(), preventOnSelect); 682 | } 683 | }, 684 | 685 | /** 686 | * return a Date object of the current selection 687 | */ 688 | getDate: function() 689 | { 690 | return isDate(this._d) ? new Date(this._d.getTime()) : null; 691 | }, 692 | 693 | /** 694 | * set the current selection 695 | */ 696 | setDate: function(date, preventOnSelect) 697 | { 698 | if (!date) { 699 | this._d = null; 700 | 701 | if (this._o.field) { 702 | this._o.field.value = ''; 703 | fireEvent(this._o.field, 'change', { firedBy: this }); 704 | } 705 | 706 | return this.draw(); 707 | } 708 | if (typeof date === 'string') { 709 | date = new Date(Date.parse(date)); 710 | } 711 | if (!isDate(date)) { 712 | return; 713 | } 714 | 715 | var min = this._o.minDate, 716 | max = this._o.maxDate; 717 | 718 | if (isDate(min) && date < min) { 719 | date = min; 720 | } else if (isDate(max) && date > max) { 721 | date = max; 722 | } 723 | 724 | this._d = new Date(date.getTime()); 725 | setToStartOfDay(this._d); 726 | this.gotoDate(this._d); 727 | 728 | if (this._o.field) { 729 | this._o.field.value = this.toString(); 730 | fireEvent(this._o.field, 'change', { firedBy: this }); 731 | } 732 | if (!preventOnSelect && typeof this._o.onSelect === 'function') { 733 | this._o.onSelect.call(this, this.getDate()); 734 | } 735 | }, 736 | 737 | /** 738 | * change view to a specific date 739 | */ 740 | gotoDate: function(date) 741 | { 742 | var newCalendar = true; 743 | 744 | if (!isDate(date)) { 745 | return; 746 | } 747 | 748 | if (this.calendars) { 749 | var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1), 750 | lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1), 751 | visibleDate = date.getTime(); 752 | // get the end of the month 753 | lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1); 754 | lastVisibleDate.setDate(lastVisibleDate.getDate()-1); 755 | newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate); 756 | } 757 | 758 | if (newCalendar) { 759 | this.calendars = [{ 760 | month: date.getMonth(), 761 | year: date.getFullYear() 762 | }]; 763 | if (this._o.mainCalendar === 'right') { 764 | this.calendars[0].month += 1 - this._o.numberOfMonths; 765 | } 766 | } 767 | 768 | this.adjustCalendars(); 769 | }, 770 | 771 | adjustCalendars: function() { 772 | this.calendars[0] = adjustCalendar(this.calendars[0]); 773 | for (var c = 1; c < this._o.numberOfMonths; c++) { 774 | this.calendars[c] = adjustCalendar({ 775 | month: this.calendars[0].month + c, 776 | year: this.calendars[0].year 777 | }); 778 | } 779 | this.draw(); 780 | }, 781 | 782 | gotoToday: function() 783 | { 784 | this.gotoDate(new Date()); 785 | }, 786 | 787 | /** 788 | * change view to a specific month (zero-index, e.g. 0: January) 789 | */ 790 | gotoMonth: function(month) 791 | { 792 | if (!isNaN(month)) { 793 | this.calendars[0].month = parseInt(month, 10); 794 | this.adjustCalendars(); 795 | } 796 | }, 797 | 798 | nextMonth: function() 799 | { 800 | this.calendars[0].month++; 801 | this.adjustCalendars(); 802 | }, 803 | 804 | prevMonth: function() 805 | { 806 | this.calendars[0].month--; 807 | this.adjustCalendars(); 808 | }, 809 | 810 | /** 811 | * change view to a specific full year (e.g. "2012") 812 | */ 813 | gotoYear: function(year) 814 | { 815 | if (!isNaN(year)) { 816 | this.calendars[0].year = parseInt(year, 10); 817 | this.adjustCalendars(); 818 | } 819 | }, 820 | 821 | /** 822 | * change the minDate 823 | */ 824 | setMinDate: function(value) 825 | { 826 | setToStartOfDay(value); 827 | this._o.minDate = value; 828 | this._o.minYear = value.getFullYear(); 829 | this._o.minMonth = value.getMonth(); 830 | }, 831 | 832 | /** 833 | * change the maxDate 834 | */ 835 | setMaxDate: function(value) 836 | { 837 | this._o.maxDate = value; 838 | }, 839 | 840 | setStartRange: function(value) 841 | { 842 | this._o.startRange = value; 843 | }, 844 | 845 | setEndRange: function(value) 846 | { 847 | this._o.endRange = value; 848 | }, 849 | 850 | /** 851 | * refresh the HTML 852 | */ 853 | draw: function(force) 854 | { 855 | if (!this._v && !force) { 856 | return; 857 | } 858 | var opts = this._o, 859 | minYear = opts.minYear, 860 | maxYear = opts.maxYear, 861 | minMonth = opts.minMonth, 862 | maxMonth = opts.maxMonth, 863 | html = ''; 864 | 865 | if (this._y <= minYear) { 866 | this._y = minYear; 867 | if (!isNaN(minMonth) && this._m < minMonth) { 868 | this._m = minMonth; 869 | } 870 | } 871 | if (this._y >= maxYear) { 872 | this._y = maxYear; 873 | if (!isNaN(maxMonth) && this._m > maxMonth) { 874 | this._m = maxMonth; 875 | } 876 | } 877 | 878 | for (var c = 0; c < opts.numberOfMonths; c++) { 879 | html += '
' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year) + this.render(this.calendars[c].year, this.calendars[c].month) + '
'; 880 | } 881 | 882 | this.el.innerHTML = html; 883 | 884 | if (opts.bound) { 885 | if(opts.field.type !== 'hidden') { 886 | sto(function() { 887 | opts.trigger.focus(); 888 | }, 1); 889 | } 890 | } 891 | 892 | if (typeof this._o.onDraw === 'function') { 893 | var self = this; 894 | sto(function() { 895 | self._o.onDraw.call(self); 896 | }, 0); 897 | } 898 | }, 899 | 900 | adjustPosition: function() 901 | { 902 | var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect; 903 | 904 | if (this._o.container) return; 905 | 906 | this.el.style.position = 'absolute'; 907 | 908 | field = this._o.trigger; 909 | pEl = field; 910 | width = this.el.offsetWidth; 911 | height = this.el.offsetHeight; 912 | viewportWidth = window.innerWidth || document.documentElement.clientWidth; 913 | viewportHeight = window.innerHeight || document.documentElement.clientHeight; 914 | scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; 915 | 916 | if (typeof field.getBoundingClientRect === 'function') { 917 | clientRect = field.getBoundingClientRect(); 918 | left = clientRect.left + window.pageXOffset; 919 | top = clientRect.bottom + window.pageYOffset; 920 | } else { 921 | left = pEl.offsetLeft; 922 | top = pEl.offsetTop + pEl.offsetHeight; 923 | while((pEl = pEl.offsetParent)) { 924 | left += pEl.offsetLeft; 925 | top += pEl.offsetTop; 926 | } 927 | } 928 | 929 | // default position is bottom & left 930 | if ((this._o.reposition && left + width > viewportWidth) || 931 | ( 932 | this._o.position.indexOf('right') > -1 && 933 | left - width + field.offsetWidth > 0 934 | ) 935 | ) { 936 | left = left - width + field.offsetWidth; 937 | } 938 | if ((this._o.reposition && top + height > viewportHeight + scrollTop) || 939 | ( 940 | this._o.position.indexOf('top') > -1 && 941 | top - height - field.offsetHeight > 0 942 | ) 943 | ) { 944 | top = top - height - field.offsetHeight; 945 | } 946 | 947 | this.el.style.left = left + 'px'; 948 | this.el.style.top = top + 'px'; 949 | }, 950 | 951 | /** 952 | * render HTML for a particular month 953 | */ 954 | render: function(year, month) 955 | { 956 | var opts = this._o, 957 | now = new Date(), 958 | days = getDaysInMonth(year, month), 959 | before = new Date(year, month, 1).getDay(), 960 | data = [], 961 | row = []; 962 | setToStartOfDay(now); 963 | if (opts.firstDay > 0) { 964 | before -= opts.firstDay; 965 | if (before < 0) { 966 | before += 7; 967 | } 968 | } 969 | var cells = days + before, 970 | after = cells; 971 | while(after > 7) { 972 | after -= 7; 973 | } 974 | cells += 7 - after; 975 | for (var i = 0, r = 0; i < cells; i++) 976 | { 977 | var dayConfig, 978 | day = new Date(year, month, 1 + (i - before)), 979 | isSelected = isDate(this._d) ? compareDates(day, this._d) : false, 980 | isToday = compareDates(day, now), 981 | isEmpty = i < before || i >= (days + before), 982 | isStartRange = opts.startRange && compareDates(opts.startRange, day), 983 | isEndRange = opts.endRange && compareDates(opts.endRange, day), 984 | isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange, 985 | isDisabled = (opts.minDate && day < opts.minDate) || 986 | (opts.maxDate && day > opts.maxDate) || 987 | (opts.disableWeekends && isWeekend(day)) || 988 | (opts.disableDayFn && opts.disableDayFn(day)), 989 | dayConfig = { 990 | day: 1 + (i - before), 991 | month: month, 992 | year: year, 993 | isSelected: isSelected, 994 | isToday: isToday, 995 | isDisabled: isDisabled, 996 | isEmpty: isEmpty, 997 | isStartRange: isStartRange, 998 | isEndRange: isEndRange, 999 | isInRange: isInRange 1000 | }; 1001 | 1002 | row.push(renderDay(dayConfig)); 1003 | 1004 | if (++r === 7) { 1005 | if (opts.showWeekNumber) { 1006 | row.unshift(renderWeek(i - before, month, year)); 1007 | } 1008 | data.push(renderRow(row, opts.isRTL)); 1009 | row = []; 1010 | r = 0; 1011 | } 1012 | } 1013 | return renderTable(opts, data); 1014 | }, 1015 | 1016 | isVisible: function() 1017 | { 1018 | return this._v; 1019 | }, 1020 | 1021 | show: function() 1022 | { 1023 | if (!this._v) { 1024 | removeClass(this.el, 'is-hidden'); 1025 | this._v = true; 1026 | this.draw(); 1027 | if (this._o.bound) { 1028 | addEvent(document, 'click', this._onClick); 1029 | this.adjustPosition(); 1030 | } 1031 | if (typeof this._o.onOpen === 'function') { 1032 | this._o.onOpen.call(this); 1033 | } 1034 | } 1035 | }, 1036 | 1037 | hide: function() 1038 | { 1039 | var v = this._v; 1040 | if (v !== false) { 1041 | if (this._o.bound) { 1042 | removeEvent(document, 'click', this._onClick); 1043 | } 1044 | this.el.style.position = 'static'; // reset 1045 | this.el.style.left = 'auto'; 1046 | this.el.style.top = 'auto'; 1047 | addClass(this.el, 'is-hidden'); 1048 | this._v = false; 1049 | if (v !== undefined && typeof this._o.onClose === 'function') { 1050 | this._o.onClose.call(this); 1051 | } 1052 | } 1053 | }, 1054 | 1055 | /** 1056 | * GAME OVER 1057 | */ 1058 | destroy: function() 1059 | { 1060 | this.hide(); 1061 | removeEvent(this.el, 'mousedown', this._onMouseDown, true); 1062 | removeEvent(this.el, 'change', this._onChange); 1063 | if (this._o.field) { 1064 | removeEvent(this._o.field, 'change', this._onInputChange); 1065 | if (this._o.bound) { 1066 | removeEvent(this._o.trigger, 'click', this._onInputClick); 1067 | removeEvent(this._o.trigger, 'focus', this._onInputFocus); 1068 | removeEvent(this._o.trigger, 'blur', this._onInputBlur); 1069 | } 1070 | } 1071 | if (this.el.parentNode) { 1072 | this.el.parentNode.removeChild(this.el); 1073 | } 1074 | } 1075 | 1076 | }; 1077 | 1078 | return Pikaday; 1079 | 1080 | })); 1081 | -------------------------------------------------------------------------------- /firefox/index.js: -------------------------------------------------------------------------------- 1 | 2 | var pageMod = require("sdk/page-mod"); 3 | 4 | pageMod.PageMod({ 5 | include: [/.*github\.com\/.*\/.*/], 6 | contentScriptFile: ['./pikaday/pikaday.js', './github-time-travel.js'], 7 | contentStyleFile: ['./pikaday/pikaday.css'] 8 | }); 9 | -------------------------------------------------------------------------------- /firefox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Github Time Travel", 3 | "name": "github-commit-dates", 4 | "version": "1.0.1", 5 | "description": "Browse commits by date on Github", 6 | "main": "index.js", 7 | "author": "Nathan Cahill", 8 | "engines": { 9 | "firefox": ">=38.0a1" 10 | }, 11 | "license": "MIT" 12 | } 13 | --------------------------------------------------------------------------------