diff --git a/src/js/src/tracker/app.css b/src/js/src/tracker/app.css new file mode 100644 index 0000000..3d26a29 --- /dev/null +++ b/src/js/src/tracker/app.css @@ -0,0 +1,43 @@ +#left { + display: inline-block; +} + + +thead tr, tr:nth-child(2n) { + background-color: #cccccc; +} + +form[name="one_event"] { + width: 95%; +} + +fieldset input[name="command_line"],fieldset input[name="event_memo"] { + width:100%; +} + +form[name="one_event"] label.short-control { + width: fit-content; +} + +#event-logs table { + width:95%; +} + +#event-logs th.col-position-* { + width:100px; +} + +#event-logs th.col-interval { + width:100px; +} + + +#event-logs th.col-action { + width:100px; +} + +#event-logs .button-small { + font-size:85%; +} + + diff --git a/src/js/src/tracker/app.html b/src/js/src/tracker/app.html new file mode 100644 index 0000000..8fad546 --- /dev/null +++ b/src/js/src/tracker/app.html @@ -0,0 +1,106 @@ + + + + + + + + + + Solitude/Tracker + + + +
+
+
+ アクション + +
+
+
+ +
+
+ 操作パネル + + + +
+
+ イベント登録フォーム + + + 空白時は現在時刻を利用します + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + +
アクションメモラップタイム開始終了
読み込まれました
+
+
+
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/src/js/src/tracker/app.js b/src/js/src/tracker/app.js new file mode 100644 index 0000000..2af77f5 --- /dev/null +++ b/src/js/src/tracker/app.js @@ -0,0 +1,208 @@ +main(window); + +function main(global) { + const tracker = {}; + global.app = tracker; + global.addEventListener("load", initialize); + global.addEventListener("keydown", checkShortKeys); + + 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; + } + + function initialize() { + /* + * アプリケーションオブジェクトの設定 + * 後でdefinePropertiesでゲッターを設定する + */ + const ui = { + now: q("#now"), + 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, + 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: addEvent, + getNow: getNow, + } + + const data = { + domain: { + latest: Date.now() + }, + + control: { + get accesskey() { + return tracker.ui.accesskey.checked; + } + }, + } + + const key = { + //特殊文字 + enter: "Enter", + shift: "Shift", + + //通常文字 + c : "C", + } + Object.defineProperties(tracker, { + ui: {value: ui}, + command: {value: command}, + key: {value: key}, + data: {value: data}, + }); + + // 初期行の変更 + const start = new Date(); + const time = start.toLocaleTimeString(); + q(tracker.ui.initRec, "td.col-position-start").textContent = time; + + // 現在時刻の初回呼び出し、後は自動で1秒ごとに更新。 + updateNow(1000); + + /* + * コントローラの設定 + */ + tracker.ui.addEvent.addEventListener('click', (event) => { + addEvent(tracker.ui.memo.value, tracker.ui.time.value); + }) + + } + + function addEvent(memo, datetime) { + + const newTime = datetime || tracker.command.getNow(); + const newMemo = memo.toString(); + 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 = newTime; + q(newEvent, "td.col-memo").textContent = newMemo; + if (tr instanceof Element) { // i.e. 行数1行 + insertNewEvent(newEvent, tr, newTime); + } else if (tr.length > 0) { // i.e. 行数2行以上 + insertNewEvent(newEvent, tr[0], newTime); + } else { // i.e. 行数0行 + q(tracker.ui.eventLogs, "tbody").appendChild(newEvent); + } + + function insertNewEvent(first, second, endTime) { + // second行の開始は作成時に入っている。 + const startTime = q(second, "td.col-position-start").textContent; + q(second, "td.col-position-end").textContent = endTime; + q(second, "td.col-interval").textContent = getInterval(startTime, endTime); + + q(tracker.ui.eventLogs, "tbody").insertBefore(first, second); + } + + + function getInterval(start, end) { + const old = tracker.data.domain.latest + tracker.data.domain.latest = Date.now(); + let interval = tracker.data.domain.latest - old; + //秒単位にする。ミリ秒以下の数字を先に0にすることで割り切れるようにしてる。 + interval = ( interval - (interval % 1000) ) / 1000 + /* + const date = (new Date()).toLocaleDateString("ja-JP"); + const before = new Date(date.replaceAll('/', '-') + "T" + start); + const after = new Date(date.replaceAll('/', '-') + "T" + end); + // Dateの起算が1970/1/1/ 00:00:00 (UTC)のため、時刻として時間を求められる。 + const interval = before.getTime() - after.getTime(); + */ + return interval; + } + } + + 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) { + console.log("do nothing"); + 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; + + } + + } + + + function updateNow(interval) { + const start = new Date(); + const time = start.toLocaleDateString() + " " + start.toLocaleTimeString(); + tracker.ui.now.textContent = time; + + setTimeout(updateNow, interval, interval); + } + +}