// ==UserScript== // @name baiduCloudInput // @name:zh-CN 百度云输入法 // @namespace baiduIME@reverland.org // @description input method in browser based on baidu online input method. // @description:zh-CN 在浏览器中自由使用百度在线输入法 // @include * // @version 1.0 // @grant GM_xmlhttpRequest // @downloadURL none // ==/UserScript== // // DONE: // : 弹窗相对于body的位置 // : 插入词而不是在结束时附加 // : 最上层!! // // TODO: CHIANFIND_RES特性 // TODO: 边沿检测特性 // TODO: 完善中文标点 setTimeout(function() { var tts = document.getElementsByTagName("textarea"); for(var i = 0; i < tts.length; i++) { initIME(tts[i]); } var tts = document.getElementsByTagName("input"); for(var i = 0; i < tts.length; i++) { initIME(tts[i]); } }, 3000); // 为了等待文本框装载进DOM function initIME(tt) { //console.log("[DEBUG]", tt); var IME = { status: 'hidden', output: '', inputString: '', TEXTS: [], page: 0 } var imePop = document.createElement('div'); initImePop(); tt.addEventListener('keydown', intercept); function initImePop() { imePop.setAttribute('id', 'baidu-cloud-input-imePop'); imePop.style.position = "absolute"; imePop.style.width = "300px"; //imePop.style.height = "80px"; imePop.style.background = "lightblue"; imePop.style.borderRadius = "5px"; imePop.style.display = "none"; imePop.style.boxShadow = "0 0 3px 0px black" imePop.style.zIndex = "9999999"; var echo = document.createElement('p'); echo.style.lineHeight = "1.5em"; echo.style.fontSize = "1em"; echo.style.margin = "0"; echo.style.padding = "0"; echo.style.paddingLeft = "0.5em"; echo.style.color = "darkblue"; echo.style.fontStyle = "bold"; imePop.appendChild(echo); var tips = document.createElement('ol'); tips.style.margin = "0px"; tips.style.padding = "0px"; tips.style.color = "black"; var tip = []; for (var i = 0; i < 5; i++) { tip[i] = document.createElement('li'); tip[i].style.margin = "0px"; tip[i].style.padding = "0px"; tip[i].style.marginLeft = "2em"; tip[i].style.listStyleType = "decimal"; tips.appendChild(tip[i]); } document.body.appendChild(imePop); var hr = document.createElement('hr') hr.style.marginTop = "0"; hr.style.marginBottom = "0.2em" imePop.appendChild(hr); imePop.appendChild(tips); } function showImePop(state) { if (state) { var coordinates = getCaretCoordinates(tt, tt.selectionEnd); var textAreaTop = findPos(tt)[1] + 20; var textAreaLeft = findPos(tt)[0]; imePop.style.left = textAreaLeft + coordinates.left + "px"; imePop.style.top = textAreaTop -tt.scrollTop + coordinates.top + "px"; imePop.style.display = "block"; } else { imePop.style.display = 'none'; } } function findPos(obj) { var curleft = curtop = 0; if (obj.offsetParent) { do { curleft += obj.offsetLeft; curtop += obj.offsetTop; } while (obj = obj.offsetParent); } return [curleft,curtop]; } function intercept(e){ // control keys if (e.ctrlKey) { return; } if (IME.status == 'POPUP') { switch (e.key) { case " ": case "1": case "2": case "3": case "4": case "5": e.preventDefault(); var index = e.key == " "?0:parseInt(e.key) - 1; var curStart = tt.selectionStart; var selectedText = imePop.querySelector('ol').children[index].textContent; tt.value = tt.value.substring(0, curStart) + selectedText + tt.value.substring(curStart, tt.value.length); tt.selectionStart = curStart + selectedText.length; tt.selectionEnd = curStart + selectedText.length; IME.inputString = ""; IME.status = 'hidden'; showImePop(false); break; case "Backspace": e.preventDefault(); IME.inputString = IME.inputString.substr(0, IME.inputString.length - 1); if (IME.inputString.length == 0) { IME.status = 'hidden'; showImePop(false); } break; case "Enter": e.preventDefault(); var curStart = tt.selectionStart; tt.value = tt.value.substring(0, curStart) + IME.inputString + tt.value.substring(curStart, tt.value.length); tt.selectionStart = curStart + IME.inputString.length; tt.selectionEnd = curStart + IME.inputString.length; IME.inputString = ""; IME.status = 'hidden'; showImePop(false); break; case "a": case "b": case "c": case "d": case "e": case "f": case "g": case "h": case "i": case "j": case "k": case "l": case "m": case "n": case "o": case "p": case "q": case "r": case "s": case "t": case "u": case "v": case "w": case "x": case "y": case "z": case "'": e.preventDefault(); IME.inputString += e.key; break; // { case "=": e.preventDefault(); IME.page += 1; //console.log("[DEBUG]", IME.page); if (IME.page < IME.TEXTS.length / 5) { updateList(IME.page); } else { IME.page -= 1; } return; case "-": e.preventDefault(); IME.page = IME.page == 0?IME.page:IME.page - 1; //console.log("[DEBUG]", IME.page); updateList(IME.page); return; // } default: e.preventDefault(); } } else if (IME.status == 'hidden') { switch (e.key) { case ",": e.preventDefault(); var curStart = tt.selectionStart; tt.value = tt.value.substring(0, curStart) + ',' + tt.value.substring(curStart, tt.value.length); tt.selectionStart = curStart + ','.length; tt.selectionEnd = curStart + ','.length; return; break; case ".": e.preventDefault(); var curStart = tt.selectionStart; tt.value = tt.value.substring(0, curStart) + '。' + tt.value.substring(curStart, tt.value.length); tt.selectionStart = curStart + '。'.length; tt.selectionEnd = curStart + '。'.length; return; break; case "a": case "b": case "c": case "d": case "e": case "f": case "g": case "h": case "i": case "j": case "k": case "l": case "m": case "n": case "o": case "p": case "q": case "r": case "s": case "t": case "u": case "v": case "w": case "x": case "y": case "z": case "'": e.preventDefault(); if (IME.inputString.length == 0) { IME.inputString += e.key; IME.status = 'POPUP'; showImePop(true); } IME.page = 0; break; default: void(0); } } imePop.querySelector('p').innerHTML = IME.inputString; // reconize key finish console.log("[IME.inputString] ", IME.inputString); var p = new Promise(function(resolve, reject) { var ret = GM_xmlhttpRequest({ method: "GET", url: `http://olime.baidu.com/py?input=${IME.inputString}&inputtype=py&bg=0&ed=20&result=hanzi&resultcoding=unicode&ch_en=0&clientinfo=web&version=1`, onload: function(res) { //console.log("[DEBUG connect]") resolve(res.responseText); } }) }); p.then(parseJSON).then(parseRes, printError); }; function printError(err) { console.log(err); }; function parseRes(resObj) { // console.log("[resObj]", resObj); if (resObj['errno'] != 0) { return; } var text = resObj['result'][0]; console.log("[text]", text[0][0]) for (var i = 0; i < text.length; i++) { IME.TEXTS[i] = text[i][0]; } updateList(IME.page); } function updateList(page) { for (var i = 0; i < 5; i++) { imePop.querySelector('ol').children[i].innerHTML = IME.TEXTS[page * 5 + i]; if (page * 5 + i >= IME.TEXTS.length) { imePop.querySelector('ol').children[i].innerHTML = "--" } } } function parseJSON(text) { // console.log("JSON response from baidu: ", text); var resObj = JSON.parse(text); return resObj; } // this function comes from https://github.com/component/textarea-caret-position/blob/master/index.js function getCaretCoordinates(element, position) { var properties = [ 'direction', // RTL support 'boxSizing', 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does 'height', 'overflowX', 'overflowY', // copy the scrollbar for IE 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderStyle', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', // https://developer.mozilla.org/en-US/docs/Web/CSS/font 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', // might not make a difference, but better be safe 'letterSpacing', 'wordSpacing', 'tabSize', 'MozTabSize' ]; // mirrored div var div = document.createElement('div'); div.id = 'input-textarea-caret-position-mirror-div'; document.body.appendChild(div); var style = div.style; var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9 // default textarea styles style.whiteSpace = 'pre-wrap'; if (element.nodeName !== 'INPUT') style.wordWrap = 'break-word'; // only for textarea-s // position off-screen style.position = 'absolute'; // required to return coordinates properly style.visibility = 'hidden'; // not 'display: none' because we want rendering // transfer the element's properties to the div properties.forEach(function (prop) { style[prop] = computed[prop]; }); var isFirefox = window.mozInnerScreenX != null; if (isFirefox) { // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 if (element.scrollHeight > parseInt(computed.height)) style.overflowY = 'scroll'; } else { style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' } div.textContent = element.value.substring(0, position); // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 if (element.nodeName === 'INPUT') div.textContent = div.textContent.replace(/\s/g, "\u00a0"); var span = document.createElement('span'); // Wrapping must be replicated *exactly*, including when a long word gets // onto the next line, with whitespace at the end of the line before (#7). // The *only* reliable way to do that is to copy the *entire* rest of the // textarea's content into the created at the caret position. // for inputs, just '.' would be enough, but why bother? span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all div.appendChild(span); var coordinates = { top: span.offsetTop + parseInt(computed['borderTopWidth']), left: span.offsetLeft + parseInt(computed['borderLeftWidth']) }; document.body.removeChild(div); return coordinates; } }