Newer
Older
solitude / src / js / src / tracker / app.js
@sylow-castle sylow-castle on 12 Jul 12 KB [update]まとめてコミット
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;

        }

    }
}