Custom Dictionary(自製字典庫)

Custom Dictionary(自製字典庫):設定自己的字典庫,可在任意網頁幫助查找,貼上。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Custom Dictionary(自製字典庫)
// @namespace    http://tampermonkey.net/
// @description  Custom Dictionary(自製字典庫):設定自己的字典庫,可在任意網頁幫助查找,貼上。
// @version      0.1
// @author       papago89
// @match        https://*/*
// @match        http://*/*
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @require      https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js
// @include      @
// @license MIT
// ==/UserScript==

let dictionaryJSON = {
    "ControlKeyPage1": {
        "name": "Control key Page 1",
        "data": [
            {
                "value": "CTRL + ↓",
                "description": "will select next row"
            },
            {
                "value": "CTRL + ↑",
                "description": "will select previous row"
            },
            {
                "value": "CTRL + →",
                "description": "will select next category"
            },
            {
                "value": "CTRL + your mouse primary button(通常是左鍵)",
                "description": "will show now selected row value to search bar"
            },
            {
                "value": "enter",
                "description": "will paste now selected row value to your browser focus element"
            }
        ]
    },
    "ControlKeyPage2": {
        "name": "Control key Page 2",
        "data": [
            {
                "value": "CTRL + ←",
                "description": "will select previous category"
            }
        ]
    },
    "record-2": {
        "name": "test-2",
        "data": [
            {
                "value": "this is test-1 value",
                "description": "simple description"
            },
            {
                "value": "this is test-2 value, don't set the description"
            }
        ]
    },
    "record-3": {
        "name": "test-3",
        "data": [
            {
                "value": "127.0.0.1",
                "description": "just test regexp find IP"
            }
        ]
    },
    "record-4": {
        "name": "test-4",
        "data": [
            {
                "value": "blablabla\n            \n            blablabla",
                "description": "data can put newline."
            }
        ]
    },
    "record-5": {
        "name": "this data from website json file",
        "url": "https://cdn.jsdelivr.net/gh/papago89/temp/fav-json"
    }
};

// 控制快捷鍵計數
let ctrlClickCounter = 0;
let ctrlCleanTimeout = null;

// overlay 相關控制
let isShowOverlay = false;
let xPlace = null;
let yPlace = null;

// 保留原先關注的元素便於回覆
let originActiveElement = null;

// 紀錄現在應該指在哪一格資料上
let xActive = 0;
let yActive = 0;

// 符合現在條件的資料
let matchKeyData = {};

(function () {
    'use strict';
    GM_addStyle("table.dicionary {background:inherit;table-layout:fixed;overflow:hidden;}}");
    GM_addStyle("tbody.dicionary,thead.dicionary,tr.dicionary {background:inherit;overflow:hidden;}");
    GM_addStyle("table.dicionary th {padding:5px;text-align:center;background:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}");
    GM_addStyle("table.dicionary td {padding:5px;background:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}");
    GM_addStyle("table.dicionary td.active,table.dicionary th.active {border:1px solid blue;font-weight:bold;color:yellow;background:rgba(255,10,20,0.5);}");
    GM_addStyle("table.dicionary td:hover {white-space:normal;overflow:auto}");

    $(document).keyup(e => {
        if (17 == e.keyCode) {
            ++ctrlClickCounter;
            if (null != ctrlCleanTimeout) {
                clearTimeout(ctrlCleanTimeout)
            }
            if (3 == ctrlClickCounter) {
                generateOverlayWhenNotExists();
                if (!isShowOverlay) {
                    displayOverlay();
                } else {
                    undisplayOverlay();
                }
                ctrlClickCounter = 0;
            } else {
                ctrlCleanTimeout = setTimeout(() => ctrlClickCounter = 0, 350);
            }
        }
    });

    $(document).mousemove(e => {
        xPlace = e.pageX;
        yPlace = e.pageY;
    });
})();

function init() {
    let gmJSON = GM_getValue('dictionaryJSON');
    if (null != gmJSON) {
        processDictionaryJSON(gmJSON);
    }
}

function processDictionaryJSON(originalJSON) {
    let promiseList = [];
    for (let key of Object.keys(originalJSON)) {
        if (null != originalJSON[key].url) {
            promiseList.push(new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: originalJSON[key].url,
                    headers: {
                        'User-Agent': 'Mozilla/5.0',
                        'Accept': 'text/json',
                        'Content-Type': 'application/json'
                    },
                    responseType: 'json',
                    onload: function (response) {
                        let tempJSON = Object.assign({}, originalJSON[key]);
                        delete tempJSON['url'];
                        tempJSON.data = response.response;
                        resolve({ 'key': key, 'obj': tempJSON });
                    }
                });
            }));
        }
    }
    if (promiseList.length > 0) {
        Promise.all(promiseList).then(values => {
            for (let value of values) {
                originalJSON[value.key] = value.obj;
            }
            dictionaryJSON = originalJSON;
            $('#dictionarySearchKey')[0].focus();
            searchKeyChangeTrigger($('#dictionarySearchKey')[0].value);
        });
    } else {
        dictionaryJSON = originalJSON;
        $('#dictionarySearchKey')[0].focus();
        searchKeyChangeTrigger($('#dictionarySearchKey')[0].value);
    }
}

