├── .gitignore ├── LICENSE ├── README.md ├── chrome ├── LinBiolinum_aS.ttf ├── background.js ├── buttons.js ├── content.js ├── d3.v7.min.js ├── downloads.png ├── icon.png ├── info.png ├── manifest.json ├── pikaday.css ├── pikaday.js ├── popup.html ├── popup.js └── wikiJourneyChrome.zip └── firefox ├── LinBiolinum_aS.ttf ├── background.js ├── buttons.js ├── content.js ├── d3.v7.min.js ├── downloads.png ├── icon.png ├── info.png ├── manifest.json ├── pikaday.css ├── pikaday.js ├── popup.html ├── popup.js └── wikiJourneyFirefox.zip /.gitignore: -------------------------------------------------------------------------------- 1 | /wiki-journey.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ege Demir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Wiki Journey** tracks your daily Wikipedia adventures and archives them in a tree format. 2 | 3 | Get it from [Chrome Web Store](https://chromewebstore.google.com/detail/wiki-journey/lehenbcbjcnkhkikgopniimobmmdcfog) or [Firefox Add-Ons](https://addons.mozilla.org/en-US/firefox/addon/wiki-journey/)! 4 | 5 | ![Screenshot 2024-04-19 181130(2)](https://github.com/demegire/wiki-journey/assets/62503047/46d64ddd-bc01-4c97-aba0-f0956e2b5e7c) 6 | ![New Project(2)](https://github.com/demegire/wiki-journey/assets/62503047/7a90edec-ae33-4ecc-889b-44a605008c31) 7 | -------------------------------------------------------------------------------- /chrome/LinBiolinum_aS.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/chrome/LinBiolinum_aS.ttf -------------------------------------------------------------------------------- /chrome/background.js: -------------------------------------------------------------------------------- 1 | let articlesTree = {}; 2 | let lastJourneyDate = ''; 3 | 4 | function toLocalISOString(date) { 5 | var localOffset = date.getTimezoneOffset() * 60000; // offset in milliseconds 6 | var localTime = new Date(date.getTime() - localOffset); 7 | return localTime.toISOString().split('T')[0]; 8 | } 9 | 10 | function addArticleToTree(title, url, parentUrl) { 11 | // If there's no parent, this is a root article 12 | if (!parentUrl) { 13 | articlesTree[url] = articlesTree[url] || { title, children: {} }; 14 | } else { 15 | // Recursively search for the parent node in the tree 16 | const parentNode = findParentNode(articlesTree, parentUrl); 17 | if (parentNode) { 18 | // Add the article under its parent 19 | parentNode.children[url] = parentNode.children[url] || { title, children: {} }; 20 | } else { 21 | // If the parent node is not found, treat it as a root article 22 | articlesTree[url] = articlesTree[url] || { title, children: {} }; 23 | } 24 | } 25 | // Save the updated tree to storage 26 | saveCurrentDayJourney() 27 | } 28 | 29 | function findParentNode(tree, parentUrl) { 30 | for (const url in tree) { 31 | if (url === parentUrl) { 32 | return tree[url]; 33 | } 34 | const foundInChildren = findParentNode(tree[url].children, parentUrl); 35 | if (foundInChildren) { 36 | return foundInChildren; 37 | } 38 | } 39 | return null; 40 | } 41 | 42 | async function handleMessage(request, sender, sendResponse) { 43 | if (request.article) { 44 | await checkNewDay(); // Ensure this completes before proceeding 45 | const { title, url, parent } = request.article; 46 | addArticleToTree(title, url, parent); 47 | } 48 | } 49 | 50 | 51 | function saveCurrentDayJourney() { 52 | const dateString = toLocalISOString(new Date()); 53 | const key = `journey_${dateString}`; 54 | chrome.storage.local.set({ [key]: articlesTree }); 55 | } 56 | 57 | // Load the articles tree from storage or start a new one each day 58 | function loadOrInitializeTree() { 59 | const dateString = toLocalISOString(new Date()); 60 | const key = `journey_${dateString}`; 61 | chrome.storage.local.get([key], (result) => { 62 | if (result[key]) { 63 | articlesTree = result[key]; 64 | } else { 65 | articlesTree = {}; // If there's no entry for today, start a new tree 66 | chrome.storage.local.set({ [key]: articlesTree }); 67 | } 68 | }); 69 | } 70 | 71 | function checkNewDay() { 72 | const dateString = toLocalISOString(new Date()); 73 | const key = `journey_${dateString}`; 74 | return new Promise((resolve, reject) => { 75 | chrome.storage.local.get([key], (result) => { 76 | if (result[key]) { 77 | articlesTree = result[key]; 78 | resolve(); 79 | } else { 80 | articlesTree = {}; // If there's no entry for today, start a new tree 81 | chrome.storage.local.set({ [key]: articlesTree }, resolve); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | 88 | loadOrInitializeTree(); 89 | 90 | chrome.runtime.onMessage.addListener(handleMessage); 91 | -------------------------------------------------------------------------------- /chrome/buttons.js: -------------------------------------------------------------------------------- 1 | document.getElementById('shareButton').addEventListener('click', function() { 2 | function drawImageWithText() { 3 | const svgElement = document.querySelector('svg'); 4 | const {width, height} = svgElement.getBoundingClientRect(); 5 | 6 | // Create a canvas element to render the SVG 7 | const canvas = document.createElement('canvas'); 8 | canvas.width = width; 9 | canvas.height = height; 10 | const ctx = canvas.getContext('2d'); 11 | 12 | // Fill the canvas with a white background 13 | ctx.fillStyle = 'white'; // Set fill color to white 14 | ctx.fillRect(0, 0, canvas.width, canvas.height); // Fill the entire canvas 15 | ctx.font = '16px "LinBiolinum_aS"'; 16 | 17 | 18 | // Use a Blob to convert SVG to a URL that can be drawn on Canvas 19 | let data = new XMLSerializer().serializeToString(svgElement); 20 | // Assume viewBox is like "viewBox = minX minY width height" 21 | const viewBox = svgElement.getAttribute('viewBox'); 22 | if (viewBox) { 23 | const viewBoxValues = viewBox.split(','); 24 | if (viewBoxValues.length === 4) { 25 | minX = parseFloat(viewBoxValues[0]); 26 | minY = parseFloat(viewBoxValues[1]); 27 | } 28 | } 29 | 30 | // Adjust the watermark position based on viewBox 31 | const watermarkString = `Wiki Journey`; 32 | data = data.replace('', `${watermarkString}`); 33 | const svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); 34 | const url = URL.createObjectURL(svgBlob); 35 | 36 | const image = new Image(); 37 | image.onload = function() { 38 | // Draw the SVG onto the Canvas 39 | ctx.drawImage(image, 0, 0); 40 | URL.revokeObjectURL(url); 41 | 42 | // Convert Canvas to PNG and trigger download 43 | canvas.toBlob(function(blob) { 44 | // Create a new URL for the blob 45 | const blobUrl = URL.createObjectURL(blob); 46 | 47 | // Create a temporary anchor element and trigger the download 48 | const downloadLink = document.createElement('a'); 49 | downloadLink.href = blobUrl; 50 | downloadLink.download = 'wikijourney.png'; // Specify the download file name 51 | document.body.appendChild(downloadLink); // Append to the document 52 | downloadLink.click(); // Trigger the download 53 | 54 | // Clean up by revoking the blob URL and removing the temporary link 55 | URL.revokeObjectURL(blobUrl); 56 | document.body.removeChild(downloadLink); 57 | }, 'image/png'); // Specify PNG format here 58 | }; 59 | image.src = url; 60 | } 61 | 62 | if ('fonts' in document) { 63 | document.fonts.load('16px "LinBiolinum_aS"').then(function () { 64 | // This ensures the font is available for the canvas 65 | drawImageWithText(); 66 | }); 67 | } else { 68 | // Fallback for browsers that do not support Font Loading API 69 | drawImageWithText(); 70 | } 71 | }); 72 | document.getElementById('infoButton').addEventListener('click', function() { 73 | const url = 'https://demegire.github.io/wikijourney'; // Replace with the URL you want to redirect to 74 | window.open(url, '_blank'); // Open in a new tab 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /chrome/content.js: -------------------------------------------------------------------------------- 1 | function isValidWikipediaArticle() { 2 | // Check if the URL follows the typical pattern for Wikipedia articles 3 | // This regex matches the main article pages but excludes special pages, user pages, etc. 4 | const urlRegex = /^https?:\/\/[a-z]+\.wikipedia\.org\/(?:wiki|zh-cn|zh-hk|zh-mo|zh-my|zh-sg|zh-tw)\/(?!Special:|User:|Wikipedia:|File:|MediaWiki:|Template:|Help:|Category:|Portal:|Draft:|TimedText:|Module:|Gadget:|Gadget_definition:|Education_Program:|Topic:|Book:|Special:Search|Special:RecentChanges).+/; 5 | return urlRegex.test(window.location.href); 6 | } 7 | 8 | function sendArticleInfo() { 9 | if (isValidWikipediaArticle()) { 10 | const title = document.querySelector('h1').innerText; 11 | const url = window.location.href; 12 | const parent = document.referrer.includes("wikipedia.org") && isValidWikipediaArticle(document.referrer) ? document.referrer : null; 13 | 14 | chrome.runtime.sendMessage({ 15 | article: { title, url, parent } 16 | }); 17 | } 18 | } 19 | 20 | // Execute when the script loads 21 | sendArticleInfo(); 22 | -------------------------------------------------------------------------------- /chrome/downloads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/chrome/downloads.png -------------------------------------------------------------------------------- /chrome/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/chrome/icon.png -------------------------------------------------------------------------------- /chrome/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/chrome/info.png -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Wiki Journey", 4 | "version": "1.1", 5 | "description": "Visualizes your daily Wikipedia adventures", 6 | "permissions": [ 7 | "storage" 8 | ], 9 | "host_permissions": [ 10 | "*://*.wikipedia.org/*" 11 | ], 12 | "background": { 13 | "service_worker": "background.js" 14 | }, 15 | "content_scripts": [ 16 | { 17 | "matches": ["*://*.wikipedia.org/*"], 18 | "js": ["content.js"] 19 | } 20 | ], 21 | "action": { 22 | "default_popup": "popup.html", 23 | "default_icon": "icon.png" 24 | }, 25 | "icons": { 26 | "48": "icon.png" 27 | }, 28 | "content_security_policy": { 29 | "extension_pages": "script-src 'self'; object-src 'self'" 30 | }, 31 | "web_accessible_resources": [ 32 | { 33 | "resources": ["d3.v7.min.js", "pikaday.js", "LinBiolinum_aS.ttf"], 34 | "matches": [""] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /chrome/pikaday.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /*! 4 | * Pikaday 5 | * Copyright © 2014 David Bushell | BSD & MIT license | https://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 | 30 | .pika-single.is-hidden { 31 | display: none; 32 | } 33 | 34 | .pika-single.is-bound { 35 | position: absolute; 36 | box-shadow: 0 5px 15px -5px rgba(0,0,0,.5); 37 | } 38 | 39 | .pika-lendar { 40 | float: left; 41 | width: 240px; 42 | margin: 8px; 43 | } 44 | 45 | .pika-title { 46 | position: relative; 47 | text-align: center; 48 | } 49 | 50 | .pika-label { 51 | display: inline-block; 52 | position: relative; 53 | z-index: 9999; 54 | overflow: hidden; 55 | margin: 0; 56 | padding: 5px 3px; 57 | font-size: 14px; 58 | line-height: 20px; 59 | font-weight: bold; 60 | background-color: #fff; 61 | } 62 | .pika-title select { 63 | cursor: pointer; 64 | position: absolute; 65 | z-index: 9998; 66 | margin: 0; 67 | left: 0; 68 | top: 5px; 69 | opacity: 0; 70 | } 71 | 72 | .pika-prev, 73 | .pika-next { 74 | display: block; 75 | cursor: pointer; 76 | position: relative; 77 | outline: none; 78 | border: 0; 79 | padding: 0; 80 | width: 20px; 81 | height: 30px; 82 | /* hide text using text-indent trick, using width value (it's enough) */ 83 | text-indent: 20px; 84 | white-space: nowrap; 85 | overflow: hidden; 86 | background-color: transparent; 87 | background-position: center center; 88 | background-repeat: no-repeat; 89 | background-size: 75% 75%; 90 | opacity: .5; 91 | } 92 | 93 | .pika-prev:hover, 94 | .pika-next:hover { 95 | opacity: 1; 96 | } 97 | 98 | .pika-prev, 99 | .is-rtl .pika-next { 100 | float: left; 101 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=='); 102 | } 103 | 104 | .pika-next, 105 | .is-rtl .pika-prev { 106 | float: right; 107 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII='); 108 | } 109 | 110 | .pika-prev.is-disabled, 111 | .pika-next.is-disabled { 112 | cursor: default; 113 | opacity: .2; 114 | } 115 | 116 | .pika-select { 117 | display: inline-block; 118 | } 119 | 120 | .pika-table { 121 | width: 100%; 122 | border-collapse: collapse; 123 | border-spacing: 0; 124 | border: 0; 125 | } 126 | 127 | .pika-table th, 128 | .pika-table td { 129 | width: 14.285714285714286%; 130 | padding: 0; 131 | } 132 | 133 | .pika-table th { 134 | color: #999; 135 | font-size: 12px; 136 | line-height: 25px; 137 | font-weight: bold; 138 | text-align: center; 139 | } 140 | 141 | .pika-button { 142 | cursor: pointer; 143 | display: block; 144 | box-sizing: border-box; 145 | -moz-box-sizing: border-box; 146 | outline: none; 147 | border: 0; 148 | margin: 0; 149 | width: 100%; 150 | padding: 5px; 151 | color: #666; 152 | font-size: 12px; 153 | line-height: 15px; 154 | text-align: right; 155 | background: #f5f5f5; 156 | height: initial; 157 | } 158 | 159 | .pika-week { 160 | font-size: 11px; 161 | color: #999; 162 | } 163 | 164 | .is-today .pika-button { 165 | color: #33aaff; 166 | font-weight: bold; 167 | } 168 | 169 | .is-selected .pika-button, 170 | .has-event .pika-button { 171 | color: #fff; 172 | font-weight: bold; 173 | background: #33aaff; 174 | box-shadow: inset 0 1px 3px #178fe5; 175 | border-radius: 3px; 176 | } 177 | 178 | .has-event .pika-button { 179 | background: #005da9; 180 | box-shadow: inset 0 1px 3px #0076c9; 181 | } 182 | 183 | .is-disabled .pika-button, 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 | .is-outside-current-month .pika-button { 210 | color: #999; 211 | opacity: .3; 212 | } 213 | 214 | .is-selection-disabled { 215 | pointer-events: none; 216 | cursor: default; 217 | } 218 | 219 | .pika-button:hover, 220 | .pika-row.pick-whole-week:hover .pika-button { 221 | color: #fff; 222 | background: #ff8000; 223 | box-shadow: none; 224 | border-radius: 3px; 225 | } 226 | 227 | /* styling for abbr */ 228 | .pika-table abbr { 229 | border-bottom: none; 230 | cursor: help; 231 | } 232 | -------------------------------------------------------------------------------- /chrome/pikaday.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Pikaday 3 | * 4 | * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/Pikaday/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 | trim = function(str) 63 | { 64 | return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,''); 65 | }, 66 | 67 | hasClass = function(el, cn) 68 | { 69 | return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1; 70 | }, 71 | 72 | addClass = function(el, cn) 73 | { 74 | if (!hasClass(el, cn)) { 75 | el.className = (el.className === '') ? cn : el.className + ' ' + cn; 76 | } 77 | }, 78 | 79 | removeClass = function(el, cn) 80 | { 81 | el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' ')); 82 | }, 83 | 84 | isArray = function(obj) 85 | { 86 | return (/Array/).test(Object.prototype.toString.call(obj)); 87 | }, 88 | 89 | isDate = function(obj) 90 | { 91 | return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); 92 | }, 93 | 94 | isWeekend = function(date) 95 | { 96 | var day = date.getDay(); 97 | return day === 0 || day === 6; 98 | }, 99 | 100 | isLeapYear = function(year) 101 | { 102 | // solution lifted from date.js (MIT license): https://github.com/datejs/Datejs 103 | return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); 104 | }, 105 | 106 | getDaysInMonth = function(year, month) 107 | { 108 | return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 109 | }, 110 | 111 | setToStartOfDay = function(date) 112 | { 113 | if (isDate(date)) date.setHours(0,0,0,0); 114 | }, 115 | 116 | compareDates = function(a,b) 117 | { 118 | // weak date comparison (use setToStartOfDay(date) to ensure correct result) 119 | return a.getTime() === b.getTime(); 120 | }, 121 | 122 | extend = function(to, from, overwrite) 123 | { 124 | var prop, hasProp; 125 | for (prop in from) { 126 | hasProp = to[prop] !== undefined; 127 | if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) { 128 | if (isDate(from[prop])) { 129 | if (overwrite) { 130 | to[prop] = new Date(from[prop].getTime()); 131 | } 132 | } 133 | else if (isArray(from[prop])) { 134 | if (overwrite) { 135 | to[prop] = from[prop].slice(0); 136 | } 137 | } else { 138 | to[prop] = extend({}, from[prop], overwrite); 139 | } 140 | } else if (overwrite || !hasProp) { 141 | to[prop] = from[prop]; 142 | } 143 | } 144 | return to; 145 | }, 146 | 147 | fireEvent = function(el, eventName, data) 148 | { 149 | var ev; 150 | 151 | if (document.createEvent) { 152 | ev = document.createEvent('HTMLEvents'); 153 | ev.initEvent(eventName, true, false); 154 | ev = extend(ev, data); 155 | el.dispatchEvent(ev); 156 | } else if (document.createEventObject) { 157 | ev = document.createEventObject(); 158 | ev = extend(ev, data); 159 | el.fireEvent('on' + eventName, ev); 160 | } 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 | // data-attribute on the input field with an aria assistance text (only applied when `bound` is set) 187 | ariaLabel: 'Use the arrow keys to pick a date', 188 | 189 | // position of the datepicker, relative to the field (default to bottom & left) 190 | // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position) 191 | position: 'bottom left', 192 | 193 | // automatically fit in the viewport even if it means repositioning from the position option 194 | reposition: true, 195 | 196 | // the default output format for `.toString()` and `field` value 197 | format: 'YYYY-MM-DD', 198 | 199 | // the toString function which gets passed a current date object and format 200 | // and returns a string 201 | toString: null, 202 | 203 | // used to create date object from current input string 204 | parse: null, 205 | 206 | // the initial date to view when first opened 207 | defaultDate: null, 208 | 209 | // make the `defaultDate` the initial selected value 210 | setDefaultDate: false, 211 | 212 | // first day of week (0: Sunday, 1: Monday etc) 213 | firstDay: 0, 214 | 215 | // minimum number of days in the week that gets week number one 216 | // default ISO 8601, week 01 is the week with the first Thursday (4) 217 | firstWeekOfYearMinDays: 4, 218 | 219 | // the default flag for moment's strict date parsing 220 | formatStrict: false, 221 | 222 | // the minimum/earliest date that can be selected 223 | minDate: null, 224 | // the maximum/latest date that can be selected 225 | maxDate: null, 226 | 227 | // number of years either side, or array of upper/lower range 228 | yearRange: 10, 229 | 230 | // show week numbers at head of row 231 | showWeekNumber: false, 232 | 233 | // Week picker mode 234 | pickWholeWeek: false, 235 | 236 | // used internally (don't config outside) 237 | minYear: 0, 238 | maxYear: 9999, 239 | minMonth: undefined, 240 | maxMonth: undefined, 241 | 242 | startRange: null, 243 | endRange: null, 244 | 245 | isRTL: false, 246 | 247 | // Additional text to append to the year in the calendar title 248 | yearSuffix: '', 249 | 250 | // Render the month after year in the calendar title 251 | showMonthAfterYear: false, 252 | 253 | // Render days of the calendar grid that fall in the next or previous month 254 | showDaysInNextAndPreviousMonths: false, 255 | 256 | // Allows user to select days that fall in the next or previous month 257 | enableSelectionDaysInNextAndPreviousMonths: false, 258 | 259 | // how many months are visible 260 | numberOfMonths: 1, 261 | 262 | // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`) 263 | // only used for the first display or when a selected date is not visible 264 | mainCalendar: 'left', 265 | 266 | // Specify a DOM element to render the calendar in 267 | container: undefined, 268 | 269 | // Blur field when date is selected 270 | blurFieldOnSelect : true, 271 | 272 | // internationalization 273 | i18n: { 274 | previousMonth : 'Previous Month', 275 | nextMonth : 'Next Month', 276 | months : ['January','February','March','April','May','June','July','August','September','October','November','December'], 277 | weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], 278 | weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] 279 | }, 280 | 281 | // Theme Classname 282 | theme: null, 283 | 284 | // events array 285 | events: [], 286 | 287 | // callback function 288 | onSelect: null, 289 | onOpen: null, 290 | onClose: null, 291 | onDraw: null, 292 | 293 | // Enable keyboard input 294 | keyboardInput: true 295 | }, 296 | 297 | 298 | /** 299 | * templating functions to abstract HTML rendering 300 | */ 301 | renderDayName = function(opts, day, abbr) 302 | { 303 | day += opts.firstDay; 304 | while (day >= 7) { 305 | day -= 7; 306 | } 307 | return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day]; 308 | }, 309 | 310 | renderDay = function(opts) 311 | { 312 | var arr = []; 313 | var ariaSelected = 'false'; 314 | if (opts.isEmpty) { 315 | if (opts.showDaysInNextAndPreviousMonths) { 316 | arr.push('is-outside-current-month'); 317 | 318 | if(!opts.enableSelectionDaysInNextAndPreviousMonths) { 319 | arr.push('is-selection-disabled'); 320 | } 321 | 322 | } else { 323 | return ''; 324 | } 325 | } 326 | if (opts.isDisabled) { 327 | arr.push('is-disabled'); 328 | } 329 | if (opts.isToday) { 330 | arr.push('is-today'); 331 | } 332 | if (opts.isSelected) { 333 | arr.push('is-selected'); 334 | ariaSelected = 'true'; 335 | } 336 | if (opts.hasEvent) { 337 | arr.push('has-event'); 338 | } 339 | if (opts.isInRange) { 340 | arr.push('is-inrange'); 341 | } 342 | if (opts.isStartRange) { 343 | arr.push('is-startrange'); 344 | } 345 | if (opts.isEndRange) { 346 | arr.push('is-endrange'); 347 | } 348 | return '' + 349 | '' + 353 | ''; 354 | }, 355 | 356 | isoWeek = function(date, firstWeekOfYearMinDays) { 357 | // Ensure we're at the start of the day. 358 | date.setHours(0, 0, 0, 0); 359 | 360 | // Thursday in current week decides the year because January 4th 361 | // is always in the first week according to ISO8601. 362 | var yearDay = date.getDate(), 363 | weekDay = date.getDay(), 364 | dayInFirstWeek = firstWeekOfYearMinDays, 365 | dayShift = dayInFirstWeek - 1, // counting starts at 0 366 | daysPerWeek = 7, 367 | prevWeekDay = function(day) { return (day + daysPerWeek - 1) % daysPerWeek; }; 368 | 369 | // Adjust to Thursday in week 1 and count number of weeks from date to week 1. 370 | date.setDate(yearDay + dayShift - prevWeekDay(weekDay)); 371 | 372 | var jan4th = new Date(date.getFullYear(), 0, dayInFirstWeek), 373 | msPerDay = 24 * 60 * 60 * 1000, 374 | daysBetween = (date.getTime() - jan4th.getTime()) / msPerDay, 375 | weekNum = 1 + Math.round((daysBetween - dayShift + prevWeekDay(jan4th.getDay())) / daysPerWeek); 376 | 377 | return weekNum; 378 | }, 379 | 380 | renderWeek = function (d, m, y, firstWeekOfYearMinDays) { 381 | var date = new Date(y, m, d), 382 | week = hasMoment ? moment(date).isoWeek() : isoWeek(date, firstWeekOfYearMinDays); 383 | 384 | return '' + week + ''; 385 | }, 386 | 387 | renderRow = function(days, isRTL, pickWholeWeek, isRowSelected) 388 | { 389 | return '' + (isRTL ? days.reverse() : days).join('') + ''; 390 | }, 391 | 392 | renderBody = function(rows) 393 | { 394 | return '' + rows.join('') + ''; 395 | }, 396 | 397 | renderHead = function(opts) 398 | { 399 | var i, arr = []; 400 | if (opts.showWeekNumber) { 401 | arr.push(''); 402 | } 403 | for (i = 0; i < 7; i++) { 404 | arr.push('' + renderDayName(opts, i, true) + ''); 405 | } 406 | return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; 407 | }, 408 | 409 | renderTitle = function(instance, c, year, month, refYear, randId) 410 | { 411 | var i, j, arr, 412 | opts = instance._o, 413 | isMinYear = year === opts.minYear, 414 | isMaxYear = year === opts.maxYear, 415 | html = '
', 416 | monthHtml, 417 | yearHtml, 418 | prev = true, 419 | next = true; 420 | 421 | for (arr = [], i = 0; i < 12; i++) { 422 | arr.push(''); 426 | } 427 | 428 | monthHtml = '
' + opts.i18n.months[month] + '
'; 429 | 430 | if (isArray(opts.yearRange)) { 431 | i = opts.yearRange[0]; 432 | j = opts.yearRange[1] + 1; 433 | } else { 434 | i = year - opts.yearRange; 435 | j = 1 + year + opts.yearRange; 436 | } 437 | 438 | for (arr = []; i < j && i <= opts.maxYear; i++) { 439 | if (i >= opts.minYear) { 440 | arr.push(''); 441 | } 442 | } 443 | yearHtml = '
' + year + opts.yearSuffix + '
'; 444 | 445 | if (opts.showMonthAfterYear) { 446 | html += yearHtml + monthHtml; 447 | } else { 448 | html += monthHtml + yearHtml; 449 | } 450 | 451 | if (isMinYear && (month === 0 || opts.minMonth >= month)) { 452 | prev = false; 453 | } 454 | 455 | if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { 456 | next = false; 457 | } 458 | 459 | if (c === 0) { 460 | html += ''; 461 | } 462 | if (c === (instance._o.numberOfMonths - 1) ) { 463 | html += ''; 464 | } 465 | 466 | return html += '
'; 467 | }, 468 | 469 | renderTable = function(opts, data, randId) 470 | { 471 | return '' + renderHead(opts) + renderBody(data) + '
'; 472 | }, 473 | 474 | 475 | /** 476 | * Pikaday constructor 477 | */ 478 | Pikaday = function(options) 479 | { 480 | var self = this, 481 | opts = self.config(options); 482 | 483 | self._onMouseDown = function(e) 484 | { 485 | if (!self._v) { 486 | return; 487 | } 488 | e = e || window.event; 489 | var target = e.target || e.srcElement; 490 | if (!target) { 491 | return; 492 | } 493 | 494 | if (!hasClass(target, 'is-disabled')) { 495 | if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) { 496 | self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day'))); 497 | if (opts.bound) { 498 | sto(function() { 499 | self.hide(); 500 | if (opts.blurFieldOnSelect && opts.field) { 501 | opts.field.blur(); 502 | } 503 | }, 100); 504 | } 505 | } 506 | else if (hasClass(target, 'pika-prev')) { 507 | self.prevMonth(); 508 | } 509 | else if (hasClass(target, 'pika-next')) { 510 | self.nextMonth(); 511 | } 512 | } 513 | if (!hasClass(target, 'pika-select')) { 514 | // if this is touch event prevent mouse events emulation 515 | if (e.preventDefault) { 516 | e.preventDefault(); 517 | } else { 518 | e.returnValue = false; 519 | return false; 520 | } 521 | } else { 522 | self._c = true; 523 | } 524 | }; 525 | 526 | self._onChange = function(e) 527 | { 528 | e = e || window.event; 529 | var target = e.target || e.srcElement; 530 | if (!target) { 531 | return; 532 | } 533 | if (hasClass(target, 'pika-select-month')) { 534 | self.gotoMonth(target.value); 535 | } 536 | else if (hasClass(target, 'pika-select-year')) { 537 | self.gotoYear(target.value); 538 | } 539 | }; 540 | 541 | self._onKeyChange = function(e) 542 | { 543 | e = e || window.event; 544 | 545 | if (self.isVisible()) { 546 | 547 | switch(e.keyCode){ 548 | case 13: 549 | case 27: 550 | if (opts.field) { 551 | opts.field.blur(); 552 | } 553 | break; 554 | case 37: 555 | self.adjustDate('subtract', 1); 556 | break; 557 | case 38: 558 | self.adjustDate('subtract', 7); 559 | break; 560 | case 39: 561 | self.adjustDate('add', 1); 562 | break; 563 | case 40: 564 | self.adjustDate('add', 7); 565 | break; 566 | case 8: 567 | case 46: 568 | self.setDate(null); 569 | break; 570 | } 571 | } 572 | }; 573 | 574 | self._parseFieldValue = function() 575 | { 576 | if (opts.parse) { 577 | return opts.parse(opts.field.value, opts.format); 578 | } else if (hasMoment) { 579 | var date = moment(opts.field.value, opts.format, opts.formatStrict); 580 | return (date && date.isValid()) ? date.toDate() : null; 581 | } else { 582 | return new Date(Date.parse(opts.field.value)); 583 | } 584 | }; 585 | 586 | self._onInputChange = function(e) 587 | { 588 | var date; 589 | 590 | if (e.firedBy === self) { 591 | return; 592 | } 593 | date = self._parseFieldValue(); 594 | if (isDate(date)) { 595 | self.setDate(date); 596 | } 597 | if (!self._v) { 598 | self.show(); 599 | } 600 | }; 601 | 602 | self._onInputFocus = function() 603 | { 604 | self.show(); 605 | }; 606 | 607 | self._onInputClick = function() 608 | { 609 | self.show(); 610 | }; 611 | 612 | self._onInputBlur = function() 613 | { 614 | // IE allows pika div to gain focus; catch blur the input field 615 | var pEl = document.activeElement; 616 | do { 617 | if (hasClass(pEl, 'pika-single')) { 618 | return; 619 | } 620 | } 621 | while ((pEl = pEl.parentNode)); 622 | 623 | if (!self._c) { 624 | self._b = sto(function() { 625 | self.hide(); 626 | }, 50); 627 | } 628 | self._c = false; 629 | }; 630 | 631 | self._onClick = function(e) 632 | { 633 | e = e || window.event; 634 | var target = e.target || e.srcElement, 635 | pEl = target; 636 | if (!target) { 637 | return; 638 | } 639 | if (!hasEventListeners && hasClass(target, 'pika-select')) { 640 | if (!target.onchange) { 641 | target.setAttribute('onchange', 'return;'); 642 | addEvent(target, 'change', self._onChange); 643 | } 644 | } 645 | do { 646 | if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) { 647 | return; 648 | } 649 | } 650 | while ((pEl = pEl.parentNode)); 651 | if (self._v && target !== opts.trigger && pEl !== opts.trigger) { 652 | self.hide(); 653 | } 654 | }; 655 | 656 | self.el = document.createElement('div'); 657 | self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : ''); 658 | 659 | addEvent(self.el, 'mousedown', self._onMouseDown, true); 660 | addEvent(self.el, 'touchend', self._onMouseDown, true); 661 | addEvent(self.el, 'change', self._onChange); 662 | 663 | if (opts.keyboardInput) { 664 | addEvent(document, 'keydown', self._onKeyChange); 665 | } 666 | 667 | if (opts.field) { 668 | if (opts.container) { 669 | opts.container.appendChild(self.el); 670 | } else if (opts.bound) { 671 | document.body.appendChild(self.el); 672 | } else { 673 | opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling); 674 | } 675 | addEvent(opts.field, 'change', self._onInputChange); 676 | 677 | if (!opts.defaultDate) { 678 | opts.defaultDate = self._parseFieldValue(); 679 | opts.setDefaultDate = true; 680 | } 681 | } 682 | 683 | var defDate = opts.defaultDate; 684 | 685 | if (isDate(defDate)) { 686 | if (opts.setDefaultDate) { 687 | self.setDate(defDate, true); 688 | } else { 689 | self.gotoDate(defDate); 690 | } 691 | } else { 692 | self.gotoDate(new Date()); 693 | } 694 | 695 | if (opts.bound) { 696 | this.hide(); 697 | self.el.className += ' is-bound'; 698 | addEvent(opts.trigger, 'click', self._onInputClick); 699 | addEvent(opts.trigger, 'focus', self._onInputFocus); 700 | addEvent(opts.trigger, 'blur', self._onInputBlur); 701 | } else { 702 | this.show(); 703 | } 704 | }; 705 | 706 | 707 | /** 708 | * public Pikaday API 709 | */ 710 | Pikaday.prototype = { 711 | 712 | 713 | /** 714 | * configure functionality 715 | */ 716 | config: function(options) 717 | { 718 | if (!this._o) { 719 | this._o = extend({}, defaults, true); 720 | } 721 | 722 | var opts = extend(this._o, options, true); 723 | 724 | opts.isRTL = !!opts.isRTL; 725 | 726 | opts.field = (opts.field && opts.field.nodeName) ? opts.field : null; 727 | 728 | opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null; 729 | 730 | opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field); 731 | 732 | opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field; 733 | 734 | opts.disableWeekends = !!opts.disableWeekends; 735 | 736 | opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null; 737 | 738 | var nom = parseInt(opts.numberOfMonths, 10) || 1; 739 | opts.numberOfMonths = nom > 4 ? 4 : nom; 740 | 741 | if (!isDate(opts.minDate)) { 742 | opts.minDate = false; 743 | } 744 | if (!isDate(opts.maxDate)) { 745 | opts.maxDate = false; 746 | } 747 | if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) { 748 | opts.maxDate = opts.minDate = false; 749 | } 750 | if (opts.minDate) { 751 | this.setMinDate(opts.minDate); 752 | } 753 | if (opts.maxDate) { 754 | this.setMaxDate(opts.maxDate); 755 | } 756 | 757 | if (isArray(opts.yearRange)) { 758 | var fallback = new Date().getFullYear() - 10; 759 | opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback; 760 | opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback; 761 | } else { 762 | opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange; 763 | if (opts.yearRange > 100) { 764 | opts.yearRange = 100; 765 | } 766 | } 767 | 768 | return opts; 769 | }, 770 | 771 | /** 772 | * return a formatted string of the current selection (using Moment.js if available) 773 | */ 774 | toString: function(format) 775 | { 776 | format = format || this._o.format; 777 | if (!isDate(this._d)) { 778 | return ''; 779 | } 780 | if (this._o.toString) { 781 | return this._o.toString(this._d, format); 782 | } 783 | if (hasMoment) { 784 | return moment(this._d).format(format); 785 | } 786 | return this._d.toDateString(); 787 | }, 788 | 789 | /** 790 | * return a Moment.js object of the current selection (if available) 791 | */ 792 | getMoment: function() 793 | { 794 | return hasMoment ? moment(this._d) : null; 795 | }, 796 | 797 | /** 798 | * set the current selection from a Moment.js object (if available) 799 | */ 800 | setMoment: function(date, preventOnSelect) 801 | { 802 | if (hasMoment && moment.isMoment(date)) { 803 | this.setDate(date.toDate(), preventOnSelect); 804 | } 805 | }, 806 | 807 | /** 808 | * return a Date object of the current selection 809 | */ 810 | getDate: function() 811 | { 812 | return isDate(this._d) ? new Date(this._d.getTime()) : null; 813 | }, 814 | 815 | /** 816 | * set the current selection 817 | */ 818 | setDate: function(date, preventOnSelect) 819 | { 820 | if (!date) { 821 | this._d = null; 822 | 823 | if (this._o.field) { 824 | this._o.field.value = ''; 825 | fireEvent(this._o.field, 'change', { firedBy: this }); 826 | } 827 | 828 | return this.draw(); 829 | } 830 | if (typeof date === 'string') { 831 | date = new Date(Date.parse(date)); 832 | } 833 | if (!isDate(date)) { 834 | return; 835 | } 836 | 837 | var min = this._o.minDate, 838 | max = this._o.maxDate; 839 | 840 | if (isDate(min) && date < min) { 841 | date = min; 842 | } else if (isDate(max) && date > max) { 843 | date = max; 844 | } 845 | 846 | this._d = new Date(date.getTime()); 847 | setToStartOfDay(this._d); 848 | this.gotoDate(this._d); 849 | 850 | if (this._o.field) { 851 | this._o.field.value = this.toString(); 852 | fireEvent(this._o.field, 'change', { firedBy: this }); 853 | } 854 | if (!preventOnSelect && typeof this._o.onSelect === 'function') { 855 | this._o.onSelect.call(this, this.getDate()); 856 | } 857 | }, 858 | 859 | /** 860 | * clear and reset the date 861 | */ 862 | clear: function() 863 | { 864 | this.setDate(null); 865 | }, 866 | 867 | /** 868 | * change view to a specific date 869 | */ 870 | gotoDate: function(date) 871 | { 872 | var newCalendar = true; 873 | 874 | if (!isDate(date)) { 875 | return; 876 | } 877 | 878 | if (this.calendars) { 879 | var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1), 880 | lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1), 881 | visibleDate = date.getTime(); 882 | // get the end of the month 883 | lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1); 884 | lastVisibleDate.setDate(lastVisibleDate.getDate()-1); 885 | newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate); 886 | } 887 | 888 | if (newCalendar) { 889 | this.calendars = [{ 890 | month: date.getMonth(), 891 | year: date.getFullYear() 892 | }]; 893 | if (this._o.mainCalendar === 'right') { 894 | this.calendars[0].month += 1 - this._o.numberOfMonths; 895 | } 896 | } 897 | 898 | this.adjustCalendars(); 899 | }, 900 | 901 | adjustDate: function(sign, days) { 902 | 903 | var day = this.getDate() || new Date(); 904 | var difference = parseInt(days)*24*60*60*1000; 905 | 906 | var newDay; 907 | 908 | if (sign === 'add') { 909 | newDay = new Date(day.valueOf() + difference); 910 | } else if (sign === 'subtract') { 911 | newDay = new Date(day.valueOf() - difference); 912 | } 913 | 914 | this.setDate(newDay); 915 | }, 916 | 917 | adjustCalendars: function() { 918 | this.calendars[0] = adjustCalendar(this.calendars[0]); 919 | for (var c = 1; c < this._o.numberOfMonths; c++) { 920 | this.calendars[c] = adjustCalendar({ 921 | month: this.calendars[0].month + c, 922 | year: this.calendars[0].year 923 | }); 924 | } 925 | this.draw(); 926 | }, 927 | 928 | gotoToday: function() 929 | { 930 | this.gotoDate(new Date()); 931 | }, 932 | 933 | /** 934 | * change view to a specific month (zero-index, e.g. 0: January) 935 | */ 936 | gotoMonth: function(month) 937 | { 938 | if (!isNaN(month)) { 939 | this.calendars[0].month = parseInt(month, 10); 940 | this.adjustCalendars(); 941 | } 942 | }, 943 | 944 | nextMonth: function() 945 | { 946 | this.calendars[0].month++; 947 | this.adjustCalendars(); 948 | }, 949 | 950 | prevMonth: function() 951 | { 952 | this.calendars[0].month--; 953 | this.adjustCalendars(); 954 | }, 955 | 956 | /** 957 | * change view to a specific full year (e.g. "2012") 958 | */ 959 | gotoYear: function(year) 960 | { 961 | if (!isNaN(year)) { 962 | this.calendars[0].year = parseInt(year, 10); 963 | this.adjustCalendars(); 964 | } 965 | }, 966 | 967 | /** 968 | * change the minDate 969 | */ 970 | setMinDate: function(value) 971 | { 972 | if(value instanceof Date) { 973 | setToStartOfDay(value); 974 | this._o.minDate = value; 975 | this._o.minYear = value.getFullYear(); 976 | this._o.minMonth = value.getMonth(); 977 | } else { 978 | this._o.minDate = defaults.minDate; 979 | this._o.minYear = defaults.minYear; 980 | this._o.minMonth = defaults.minMonth; 981 | this._o.startRange = defaults.startRange; 982 | } 983 | 984 | this.draw(); 985 | }, 986 | 987 | /** 988 | * change the maxDate 989 | */ 990 | setMaxDate: function(value) 991 | { 992 | if(value instanceof Date) { 993 | setToStartOfDay(value); 994 | this._o.maxDate = value; 995 | this._o.maxYear = value.getFullYear(); 996 | this._o.maxMonth = value.getMonth(); 997 | } else { 998 | this._o.maxDate = defaults.maxDate; 999 | this._o.maxYear = defaults.maxYear; 1000 | this._o.maxMonth = defaults.maxMonth; 1001 | this._o.endRange = defaults.endRange; 1002 | } 1003 | 1004 | this.draw(); 1005 | }, 1006 | 1007 | setStartRange: function(value) 1008 | { 1009 | this._o.startRange = value; 1010 | }, 1011 | 1012 | setEndRange: function(value) 1013 | { 1014 | this._o.endRange = value; 1015 | }, 1016 | 1017 | /** 1018 | * refresh the HTML 1019 | */ 1020 | draw: function(force) 1021 | { 1022 | if (!this._v && !force) { 1023 | return; 1024 | } 1025 | var opts = this._o, 1026 | minYear = opts.minYear, 1027 | maxYear = opts.maxYear, 1028 | minMonth = opts.minMonth, 1029 | maxMonth = opts.maxMonth, 1030 | html = '', 1031 | randId; 1032 | 1033 | if (this._y <= minYear) { 1034 | this._y = minYear; 1035 | if (!isNaN(minMonth) && this._m < minMonth) { 1036 | this._m = minMonth; 1037 | } 1038 | } 1039 | if (this._y >= maxYear) { 1040 | this._y = maxYear; 1041 | if (!isNaN(maxMonth) && this._m > maxMonth) { 1042 | this._m = maxMonth; 1043 | } 1044 | } 1045 | 1046 | for (var c = 0; c < opts.numberOfMonths; c++) { 1047 | randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2); 1048 | html += '
' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId) + '
'; 1049 | } 1050 | 1051 | this.el.innerHTML = html; 1052 | 1053 | if (opts.bound) { 1054 | if(opts.field.type !== 'hidden') { 1055 | sto(function() { 1056 | opts.trigger.focus(); 1057 | }, 1); 1058 | } 1059 | } 1060 | 1061 | if (typeof this._o.onDraw === 'function') { 1062 | this._o.onDraw(this); 1063 | } 1064 | 1065 | if (opts.bound) { 1066 | // let the screen reader user know to use arrow keys 1067 | opts.field.setAttribute('aria-label', opts.ariaLabel); 1068 | } 1069 | }, 1070 | 1071 | adjustPosition: function() 1072 | { 1073 | var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect, leftAligned, bottomAligned; 1074 | 1075 | if (this._o.container) return; 1076 | 1077 | this.el.style.position = 'absolute'; 1078 | 1079 | field = this._o.trigger; 1080 | pEl = field; 1081 | width = this.el.offsetWidth; 1082 | height = this.el.offsetHeight; 1083 | viewportWidth = window.innerWidth || document.documentElement.clientWidth; 1084 | viewportHeight = window.innerHeight || document.documentElement.clientHeight; 1085 | scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; 1086 | leftAligned = true; 1087 | bottomAligned = true; 1088 | 1089 | if (typeof field.getBoundingClientRect === 'function') { 1090 | clientRect = field.getBoundingClientRect(); 1091 | left = clientRect.left + window.pageXOffset; 1092 | top = clientRect.bottom + window.pageYOffset; 1093 | } else { 1094 | left = pEl.offsetLeft; 1095 | top = pEl.offsetTop + pEl.offsetHeight; 1096 | while((pEl = pEl.offsetParent)) { 1097 | left += pEl.offsetLeft; 1098 | top += pEl.offsetTop; 1099 | } 1100 | } 1101 | 1102 | // default position is bottom & left 1103 | if ((this._o.reposition && left + width > viewportWidth) || 1104 | ( 1105 | this._o.position.indexOf('right') > -1 && 1106 | left - width + field.offsetWidth > 0 1107 | ) 1108 | ) { 1109 | left = left - width + field.offsetWidth; 1110 | leftAligned = false; 1111 | } 1112 | if ((this._o.reposition && top + height > viewportHeight + scrollTop) || 1113 | ( 1114 | this._o.position.indexOf('top') > -1 && 1115 | top - height - field.offsetHeight > 0 1116 | ) 1117 | ) { 1118 | top = top - height - field.offsetHeight; 1119 | bottomAligned = false; 1120 | } 1121 | 1122 | this.el.style.left = left + 'px'; 1123 | this.el.style.top = top + 'px'; 1124 | 1125 | addClass(this.el, leftAligned ? 'left-aligned' : 'right-aligned'); 1126 | addClass(this.el, bottomAligned ? 'bottom-aligned' : 'top-aligned'); 1127 | removeClass(this.el, !leftAligned ? 'left-aligned' : 'right-aligned'); 1128 | removeClass(this.el, !bottomAligned ? 'bottom-aligned' : 'top-aligned'); 1129 | }, 1130 | 1131 | /** 1132 | * render HTML for a particular month 1133 | */ 1134 | render: function(year, month, randId) 1135 | { 1136 | var opts = this._o, 1137 | now = new Date(), 1138 | days = getDaysInMonth(year, month), 1139 | before = new Date(year, month, 1).getDay(), 1140 | data = [], 1141 | row = []; 1142 | setToStartOfDay(now); 1143 | if (opts.firstDay > 0) { 1144 | before -= opts.firstDay; 1145 | if (before < 0) { 1146 | before += 7; 1147 | } 1148 | } 1149 | var previousMonth = month === 0 ? 11 : month - 1, 1150 | nextMonth = month === 11 ? 0 : month + 1, 1151 | yearOfPreviousMonth = month === 0 ? year - 1 : year, 1152 | yearOfNextMonth = month === 11 ? year + 1 : year, 1153 | daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth); 1154 | var cells = days + before, 1155 | after = cells; 1156 | while(after > 7) { 1157 | after -= 7; 1158 | } 1159 | cells += 7 - after; 1160 | var isWeekSelected = false; 1161 | for (var i = 0, r = 0; i < cells; i++) 1162 | { 1163 | var day = new Date(year, month, 1 + (i - before)), 1164 | isSelected = isDate(this._d) ? compareDates(day, this._d) : false, 1165 | isToday = compareDates(day, now), 1166 | hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false, 1167 | isEmpty = i < before || i >= (days + before), 1168 | dayNumber = 1 + (i - before), 1169 | monthNumber = month, 1170 | yearNumber = year, 1171 | isStartRange = opts.startRange && compareDates(opts.startRange, day), 1172 | isEndRange = opts.endRange && compareDates(opts.endRange, day), 1173 | isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange, 1174 | isDisabled = (opts.minDate && day < opts.minDate) || 1175 | (opts.maxDate && day > opts.maxDate) || 1176 | (opts.disableWeekends && isWeekend(day)) || 1177 | (opts.disableDayFn && opts.disableDayFn(day)); 1178 | 1179 | if (isEmpty) { 1180 | if (i < before) { 1181 | dayNumber = daysInPreviousMonth + dayNumber; 1182 | monthNumber = previousMonth; 1183 | yearNumber = yearOfPreviousMonth; 1184 | } else { 1185 | dayNumber = dayNumber - days; 1186 | monthNumber = nextMonth; 1187 | yearNumber = yearOfNextMonth; 1188 | } 1189 | } 1190 | 1191 | var dayConfig = { 1192 | day: dayNumber, 1193 | month: monthNumber, 1194 | year: yearNumber, 1195 | hasEvent: hasEvent, 1196 | isSelected: isSelected, 1197 | isToday: isToday, 1198 | isDisabled: isDisabled, 1199 | isEmpty: isEmpty, 1200 | isStartRange: isStartRange, 1201 | isEndRange: isEndRange, 1202 | isInRange: isInRange, 1203 | showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths, 1204 | enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths 1205 | }; 1206 | 1207 | if (opts.pickWholeWeek && isSelected) { 1208 | isWeekSelected = true; 1209 | } 1210 | 1211 | row.push(renderDay(dayConfig)); 1212 | 1213 | if (++r === 7) { 1214 | if (opts.showWeekNumber) { 1215 | row.unshift(renderWeek(i - before, month, year, opts.firstWeekOfYearMinDays)); 1216 | } 1217 | data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected)); 1218 | row = []; 1219 | r = 0; 1220 | isWeekSelected = false; 1221 | } 1222 | } 1223 | return renderTable(opts, data, randId); 1224 | }, 1225 | 1226 | isVisible: function() 1227 | { 1228 | return this._v; 1229 | }, 1230 | 1231 | show: function() 1232 | { 1233 | if (!this.isVisible()) { 1234 | this._v = true; 1235 | this.draw(); 1236 | removeClass(this.el, 'is-hidden'); 1237 | if (this._o.bound) { 1238 | addEvent(document, 'click', this._onClick); 1239 | this.adjustPosition(); 1240 | } 1241 | if (typeof this._o.onOpen === 'function') { 1242 | this._o.onOpen.call(this); 1243 | } 1244 | } 1245 | }, 1246 | 1247 | hide: function() 1248 | { 1249 | var v = this._v; 1250 | if (v !== false) { 1251 | if (this._o.bound) { 1252 | removeEvent(document, 'click', this._onClick); 1253 | } 1254 | 1255 | if (!this._o.container) { 1256 | this.el.style.position = 'static'; // reset 1257 | this.el.style.left = 'auto'; 1258 | this.el.style.top = 'auto'; 1259 | } 1260 | addClass(this.el, 'is-hidden'); 1261 | this._v = false; 1262 | if (v !== undefined && typeof this._o.onClose === 'function') { 1263 | this._o.onClose.call(this); 1264 | } 1265 | } 1266 | }, 1267 | 1268 | /** 1269 | * GAME OVER 1270 | */ 1271 | destroy: function() 1272 | { 1273 | var opts = this._o; 1274 | 1275 | this.hide(); 1276 | removeEvent(this.el, 'mousedown', this._onMouseDown, true); 1277 | removeEvent(this.el, 'touchend', this._onMouseDown, true); 1278 | removeEvent(this.el, 'change', this._onChange); 1279 | if (opts.keyboardInput) { 1280 | removeEvent(document, 'keydown', this._onKeyChange); 1281 | } 1282 | if (opts.field) { 1283 | removeEvent(opts.field, 'change', this._onInputChange); 1284 | if (opts.bound) { 1285 | removeEvent(opts.trigger, 'click', this._onInputClick); 1286 | removeEvent(opts.trigger, 'focus', this._onInputFocus); 1287 | removeEvent(opts.trigger, 'blur', this._onInputBlur); 1288 | } 1289 | } 1290 | if (this.el.parentNode) { 1291 | this.el.parentNode.removeChild(this.el); 1292 | } 1293 | } 1294 | 1295 | }; 1296 | 1297 | return Pikaday; 1298 | })); 1299 | -------------------------------------------------------------------------------- /chrome/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wiki Journey 5 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |

Wiki Journey

68 | 69 |
70 | 73 | 76 |
77 |
78 |
79 |
80 | 81 | 82 | d.children ? Object.values(d.children) : []); 35 | 36 | const dx = 64; 37 | const dy = 192; 38 | const leftTextOffset = 128; // Estimated offset to accommodate leftmost text 39 | const width = (root.height + 1) * dy + leftTextOffset; 40 | const tree = d3.tree().nodeSize([dx, dy]); 41 | 42 | root.sort((a, b) => d3.ascending(a.data.title, b.data.title)); 43 | tree(root); 44 | 45 | let x0 = Infinity; 46 | let x1 = -x0; 47 | root.each(d => { 48 | if (d.x > x1) x1 = d.x; 49 | if (d.x < x0) x0 = d.x; 50 | }); 51 | 52 | const height = x1 - x0 + dx * 2; 53 | 54 | // Since you're in a browser extension popup, use d3.select instead of d3.create 55 | const svg = d3.select("#graph").append("svg") 56 | .attr("width", width) 57 | .attr("height", height) 58 | .attr("viewBox", [-leftTextOffset, x0 - dx, width + leftTextOffset, height]) // Adjust viewBox by leftTextOffset 59 | .style("font", "18px sans-serif"); 60 | 61 | const link = svg.append("g") 62 | .attr("fill", "none") 63 | .attr("stroke", "#555") 64 | .attr("stroke-opacity", 0.4) 65 | .attr("stroke-width", 1.5) 66 | .selectAll("path") 67 | .data(root.links()) 68 | .join("path") 69 | .attr("d", d3.linkHorizontal() 70 | .x(d => d.y) 71 | .y(d => d.x)); 72 | 73 | const node = svg.append("g") 74 | .attr("stroke-linejoin", "round") 75 | .attr("stroke-width", 3) 76 | .selectAll("g") 77 | .data(root.descendants()) 78 | .join("g") 79 | .attr("transform", d => `translate(${d.y},${d.x})`); 80 | 81 | node.append("circle") 82 | .attr("fill", d => d.children ? "#555" : "#999") 83 | .attr("r", 2.5); 84 | 85 | node.append("text") 86 | .attr("dy", "0.31em") 87 | .attr("x", d => d.children ? -6 : 6) 88 | .attr("text-anchor", d => d.children ? "end" : "start") 89 | .text(d => d.data.title) 90 | .attr("fill", "black") // Set the text color 91 | .clone(true).lower() 92 | .attr("stroke", "white"); // Remove the stroke or set to a contrasting color if needed 93 | 94 | 95 | // Add this SVG to your popup 96 | document.querySelector("#graph").appendChild(svg.node()); 97 | } 98 | const today = new Date(); 99 | document.getElementById('datePicker').value = toLocalISOString(today); // Set date picker value to today 100 | loadJourney(toLocalISOString(today)); 101 | }); 102 | -------------------------------------------------------------------------------- /chrome/wikiJourneyChrome.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/chrome/wikiJourneyChrome.zip -------------------------------------------------------------------------------- /firefox/LinBiolinum_aS.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/firefox/LinBiolinum_aS.ttf -------------------------------------------------------------------------------- /firefox/background.js: -------------------------------------------------------------------------------- 1 | let articlesTree = {}; 2 | let lastJourneyDate = ''; 3 | 4 | function toLocalISOString(date) { 5 | var localOffset = date.getTimezoneOffset() * 60000; // offset in milliseconds 6 | var localTime = new Date(date.getTime() - localOffset); 7 | return localTime.toISOString().split('T')[0]; 8 | } 9 | 10 | function addArticleToTree(title, url, parentUrl) { 11 | // If there's no parent, this is a root article 12 | if (!parentUrl) { 13 | articlesTree[url] = articlesTree[url] || { title, children: {} }; 14 | } else { 15 | // Recursively search for the parent node in the tree 16 | const parentNode = findParentNode(articlesTree, parentUrl); 17 | if (parentNode) { 18 | // Add the article under its parent 19 | parentNode.children[url] = parentNode.children[url] || { title, children: {} }; 20 | } else { 21 | // If the parent node is not found, treat it as a root article 22 | articlesTree[url] = articlesTree[url] || { title, children: {} }; 23 | } 24 | } 25 | // Save the updated tree to storage 26 | saveCurrentDayJourney() 27 | } 28 | 29 | function findParentNode(tree, parentUrl) { 30 | for (const url in tree) { 31 | if (url === parentUrl) { 32 | return tree[url]; 33 | } 34 | const foundInChildren = findParentNode(tree[url].children, parentUrl); 35 | if (foundInChildren) { 36 | return foundInChildren; 37 | } 38 | } 39 | return null; 40 | } 41 | 42 | async function handleMessage(request, sender, sendResponse) { 43 | if (request.article) { 44 | await checkNewDay(); // Ensure this completes before proceeding 45 | const { title, url, parent } = request.article; 46 | addArticleToTree(title, url, parent); 47 | } 48 | } 49 | 50 | 51 | function saveCurrentDayJourney() { 52 | const dateString = toLocalISOString(new Date()); 53 | const key = `journey_${dateString}`; 54 | browser.storage.local.set({ [key]: articlesTree }); 55 | } 56 | 57 | // Load the articles tree from storage or start a new one each day 58 | function loadOrInitializeTree() { 59 | const dateString = toLocalISOString(new Date()); 60 | const key = `journey_${dateString}`; 61 | browser.storage.local.get([key], (result) => { 62 | if (result[key]) { 63 | articlesTree = result[key]; 64 | } else { 65 | articlesTree = {}; // If there's no entry for today, start a new tree 66 | browser.storage.local.set({ [key]: articlesTree }); 67 | } 68 | }); 69 | } 70 | 71 | function checkNewDay() { 72 | const dateString = toLocalISOString(new Date()); 73 | const key = `journey_${dateString}`; 74 | return new Promise((resolve, reject) => { 75 | browser.storage.local.get([key], (result) => { 76 | if (result[key]) { 77 | articlesTree = result[key]; 78 | resolve(); 79 | } else { 80 | articlesTree = {}; // If there's no entry for today, start a new tree 81 | browser.storage.local.set({ [key]: articlesTree }, resolve); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | loadOrInitializeTree(); 88 | 89 | browser.runtime.onMessage.addListener(handleMessage); 90 | -------------------------------------------------------------------------------- /firefox/buttons.js: -------------------------------------------------------------------------------- 1 | document.getElementById('shareButton').addEventListener('click', function() { 2 | function drawImageWithText() { 3 | const svgElement = document.querySelector('svg'); 4 | const {width, height} = svgElement.getBoundingClientRect(); 5 | 6 | // Create a canvas element to render the SVG 7 | const canvas = document.createElement('canvas'); 8 | canvas.width = width; 9 | canvas.height = height; 10 | const ctx = canvas.getContext('2d'); 11 | 12 | // Fill the canvas with a white background 13 | ctx.fillStyle = 'white'; // Set fill color to white 14 | ctx.fillRect(0, 0, canvas.width, canvas.height); // Fill the entire canvas 15 | ctx.font = '16px "LinBiolinum_aS"'; 16 | 17 | 18 | // Use a Blob to convert SVG to a URL that can be drawn on Canvas 19 | let data = new XMLSerializer().serializeToString(svgElement); 20 | // Assume viewBox is like "viewBox = minX minY width height" 21 | const viewBox = svgElement.getAttribute('viewBox'); 22 | if (viewBox) { 23 | const viewBoxValues = viewBox.split(','); 24 | if (viewBoxValues.length === 4) { 25 | minX = parseFloat(viewBoxValues[0]); 26 | minY = parseFloat(viewBoxValues[1]); 27 | } 28 | } 29 | 30 | // Adjust the watermark position based on viewBox 31 | const watermarkString = `Wiki Journey`; 32 | data = data.replace('', `${watermarkString}`); 33 | const svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'}); 34 | const url = URL.createObjectURL(svgBlob); 35 | 36 | const image = new Image(); 37 | image.onload = function() { 38 | // Draw the SVG onto the Canvas 39 | ctx.drawImage(image, 0, 0); 40 | URL.revokeObjectURL(url); 41 | 42 | // Convert Canvas to PNG and trigger download 43 | canvas.toBlob(function(blob) { 44 | // Create a new URL for the blob 45 | const blobUrl = URL.createObjectURL(blob); 46 | 47 | // Create a temporary anchor element and trigger the download 48 | const downloadLink = document.createElement('a'); 49 | downloadLink.href = blobUrl; 50 | downloadLink.download = 'wikijourney.png'; // Specify the download file name 51 | document.body.appendChild(downloadLink); // Append to the document 52 | downloadLink.click(); // Trigger the download 53 | 54 | // Clean up by revoking the blob URL and removing the temporary link 55 | URL.revokeObjectURL(blobUrl); 56 | document.body.removeChild(downloadLink); 57 | }, 'image/png'); // Specify PNG format here 58 | }; 59 | image.src = url; 60 | } 61 | 62 | if ('fonts' in document) { 63 | document.fonts.load('16px "LinBiolinum_aS"').then(function () { 64 | // This ensures the font is available for the canvas 65 | drawImageWithText(); 66 | }); 67 | } else { 68 | // Fallback for browsers that do not support Font Loading API 69 | drawImageWithText(); 70 | } 71 | }); 72 | document.getElementById('infoButton').addEventListener('click', function() { 73 | const url = 'https://demegire.github.io/wikijourney'; // Replace with the URL you want to redirect to 74 | window.open(url, '_blank'); // Open in a new tab 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /firefox/content.js: -------------------------------------------------------------------------------- 1 | function isValidWikipediaArticle() { 2 | // Check if the URL follows the typical pattern for Wikipedia articles 3 | // This regex matches the main article pages but excludes special pages, user pages, etc. 4 | const urlRegex = /^https?:\/\/[a-z]+\.wikipedia\.org\/(?:wiki|zh-cn|zh-hk|zh-mo|zh-my|zh-sg|zh-tw)\/(?!Special:|User:|Wikipedia:|File:|MediaWiki:|Template:|Help:|Category:|Portal:|Draft:|TimedText:|Module:|Gadget:|Gadget_definition:|Education_Program:|Topic:|Book:|Special:Search|Special:RecentChanges).+/; 5 | return urlRegex.test(window.location.href); 6 | } 7 | 8 | function sendArticleInfo() { 9 | if (isValidWikipediaArticle()) { 10 | const title = document.querySelector('h1').innerText; 11 | const url = window.location.href; 12 | const parent = document.referrer.includes("wikipedia.org") && isValidWikipediaArticle(document.referrer) ? document.referrer : null; 13 | 14 | browser.runtime.sendMessage({ 15 | article: { title, url, parent } 16 | }); 17 | } 18 | } 19 | 20 | // Execute when the script loads 21 | sendArticleInfo(); 22 | -------------------------------------------------------------------------------- /firefox/downloads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/firefox/downloads.png -------------------------------------------------------------------------------- /firefox/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/firefox/icon.png -------------------------------------------------------------------------------- /firefox/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/firefox/info.png -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Wiki Journey", 4 | "version": "1.2", 5 | "description": "Visualizes your daily Wikipedia adventures", 6 | "permissions": [ 7 | "tabs", 8 | "storage", 9 | "*://*.wikipedia.org/*" 10 | ], 11 | "background": { 12 | "scripts": ["background.js"] 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": ["*://*.wikipedia.org/*"], 17 | "js": ["content.js"] 18 | } 19 | ], 20 | "browser_action": { 21 | "default_popup": "popup.html", 22 | "default_icon": "icon.png" 23 | }, 24 | "icons": { 25 | "48": "icon.png" 26 | }, 27 | "content_security_policy": "script-src 'self'; object-src 'self'", 28 | "web_accessible_resources": [ 29 | "d3.v7.min.js", 30 | "pikaday.js", 31 | "LinBiolinum_aS.ttf" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /firefox/pikaday.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /*! 4 | * Pikaday 5 | * Copyright © 2014 David Bushell | BSD & MIT license | https://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 | 30 | .pika-single.is-hidden { 31 | display: none; 32 | } 33 | 34 | .pika-single.is-bound { 35 | position: absolute; 36 | box-shadow: 0 5px 15px -5px rgba(0,0,0,.5); 37 | } 38 | 39 | .pika-lendar { 40 | float: left; 41 | width: 240px; 42 | margin: 8px; 43 | } 44 | 45 | .pika-title { 46 | position: relative; 47 | text-align: center; 48 | } 49 | 50 | .pika-label { 51 | display: inline-block; 52 | position: relative; 53 | z-index: 9999; 54 | overflow: hidden; 55 | margin: 0; 56 | padding: 5px 3px; 57 | font-size: 14px; 58 | line-height: 20px; 59 | font-weight: bold; 60 | background-color: #fff; 61 | } 62 | .pika-title select { 63 | cursor: pointer; 64 | position: absolute; 65 | z-index: 9998; 66 | margin: 0; 67 | left: 0; 68 | top: 5px; 69 | opacity: 0; 70 | } 71 | 72 | .pika-prev, 73 | .pika-next { 74 | display: block; 75 | cursor: pointer; 76 | position: relative; 77 | outline: none; 78 | border: 0; 79 | padding: 0; 80 | width: 20px; 81 | height: 30px; 82 | /* hide text using text-indent trick, using width value (it's enough) */ 83 | text-indent: 20px; 84 | white-space: nowrap; 85 | overflow: hidden; 86 | background-color: transparent; 87 | background-position: center center; 88 | background-repeat: no-repeat; 89 | background-size: 75% 75%; 90 | opacity: .5; 91 | } 92 | 93 | .pika-prev:hover, 94 | .pika-next:hover { 95 | opacity: 1; 96 | } 97 | 98 | .pika-prev, 99 | .is-rtl .pika-next { 100 | float: left; 101 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=='); 102 | } 103 | 104 | .pika-next, 105 | .is-rtl .pika-prev { 106 | float: right; 107 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII='); 108 | } 109 | 110 | .pika-prev.is-disabled, 111 | .pika-next.is-disabled { 112 | cursor: default; 113 | opacity: .2; 114 | } 115 | 116 | .pika-select { 117 | display: inline-block; 118 | } 119 | 120 | .pika-table { 121 | width: 100%; 122 | border-collapse: collapse; 123 | border-spacing: 0; 124 | border: 0; 125 | } 126 | 127 | .pika-table th, 128 | .pika-table td { 129 | width: 14.285714285714286%; 130 | padding: 0; 131 | } 132 | 133 | .pika-table th { 134 | color: #999; 135 | font-size: 12px; 136 | line-height: 25px; 137 | font-weight: bold; 138 | text-align: center; 139 | } 140 | 141 | .pika-button { 142 | cursor: pointer; 143 | display: block; 144 | box-sizing: border-box; 145 | -moz-box-sizing: border-box; 146 | outline: none; 147 | border: 0; 148 | margin: 0; 149 | width: 100%; 150 | padding: 5px; 151 | color: #666; 152 | font-size: 12px; 153 | line-height: 15px; 154 | text-align: right; 155 | background: #f5f5f5; 156 | height: initial; 157 | } 158 | 159 | .pika-week { 160 | font-size: 11px; 161 | color: #999; 162 | } 163 | 164 | .is-today .pika-button { 165 | color: #33aaff; 166 | font-weight: bold; 167 | } 168 | 169 | .is-selected .pika-button, 170 | .has-event .pika-button { 171 | color: #fff; 172 | font-weight: bold; 173 | background: #33aaff; 174 | box-shadow: inset 0 1px 3px #178fe5; 175 | border-radius: 3px; 176 | } 177 | 178 | .has-event .pika-button { 179 | background: #005da9; 180 | box-shadow: inset 0 1px 3px #0076c9; 181 | } 182 | 183 | .is-disabled .pika-button, 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 | .is-outside-current-month .pika-button { 210 | color: #999; 211 | opacity: .3; 212 | } 213 | 214 | .is-selection-disabled { 215 | pointer-events: none; 216 | cursor: default; 217 | } 218 | 219 | .pika-button:hover, 220 | .pika-row.pick-whole-week:hover .pika-button { 221 | color: #fff; 222 | background: #ff8000; 223 | box-shadow: none; 224 | border-radius: 3px; 225 | } 226 | 227 | /* styling for abbr */ 228 | .pika-table abbr { 229 | border-bottom: none; 230 | cursor: help; 231 | } 232 | -------------------------------------------------------------------------------- /firefox/pikaday.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Pikaday 3 | * 4 | * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/Pikaday/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 | trim = function(str) 63 | { 64 | return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,''); 65 | }, 66 | 67 | hasClass = function(el, cn) 68 | { 69 | return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1; 70 | }, 71 | 72 | addClass = function(el, cn) 73 | { 74 | if (!hasClass(el, cn)) { 75 | el.className = (el.className === '') ? cn : el.className + ' ' + cn; 76 | } 77 | }, 78 | 79 | removeClass = function(el, cn) 80 | { 81 | el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' ')); 82 | }, 83 | 84 | isArray = function(obj) 85 | { 86 | return (/Array/).test(Object.prototype.toString.call(obj)); 87 | }, 88 | 89 | isDate = function(obj) 90 | { 91 | return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); 92 | }, 93 | 94 | isWeekend = function(date) 95 | { 96 | var day = date.getDay(); 97 | return day === 0 || day === 6; 98 | }, 99 | 100 | isLeapYear = function(year) 101 | { 102 | // solution lifted from date.js (MIT license): https://github.com/datejs/Datejs 103 | return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); 104 | }, 105 | 106 | getDaysInMonth = function(year, month) 107 | { 108 | return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 109 | }, 110 | 111 | setToStartOfDay = function(date) 112 | { 113 | if (isDate(date)) date.setHours(0,0,0,0); 114 | }, 115 | 116 | compareDates = function(a,b) 117 | { 118 | // weak date comparison (use setToStartOfDay(date) to ensure correct result) 119 | return a.getTime() === b.getTime(); 120 | }, 121 | 122 | extend = function(to, from, overwrite) 123 | { 124 | var prop, hasProp; 125 | for (prop in from) { 126 | hasProp = to[prop] !== undefined; 127 | if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) { 128 | if (isDate(from[prop])) { 129 | if (overwrite) { 130 | to[prop] = new Date(from[prop].getTime()); 131 | } 132 | } 133 | else if (isArray(from[prop])) { 134 | if (overwrite) { 135 | to[prop] = from[prop].slice(0); 136 | } 137 | } else { 138 | to[prop] = extend({}, from[prop], overwrite); 139 | } 140 | } else if (overwrite || !hasProp) { 141 | to[prop] = from[prop]; 142 | } 143 | } 144 | return to; 145 | }, 146 | 147 | fireEvent = function(el, eventName, data) 148 | { 149 | var ev; 150 | 151 | if (document.createEvent) { 152 | ev = document.createEvent('HTMLEvents'); 153 | ev.initEvent(eventName, true, false); 154 | ev = extend(ev, data); 155 | el.dispatchEvent(ev); 156 | } else if (document.createEventObject) { 157 | ev = document.createEventObject(); 158 | ev = extend(ev, data); 159 | el.fireEvent('on' + eventName, ev); 160 | } 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 | // data-attribute on the input field with an aria assistance text (only applied when `bound` is set) 187 | ariaLabel: 'Use the arrow keys to pick a date', 188 | 189 | // position of the datepicker, relative to the field (default to bottom & left) 190 | // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position) 191 | position: 'bottom left', 192 | 193 | // automatically fit in the viewport even if it means repositioning from the position option 194 | reposition: true, 195 | 196 | // the default output format for `.toString()` and `field` value 197 | format: 'YYYY-MM-DD', 198 | 199 | // the toString function which gets passed a current date object and format 200 | // and returns a string 201 | toString: null, 202 | 203 | // used to create date object from current input string 204 | parse: null, 205 | 206 | // the initial date to view when first opened 207 | defaultDate: null, 208 | 209 | // make the `defaultDate` the initial selected value 210 | setDefaultDate: false, 211 | 212 | // first day of week (0: Sunday, 1: Monday etc) 213 | firstDay: 0, 214 | 215 | // minimum number of days in the week that gets week number one 216 | // default ISO 8601, week 01 is the week with the first Thursday (4) 217 | firstWeekOfYearMinDays: 4, 218 | 219 | // the default flag for moment's strict date parsing 220 | formatStrict: false, 221 | 222 | // the minimum/earliest date that can be selected 223 | minDate: null, 224 | // the maximum/latest date that can be selected 225 | maxDate: null, 226 | 227 | // number of years either side, or array of upper/lower range 228 | yearRange: 10, 229 | 230 | // show week numbers at head of row 231 | showWeekNumber: false, 232 | 233 | // Week picker mode 234 | pickWholeWeek: false, 235 | 236 | // used internally (don't config outside) 237 | minYear: 0, 238 | maxYear: 9999, 239 | minMonth: undefined, 240 | maxMonth: undefined, 241 | 242 | startRange: null, 243 | endRange: null, 244 | 245 | isRTL: false, 246 | 247 | // Additional text to append to the year in the calendar title 248 | yearSuffix: '', 249 | 250 | // Render the month after year in the calendar title 251 | showMonthAfterYear: false, 252 | 253 | // Render days of the calendar grid that fall in the next or previous month 254 | showDaysInNextAndPreviousMonths: false, 255 | 256 | // Allows user to select days that fall in the next or previous month 257 | enableSelectionDaysInNextAndPreviousMonths: false, 258 | 259 | // how many months are visible 260 | numberOfMonths: 1, 261 | 262 | // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`) 263 | // only used for the first display or when a selected date is not visible 264 | mainCalendar: 'left', 265 | 266 | // Specify a DOM element to render the calendar in 267 | container: undefined, 268 | 269 | // Blur field when date is selected 270 | blurFieldOnSelect : true, 271 | 272 | // internationalization 273 | i18n: { 274 | previousMonth : 'Previous Month', 275 | nextMonth : 'Next Month', 276 | months : ['January','February','March','April','May','June','July','August','September','October','November','December'], 277 | weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], 278 | weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] 279 | }, 280 | 281 | // Theme Classname 282 | theme: null, 283 | 284 | // events array 285 | events: [], 286 | 287 | // callback function 288 | onSelect: null, 289 | onOpen: null, 290 | onClose: null, 291 | onDraw: null, 292 | 293 | // Enable keyboard input 294 | keyboardInput: true 295 | }, 296 | 297 | 298 | /** 299 | * templating functions to abstract HTML rendering 300 | */ 301 | renderDayName = function(opts, day, abbr) 302 | { 303 | day += opts.firstDay; 304 | while (day >= 7) { 305 | day -= 7; 306 | } 307 | return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day]; 308 | }, 309 | 310 | renderDay = function(opts) 311 | { 312 | var arr = []; 313 | var ariaSelected = 'false'; 314 | if (opts.isEmpty) { 315 | if (opts.showDaysInNextAndPreviousMonths) { 316 | arr.push('is-outside-current-month'); 317 | 318 | if(!opts.enableSelectionDaysInNextAndPreviousMonths) { 319 | arr.push('is-selection-disabled'); 320 | } 321 | 322 | } else { 323 | return ''; 324 | } 325 | } 326 | if (opts.isDisabled) { 327 | arr.push('is-disabled'); 328 | } 329 | if (opts.isToday) { 330 | arr.push('is-today'); 331 | } 332 | if (opts.isSelected) { 333 | arr.push('is-selected'); 334 | ariaSelected = 'true'; 335 | } 336 | if (opts.hasEvent) { 337 | arr.push('has-event'); 338 | } 339 | if (opts.isInRange) { 340 | arr.push('is-inrange'); 341 | } 342 | if (opts.isStartRange) { 343 | arr.push('is-startrange'); 344 | } 345 | if (opts.isEndRange) { 346 | arr.push('is-endrange'); 347 | } 348 | return '' + 349 | '' + 353 | ''; 354 | }, 355 | 356 | isoWeek = function(date, firstWeekOfYearMinDays) { 357 | // Ensure we're at the start of the day. 358 | date.setHours(0, 0, 0, 0); 359 | 360 | // Thursday in current week decides the year because January 4th 361 | // is always in the first week according to ISO8601. 362 | var yearDay = date.getDate(), 363 | weekDay = date.getDay(), 364 | dayInFirstWeek = firstWeekOfYearMinDays, 365 | dayShift = dayInFirstWeek - 1, // counting starts at 0 366 | daysPerWeek = 7, 367 | prevWeekDay = function(day) { return (day + daysPerWeek - 1) % daysPerWeek; }; 368 | 369 | // Adjust to Thursday in week 1 and count number of weeks from date to week 1. 370 | date.setDate(yearDay + dayShift - prevWeekDay(weekDay)); 371 | 372 | var jan4th = new Date(date.getFullYear(), 0, dayInFirstWeek), 373 | msPerDay = 24 * 60 * 60 * 1000, 374 | daysBetween = (date.getTime() - jan4th.getTime()) / msPerDay, 375 | weekNum = 1 + Math.round((daysBetween - dayShift + prevWeekDay(jan4th.getDay())) / daysPerWeek); 376 | 377 | return weekNum; 378 | }, 379 | 380 | renderWeek = function (d, m, y, firstWeekOfYearMinDays) { 381 | var date = new Date(y, m, d), 382 | week = hasMoment ? moment(date).isoWeek() : isoWeek(date, firstWeekOfYearMinDays); 383 | 384 | return '' + week + ''; 385 | }, 386 | 387 | renderRow = function(days, isRTL, pickWholeWeek, isRowSelected) 388 | { 389 | return '' + (isRTL ? days.reverse() : days).join('') + ''; 390 | }, 391 | 392 | renderBody = function(rows) 393 | { 394 | return '' + rows.join('') + ''; 395 | }, 396 | 397 | renderHead = function(opts) 398 | { 399 | var i, arr = []; 400 | if (opts.showWeekNumber) { 401 | arr.push(''); 402 | } 403 | for (i = 0; i < 7; i++) { 404 | arr.push('' + renderDayName(opts, i, true) + ''); 405 | } 406 | return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; 407 | }, 408 | 409 | renderTitle = function(instance, c, year, month, refYear, randId) 410 | { 411 | var i, j, arr, 412 | opts = instance._o, 413 | isMinYear = year === opts.minYear, 414 | isMaxYear = year === opts.maxYear, 415 | html = '
', 416 | monthHtml, 417 | yearHtml, 418 | prev = true, 419 | next = true; 420 | 421 | for (arr = [], i = 0; i < 12; i++) { 422 | arr.push(''); 426 | } 427 | 428 | monthHtml = '
' + opts.i18n.months[month] + '
'; 429 | 430 | if (isArray(opts.yearRange)) { 431 | i = opts.yearRange[0]; 432 | j = opts.yearRange[1] + 1; 433 | } else { 434 | i = year - opts.yearRange; 435 | j = 1 + year + opts.yearRange; 436 | } 437 | 438 | for (arr = []; i < j && i <= opts.maxYear; i++) { 439 | if (i >= opts.minYear) { 440 | arr.push(''); 441 | } 442 | } 443 | yearHtml = '
' + year + opts.yearSuffix + '
'; 444 | 445 | if (opts.showMonthAfterYear) { 446 | html += yearHtml + monthHtml; 447 | } else { 448 | html += monthHtml + yearHtml; 449 | } 450 | 451 | if (isMinYear && (month === 0 || opts.minMonth >= month)) { 452 | prev = false; 453 | } 454 | 455 | if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { 456 | next = false; 457 | } 458 | 459 | if (c === 0) { 460 | html += ''; 461 | } 462 | if (c === (instance._o.numberOfMonths - 1) ) { 463 | html += ''; 464 | } 465 | 466 | return html += '
'; 467 | }, 468 | 469 | renderTable = function(opts, data, randId) 470 | { 471 | return '' + renderHead(opts) + renderBody(data) + '
'; 472 | }, 473 | 474 | 475 | /** 476 | * Pikaday constructor 477 | */ 478 | Pikaday = function(options) 479 | { 480 | var self = this, 481 | opts = self.config(options); 482 | 483 | self._onMouseDown = function(e) 484 | { 485 | if (!self._v) { 486 | return; 487 | } 488 | e = e || window.event; 489 | var target = e.target || e.srcElement; 490 | if (!target) { 491 | return; 492 | } 493 | 494 | if (!hasClass(target, 'is-disabled')) { 495 | if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) { 496 | self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day'))); 497 | if (opts.bound) { 498 | sto(function() { 499 | self.hide(); 500 | if (opts.blurFieldOnSelect && opts.field) { 501 | opts.field.blur(); 502 | } 503 | }, 100); 504 | } 505 | } 506 | else if (hasClass(target, 'pika-prev')) { 507 | self.prevMonth(); 508 | } 509 | else if (hasClass(target, 'pika-next')) { 510 | self.nextMonth(); 511 | } 512 | } 513 | if (!hasClass(target, 'pika-select')) { 514 | // if this is touch event prevent mouse events emulation 515 | if (e.preventDefault) { 516 | e.preventDefault(); 517 | } else { 518 | e.returnValue = false; 519 | return false; 520 | } 521 | } else { 522 | self._c = true; 523 | } 524 | }; 525 | 526 | self._onChange = function(e) 527 | { 528 | e = e || window.event; 529 | var target = e.target || e.srcElement; 530 | if (!target) { 531 | return; 532 | } 533 | if (hasClass(target, 'pika-select-month')) { 534 | self.gotoMonth(target.value); 535 | } 536 | else if (hasClass(target, 'pika-select-year')) { 537 | self.gotoYear(target.value); 538 | } 539 | }; 540 | 541 | self._onKeyChange = function(e) 542 | { 543 | e = e || window.event; 544 | 545 | if (self.isVisible()) { 546 | 547 | switch(e.keyCode){ 548 | case 13: 549 | case 27: 550 | if (opts.field) { 551 | opts.field.blur(); 552 | } 553 | break; 554 | case 37: 555 | self.adjustDate('subtract', 1); 556 | break; 557 | case 38: 558 | self.adjustDate('subtract', 7); 559 | break; 560 | case 39: 561 | self.adjustDate('add', 1); 562 | break; 563 | case 40: 564 | self.adjustDate('add', 7); 565 | break; 566 | case 8: 567 | case 46: 568 | self.setDate(null); 569 | break; 570 | } 571 | } 572 | }; 573 | 574 | self._parseFieldValue = function() 575 | { 576 | if (opts.parse) { 577 | return opts.parse(opts.field.value, opts.format); 578 | } else if (hasMoment) { 579 | var date = moment(opts.field.value, opts.format, opts.formatStrict); 580 | return (date && date.isValid()) ? date.toDate() : null; 581 | } else { 582 | return new Date(Date.parse(opts.field.value)); 583 | } 584 | }; 585 | 586 | self._onInputChange = function(e) 587 | { 588 | var date; 589 | 590 | if (e.firedBy === self) { 591 | return; 592 | } 593 | date = self._parseFieldValue(); 594 | if (isDate(date)) { 595 | self.setDate(date); 596 | } 597 | if (!self._v) { 598 | self.show(); 599 | } 600 | }; 601 | 602 | self._onInputFocus = function() 603 | { 604 | self.show(); 605 | }; 606 | 607 | self._onInputClick = function() 608 | { 609 | self.show(); 610 | }; 611 | 612 | self._onInputBlur = function() 613 | { 614 | // IE allows pika div to gain focus; catch blur the input field 615 | var pEl = document.activeElement; 616 | do { 617 | if (hasClass(pEl, 'pika-single')) { 618 | return; 619 | } 620 | } 621 | while ((pEl = pEl.parentNode)); 622 | 623 | if (!self._c) { 624 | self._b = sto(function() { 625 | self.hide(); 626 | }, 50); 627 | } 628 | self._c = false; 629 | }; 630 | 631 | self._onClick = function(e) 632 | { 633 | e = e || window.event; 634 | var target = e.target || e.srcElement, 635 | pEl = target; 636 | if (!target) { 637 | return; 638 | } 639 | if (!hasEventListeners && hasClass(target, 'pika-select')) { 640 | if (!target.onchange) { 641 | target.setAttribute('onchange', 'return;'); 642 | addEvent(target, 'change', self._onChange); 643 | } 644 | } 645 | do { 646 | if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) { 647 | return; 648 | } 649 | } 650 | while ((pEl = pEl.parentNode)); 651 | if (self._v && target !== opts.trigger && pEl !== opts.trigger) { 652 | self.hide(); 653 | } 654 | }; 655 | 656 | self.el = document.createElement('div'); 657 | self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : ''); 658 | 659 | addEvent(self.el, 'mousedown', self._onMouseDown, true); 660 | addEvent(self.el, 'touchend', self._onMouseDown, true); 661 | addEvent(self.el, 'change', self._onChange); 662 | 663 | if (opts.keyboardInput) { 664 | addEvent(document, 'keydown', self._onKeyChange); 665 | } 666 | 667 | if (opts.field) { 668 | if (opts.container) { 669 | opts.container.appendChild(self.el); 670 | } else if (opts.bound) { 671 | document.body.appendChild(self.el); 672 | } else { 673 | opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling); 674 | } 675 | addEvent(opts.field, 'change', self._onInputChange); 676 | 677 | if (!opts.defaultDate) { 678 | opts.defaultDate = self._parseFieldValue(); 679 | opts.setDefaultDate = true; 680 | } 681 | } 682 | 683 | var defDate = opts.defaultDate; 684 | 685 | if (isDate(defDate)) { 686 | if (opts.setDefaultDate) { 687 | self.setDate(defDate, true); 688 | } else { 689 | self.gotoDate(defDate); 690 | } 691 | } else { 692 | self.gotoDate(new Date()); 693 | } 694 | 695 | if (opts.bound) { 696 | this.hide(); 697 | self.el.className += ' is-bound'; 698 | addEvent(opts.trigger, 'click', self._onInputClick); 699 | addEvent(opts.trigger, 'focus', self._onInputFocus); 700 | addEvent(opts.trigger, 'blur', self._onInputBlur); 701 | } else { 702 | this.show(); 703 | } 704 | }; 705 | 706 | 707 | /** 708 | * public Pikaday API 709 | */ 710 | Pikaday.prototype = { 711 | 712 | 713 | /** 714 | * configure functionality 715 | */ 716 | config: function(options) 717 | { 718 | if (!this._o) { 719 | this._o = extend({}, defaults, true); 720 | } 721 | 722 | var opts = extend(this._o, options, true); 723 | 724 | opts.isRTL = !!opts.isRTL; 725 | 726 | opts.field = (opts.field && opts.field.nodeName) ? opts.field : null; 727 | 728 | opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null; 729 | 730 | opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field); 731 | 732 | opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field; 733 | 734 | opts.disableWeekends = !!opts.disableWeekends; 735 | 736 | opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null; 737 | 738 | var nom = parseInt(opts.numberOfMonths, 10) || 1; 739 | opts.numberOfMonths = nom > 4 ? 4 : nom; 740 | 741 | if (!isDate(opts.minDate)) { 742 | opts.minDate = false; 743 | } 744 | if (!isDate(opts.maxDate)) { 745 | opts.maxDate = false; 746 | } 747 | if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) { 748 | opts.maxDate = opts.minDate = false; 749 | } 750 | if (opts.minDate) { 751 | this.setMinDate(opts.minDate); 752 | } 753 | if (opts.maxDate) { 754 | this.setMaxDate(opts.maxDate); 755 | } 756 | 757 | if (isArray(opts.yearRange)) { 758 | var fallback = new Date().getFullYear() - 10; 759 | opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback; 760 | opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback; 761 | } else { 762 | opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange; 763 | if (opts.yearRange > 100) { 764 | opts.yearRange = 100; 765 | } 766 | } 767 | 768 | return opts; 769 | }, 770 | 771 | /** 772 | * return a formatted string of the current selection (using Moment.js if available) 773 | */ 774 | toString: function(format) 775 | { 776 | format = format || this._o.format; 777 | if (!isDate(this._d)) { 778 | return ''; 779 | } 780 | if (this._o.toString) { 781 | return this._o.toString(this._d, format); 782 | } 783 | if (hasMoment) { 784 | return moment(this._d).format(format); 785 | } 786 | return this._d.toDateString(); 787 | }, 788 | 789 | /** 790 | * return a Moment.js object of the current selection (if available) 791 | */ 792 | getMoment: function() 793 | { 794 | return hasMoment ? moment(this._d) : null; 795 | }, 796 | 797 | /** 798 | * set the current selection from a Moment.js object (if available) 799 | */ 800 | setMoment: function(date, preventOnSelect) 801 | { 802 | if (hasMoment && moment.isMoment(date)) { 803 | this.setDate(date.toDate(), preventOnSelect); 804 | } 805 | }, 806 | 807 | /** 808 | * return a Date object of the current selection 809 | */ 810 | getDate: function() 811 | { 812 | return isDate(this._d) ? new Date(this._d.getTime()) : null; 813 | }, 814 | 815 | /** 816 | * set the current selection 817 | */ 818 | setDate: function(date, preventOnSelect) 819 | { 820 | if (!date) { 821 | this._d = null; 822 | 823 | if (this._o.field) { 824 | this._o.field.value = ''; 825 | fireEvent(this._o.field, 'change', { firedBy: this }); 826 | } 827 | 828 | return this.draw(); 829 | } 830 | if (typeof date === 'string') { 831 | date = new Date(Date.parse(date)); 832 | } 833 | if (!isDate(date)) { 834 | return; 835 | } 836 | 837 | var min = this._o.minDate, 838 | max = this._o.maxDate; 839 | 840 | if (isDate(min) && date < min) { 841 | date = min; 842 | } else if (isDate(max) && date > max) { 843 | date = max; 844 | } 845 | 846 | this._d = new Date(date.getTime()); 847 | setToStartOfDay(this._d); 848 | this.gotoDate(this._d); 849 | 850 | if (this._o.field) { 851 | this._o.field.value = this.toString(); 852 | fireEvent(this._o.field, 'change', { firedBy: this }); 853 | } 854 | if (!preventOnSelect && typeof this._o.onSelect === 'function') { 855 | this._o.onSelect.call(this, this.getDate()); 856 | } 857 | }, 858 | 859 | /** 860 | * clear and reset the date 861 | */ 862 | clear: function() 863 | { 864 | this.setDate(null); 865 | }, 866 | 867 | /** 868 | * change view to a specific date 869 | */ 870 | gotoDate: function(date) 871 | { 872 | var newCalendar = true; 873 | 874 | if (!isDate(date)) { 875 | return; 876 | } 877 | 878 | if (this.calendars) { 879 | var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1), 880 | lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1), 881 | visibleDate = date.getTime(); 882 | // get the end of the month 883 | lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1); 884 | lastVisibleDate.setDate(lastVisibleDate.getDate()-1); 885 | newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate); 886 | } 887 | 888 | if (newCalendar) { 889 | this.calendars = [{ 890 | month: date.getMonth(), 891 | year: date.getFullYear() 892 | }]; 893 | if (this._o.mainCalendar === 'right') { 894 | this.calendars[0].month += 1 - this._o.numberOfMonths; 895 | } 896 | } 897 | 898 | this.adjustCalendars(); 899 | }, 900 | 901 | adjustDate: function(sign, days) { 902 | 903 | var day = this.getDate() || new Date(); 904 | var difference = parseInt(days)*24*60*60*1000; 905 | 906 | var newDay; 907 | 908 | if (sign === 'add') { 909 | newDay = new Date(day.valueOf() + difference); 910 | } else if (sign === 'subtract') { 911 | newDay = new Date(day.valueOf() - difference); 912 | } 913 | 914 | this.setDate(newDay); 915 | }, 916 | 917 | adjustCalendars: function() { 918 | this.calendars[0] = adjustCalendar(this.calendars[0]); 919 | for (var c = 1; c < this._o.numberOfMonths; c++) { 920 | this.calendars[c] = adjustCalendar({ 921 | month: this.calendars[0].month + c, 922 | year: this.calendars[0].year 923 | }); 924 | } 925 | this.draw(); 926 | }, 927 | 928 | gotoToday: function() 929 | { 930 | this.gotoDate(new Date()); 931 | }, 932 | 933 | /** 934 | * change view to a specific month (zero-index, e.g. 0: January) 935 | */ 936 | gotoMonth: function(month) 937 | { 938 | if (!isNaN(month)) { 939 | this.calendars[0].month = parseInt(month, 10); 940 | this.adjustCalendars(); 941 | } 942 | }, 943 | 944 | nextMonth: function() 945 | { 946 | this.calendars[0].month++; 947 | this.adjustCalendars(); 948 | }, 949 | 950 | prevMonth: function() 951 | { 952 | this.calendars[0].month--; 953 | this.adjustCalendars(); 954 | }, 955 | 956 | /** 957 | * change view to a specific full year (e.g. "2012") 958 | */ 959 | gotoYear: function(year) 960 | { 961 | if (!isNaN(year)) { 962 | this.calendars[0].year = parseInt(year, 10); 963 | this.adjustCalendars(); 964 | } 965 | }, 966 | 967 | /** 968 | * change the minDate 969 | */ 970 | setMinDate: function(value) 971 | { 972 | if(value instanceof Date) { 973 | setToStartOfDay(value); 974 | this._o.minDate = value; 975 | this._o.minYear = value.getFullYear(); 976 | this._o.minMonth = value.getMonth(); 977 | } else { 978 | this._o.minDate = defaults.minDate; 979 | this._o.minYear = defaults.minYear; 980 | this._o.minMonth = defaults.minMonth; 981 | this._o.startRange = defaults.startRange; 982 | } 983 | 984 | this.draw(); 985 | }, 986 | 987 | /** 988 | * change the maxDate 989 | */ 990 | setMaxDate: function(value) 991 | { 992 | if(value instanceof Date) { 993 | setToStartOfDay(value); 994 | this._o.maxDate = value; 995 | this._o.maxYear = value.getFullYear(); 996 | this._o.maxMonth = value.getMonth(); 997 | } else { 998 | this._o.maxDate = defaults.maxDate; 999 | this._o.maxYear = defaults.maxYear; 1000 | this._o.maxMonth = defaults.maxMonth; 1001 | this._o.endRange = defaults.endRange; 1002 | } 1003 | 1004 | this.draw(); 1005 | }, 1006 | 1007 | setStartRange: function(value) 1008 | { 1009 | this._o.startRange = value; 1010 | }, 1011 | 1012 | setEndRange: function(value) 1013 | { 1014 | this._o.endRange = value; 1015 | }, 1016 | 1017 | /** 1018 | * refresh the HTML 1019 | */ 1020 | draw: function(force) 1021 | { 1022 | if (!this._v && !force) { 1023 | return; 1024 | } 1025 | var opts = this._o, 1026 | minYear = opts.minYear, 1027 | maxYear = opts.maxYear, 1028 | minMonth = opts.minMonth, 1029 | maxMonth = opts.maxMonth, 1030 | html = '', 1031 | randId; 1032 | 1033 | if (this._y <= minYear) { 1034 | this._y = minYear; 1035 | if (!isNaN(minMonth) && this._m < minMonth) { 1036 | this._m = minMonth; 1037 | } 1038 | } 1039 | if (this._y >= maxYear) { 1040 | this._y = maxYear; 1041 | if (!isNaN(maxMonth) && this._m > maxMonth) { 1042 | this._m = maxMonth; 1043 | } 1044 | } 1045 | 1046 | for (var c = 0; c < opts.numberOfMonths; c++) { 1047 | randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2); 1048 | html += '
' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId) + '
'; 1049 | } 1050 | 1051 | this.el.innerHTML = html; 1052 | 1053 | if (opts.bound) { 1054 | if(opts.field.type !== 'hidden') { 1055 | sto(function() { 1056 | opts.trigger.focus(); 1057 | }, 1); 1058 | } 1059 | } 1060 | 1061 | if (typeof this._o.onDraw === 'function') { 1062 | this._o.onDraw(this); 1063 | } 1064 | 1065 | if (opts.bound) { 1066 | // let the screen reader user know to use arrow keys 1067 | opts.field.setAttribute('aria-label', opts.ariaLabel); 1068 | } 1069 | }, 1070 | 1071 | adjustPosition: function() 1072 | { 1073 | var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect, leftAligned, bottomAligned; 1074 | 1075 | if (this._o.container) return; 1076 | 1077 | this.el.style.position = 'absolute'; 1078 | 1079 | field = this._o.trigger; 1080 | pEl = field; 1081 | width = this.el.offsetWidth; 1082 | height = this.el.offsetHeight; 1083 | viewportWidth = window.innerWidth || document.documentElement.clientWidth; 1084 | viewportHeight = window.innerHeight || document.documentElement.clientHeight; 1085 | scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; 1086 | leftAligned = true; 1087 | bottomAligned = true; 1088 | 1089 | if (typeof field.getBoundingClientRect === 'function') { 1090 | clientRect = field.getBoundingClientRect(); 1091 | left = clientRect.left + window.pageXOffset; 1092 | top = clientRect.bottom + window.pageYOffset; 1093 | } else { 1094 | left = pEl.offsetLeft; 1095 | top = pEl.offsetTop + pEl.offsetHeight; 1096 | while((pEl = pEl.offsetParent)) { 1097 | left += pEl.offsetLeft; 1098 | top += pEl.offsetTop; 1099 | } 1100 | } 1101 | 1102 | // default position is bottom & left 1103 | if ((this._o.reposition && left + width > viewportWidth) || 1104 | ( 1105 | this._o.position.indexOf('right') > -1 && 1106 | left - width + field.offsetWidth > 0 1107 | ) 1108 | ) { 1109 | left = left - width + field.offsetWidth; 1110 | leftAligned = false; 1111 | } 1112 | if ((this._o.reposition && top + height > viewportHeight + scrollTop) || 1113 | ( 1114 | this._o.position.indexOf('top') > -1 && 1115 | top - height - field.offsetHeight > 0 1116 | ) 1117 | ) { 1118 | top = top - height - field.offsetHeight; 1119 | bottomAligned = false; 1120 | } 1121 | 1122 | this.el.style.left = left + 'px'; 1123 | this.el.style.top = top + 'px'; 1124 | 1125 | addClass(this.el, leftAligned ? 'left-aligned' : 'right-aligned'); 1126 | addClass(this.el, bottomAligned ? 'bottom-aligned' : 'top-aligned'); 1127 | removeClass(this.el, !leftAligned ? 'left-aligned' : 'right-aligned'); 1128 | removeClass(this.el, !bottomAligned ? 'bottom-aligned' : 'top-aligned'); 1129 | }, 1130 | 1131 | /** 1132 | * render HTML for a particular month 1133 | */ 1134 | render: function(year, month, randId) 1135 | { 1136 | var opts = this._o, 1137 | now = new Date(), 1138 | days = getDaysInMonth(year, month), 1139 | before = new Date(year, month, 1).getDay(), 1140 | data = [], 1141 | row = []; 1142 | setToStartOfDay(now); 1143 | if (opts.firstDay > 0) { 1144 | before -= opts.firstDay; 1145 | if (before < 0) { 1146 | before += 7; 1147 | } 1148 | } 1149 | var previousMonth = month === 0 ? 11 : month - 1, 1150 | nextMonth = month === 11 ? 0 : month + 1, 1151 | yearOfPreviousMonth = month === 0 ? year - 1 : year, 1152 | yearOfNextMonth = month === 11 ? year + 1 : year, 1153 | daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth); 1154 | var cells = days + before, 1155 | after = cells; 1156 | while(after > 7) { 1157 | after -= 7; 1158 | } 1159 | cells += 7 - after; 1160 | var isWeekSelected = false; 1161 | for (var i = 0, r = 0; i < cells; i++) 1162 | { 1163 | var day = new Date(year, month, 1 + (i - before)), 1164 | isSelected = isDate(this._d) ? compareDates(day, this._d) : false, 1165 | isToday = compareDates(day, now), 1166 | hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false, 1167 | isEmpty = i < before || i >= (days + before), 1168 | dayNumber = 1 + (i - before), 1169 | monthNumber = month, 1170 | yearNumber = year, 1171 | isStartRange = opts.startRange && compareDates(opts.startRange, day), 1172 | isEndRange = opts.endRange && compareDates(opts.endRange, day), 1173 | isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange, 1174 | isDisabled = (opts.minDate && day < opts.minDate) || 1175 | (opts.maxDate && day > opts.maxDate) || 1176 | (opts.disableWeekends && isWeekend(day)) || 1177 | (opts.disableDayFn && opts.disableDayFn(day)); 1178 | 1179 | if (isEmpty) { 1180 | if (i < before) { 1181 | dayNumber = daysInPreviousMonth + dayNumber; 1182 | monthNumber = previousMonth; 1183 | yearNumber = yearOfPreviousMonth; 1184 | } else { 1185 | dayNumber = dayNumber - days; 1186 | monthNumber = nextMonth; 1187 | yearNumber = yearOfNextMonth; 1188 | } 1189 | } 1190 | 1191 | var dayConfig = { 1192 | day: dayNumber, 1193 | month: monthNumber, 1194 | year: yearNumber, 1195 | hasEvent: hasEvent, 1196 | isSelected: isSelected, 1197 | isToday: isToday, 1198 | isDisabled: isDisabled, 1199 | isEmpty: isEmpty, 1200 | isStartRange: isStartRange, 1201 | isEndRange: isEndRange, 1202 | isInRange: isInRange, 1203 | showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths, 1204 | enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths 1205 | }; 1206 | 1207 | if (opts.pickWholeWeek && isSelected) { 1208 | isWeekSelected = true; 1209 | } 1210 | 1211 | row.push(renderDay(dayConfig)); 1212 | 1213 | if (++r === 7) { 1214 | if (opts.showWeekNumber) { 1215 | row.unshift(renderWeek(i - before, month, year, opts.firstWeekOfYearMinDays)); 1216 | } 1217 | data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected)); 1218 | row = []; 1219 | r = 0; 1220 | isWeekSelected = false; 1221 | } 1222 | } 1223 | return renderTable(opts, data, randId); 1224 | }, 1225 | 1226 | isVisible: function() 1227 | { 1228 | return this._v; 1229 | }, 1230 | 1231 | show: function() 1232 | { 1233 | if (!this.isVisible()) { 1234 | this._v = true; 1235 | this.draw(); 1236 | removeClass(this.el, 'is-hidden'); 1237 | if (this._o.bound) { 1238 | addEvent(document, 'click', this._onClick); 1239 | this.adjustPosition(); 1240 | } 1241 | if (typeof this._o.onOpen === 'function') { 1242 | this._o.onOpen.call(this); 1243 | } 1244 | } 1245 | }, 1246 | 1247 | hide: function() 1248 | { 1249 | var v = this._v; 1250 | if (v !== false) { 1251 | if (this._o.bound) { 1252 | removeEvent(document, 'click', this._onClick); 1253 | } 1254 | 1255 | if (!this._o.container) { 1256 | this.el.style.position = 'static'; // reset 1257 | this.el.style.left = 'auto'; 1258 | this.el.style.top = 'auto'; 1259 | } 1260 | addClass(this.el, 'is-hidden'); 1261 | this._v = false; 1262 | if (v !== undefined && typeof this._o.onClose === 'function') { 1263 | this._o.onClose.call(this); 1264 | } 1265 | } 1266 | }, 1267 | 1268 | /** 1269 | * GAME OVER 1270 | */ 1271 | destroy: function() 1272 | { 1273 | var opts = this._o; 1274 | 1275 | this.hide(); 1276 | removeEvent(this.el, 'mousedown', this._onMouseDown, true); 1277 | removeEvent(this.el, 'touchend', this._onMouseDown, true); 1278 | removeEvent(this.el, 'change', this._onChange); 1279 | if (opts.keyboardInput) { 1280 | removeEvent(document, 'keydown', this._onKeyChange); 1281 | } 1282 | if (opts.field) { 1283 | removeEvent(opts.field, 'change', this._onInputChange); 1284 | if (opts.bound) { 1285 | removeEvent(opts.trigger, 'click', this._onInputClick); 1286 | removeEvent(opts.trigger, 'focus', this._onInputFocus); 1287 | removeEvent(opts.trigger, 'blur', this._onInputBlur); 1288 | } 1289 | } 1290 | if (this.el.parentNode) { 1291 | this.el.parentNode.removeChild(this.el); 1292 | } 1293 | } 1294 | 1295 | }; 1296 | 1297 | return Pikaday; 1298 | })); 1299 | -------------------------------------------------------------------------------- /firefox/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wiki Journey 5 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |

Wiki Journey

68 | 69 |
70 | 73 | 76 |
77 |
78 |
79 |
80 | 81 | 82 | d.children ? Object.values(d.children) : []); 35 | 36 | const dx = 64; 37 | const dy = 192; 38 | const leftTextOffset = 128; // Estimated offset to accommodate leftmost text 39 | const width = (root.height + 1) * dy + leftTextOffset; 40 | const tree = d3.tree().nodeSize([dx, dy]); 41 | 42 | root.sort((a, b) => d3.ascending(a.data.title, b.data.title)); 43 | tree(root); 44 | 45 | let x0 = Infinity; 46 | let x1 = -x0; 47 | root.each(d => { 48 | if (d.x > x1) x1 = d.x; 49 | if (d.x < x0) x0 = d.x; 50 | }); 51 | 52 | const height = x1 - x0 + dx * 2; 53 | 54 | // Since you're in a browser extension popup, use d3.select instead of d3.create 55 | const svg = d3.select("#graph").append("svg") 56 | .attr("width", width) 57 | .attr("height", height) 58 | .attr("viewBox", [-leftTextOffset, x0 - dx, width + leftTextOffset, height]) // Adjust viewBox by leftTextOffset 59 | .style("font", "18px sans-serif"); 60 | 61 | const link = svg.append("g") 62 | .attr("fill", "none") 63 | .attr("stroke", "#555") 64 | .attr("stroke-opacity", 0.4) 65 | .attr("stroke-width", 1.5) 66 | .selectAll("path") 67 | .data(root.links()) 68 | .join("path") 69 | .attr("d", d3.linkHorizontal() 70 | .x(d => d.y) 71 | .y(d => d.x)); 72 | 73 | const node = svg.append("g") 74 | .attr("stroke-linejoin", "round") 75 | .attr("stroke-width", 3) 76 | .selectAll("g") 77 | .data(root.descendants()) 78 | .join("g") 79 | .attr("transform", d => `translate(${d.y},${d.x})`); 80 | 81 | node.append("circle") 82 | .attr("fill", d => d.children ? "#555" : "#999") 83 | .attr("r", 2.5); 84 | 85 | node.append("text") 86 | .attr("dy", "0.31em") 87 | .attr("x", d => d.children ? -6 : 6) 88 | .attr("text-anchor", d => d.children ? "end" : "start") 89 | .text(d => d.data.title) 90 | .attr("fill", "black") // Set the text color 91 | .clone(true).lower() 92 | .attr("stroke", "white"); // Remove the stroke or set to a contrasting color if needed 93 | 94 | 95 | // Add this SVG to your popup 96 | document.querySelector("#graph").appendChild(svg.node()); 97 | } 98 | const today = new Date(); 99 | document.getElementById('datePicker').value = toLocalISOString(today); // Set date picker value to today 100 | loadJourney(toLocalISOString(today)); 101 | }); 102 | -------------------------------------------------------------------------------- /firefox/wikiJourneyFirefox.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demegire/wiki-journey/e6dfb2d17f4dcd58c17cfa152597abfc0f2dff84/firefox/wikiJourneyFirefox.zip --------------------------------------------------------------------------------