您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Keep track of the HITs you've done (and more!)
当前为
// ==UserScript== // @name MTurk HIT Database Mk.II // @author feihtality // @namespace https://greasyfork.org/en/users/12709 // @version 0.7.681 // @description Keep track of the HITs you've done (and more!) // @include /^https://www\.mturk\.com/mturk/(dash|view|sort|find|prev|search).*/ // @exclude https://www.mturk.com/mturk/findhits?*hit_scraper // @grant none // ==/UserScript== /**\ ** ** This is a complete rewrite of the MTurk HIT Database script from the ground up, which ** eliminates obsolete methods, fixes many bugs, and brings this script up-to-date ** with the current modern browser environment. ** \**/ /* * TODO * projected earnings * tagging (?) * searching via R/T buttons * */ const DB_VERSION = 2; const MTURK_BASE = 'https://www.mturk.com/mturk/'; //const TO_BASE = 'http://turkopticon.ucsd.edu/api/multi-attrs.php'; // polyfill for chrome until v45(?) if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // format leading zeros Number.prototype.toPadded = function(length) { 'use strict'; length = length || 2; return ("0000000"+this).substr(-length); }; // decimal rounding Math.decRound = function(v, shift) { 'use strict'; v = Math.round(+(v+"e"+shift)); return +(v+"e"+-shift); }; var qc = { extraDays: !!localStorage.getItem("hitdb_extraDays") || false, seen: {} }; if (localStorage.getItem("hitdb_fetchData")) qc.fetchData = JSON.parse(localStorage.getItem("hitdb_fetchData")); else qc.fetchData = {}; var HITStorage = { //{{{ data: {}, versionChange: function hsversionChange() { //{{{ 'use strict'; var db = this.result; db.onerror = HITStorage.error; db.onversionchange = function(e) { console.log("detected version change??",console.dir(e)); db.close(); }; this.onsuccess = function() { db.close(); }; var dbo; console.groupCollapsed("HITStorage.versionChange::onupgradeneeded"); if (!db.objectStoreNames.contains("HIT")) { console.log("creating HIT OS"); dbo = db.createObjectStore("HIT", { keyPath: "hitId" }); dbo.createIndex("date", "date", { unique: false }); dbo.createIndex("requesterName", "requesterName", { unique: false}); dbo.createIndex("title", "title", { unique: false }); dbo.createIndex("reward", "reward", { unique: false }); dbo.createIndex("status", "status", { unique: false }); dbo.createIndex("requesterId", "requesterId", { unique: false }); localStorage.setItem("hitdb_extraDays", true); qc.extraDays = true; } if (!db.objectStoreNames.contains("STATS")) { console.log("creating STATS OS"); dbo = db.createObjectStore("STATS", { keyPath: "date" }); } if (this.transaction.objectStore("STATS").indexNames.length < 5) { // new in v5: schema additions this.transaction.objectStore("STATS").createIndex("approved", "approved", { unique: false }); this.transaction.objectStore("STATS").createIndex("earnings", "earnings", { unique: false }); this.transaction.objectStore("STATS").createIndex("pending", "pending", { unique: false }); this.transaction.objectStore("STATS").createIndex("rejected", "rejected", { unique: false }); this.transaction.objectStore("STATS").createIndex("submitted", "submitted", { unique: false }); } (function _updateNotes(dbt) { // new in v5: schema change if (!db.objectStoreNames.contains("NOTES")) { console.log("creating NOTES OS"); dbo = db.createObjectStore("NOTES", { keyPath: "id", autoIncrement: true }); dbo.createIndex("hitId", "hitId", { unique: false }); dbo.createIndex("requesterId", "requesterId", { unique: false }); dbo.createIndex("tags", "tags", { unique: false, multiEntry: true }); dbo.createIndex("date", "date", { unique: false }); } if (db.objectStoreNames.contains("NOTES") && dbt.objectStore("NOTES").indexNames.length < 3) { _mv(db, dbt, "NOTES", "NOTES", _updateNotes); } })(this.transaction); if (db.objectStoreNames.contains("BLOCKS")) { console.log("migrating BLOCKS to NOTES"); var temp = []; this.transaction.objectStore("BLOCKS").openCursor().onsuccess = function() { var cursor = this.result; if (cursor) { temp.push( { requesterId: cursor.value.requesterId, tags: "Blocked", note: "This requester was blocked under the old HitDB. Blocking has been deprecated and removed "+ "from HIT Databse. All blocks have been converted to a Note." } ); cursor.continue(); } else { console.log("deleting blocks"); db.deleteObjectStore("BLOCKS"); for (var entry of temp) this.transaction.objectStore("NOTES").add(entry); } }; } function _mv(db, transaction, source, dest, fn) { //{{{ var _data = []; transaction.objectStore(source).openCursor().onsuccess = function() { var cursor = this.result; if (cursor) { _data.push(cursor.value); cursor.continue(); } else { db.deleteObjectStore(source); fn(transaction); if (_data.length) for (var i=0;i<_data.length;i++) transaction.objectStore(dest).add(_data[i]); //console.dir(_data); } }; } //}}} console.groupEnd(); }, // }}} versionChange error: function(e) { //{{{ 'use strict'; if (typeof e === "string") console.log(e); else console.log("Encountered",e.target.error.name,"--",e.target.error.message,e); }, //}}} onerror parseDOM: function(doc) {//{{{ 'use strict'; var statusLabel = document.querySelector("#hdbStatusText"); statusLabel.style.color = "black"; var errorCheck = doc.querySelector('td[class="error_title"]'); if (doc.title.search(/Status$/) > 0) // status overview parseStatus(); else if (doc.querySelector('td[colspan="4"]')) // valid status detail, but no data parseMisc("next"); else if (doc.title.search(/Status Detail/) > 0) // status detail with data parseDetail(); else if (errorCheck) { // encountered an error page // hit max request rate if (~errorCheck.textContent.indexOf("page request rate")) { console.log("exceeded max requests; refetching", doc.documentURI); setTimeout(HITStorage.fetch, 350, doc.documentURI); return; } // no more staus details left in range else if (qc.extraDays) parseMisc("end"); } else throw "ParseError::unhandled document received @"+doc.documentURI; function parseStatus() {//{{{ HITStorage.data = { HIT: [], STATS: [] }; qc.seen = {}; var _pastDataExists = Boolean(Object.keys(qc.fetchData).length); var raw = { day: doc.querySelectorAll(".statusDateColumnValue"), sub: doc.querySelectorAll(".statusSubmittedColumnValue"), app: doc.querySelectorAll(".statusApprovedColumnValue"), rej: doc.querySelectorAll(".statusRejectedColumnValue"), pen: doc.querySelectorAll(".statusPendingColumnValue"), pay: doc.querySelectorAll(".statusEarningsColumnValue") }; var timeout = 0; for (var i=0;i<raw.day.length;i++) { var d = {}; var _date = raw.day[i].childNodes[1].href.substr(53); d.date = HITStorage.ISODate(_date); d.submitted = +raw.sub[i].textContent; d.approved = +raw.app[i].textContent; d.rejected = +raw.rej[i].textContent; d.pending = +raw.pen[i].textContent; d.earnings = +raw.pay[i].textContent.substr(1); HITStorage.data.STATS.push(d); // check whether or not we need to get status detail pages for date, then // fetch status detail pages per date in range and slightly slow // down GET requests to avoid making too many in too short an interval var payload = { encodedDate: _date, pageNumber: 1, sortType: "All" }; if (_pastDataExists) { // date not in range but is new date (or old date but we need updates) // lastDate stored in ISO format, fetchData date keys stored in mturk's URI ecnodedDate format if ( (d.date > qc.fetchData.lastDate) || ~(Object.keys(qc.fetchData).indexOf(_date)) ) { setTimeout(HITStorage.fetch, timeout, MTURK_BASE+"statusdetail", payload); timeout += 250; qc.fetchData[_date] = { submitted: d.submitted, pending: d.pending }; } } else { // get everything setTimeout(HITStorage.fetch, timeout, MTURK_BASE+"statusdetail", payload); timeout += 250; qc.fetchData[_date] = { submitted: d.submitted, pending: d.pending }; } } // for qc.fetchData.expectedTotal = _calcTotals(qc.fetchData); // try for extra days if (qc.extraDays === true) { localStorage.removeItem("hitdb_extraDays"); d = _decDate(HITStorage.data.STATS[HITStorage.data.STATS.length-1].date); qc.extraDays = d; // repurpose extraDays for QC payload = { encodedDate: d, pageNumber: 1, sortType: "All" }; console.log("fetchrequest for", d, "sent by parseStatus"); setTimeout(HITStorage.fetch, 1000, MTURK_BASE+"statusdetail", payload); } qc.fetchData.lastDate = HITStorage.data.STATS[0].date; // most recent date seen }//}}} parseStatus function parseDetail() {//{{{ var _date = doc.documentURI.replace(/.+(\d{8}).+/, "$1"); var _page = doc.documentURI.replace(/.+ber=(\d+).+/, "$1"); console.log("page:", _page, "date:", _date); statusLabel.textContent = "Processing "+HITStorage.ISODate(_date)+" page "+_page; var raw = { req: doc.querySelectorAll(".statusdetailRequesterColumnValue"), title: doc.querySelectorAll(".statusdetailTitleColumnValue"), pay: doc.querySelectorAll(".statusdetailAmountColumnValue"), status: doc.querySelectorAll(".statusdetailStatusColumnValue"), feedback: doc.querySelectorAll(".statusdetailRequesterFeedbackColumnValue") }; for (var i=0;i<raw.req.length;i++) { var d = {}; d.date = HITStorage.ISODate(_date); d.feedback = raw.feedback[i].textContent.trim(); d.hitId = raw.req[i].childNodes[1].href.replace(/.+HIT\+(.+)/, "$1"); d.requesterId = raw.req[i].childNodes[1].href.replace(/.+rId=(.+?)&.+/, "$1"); d.requesterName = raw.req[i].textContent.trim().replace(/\|/g,""); d.reward = +raw.pay[i].textContent.substr(1); d.status = raw.status[i].textContent; d.title = raw.title[i].textContent.replace(/\|/g, ""); HITStorage.data.HIT.push(d); if (!qc.seen[_date]) qc.seen[_date] = {}; qc.seen[_date] = { submitted: qc.seen[_date].submitted + 1 || 1, pending: ~d.status.search(/pending/i) ? (qc.seen[_date].pending + 1 || 1) : (qc.seen[_date].pending || 0) }; } // additional pages remain; get them if (doc.querySelector('img[src="/media/right_dbl_arrow.gif"]')) { var payload = { encodedDate: _date, pageNumber: +_page+1, sortType: "All" }; setTimeout(HITStorage.fetch, 250, MTURK_BASE+"statusdetail", payload); return; } if (!qc.extraDays) { // not fetching extra days //no longer any more useful data here, don't need to keep rechecking this date if (HITStorage.ISODate(_date) !== qc.fetchData.lastDate && qc.seen[_date].submitted === qc.fetchData[_date].submitted && qc.seen[_date].pending === 0) { console.log("no more pending hits, removing",_date,"from fetchData"); delete qc.fetchData[_date]; localStorage.setItem("hitdb_fetchData", JSON.stringify(qc.fetchData)); } // finished scraping; start writing console.log("totals", _calcTotals(qc.seen), qc.fetchData.expectedTotal); statusLabel.textContent += " [ "+_calcTotals(qc.seen)+"/"+ qc.fetchData.expectedTotal+" ]"; if (_calcTotals(qc.seen) === qc.fetchData.expectedTotal) { statusLabel.textContent = "Writing to database..."; HITStorage.write(HITStorage.data, "update"); } } else if (_date <= qc.extraDays) { // day is older than default range and still fetching extra days parseMisc("next","detail"); console.log("fetchrequest for", _decDate(HITStorage.ISODate(_date))); } }//}}} parseDetail function parseMisc(type, src) {//{{{ var d = doc.documentURI.replace(/.+(\d{8}).+/, "$1"); var payload = { encodedDate: _decDate(HITStorage.ISODate(d)), pageNumber: 1, sortType: "All" }; src = src || "dispatch"; if (type === "next" && +qc.extraDays > 1) { setTimeout(HITStorage.fetch, 250, MTURK_BASE+"statusdetail", payload); console.log("going to next page", payload.encodedDate); } else if (type === "end" && +qc.extraDays > 1) { statusLabel.textContent = "Writing to database..."; HITStorage.write(HITStorage.data, "update"); } else throw 'Unhandled case "'+src+'" '+qc.extraDays+' -- "'+type+'" in '+doc.documentURI; }//}}} function _decDate(date) {//{{{ var y = date.substr(0,4); var m = date.substr(5,2); var d = date.substr(8,2); date = new Date(y,m-1,d-1); return Number(date.getMonth()+1).toPadded() + Number(date.getDate()).toPadded() + date.getFullYear(); }//}}} function _calcTotals(obj) {//{{{ var sum = 0; for (var k in obj){ if (obj.hasOwnProperty(k) && !isNaN(+k)) sum += obj[k].submitted; } return sum; }//}}} },//}}} parseDOM ISODate: function(date) { //{{{ MMDDYYYY -> YYYY-MM-DD 'use strict'; return date.substr(4)+"-"+date.substr(0,2)+"-"+date.substr(2,2); }, //}}} ISODate fetch: function(url, payload) { //{{{ 'use strict'; //format GET request with query payload if (payload) { var args = 0; url += "?"; for (var k in payload) { if (payload.hasOwnProperty(k)) { if (args++) url += "&"; url += k + "=" + payload[k]; } } } // defer XHR to a promise var fetch = new Promise( function(fulfill, deny) { var urlreq = new XMLHttpRequest(); urlreq.open("GET", url, true); urlreq.responseType = "document"; urlreq.send(); urlreq.onload = function() { if (this.status === 200) { fulfill(this.response); } else { deny("Error ".concat(String(this.status)).concat(": "+this.statusText)); } }; urlreq.onerror = function() { deny("Error ".concat(String(this.status)).concat(": "+this.statusText)); }; urlreq.ontimeout = function() { deny("Error ".concat(String(this.status)).concat(": "+this.statusText)); }; } ); fetch.then( HITStorage.parseDOM, HITStorage.error ); }, //}}} fetch write: function(input, statusUpdate) { //{{{ 'use strict'; var dbh = window.indexedDB.open("HITDB_TESTING"); dbh.onerror = HITStorage.error; dbh.onsuccess = function() { _write(this.result); }; var counts = { requests: 0, total: 0 }; function _write(db) { db.onerror = HITStorage.error; var os = Object.keys(input); var dbt = db.transaction(os, "readwrite"); var dbo = []; for (var i=0;i<os.length;i++) { // cycle object stores dbo[i] = dbt.objectStore(os[i]); for (var k of input[os[i]]) { // cycle entries to put into object stores if (statusUpdate && ++counts.requests) dbo[i].put(k).onsuccess = _statusCallback; else dbo[i].put(k); } } db.close(); } function _statusCallback() { if (++counts.total === counts.requests) { var statusLabel = document.querySelector("#hdbStatusText"); statusLabel.style.color = "green"; statusLabel.textContent = statusUpdate === "update" ? "Update Complete!" : statusUpdate === "restore" ? "Restoring " + counts.total + " entries... Done!" : "Done!"; document.querySelector("#hdbProgressBar").style.display = "none"; } } }, //}}} write recall: function(store, options) {//{{{ 'use strict'; var index = options ? (options.index || null) : null, range = options ? (options.range || null) : null, dir = options ? (options.dir || "next") : "next", fs = options ? (options.filter ? options.filter.status !== "*" ? options.filter.status : false : false) : false, fq = options ? (options.filter ? options.filter.query !== "*" ? new RegExp(options.filter.query,"i") : false : false) : false, limit = 0; if (options && options.progress) { var progressBar = document.querySelector("#hdbProgressBar"); //statusText = document.querySelector("#hdbStatusText"); progressBar.style.display = "block"; } var sr = new DatabaseResult(); return new Promise( function(resolve) { window.indexedDB.open("HITDB_TESTING").onsuccess = function() { var dbo = this.result.transaction(store, "readonly").objectStore(store), dbq = null; if (index) dbq = dbo.index(index).openCursor(range, dir); else dbq = dbo.openCursor(range, dir); dbq.onsuccess = function() { var c = this.result; if (c && limit++ < 2000) { // limit to 2000 to save memory usage in large databases if ( (!fs && !fq) || // no query filter and no status filter OR (fs && !fq && ~c.value.status.search(fs)) || // status match and no query filter OR (!fs && fq && // query match and no status filter OR (~c.value.title.search(fq) || ~c.value.requesterName.search(fq) || ~c.value.hitId.search(fq))) || (fs && fq && ~c.value.status.search(fs) && // status match and query match (~c.value.title.search(fq) || ~c.value.requesterName.search(fq) || ~c.value.hitId.search(fq))) ) sr.include(c.value); c.continue(); } else resolve(sr); }; }; } ); // promise },//}}} recall backup: function() {//{{{ 'use strict'; var bData = {}, os = ["STATS", "NOTES", "HIT"], count = 0, prog = document.querySelector("#hdbProgressBar"); prog.style.display = "block"; window.indexedDB.open("HITDB_TESTING").onsuccess = function() { for (var store of os) { this.result.transaction(os, "readonly").objectStore(store).openCursor().onsuccess = populateBackup; } }; function populateBackup(e) { var cursor = e.target.result; if (cursor) { if (!bData[cursor.source.name]) bData[cursor.source.name] = []; bData[cursor.source.name].push(cursor.value); cursor.continue(); } else if (++count === 3) finalizeBackup(); } function finalizeBackup() { var backupblob = new Blob([JSON.stringify(bData)], {type:""}); var date = new Date(); var dl = document.createElement("A"); date = date.getFullYear() + Number(date.getMonth()+1).toPadded() + Number(date.getDate()).toPadded(); dl.href = URL.createObjectURL(backupblob); console.log(dl.href); dl.download = "hitdb_"+date+".bak"; dl.click(); prog.style.display = "none"; } }//}}} backup };//}}} HITStorage function DatabaseResult() {//{{{ 'use strict'; this.results = []; this.formatHTML = function(type, simple) { simple = simple || false; var count = 0, htmlTxt = [], entry = null, _trClass = null; if (this.results.length < 1) return "<h2>No entries found matching your query.</h2>"; if (type === "daily") { htmlTxt.push('<tr style="background:#7fb448;font-size:12px;color:white"><th>Date</th><th>Submitted</th>' + '<th>Approved</th><th>Rejected</th><th>Pending</th><th>Earnings</th></tr>'); for (entry of this.results) { _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"'; htmlTxt.push('<tr '+_trClass+' align="center"><td>' + entry.date + '</td><td>' + entry.submitted + '</td>' + '<td>' + entry.approved + '</td><td>' + entry.rejected + '</td><td>' + entry.pending + '</td>' + '<td>' + Number(entry.earnings).toFixed(2) + '</td></tr>'); } } else if (type === "pending" || type === "requester") { htmlTxt.push('<tr data-sort="99999" style="background:#7fb448;font-size:12px;color:white"><th>Requester ID</th>' + '<th width="504px">Requester</th><th>' + (type === "pending" ? 'Pending' : 'HITs') + '</th><th>Rewards</th></tr>'); var r = _collate(this.results); for (var k in r) { if (r.hasOwnProperty(k)) { var tr = ['<tr data-hits="'+r[k].length+'"><td>' + '<span style="cursor:pointer;color:blue;" class="hdbExpandRow" title="Display all pending HITs from this requester">' + '[+]</span> ' + r[k][0].requesterId + '</td><td>' + r[k][0].requesterName + '</td>' + '<td>' + r[k].length + '</td><td>' + Number(Math.decRound(r[k].pay,2)).toFixed(2) + '</td></tr>']; for (var hit of r[k]) { // hits in range per requester id tr.push('<tr data-rid="'+r[k][0].requesterId+'" style="color:#c60000;display:none;"><td align="right">' + hit.date + '</td>' + '<td max-width="504px">' + hit.title + '</td><td></td><td align="right">' + (typeof hit.reward === "object" ? Number(hit.reward.pay).toFixed(2) : Number(hit.reward).toFixed(2)) + '</td></tr>'); } htmlTxt.push(tr.join('')); } } htmlTxt.sort(function(a,b) { return +b.substr(15,5).match(/\d+/) - +a.substr(15,5).match(/\d+/); }); } else { // default if (!simple) htmlTxt.push('<tr style="background:#7FB448;font-size:12px;color:white"><th colspan="3"></th>' + '<th colspan="2" title="Bonuses must be added in manually.\n\nClick inside' + 'the cell to edit, click out of the cell to save">Reward</th><th colspan="2"></th></tr>'+ '<tr style="background:#7FB448;font-size:12px;color:white">' + '<th>Date</th><th>Requester</th><th>HIT title</th><th style="font-size:10px;">Pay</th>'+ '<th style="font-size:10px;">Bonus</th><th>Status</th><th>Feedback</th></tr>'); for (entry of this.results) { _trClass = (count++ % 2 === 0) ? 'class="even"' : 'class="odd"'; var _stColor = ~entry.status.search(/(paid|approved)/i) ? 'style="color:green;"' : entry.status === "Pending Approval" ? 'style="color:orange;"' : 'style="color:red;"'; var href = MTURK_BASE+'contact?requesterId='+entry.requesterId+'&requesterName='+entry.requesterName+ '&subject=Regarding+Amazon+Mechanical+Turk+HIT+'+entry.hitId; if (!simple) htmlTxt.push('<tr '+_trClass+' data-id="'+entry.hitId+'">'+ '<td width="74px">' + entry.date + '</td><td style="max-width:145px;">' + '<a target="_blank" title="Contact this requester" href="'+href+'">' + entry.requesterName + '</a></td>' + '<td width="375px" title="HIT ID: '+entry.hitId+'">' + '<span title="Add a note" id="note-'+entry.hitId+'" style="cursor:pointer;"> 📝 </span>' + entry.title + '</td><td>' + (typeof entry.reward === "object" ? Number(entry.reward.pay).toFixed(2) : Number(entry.reward).toFixed(2)) + '</td><td width="36px" class="bonusCell" title="Click to add/edit" contenteditable="true" data-hitid="'+entry.hitId+'">' + (typeof entry.reward === "object" ? (+entry.reward.bonus ? Number(entry.reward.bonus).toFixed(2) : "") : "") + '</td><td '+_stColor+'>' + entry.status + '</td><td>' + entry.feedback + '</td></tr>'); else htmlTxt.push('<tr data-rid="'+entry.requesterId+'" style="display:none"><td>'+entry.date+'</td><td>'+entry.title+'</td><td>'+ (typeof entry.reward === "object" ? Number(entry.reward.pay).toFixed(2) : Number(entry.reward).toFixed(2)) + '</td><td>'+ entry.status+'</td></tr>'); } } return htmlTxt.join(''); }; // formatHTML this.formatCSV = function(type) { var csvTxt = [], entry = null; if (type === "daily") { csvTxt.push("Date|Submitted|Approved|Rejected|Pending|Earnings\n"); for (entry of this.results) { csvTxt.push(entry.date+"|"+entry.submitted+"|"+entry.approved+"|"+entry.rejected+ "|"+entry.pending+"|"+Number(entry.earnings).toFixed(2)+"\n"); } csvToFile(csvTxt, "hitdb_dailyOverview.csv"); } else if (type === "pending" || type === "requester") { csvTxt.push("RequesterId|Requester|" + (type === "pending" ? "Pending" : "HITs") + "|Rewards\n"); var r = _collate(this.results); for (var k in r) { if (r.hasOwnProperty(k)) csvTxt.push(k+"|"+r[k][0].requesterName+"|"+r[k].length+"|"+Number(Math.decRound(r[k].pay,2)).toFixed(2)+"\n"); } csvToFile(csvTxt, "hitdb_"+type+"Overview.csv"); } else { csvTxt.push("Date|Requester|Title|Pay|Bonus|Status|Feedback\n"); for (entry of this.results) { csvTxt.push(entry.date+"|"+entry.requesterName+"|"+entry.title+"|"+ (typeof entry.reward === "object" ? Number(entry.reward.pay).toFixed(2) : Number(entry.reward).toFixed(2))+"|"+ (typeof entry.reward === "object" ? (+entry.reward.bonus ? Number(entry.reward.bonus).toFixed(2) : "") : "")+"|"+ entry.status+"|"+entry.feedback+"\n"); } csvToFile(csvTxt, "hitdb_queryResults.csv"); } return "<pre>"+csvTxt.join('')+"</pre>"; function csvToFile(csv, filename) { var blob = new Blob(csv, {type: "text/csv", endings: "native"}), dl = document.createElement("A"); dl.href = URL.createObjectURL(blob); dl.download = filename; dl.click(); return dl; } }; this.include = function(value) { this.results.push(value); }; function _collate(data) { var r = {}; for (var e of data) { if (!r[e.requesterId]) r[e.requesterId] = []; r[e.requesterId].push(e); r[e.requesterId].pay = r[e.requesterId].pay ? typeof e.reward === "object" ? r[e.requesterId].pay + (+e.reward.pay) : r[e.requesterId].pay + (+e.reward) : typeof e.reward === "object" ? +e.reward.pay : +e.reward; } return r; } }//}}} databaseresult /* * * Above contains the core functions. Below is the * main body, interface, and tangential functions. * *///{{{ // the Set() constructor is never actually used other than to test for Chrome v38+ if (!("indexedDB" in window && "Set" in window)) alert("HITDB::Your browser is too outdated or otherwise incompatible with this script!"); else { /* var tdbh = window.indexedDB.open("HITDB_TESTING"); tdbh.onerror = function(e) { 'use strict'; console.log("[TESTDB]",e.target.error.name+":", e.target.error.message, e); }; tdbh.onsuccess = INFLATEDUMMYVALUES; tdbh.onupgradeneeded = BLANKSLATE; var dbh = null; */ var dbh = window.indexedDB.open("HITDB_TESTING", DB_VERSION); dbh.onerror = function(e) { 'use strict'; console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); }; dbh.onupgradeneeded = HITStorage.versionChange; if (document.location.pathname.search(/dashboard/) > 0) dashboardUI(); else beenThereDoneThat(); } /*}}} * * Above is the main body and core functions. Below * defines UI layout/appearance and tangential functions. * */ // {{{ css injection var css = "<style type='text/css'>" + ".hitdbRTButtons {border:1px solid; font-size: 10px; height: 18px; padding-left: 5px; padding-right: 5px; background: pink;}" + ".hitdbRTButtons-green {background: lightgreen;}" + ".hitdbRTButtons-large {width:80px;}" + ".hdbProgressContainer {margin:auto; width:500px; height:6px; position:relative; display:none; border-radius:10px; overflow:hidden; background:#d3d8db;}" + ".hdbProgressInner {width:100%; position:absolute; left:0;top:0;bottom:0; animation: kfpin 1.4s infinite; background:" + "linear-gradient(262deg, rgba(208,69,247,0), rgba(208,69,247,1), rgba(69,197,247,1), rgba(69,197,247,0)); background-size: 300% 500%;}" + ".hdbProgressOuter {width:30%; position:absolute; left:0;top:0;bottom:0; animation: kfpout 2s cubic-bezier(0,0.55,0.2,1) infinite;}" + "@keyframes kfpout { 0% {left:-100%;} 70%{left:100%;} 100%{left:100%;} }" + "@keyframes kfpin { 0%{background-position: 0% 50%} 50%{background-position: 100% 15%} 100%{background-position:0% 30%} }" + ".hdbCalControls {cursor:pointer;} .hdbCalControls:hover {color:c27fcf;}" + ".hdbCalCells {background:#f0f6f9; height:19px}" + ".hdbCalDays {cursor:pointer; text-align:center;} .hdbCalDays:hover {background:#7fb4cf; color:white;}" + ".hdbDayHeader {width:26px; text-align:center; font-weight:bold; font-size:12px; background:#f0f6f9;}" + ".hdbCalHeader {background:#7fb4cf; color:white; font-weight:bold; text-align:center; font-size:11px; padding:3px 0px;}" + "#hdbCalendarPanel {position:absolute; z-index:10; box-shadow:-2px 3px 5px 0px rgba(0,0,0,0.68);}" + "</style>"; document.head.innerHTML += css; // }}} function beenThereDoneThat() {//{{{ // // TODO add search on button click // 'use strict'; var qualNode = document.querySelector('td[colspan="11"]'); if (qualNode) { // we're on the preview page! var requester = document.querySelector('input[name="requesterId"]').value, hitId = document.querySelector('input[name="hitId"]').value, autoApproval = document.querySelector('input[name="hitAutoAppDelayInSeconds"]').value, hitTitle = document.querySelector('div[style*="ellipsis"]').textContent.trim().replace(/\|/g,""), insertionNode = qualNode.parentNode.parentNode; var row = document.createElement("TR"), cellL = document.createElement("TD"), cellR = document.createElement("TD"); cellR.innerHTML = '<span class="capsule_field_title">Auto-Approval:</span> '+_ftime(autoApproval); var rbutton = document.createElement("BUTTON"); rbutton.classList.add("hitdbRTButtons","hitdbRTButtons-large"); rbutton.textContent = "Requester"; rbutton.onclick = function(e) { e.preventDefault(); //if (e.target.classList.contains("hitdbRTButtons-green")) // showResults(requester, null); }; var tbutton = rbutton.cloneNode(false); tbutton.textContent = "HIT Title"; tbutton.onclick = function(e) { e.preventDefault(); }; HITStorage.recall("HIT", {index: "requesterId", range: window.IDBKeyRange.only(requester)}) .then(processResults.bind(rbutton)); HITStorage.recall("HIT", {index: "title", range: window.IDBKeyRange.only(hitTitle)}) .then(processResults.bind(tbutton)); row.appendChild(cellL); row.appendChild(cellR); cellL.appendChild(rbutton); cellL.appendChild(tbutton); cellL.colSpan = "3"; cellR.colSpan = "8"; insertionNode.appendChild(row); } else { // browsing HITs n sutff var titleNodes = document.querySelectorAll('a[class="capsulelink"]'); if (titleNodes.length < 1) return; // nothing left to do here! var requesterNodes = document.querySelectorAll('a[href*="hitgroups&requester"]'); var insertionNodes = []; for (var i=0;i<titleNodes.length;i++) { var _title = titleNodes[i].textContent.trim().replace(/\|/g,""); var _tbutton = document.createElement("BUTTON"); var _id = requesterNodes[i].href.replace(/.+Id=(.+)/, "$1"); var _rbutton = document.createElement("BUTTON"); var _div = document.createElement("DIV"), _tr = document.createElement("TR"); var _resultsTable = document.createElement("TABLE"); insertionNodes.push(requesterNodes[i].parentNode.parentNode.parentNode); insertionNodes[i].offsetParent.offsetParent.offsetParent.offsetParent.appendChild(_resultsTable); _resultsTable.id = "resultsTableFor"+_id; HITStorage.recall("HIT", {index: "title", range: window.IDBKeyRange.only(_title)} ) .then(processResults.bind(_tbutton)); HITStorage.recall("HIT", {index: "requesterId", range: window.IDBKeyRange.only(_id)} ) .then(processResults.bind(_rbutton)); _tr.appendChild(_div); _div.id = "hitdbRTInjection-"+i; _div.appendChild(_rbutton); _rbutton.textContent = 'R'; _rbutton.classList.add("hitdbRTButtons"); _rbutton.dataset.id = _id; _rbutton.onclick = showResults.bind(null, _id, null); _div.appendChild(_tbutton); _tbutton.textContent = 'T'; _tbutton.classList.add("hitdbRTButtons"); insertionNodes[i].appendChild(_tr); } } // else function showResults(rid, title) { console.log(rid,title); var el = null; if (rid) { for (el of document.querySelectorAll('tr[data-rid="'+rid+'"]')) { if (el.style.display === "none") el.style.display = "table-row"; else el.style.display = "none"; } } } function processResults(r) { /*jshint validthis: true*/ if (r.results.length) { this.classList.add("hitdbRTButtons-green"); if (this.dataset.id) { var rtable = document.querySelector("#resultsTableFor"+this.dataset.id); rtable.innerHTML += r.formatHTML(null,true); } } } function _ftime(t) { var d = Math.floor(t/86400); var h = Math.floor(t%86400/3600); var m = Math.floor(t%86400%3600/60); var s = t%86400%3600%60; return ((d>0) ? d+" day"+(d>1 ? "s " : " ") : "") + ((h>0) ? h+"h " : "") + ((m>0) ? m+"m " : "") + ((s>0) ? s+"s" : ""); } }//}}} btdt function dashboardUI() {//{{{ // // TODO refactor // 'use strict'; var controlPanel = document.createElement("TABLE"); var insertionNode = document.querySelector(".footer_separator").previousSibling; document.body.insertBefore(controlPanel, insertionNode); controlPanel.width = "760"; controlPanel.align = "center"; controlPanel.cellSpacing = "0"; controlPanel.cellPadding = "0"; controlPanel.innerHTML = '<tr height="25px"><td width="10" bgcolor="#7FB448" style="padding-left: 10px;"></td>' + '<td class="white_text_14_bold" style="padding-left:10px; background-color:#7FB448;">' + 'HIT Database Mk. II <a href="https://greasyfork.org/en/scripts/11733-mturk-hit-database-mk-ii" class="whatis" target="_blank">' + '(What\'s this?)</a></td></tr>' + '<tr><td class="container-content" colspan="2">' + '<div style="text-align:center;" id="hdbDashboardInterface">' + '<button id="hdbBackup" title="Export your entire database!\nPerfect for moving between computers or as a periodic backup">Create Backup</button>' + '<button id="hdbRestore" title="Restore database from external backup file" style="margin:5px">Restore</button>' + '<button id="hdbUpdate" title="Update... the database" style="color:green;">Update Database</button>' + '<div id="hdbFileSelector" style="display:none"><input id="hdbFileInput" type="file" /></div>' + '<br>' + '<button id="hdbPending" title="Summary of all pending HITs\n Can be exported as CSV" style="margin: 0px 5px 5px;">Pending Overview</button>' + '<button id="hdbRequester" title="Summary of all requesters\n Can be exported as CSV" style="margin: 0px 5px 5px;">Requester Overview</button>' + '<button id="hdbDaily" title="Summary of each day you\'ve worked\nCan be exported as CSV" style="margin:0px 5px 5px;">Daily Overview</button>' + '<br>' + '<label>Find </label>' + '<select id="hdbStatusSelect"><option value="*">ALL</option><option value="Approval" style="color: orange;">Pending Approval</option>' + '<option value="Rejected" style="color: red;">Rejected</option><option value="Approved" style="color:green;">Approved - Pending Payment</option>' + '<option value="(Paid|Approved)" style="color:green;">Paid OR Approved</option></select>' + '<label> HITs matching: </label><input id="hdbSearchInput" title="Query can be HIT title, HIT ID, or requester name" />' + '<button id="hdbSearch">Search</button>' + '<br>' + '<label>from date </label><input id="hdbMinDate" maxlength="10" size="10" title="Specify a date, or leave blank">' + '<label> to </label><input id="hdbMaxDate" malength="10" size="10" title="Specify a date, or leave blank">' + '<label for="hdbCSVInput" title="Export results as CSV file" style="margin-left:50px; vertical-align:middle;">export CSV</label>' + '<input id="hdbCSVInput" title="Export results as CSV file" type="checkbox" style="vertical-align:middle;">' + '<br>' + '<label id="hdbStatusText">placeholder status text</label>' + '<div id="hdbProgressBar" class="hdbProgressContainer"><div class="hdbProgressOuter"><div class="hdbProgressInner"></div></div></div>' + '</div></td></tr>'; var updateBtn = document.querySelector("#hdbUpdate"), backupBtn = document.querySelector("#hdbBackup"), restoreBtn = document.querySelector("#hdbRestore"), fileInput = document.querySelector("#hdbFileInput"), exportCSVInput = document.querySelector("#hdbCSVInput"), searchBtn = document.querySelector("#hdbSearch"), searchInput = document.querySelector("#hdbSearchInput"), pendingBtn = document.querySelector("#hdbPending"), reqBtn = document.querySelector("#hdbRequester"), dailyBtn = document.querySelector("#hdbDaily"), fromdate = document.querySelector("#hdbMinDate"), todate = document.querySelector("#hdbMaxDate"), statusSelect = document.querySelector("#hdbStatusSelect"), progressBar = document.querySelector("#hdbProgressBar"); var searchResults = document.createElement("DIV"); searchResults.align = "center"; searchResults.id = "hdbSearchResults"; searchResults.style.display = "block"; searchResults.innerHTML = '<table cellSpacing="0" cellpadding="2" id="hdbResultsTable"></table>'; document.body.insertBefore(searchResults, insertionNode); updateBtn.onclick = function() { progressBar.style.display = "block"; HITStorage.fetch(MTURK_BASE+"status"); document.querySelector("#hdbStatusText").textContent = "fetching status page...."; }; exportCSVInput.addEventListener("click", function() { if (exportCSVInput.checked) { searchBtn.textContent = "Export CSV"; pendingBtn.textContent += " (csv)"; reqBtn.textContent += " (csv)"; dailyBtn.textContent += " (csv)"; } else { searchBtn.textContent = "Search"; pendingBtn.textContent = pendingBtn.textContent.replace(" (csv)",""); reqBtn.textContent = reqBtn.textContent.replace(" (csv)",""); dailyBtn.textContent = dailyBtn.textContent.replace(" (csv)", ""); } }); fromdate.addEventListener("focus", function() { var offsets = getPosition(this, true); new Calendar(offsets.x, offsets.y, this).drawCalendar(); }); todate.addEventListener("focus", function() { var offsets = getPosition(this, true); new Calendar(offsets.x, offsets.y, this).drawCalendar(); }); backupBtn.onclick = HITStorage.backup; restoreBtn.onclick = function() { fileInput.click(); }; fileInput.onchange = processFile; searchBtn.onclick = function() { var r = getRange(); var _filter = { status: statusSelect.value, query: searchInput.value.trim().length > 0 ? searchInput.value : "*" }; var _opt = { index: "date", range: r.range, dir: r.dir, filter: _filter, progress: true }; HITStorage.recall("HIT", _opt).then(function(r) { searchResults.firstChild.innerHTML = exportCSVInput.checked ? r.formatCSV() : r.formatHTML(); autoScroll("#hdbSearchResults"); for (var _r of r.results) { // retrieve and append notes HITStorage.recall("NOTES", { index: "hitId", range: window.IDBKeyRange.only(_r.hitId) }).then(noteHandler.bind(null,"attach")); } var el = null; for (el of document.querySelectorAll(".bonusCell")) { el.dataset.initial = el.textContent; el.onblur = updateBonus; el.onkeydown = updateBonus; } for (el of document.querySelectorAll('span[id^="note-"]')) { el.onclick = noteHandler.bind(null,"new"); } progressBar.style.display = "none"; }); }; // search button click event pendingBtn.onclick = function() { var r = getRange(); var _filter = { status: "Approval", query: searchInput.value.trim().length > 0 ? searchInput.value : "*" }, _opt = { index: "date", dir: "prev", range: r.range, filter: _filter, progress: true }; HITStorage.recall("HIT", _opt).then(function(r) { searchResults.firstChild.innerHTML = exportCSVInput.checked ? r.formatCSV("pending") : r.formatHTML("pending"); autoScroll("#hdbSearchResults"); var expands = document.querySelectorAll(".hdbExpandRow"); for (var el of expands) { el.onclick = showHiddenRows; } progressBar.style.display = "none"; }); }; //pending overview click event reqBtn.onclick = function() { var r = getRange(); var _opt = { index: "date", range: r.range, progress: true }; HITStorage.recall("HIT", _opt).then(function(r) { searchResults.firstChild.innerHTML = exportCSVInput.checked ? r.formatCSV("requester") : r.formatHTML("requester"); autoScroll("#hdbSearchResults"); var expands = document.querySelectorAll(".hdbExpandRow"); for (var el of expands) { el.onclick = showHiddenRows; } progressBar.style.display = "none"; }); }; //requester overview click event dailyBtn.onclick = function() { HITStorage.recall("STATS", { dir: "prev" }).then(function(r) { searchResults.firstChild.innerHTML = exportCSVInput.checked ? r.formatCSV("daily") : r.formatHTML("daily"); autoScroll("#hdbSearchResults"); }); }; //daily overview click event function getRange() { var _min = fromdate.value.length === 10 ? fromdate.value : undefined, _max = todate.value.length === 10 ? todate.value : undefined; var _range = (_min === undefined && _max === undefined) ? null : (_min === undefined) ? window.IDBKeyRange.upperBound(_max) : (_max === undefined) ? window.IDBKeyRange.lowerBound(_min) : (_max < _min) ? window.IDBKeyRange.bound(_max,_min) : window.IDBKeyRange.bound(_min,_max); return { min: _min, max: _max, range: _range, dir: _max < _min ? "prev" : "next" }; } function getPosition(element, includeHeight) { var offsets = { x: 0, y: includeHeight ? element.offsetHeight : 0 }; do { offsets.x += element.offsetLeft; offsets.y += element.offsetTop; element = element.offsetParent; } while (element); return offsets; } }//}}} dashboard function showHiddenRows(e) {//{{{ 'use strict'; var rid = e.target.parentNode.textContent.substr(4); var nodes = document.querySelectorAll('tr[data-rid="'+rid+'"]'), el = null; if (e.target.textContent === "[+]") { for (el of nodes) el.style.display="table-row"; e.target.textContent = "[-]"; } else { for (el of nodes) el.style.display="none"; e.target.textContent = "[+]"; } }//}}} function updateBonus(e) {//{{{ 'use strict'; if (e instanceof window.KeyboardEvent && e.keyCode === 13) { e.target.blur(); return false; } else if (e instanceof window.FocusEvent) { var _bonus = +e.target.textContent.replace(/\$/,""); if (_bonus !== +e.target.dataset.initial) { console.log("updating bonus to",_bonus,"from",e.target.dataset.initial,"("+e.target.dataset.hitid+")"); e.target.dataset.initial = _bonus; var _pay = +e.target.previousSibling.textContent, _range = window.IDBKeyRange.only(e.target.dataset.hitid); window.indexedDB.open("HITDB_TESTING").onsuccess = function() { this.result.transaction("HIT", "readwrite").objectStore("HIT").openCursor(_range).onsuccess = function() { var c = this.result; if (c) { var v = c.value; v.reward = { pay: _pay, bonus: _bonus }; c.update(v); } }; // idbcursor }; // idbopen } // bonus is new value } // keycode } //}}} updateBonus function noteHandler(type, e) {//{{{ // // TODO restructure event handling/logic tree // combine save and delete; it's ugly :( // actually this whole thing is messy and in need of refactoring // 'use strict'; if (e instanceof window.KeyboardEvent) { if (e.keyCode === 13) { e.target.blur(); return false; } return; } if (e instanceof window.FocusEvent) { if (e.target.textContent.trim() !== e.target.dataset.initial) { if (!e.target.textContent.trim()) { e.target.previousSibling.previousSibling.firstChild.click(); return; } var note = e.target.textContent.trim(), _range = window.IDBKeyRange.only(e.target.dataset.id), inote = e.target.dataset.initial, hitId = e.target.dataset.id, date = e.target.previousSibling.textContent; e.target.dataset.initial = note; window.indexedDB.open("HITDB_TESTING").onsuccess = function() { this.result.transaction("NOTES", "readwrite").objectStore("NOTES").index("hitId").openCursor(_range).onsuccess = function() { if (this.result) { var r = this.result.value; if (r.note === inote) { // note already exists in database, so we update its value r.note = note; this.result.update(r); return; } this.result.continue(); } else { if (this.source instanceof window.IDBObjectStore) this.source.put({ note:note, date:date, hitId:hitId }); else this.source.objectStore.put({ note:note, date:date, hitId:hitId }); } }; this.result.close(); }; } return; // end of save event; no need to proceed } if (type === "delete") { var tr = e.target.parentNode.parentNode, noteCell = tr.lastChild; _range = window.IDBKeyRange.only(noteCell.dataset.id); if (!noteCell.dataset.initial) tr.remove(); else { window.indexedDB.open("HITDB_TESTING").onsuccess = function() { this.result.transaction("NOTES", "readwrite").objectStore("NOTES").index("hitId").openCursor(_range).onsuccess = function() { if (this.result) { if (this.result.value.note === noteCell.dataset.initial) { this.result.delete(); tr.remove(); return; } this.result.continue(); } }; this.result.close(); }; } return; // end of deletion event; no need to proceed } else { if (type === "attach" && !e.results.length) return; var trow = e instanceof window.MouseEvent ? e.target.parentNode.parentNode : null, tbody = trow ? trow.parentNode : null, row = document.createElement("TR"), c1 = row.insertCell(0), c2 = row.insertCell(1), c3 = row.insertCell(2); date = new Date(); hitId = e instanceof window.MouseEvent ? e.target.id.substr(5) : null; c1.innerHTML = '<span class="removeNote" title="Delete this note" style="cursor:pointer;color:crimson;">[x]</span>'; c1.firstChild.onclick = noteHandler.bind(null,"delete"); c1.style.textAlign = "right"; c2.title = "Date on which the note was added"; c3.style.color = "crimson"; c3.colSpan = "5"; c3.contentEditable = "true"; c3.onblur = noteHandler.bind(null,"blur"); c3.onkeydown = noteHandler.bind(null, "kb"); if (type === "new") { row.classList.add(trow.classList); tbody.insertBefore(row, trow.nextSibling); c2.textContent = date.getFullYear()+"-"+Number(date.getMonth()+1).toPadded()+"-"+Number(date.getDate()).toPadded(); c3.dataset.initial = ""; c3.dataset.id = hitId; c3.focus(); return; } for (var entry of e.results) { trow = document.querySelector('tr[data-id="'+entry.hitId+'"]'); tbody = trow.parentNode; row = row.cloneNode(true); c1 = row.firstChild; c2 = c1.nextSibling; c3 = row.lastChild; row.classList.add(trow.classList); tbody.insertBefore(row, trow.nextSibling); c1.firstChild.onclick = noteHandler.bind(null,"delete"); c2.textContent = entry.date; c3.textContent = entry.note; c3.dataset.initial = entry.note; c3.dataset.id = entry.hitId; c3.onblur = noteHandler.bind(null,"blur"); c3.onkeydown = noteHandler.bind(null, "kb"); } } // new/attach }//}}} noteHandler function processFile(e) {//{{{ 'use strict'; var f = e.target.files; if (f.length && f[0].name.search(/\.bak$/) && ~f[0].type.search(/text/)) { var reader = new FileReader(), testing = true; reader.readAsText(f[0].slice(0,10)); reader.onload = function(e) { if (testing && e.target.result.search(/(STATS|NOTES|HIT)/) < 0) { return error(); } else if (testing) { testing = false; document.querySelector("#hdbProgressBar").style.display = "block"; reader.readAsText(f[0]); } else { var data = JSON.parse(e.target.result); console.log(data); HITStorage.write(data, "restore"); } }; // reader.onload } else { error(); } function error() { var s = document.querySelector("#hdbStatusText"), e = "Restore::FileReadError : encountered unsupported file"; s.style.color = "red"; s.textContent = e; throw e; } }//}}} processFile function autoScroll(location, dt) {//{{{ 'use strict'; var target = document.querySelector(location).offsetTop, pos = window.scrollY, dpos = Math.ceil((target - pos)/3); dt = dt ? dt-1 : 25; // time step/max recursions if (target === pos || dpos === 0 || dt === 0) return; window.scrollBy(0, dpos); setTimeout(function() { autoScroll(location, dt); }, dt); }//}}} function Calendar(offsetX, offsetY, caller) {//{{{ 'use strict'; this.date = new Date(); this.offsetX = offsetX; this.offsetY = offsetY; this.caller = caller; this.drawCalendar = function(year,month,day) {//{{{ year = year || this.date.getFullYear(); month = month || this.date.getMonth()+1; day = day || this.date.getDate(); var longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var date = new Date(year,month-1,day); var anchors = _getAnchors(date); //make new container if one doesn't already exist var container = null; if (document.querySelector("#hdbCalendarPanel")) { container = document.querySelector("#hdbCalendarPanel"); container.removeChild( container.getElementsByTagName("TABLE")[0] ); } else { container = document.createElement("DIV"); container.id = "hdbCalendarPanel"; document.body.appendChild(container); } container.style.left = this.offsetX; container.style.top = this.offsetY; var cal = document.createElement("TABLE"); cal.cellSpacing = "0"; cal.cellPadding = "0"; cal.border = "0"; container.appendChild(cal); cal.innerHTML = '<tr>' + '<th class="hdbCalHeader hdbCalControls" title="Previous month" style="text-align:right;"><span><</span></th>' + '<th class="hdbCalHeader hdbCalControls" title="Previous year" style="text-align:center;"><span>≪</span></th>' + '<th colspan="3" id="hdbCalTableTitle" class="hdbCalHeader">'+date.getFullYear()+'<br>'+longMonths[date.getMonth()]+'</th>' + '<th class="hdbCalHeader hdbCalControls" title="Next year" style="text-align:center;"><span>≫</span></th>' + '<th class="hdbCalHeader hdbCalControls" title="Next month" style="text-align:left;"><span>></span></th>' + '</tr><tr><th class="hdbDayHeader" style="color:red;">S</th><th class="hdbDayHeader">M</th>' + '<th class="hdbDayHeader">T</th><th class="hdbDayHeader">W</th><th class="hdbDayHeader">T</th>' + '<th class="hdbDayHeader">F</th><th class="hdbDayHeader">S</th></tr>'; document.querySelector('th[title="Previous month"]').addEventListener( "click", function() { this.drawCalendar(date.getFullYear(), date.getMonth(), 1); }.bind(this) ); document.querySelector('th[title="Previous year"]').addEventListener( "click", function() { this.drawCalendar(date.getFullYear()-1, date.getMonth()+1, 1); }.bind(this) ); document.querySelector('th[title="Next month"]').addEventListener( "click", function() { this.drawCalendar(date.getFullYear(), date.getMonth()+2, 1); }.bind(this) ); document.querySelector('th[title="Next year"]').addEventListener( "click", function() { this.drawCalendar(date.getFullYear()+1, date.getMonth()+1, 1); }.bind(this) ); var hasDay = false, thisDay = 1; for (var i=0;i<6;i++) { // cycle weeks var row = document.createElement("TR"); for (var j=0;j<7;j++) { // cycle days if (!hasDay && j === anchors.first && thisDay < anchors.total) hasDay = true; else if (hasDay && thisDay > anchors.total) hasDay = false; var cell = document.createElement("TD"); cell.classList.add("hdbCalCells"); row.appendChild(cell); if (hasDay) { cell.classList.add("hdbCalDays"); cell.textContent = thisDay; cell.addEventListener("click", _clickHandler.bind(this)); cell.dataset.year = date.getFullYear(); cell.dataset.month = date.getMonth()+1; cell.dataset.day = thisDay++; } } // for j cal.appendChild(row); } // for i function _clickHandler(e) { /*jshint validthis:true*/ var y = e.target.dataset.year; var m = Number(e.target.dataset.month).toPadded(); var d = Number(e.target.dataset.day).toPadded(); this.caller.value = y+"-"+m+"-"+d; this.die(); } function _getAnchors(date) { var _anchors = {}; date.setMonth(date.getMonth()+1); date.setDate(0); _anchors.total = date.getDate(); date.setDate(1); _anchors.first = date.getDay(); return _anchors; } };//}}} drawCalendar this.die = function() { document.querySelector("#hdbCalendarPanel").remove(); }; }//}}} Calendar /* * * * * * * * * * * * * * * * TESTING FUNCTIONS -- DELETE BEFORE FINAL RELEASE * * * * * * * * * * * * * */ function INFLATEDUMMYVALUES() { //{{{ 'use strict'; var tdb = this.result; tdb.onerror = function(e) { console.log("requesterror",e.target.error.name,e.target.error.message,e); }; tdb.onversionchange = function(e) { console.log("tdb received versionchange request", e); tdb.close(); }; //console.log(tdb.transaction("HIT").objectStore("HIT").indexNames.contains("date")); console.groupCollapsed("Populating test database"); var tdbt = {}; tdbt.trans = tdb.transaction(["HIT", "NOTES", "BLOCKS"], "readwrite"); tdbt.hit = tdbt.trans.objectStore("HIT"); tdbt.notes = tdbt.trans.objectStore("NOTES"); tdbt.blocks= tdbt.trans.objectStore("BLOCKS"); var filler = { notes:[], hit:[], blocks:[]}; for (var n=0;n<100000;n++) { filler.hit.push({ date: "2015-08-00", requesterName: "tReq"+(n+1), title: "Greatest Title Ever #"+(n+1), reward: Number((n+1)%(200/n)+(((n+1)%200)/100)).toFixed(2), status: "moo", requesterId: ("RRRRRRR"+n).substr(-7), hitId: ("HHHHHHH"+n).substr(-7) }); if (n%1000 === 0) { filler.notes.push({ requesterId: ("RRRRRRR"+n).substr(-7), note: n+1 + " Proin vel erat commodo mi interdum rhoncus. Sed lobortis porttitor arcu, et tristique ipsum semper a." + " Donec eget aliquet lectus, vel scelerisque ligula." }); filler.blocks.push({requesterId: ("RRRRRRR"+n).substr(-7)}); } } _write(tdbt.hit, filler.hit); _write(tdbt.notes, filler.notes); _write(tdbt.blocks, filler.blocks); function _write(store, obj) { if (obj.length) { var t = obj.pop(); store.put(t).onsuccess = function() { _write(store, obj) }; } else { console.log("population complete"); } } console.groupEnd(); dbh = window.indexedDB.open("HITDB_TESTING", DB_VERSION); dbh.onerror = function(e) { console.log("[HITDB]",e.target.error.name+":", e.target.error.message, e); }; console.log(dbh.readyState, dbh); dbh.onupgradeneeded = HITStorage.versionChange; dbh.onblocked = function(e) { console.log("blocked event triggered:", e); }; tdb.close(); }//}}} function BLANKSLATE() { //{{{ create empty db equivalent to original schema to test upgrade 'use strict'; var tdb = this.result; if (!tdb.objectStoreNames.contains("HIT")) { console.log("creating HIT OS"); var dbo = tdb.createObjectStore("HIT", { keyPath: "hitId" }); dbo.createIndex("date", "date", { unique: false }); dbo.createIndex("requesterName", "requesterName", { unique: false}); dbo.createIndex("title", "title", { unique: false }); dbo.createIndex("reward", "reward", { unique: false }); dbo.createIndex("status", "status", { unique: false }); dbo.createIndex("requesterId", "requesterId", { unique: false }); } if (!tdb.objectStoreNames.contains("STATS")) { console.log("creating STATS OS"); dbo = tdb.createObjectStore("STATS", { keyPath: "date" }); } if (!tdb.objectStoreNames.contains("NOTES")) { console.log("creating NOTES OS"); dbo = tdb.createObjectStore("NOTES", { keyPath: "requesterId" }); } if (!tdb.objectStoreNames.contains("BLOCKS")) { console.log("creating BLOCKS OS"); dbo = tdb.createObjectStore("BLOCKS", { keyPath: "id", autoIncrement: true }); dbo.createIndex("requesterId", "requesterId", { unique: false }); } } //}}} // vim: ts=2:sw=2:et:fdm=marker:noai