function startSetting() {
    let top = (window.screen.height / 2) - 425;
    let left = (window.screen.width / 2) - 540;
    $('body').append(
        '  <div id="dictionarySetting" style="left: ' + left + 'px; top: ' + top + 'px; width: 680px; height: 550px; background: rgba(0, 161, 155, 0.5); color: #ffffff; z - index: 9998; position: fixed; padding: 5px; text - align: center; border - bottom - left - radius: 4px; border - bottom - right - radius: 4px; border - top - left - radius: 4px; border - top - right - radius: 4px;">\n' +
        '    <button id="saveThenCloseBtton" style="height: 40px;">點擊保存並關閉</button>\n' +
        '    <textarea id="dictionarySettingContent" style="top:40px; width: 680px; height: 510px; overflow-y: scroll; z - index: 9999; background: rgba(0, 171, 164, 0.5); color: #ffffff;"></textarea>\n' +
        '  </div>'
    );
    let gmValue = GM_getValue('dictionaryJSON');

    if (null == gmValue) {
        gmValue = dictionaryJSON;
    }

    $('#dictionarySettingContent')[0].value = JSON.stringify(gmValue);

    $('#saveThenCloseBtton').click(saveThenClose);
}

function saveThenClose() {
    let originalJSON = JSON.parse($('#dictionarySettingContent')[0].value);
    GM_setValue('dictionaryJSON', originalJSON);
    processDictionaryJSON(originalJSON);
    $('#dictionarySetting').remove();
}

/**
* 產生 Overlay
*/
function generateOverlayWhenNotExists() {
    if (null != $('#dictionaryOverlay')[0]) {
        return;
    }
    init();
    let top = (window.screen.height / 2) - 425;
    let left = (window.screen.width / 2) - 540;
    $('body').append(
        '  <div id="dictionaryOverlay" style="left: ' + left + 'px; top: ' + top + 'px; width: 680px; height: 550px; display:none; background: rgba(0, 161, 155, 0.5); color: #ffffff; overflow: hidden; z - index: 9998; position: fixed; padding: 5px; text - align: center; border - bottom - left - radius: 4px; border - bottom - right - radius: 4px; border - top - left - radius: 4px; border - top - right - radius: 4px;">\n' +
        '    <button id="dictionarySettingButton" style="font-size: 10px; height: 20px;">設定字典</button>\n' +
        '    <textarea id="dictionarySearchKey" style="top: 20px; width: 680px; height: 20px; background: rgba(0, 171, 164, 0.5); color: #ffffff;"></textarea>\n' +
        '    <div id="dictionaryMain" style="top: 40px; width: 680px; height: 510px; overflow-y: scroll;"></div>\n' +
        '  </div>'
    );

    $('#dictionarySettingButton').click(startSetting);

    matchKeyData = dictionaryJSON;
    generateTableByMatchKeyData();

    $(document).keydown(e => {

        debounce(() => {
            if (isShowOverlay && e.ctrlKey) {
                reCalculate(e);
                rePosition();
            }
        }, 100)();

    });

    $('#dictionarySearchKey').on('input propertychange keyup', e => {
        if (13 == e.keyCode) {
            undisplayOverlay();
            let activeValue = $('#dictionaryData tr td.value.active')[0];
            if (null != activeValue && null != originActiveElement.value) {
                originActiveElement.value += decodeURI(activeValue.dataset.value);
            }
        } else {
            let searchKey = e.currentTarget.value;

            debounce(() => searchKeyChangeTrigger(searchKey), 750)();
        }
    });
}

function debounce(func, delay) {
    // timeout 初始值
    let timeout = null;
    return function () {
        let context = this;  // 指向 myDebounce 這個 input
        let args = arguments;  // KeyboardEvent
        clearTimeout(timeout)

        timeout = setTimeout(function () {
            func.apply(context, args)
        }, delay);
    };
}


/**
 * 根據 searchKey 的變更重新處理相關作業
 */
