main(window);
function main(global) {
const tracker = {};
const domain = createDomain({},q("#initial_record .col-memo").textContent);
const app = tracker;
global.addEventListener("load", initializeApp);
global.addEventListener("keydown", checkShortKeys);
return;
function q(element, selector) {
// 引数一つはdocumentを起点に検索する
if (arguments.length <= 1)
return q(global.document, element);
/*
* 任意のデータをクエリと解釈する。
* 不適切なクエリの検索結果は空配列とした。
* エラーのほうが適切なのかな……?
*/
var result = []
if (typeof selector == 'string') {
result = element.querySelectorAll(selector);
if(result.length == 1) {
result = result[0];
}
}
return result;
}
//恒等写像。array.mapで配列のコピー作るときとかに使う。
function id(x) {
return x;
}
function createDomain(domain, firstmemo) {
domain.log = {
//publics
list: listLog,
start: startLog,
end: endLog,
getLatest: getLatestLog,
toCSV: listAsCsvString,
private: {
latest: false,
initialize: initializeLog,
list : [],
},
};
domain.latest= Date.now();
domain.log.private.initialize(firstmemo);
return domain;
/*
* 関数宣言
*/
// Log操作
function initializeLog(memo) {
const log = domain.log;
if(log.private.latest) {
return;
}
//ユーザーは末端の処理を気にしないようにこちらで実施。
const firstLog = {
memo: memo,
start: new Date(),
end: "",
interval: intervalLog,
};
log.private.latest = firstLog
log.private.list.push(firstLog);
}
function getLatestLog() {
return domain.log.private.latest;
}
// 新しい記録を始める。古い記録は終了される。
function startLog(memo, start) {
const log = domain.log;
const secondary = log.private.latest;
// 実質Logオブジェクトを生成している箇所
const primary = {
memo: memo,
start: start,
end: "",
interval: intervalLog,
};
secondary.end = start;
domain.latest = Date.now();
log.private.latest = primary;
log.private.list.push(primary)
return new Promise((resolve, reject) => {
resolve({primary: primary, secondary: secondary});
});
}
function intervalLog() {
if(this.end instanceof Date) {
return this.end - this.start;
}
return 0;
}
function endLog(end) {
}
function listLog() {
return domain.log.private.list.map(id)
}
function listAsCsvString(delim) {
delim = delim || ","
const CRLF = "\r\n";
const LF ="\n";
// 出力するCSVフォーマットはRFC4180を正とする:
// https://www.ietf.org/rfc/rfc4180.txt
const csvbody = domain.log.list().map(log => {
const memo = log.memo.replaceAll('"', '""').replaceAll(CRLF, LF);
const end = log.end instanceof Date ? log.end.toLocaleTimeString() : "";
const start = log.start.toLocaleTimeString();
let interval = log.interval();
//整数にしたい。
interval = (interval - (interval % 1000 )) / 1000
return [memo, interval , start, end];
}).map(line => line.map(string => `"${string}"`))
.map(line => line.join(delim))
.join("\r\n");
const header = `"メモ","ラップタイム","開始","終了"${CRLF}`
return header.replaceAll(",", delim) + csvbody;
}
}
function initializeApp() {
// 実行するタイミングは後になるけど、
// 本関数が最終的に行いたいことを示す部分のため先に記述
function setupApp() {
Object.defineProperties(tracker, {
ui: {value: ui},
command: {value: command},
key: {value: key},
data: {value: data},
domain:{value: domain},
});
return;
}
/*
* アプリケーションオブジェクトの設定
* 後でdefinePropertiesでゲッターを設定する
*/
const ui = {
now: q("#now"),
menuSaveTSV: q("#save-as-tsv"),
menuSaveCSV: q("#save-as-csv"),
menuLicense: q("#license-purecss"),
accesskey: document.forms.one_event.elements.accesskey,
command: document.forms.one_event.elements.command_line,
memo: document.forms.one_event.elements.event_memo,
time: document.forms.one_event.elements.event_time,
addEvent: document.forms.one_event.elements.event_add,
logTable: q("#event-logs tbody"),
initRec: q("#initial_record"),
get active() {
return document.activeElement;
},
eventLogs: q("#event-logs"),
tmpl: {
eventRec: q("#event_record"),
}
}
const command = {
readyCommand: readyCommand,
readyMemo: readyMemo,
addEvent: addEventLog,
getNow: getNow,
}
const data = {
domain: domain,
control: {
get accesskey() {
return tracker.ui.accesskey.checked;
}
},
}
const key = {
//特殊文字
enter: "Enter",
shift: "Shift",
//通常文字
c : "C",
}
//trackerにまとめあげる
setupApp();
// 初期行の変更
const time = domain.log.getLatest().start.toLocaleTimeString();
q(tracker.ui.initRec, "td.col-position-start").textContent = time;
// 現在時刻の初回呼び出し、後は自動で1秒ごとに更新。
updateNow(1000);
//各種コントローラのイベントハンドリングなど
setupController()
return;
function setupController() {
tracker.ui.addEvent.addEventListener('click', (event) => {
addEventLog(tracker.ui.memo.value, tracker.ui.time.value);
});
tracker.ui.logTable.addEventListener('click', (event) => {
if (isDeleteButton(event.target)) {
console.log("todo delete process")
}
});
tracker.ui.menuSaveCSV.addEventListener('click', (event) => showCSV());
tracker.ui.menuSaveTSV.addEventListener('click', (event) => showCSV("\t"));
tracker.ui.menuLicense.addEventListener('click', (event) => {
const csvWindow = window.open("license/PURE-CSS.txt", "_blank", "popup,width=640,height=480");
});
tracker.ui.command.focus();
}
function showCSV(delim) {
const csvWindow = window.open("", "_blank", "popup,width=640,height=480");
csvWindow.document.open();
csvWindow.document.write("<pre>" + domain.log.toCSV(delim) + "</pre>");
csvWindow.document.close();
}
function isDeleteButton(element) {
return element.classList.contains("delete-event");
}
function updateNow(interval) {
const start = new Date();
const time = start.toLocaleDateString() + " " + start.toLocaleTimeString();
tracker.ui.now.textContent = time;
setTimeout(updateNow, interval, interval);
}
}
function addEventLog(memo, time) {
// 規定値
let finishTime = new Date();
if (time && time.length > 0) {
if (isIn(validTimeSyntax(time)) || isIn(validTimeSemantics(time))) {
alert("日付の指定に誤りがあります");
return;
}
finishTime = guessDateTime(time)
} else {
//入力欄が空白は規定値を与えられたとみなす。
}
domain.log
.start(memo.toString(), finishTime)
.then(log => _addEventLog(log.primary, log.secondary));
return;
//invalid と見せたいだけ。
function isIn(bool) {
return !bool;
}
function validTimeSyntax(timeString) {
const valid = /^[0-9]{2}:[0-9]{2}:[0-9]{2}$/;
return valid.test(timeString.trim())
}
function validTimeSemantics(timeString) {
const strings = timeString.trim().split(':');
const nums = strings.map(x => parseInt(x, 10));
const hours = (0 <= nums[0] && nums[0] < 24);
const minits = (0 <= nums[1] && nums[1] < 60);
const seconds =(0 <= nums[2] && nums[2] < 60);
return hours && minits && seconds
}
function guessDateTime(time) {
//日付の情報はないため補う。datetimeは「今日」と解釈する。
//ユーザーからのdatetime、「今日」はJST時刻のためずらす。
const timezone = 9 * 60 * 60 * 1000;
const jst = Date.now() + timezone;
//ISOString形式の例: 14:02:12T00:12:34.000Z"
const today = (new Date(jst)).toISOString().substr(0, 10);
let dateTime = "";
if (time) {
const jst = new Date(`${today}T${time}.000Z`);
dateTime = new Date(jst.valueOf() - timezone);
} else {
dateTime = new Date();
}
return dateTime;
}
function _addEventLog(firstLog, secondLog) {
//新しい行の設定
const newEvent = q(tracker.ui.tmpl.eventRec.content.cloneNode(true), "tr");
let tr = q(tracker.ui.eventLogs, "tbody tr ");
q(newEvent, "td.col-position-start").textContent = firstLog.start.toLocaleTimeString('ja-JP');
q(newEvent, "td.col-memo").textContent = firstLog.memo;
//追加時のリストの状況を見分ける
if (tr instanceof Element) { // i.e. 行数1行
insertNewEvent(newEvent, tr, secondLog);
} else if (tr.length > 0) { // i.e. 行数2行以上
insertNewEvent(newEvent, tr[0], secondLog);
} else { // i.e. 行数0行
q(tracker.ui.eventLogs, "tbody").appendChild(newEvent);
}
return;
function insertNewEvent(first, second, secondaryLog) {
// second行の残りを埋めていく、開始列は作成時に入っている。
q(second, "td.col-position-end").textContent = secondaryLog.end.toLocaleTimeString();
//秒で表示するためにミリ秒を秒に変換
const interval = Math.floor(secondaryLog.interval() / 1000);
q(second, "td.col-interval").textContent = interval
q(tracker.ui.eventLogs, "tbody").insertBefore(first, second);
}
}
}
function getNow() {
return tracker.ui.now.textContent.replace(/^.* /, "");
}
function readyCommand() {
readyOf(tracker.ui.command);
}
function readyMemo() {
readyOf(tracker.ui.memo);
}
function readyOf(ui) {
if(tracker.ui.active == ui) {
//もともとフォーカス当たっているので何もせずそのまま
return;
}
neutral();
ui.focus();
}
function neutral() {
for ( elem in tracker.ui) {
if((elem instanceof Element) && (elem.hasFocus())) {
elem.blur();
}
}
}
function checkShortKeys(event) {
if((!event.shiftKey) || (!tracker.data.control.accesskey)) {
return;
}
const binds = tracker.key
switch (event.key) {
case binds.c:
if( tracker.ui.command !== tracker.ui.active ) {
tracker.command.readyCommand();
} else {
console.log("do nothing");
}
break;
case binds.enter:
tracker.command.readyMemo();
break;
}
}
}