Greasy Fork is available in English.

baiduCloudInput

input method in browser based on baidu online input method.

// ==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.2
// @grant       GM_xmlhttpRequest
// ==/UserScript==
//
// DONE:
// : 弹窗相对于body的位置
// : 插入词而不是在结束时附加
// : 最上层!!
// : ff/chromium兼容
//
// TODO: CHIANFIND_RES特性
// TODO: 边沿检测特性
// TODO: 完善中文标点
//
// `+/-` 翻页
// `Space/1/2/3/4/5` 选词
// `Shift` 全角/半角逗号句号
//

document.body.addEventListener('keydown', configQuanjiao);
function configQuanjiao(e) {
  if (e.which == 16) {
    IME.quanjiao = !IME.quanjiao;
    console.log(e);
    e.preventDefault();
  }
}

var IME = {
  status: 'hidden',
  output: '',
  inputString: '',
  TEXTS: [],
  page: 0,
  quanjiao: true,
}


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]);
    }
}, 2000); // 为了等待文本框装载进DOM

function initIME(tt) {
    console.log("[DEBUG]", tt);

    var imePop = document.createElement('div');

    initImePop();

    tt.addEventListener('keydown', checkNonCharacter);
    tt.addEventListener('keyup', reqAndRefresh);

    tt.addEventListener('keypress', intercept); 

    function checkNonCharacter(e) {
      if (IME.status == 'POPUP') {
        switch (String.fromCharCode(e.which)) {
          case String.fromCharCode(8):
            e.preventDefault();

          IME.inputString = IME.inputString.substr(0, IME.inputString.length - 1);
          if (IME.inputString.length == 0) {
            IME.status = 'hidden';
            showImePop(false);
          }
          break;
          case String.fromCharCode(13):
            e.preventDefault();
          var curStart = tt.selectionStart;
          var curEnd = tt.selectionEnd;
          tt.value = tt.value.substring(0, curStart) + IME.inputString + tt.value.substring(curEnd, tt.value.length);
          tt.selectionStart = curStart + IME.inputString.length;
          tt.selectionEnd = curStart + IME.inputString.length;

          IME.inputString = "";
          IME.status = 'hidden';
          showImePop(false);
          break;
        }
      }
      imePop.querySelector('p').innerHTML = IME.inputString;
    } 

    function reqAndRefresh(e) {
      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=100&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 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.height = "1.5em";  //只为防止抖动
      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"
      hr.style.color = "grey";
      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 (String.fromCharCode(e.which)) {
          case " ":
            case "1":
            case "2":
            case "3":
            case "4":
            case "5":
            e.preventDefault();
          var index = String.fromCharCode(e.which) == " "?0:parseInt(String.fromCharCode(e.which)) - 1;
          console.log(index);
          var curStart = tt.selectionStart;
          var curEnd = tt.selectionEnd;
          var selectedText = imePop.querySelector('ol').children[index].textContent;
          tt.value = tt.value.substring(0, curStart) + selectedText + tt.value.substring(curEnd, tt.value.length);
          tt.selectionStart = curStart + selectedText.length;
          tt.selectionEnd = curStart + selectedText.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 += String.fromCharCode(e.which);
          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 (String.fromCharCode(e.which)) {
          case ",":
            if (IME.quanjiao) {
              e.preventDefault();
              var curStart = tt.selectionStart;
              var curEnd = tt.selectionEnd;
              tt.value = tt.value.substring(0, curStart) + ',' + tt.value.substring(curEnd, tt.value.length);
              tt.selectionStart = curStart + ','.length;
              tt.selectionEnd = curStart + ','.length;
              return;
            }
          break;
          case ".":
            if (IME.quanjiao) {
              e.preventDefault();
            var curStart = tt.selectionStart;
            var curEnd = tt.selectionEnd;
            tt.value = tt.value.substring(0, curStart) + '。' + tt.value.substring(curEnd, 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 += String.fromCharCode(e.which);
            IME.status = 'POPUP';
            showImePop(true);
          }
          IME.page = 0;
          break;
          default:
            void(0);
        }
      }
    };

    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 <span> 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;
    }
}