function searchKeyChangeTrigger(searchKey) {
    if (null != searchKey.match(/^\/(.*)\//)) {
        searchKey = searchKey.match(/^\/(.*)\//)[1];
    } else {
        searchKey = searchKey.replace(/([.(){}\[\]*+\\])/g, '\\$1')
    }
    filterData(new RegExp('.*' + searchKey + '.*'));
    generateTableByMatchKeyData();
}

/**
 * 篩選資料
 */
function filterData(regexp) {
    let temp = {};
    for (let key of Object.keys(dictionaryJSON)) {
        let tempCategory = Object.assign({}, dictionaryJSON[key]);
        tempCategory.data = tempCategory?.data?.filter(data => null != data.value.match(regexp) || null != data?.description?.match(regexp));
        if (tempCategory?.data?.length > 0) {
            temp[key] = tempCategory;
        }
    }
    matchKeyData = temp;
}

/**
 * 根據符合現行條件的內容產生資料
 */
function generateTableByMatchKeyData() {
    $('#dictionaryMain')[0].innerHTML = '';

    if (xActive >= Object.keys(matchKeyData).length) {
        xActive = 0;
    }

    let activeKey = Object.keys(matchKeyData)[xActive];

    if (yActive > matchKeyData[activeKey]?.data?.length) {
        yActive = 0;
    }

    let categoryHTML = '<table id="dictionaryCategory" class="dicionary" style="width:100%;"><thead><tr>';
    for (let key of Object.keys(matchKeyData)) {
        categoryHTML += `<th>${matchKeyData[key].name}</th>`;
    }
    categoryHTML += '</tr></thead></table>';
    $('#dictionaryMain').append(categoryHTML);

    let dataHTML = '<table id="dictionaryData" class="dicionary" style="width:100%;"><tbody>';

    for (let i in matchKeyData[activeKey]?.data) {
        let data = matchKeyData[activeKey]?.data[i];
        dataHTML += `<tr data-x-position="${xActive}" data-y-position="${i}"><td class="value" data-value="${encodeURI(data.value)}" style="width:70%;">${data.value}</td><td style="width:30%;">${null != data.description ? data.description : ''}</td></tr>`;
    }
    dataHTML += '</tbody></table>';
    $('#dictionaryMain').append(dataHTML);

    rePosition();
    $('#dictionaryData tr').click(e => {
        if (!isShowOverlay) {
            return;
        }
        xActive = parseInt(e.currentTarget.dataset.xPosition);
        yActive = parseInt(e.currentTarget.dataset.yPosition);
        rePosition();

        let activeValue = $('#dictionaryData tr td.value.active')[0];
        let decodedValue = decodeURI(activeValue.dataset.value);
        if (e.button == 0 && e.ctrlKey) {
            $('#dictionarySearchKey')[0].value = decodedValue;
            $('#dictionarySearchKey')[0].focus();
            searchKeyChangeTrigger(decodedValue);
        } else if (e.button == 0) {
            undisplayOverlay();
            if (null != originActiveElement.value) {
                originActiveElement.value += decodedValue;
            }
        }
    });
}

/**
 * 顯示 Overlay
 */
function displayOverlay() {
    $('#dictionaryOverlay')[0].style.display = '';
    $('#dictionaryOverlay')[0].style.left = xPlace + 'px';
    $('#dictionaryOverlay')[0].style.top = yPlace + 'px';
    $('#dictionaryOverlay')[0].style.top = yPlace + 'px';
    originActiveElement = document.activeElement;
    $('#dictionarySearchKey')[0].focus();
    isShowOverlay = !isShowOverlay;
}

/**
 * 隱藏 Overlay
 */
function undisplayOverlay() {
    $('#dictionaryOverlay')[0].style.display = 'none';
    $('#dictionarySearchKey')[0].value = '';
    originActiveElement.focus();
    isShowOverlay = !isShowOverlay;
}

/**
 * 計算現在有效索引的資料
 */
function reCalculate(e) {
    let dataCount = $('#dictionaryData tr').length;
    let categoryCount = Object.keys(matchKeyData).length;
    let switchCategory = false;

    if (37 == e.keyCode) { //move left or wrap
        if (xActive > 0) {
            xActive = xActive - 1;
            switchCategory = true;
        }
    }
    if (38 == e.keyCode) { // move up
        yActive = (yActive > 0) ? yActive - 1 : yActive;
    }
    if (39 == e.keyCode) { // move right or wrap
        if (xActive < categoryCount - 1) {
            xActive = xActive + 1;
            switchCategory = true;
        }
    }
    if (40 == e.keyCode) { // move down
        yActive = (yActive < dataCount - 1) ? yActive + 1 : yActive;
    }

    if (switchCategory) {
        generateTableByMatchKeyData();
    }
}

/**
 * 根據有效定位上色
 */
function rePosition() {
    $('.active').removeClass('active');
    $('#dictionaryData tr td').eq(yActive * 2).addClass('active');
    $('#dictionaryData tr td').eq(yActive * 2 + 1).addClass('active');
    $('#dictionaryMain tr th').eq(xActive).addClass('active');
    let calcTop = $('#dictionaryCategory')[0].offsetHeight + yActive * ($('#dictionaryData')[0].offsetHeight / $('#dictionaryData tr').length);
    if (calcTop - 255 > 0) {
        $('#dictionaryMain')[0].scrollTop = calcTop - 255;
    } else {
        $('#dictionaryMain')[0].scrollTop = 0;
    }
}