Custom Dictionary(自製字典庫)

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
    }
}