baiduCloudInput

input method in browser based on baidu online input method.

Versión del día 19/10/2015. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name        baiduCloudInput
// @name:zh-CN  百度云输入法
// @namespace   [email protected]
// @description input method in browser based on baidu online input method.
// @description:zh-CN 在浏览器中自由使用百度在线输入法
// @include     *
// @version     1.0
// @grant       GM_xmlhttpRequest
// ==/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 <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;
  }
}