baiduCloudInput

input method in browser based on baidu online input method.

  1. // ==UserScript==
  2. // @name baiduCloudInput
  3. // @name:zh-CN 百度云输入法
  4. // @namespace baiduIME@reverland.org
  5. // @description input method in browser based on baidu online input method.
  6. // @description:zh-CN 在浏览器中自由使用百度在线输入法
  7. // @include *
  8. // @version 1.2
  9. // @grant GM_xmlhttpRequest
  10. // ==/UserScript==
  11. //
  12. // DONE:
  13. // : 弹窗相对于body的位置
  14. // : 插入词而不是在结束时附加
  15. // : 最上层!!
  16. // : ff/chromium兼容
  17. //
  18. // TODO: CHIANFIND_RES特性
  19. // TODO: 边沿检测特性
  20. // TODO: 完善中文标点
  21. //
  22. // `+/-` 翻页
  23. // `Space/1/2/3/4/5` 选词
  24. // `Shift` 全角/半角逗号句号
  25. //
  26.  
  27. document.body.addEventListener('keydown', configQuanjiao);
  28. function configQuanjiao(e) {
  29. if (e.which == 16) {
  30. IME.quanjiao = !IME.quanjiao;
  31. console.log(e);
  32. e.preventDefault();
  33. }
  34. }
  35.  
  36. var IME = {
  37. status: 'hidden',
  38. output: '',
  39. inputString: '',
  40. TEXTS: [],
  41. page: 0,
  42. quanjiao: true,
  43. }
  44.  
  45.  
  46. setTimeout(function() {
  47. var tts = document.getElementsByTagName("textarea");
  48. for(var i = 0; i < tts.length; i++) {
  49. initIME(tts[i]);
  50. }
  51. var tts = document.getElementsByTagName("input");
  52. for(var i = 0; i < tts.length; i++) {
  53. initIME(tts[i]);
  54. }
  55. }, 2000); // 为了等待文本框装载进DOM
  56.  
  57. function initIME(tt) {
  58. console.log("[DEBUG]", tt);
  59.  
  60. var imePop = document.createElement('div');
  61.  
  62. initImePop();
  63.  
  64. tt.addEventListener('keydown', checkNonCharacter);
  65. tt.addEventListener('keyup', reqAndRefresh);
  66.  
  67. tt.addEventListener('keypress', intercept);
  68.  
  69. function checkNonCharacter(e) {
  70. if (IME.status == 'POPUP') {
  71. switch (String.fromCharCode(e.which)) {
  72. case String.fromCharCode(8):
  73. e.preventDefault();
  74.  
  75. IME.inputString = IME.inputString.substr(0, IME.inputString.length - 1);
  76. if (IME.inputString.length == 0) {
  77. IME.status = 'hidden';
  78. showImePop(false);
  79. }
  80. break;
  81. case String.fromCharCode(13):
  82. e.preventDefault();
  83. var curStart = tt.selectionStart;
  84. var curEnd = tt.selectionEnd;
  85. tt.value = tt.value.substring(0, curStart) + IME.inputString + tt.value.substring(curEnd, tt.value.length);
  86. tt.selectionStart = curStart + IME.inputString.length;
  87. tt.selectionEnd = curStart + IME.inputString.length;
  88.  
  89. IME.inputString = "";
  90. IME.status = 'hidden';
  91. showImePop(false);
  92. break;
  93. }
  94. }
  95. imePop.querySelector('p').innerHTML = IME.inputString;
  96. }
  97.  
  98. function reqAndRefresh(e) {
  99. imePop.querySelector('p').innerHTML = IME.inputString;
  100. // reconize key finish
  101. // console.log("[IME.inputString] ", IME.inputString);
  102.  
  103. var p = new Promise(function(resolve, reject) {
  104. var ret = GM_xmlhttpRequest({
  105. method: "GET",
  106. 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`,
  107. onload: function(res) {
  108. //console.log("[DEBUG connect]")
  109. resolve(res.responseText);
  110. }
  111. })
  112. });
  113.  
  114. p.then(parseJSON).then(parseRes, printError);
  115. }
  116.  
  117. function initImePop() {
  118. imePop.setAttribute('id', 'baidu-cloud-input-imePop');
  119. imePop.style.position = "absolute";
  120. imePop.style.width = "300px";
  121. //imePop.style.height = "80px";
  122. imePop.style.background = "lightblue";
  123. imePop.style.borderRadius = "5px";
  124. imePop.style.display = "none";
  125. imePop.style.boxShadow = "0 0 3px 0px black"
  126. imePop.style.zIndex = "9999999";
  127. var echo = document.createElement('p');
  128. echo.style.height = "1.5em"; //只为防止抖动
  129. echo.style.lineHeight = "1.5em";
  130. echo.style.fontSize = "1em";
  131. echo.style.margin = "0";
  132. echo.style.padding = "0";
  133. echo.style.paddingLeft = "0.5em";
  134. echo.style.color = "darkblue";
  135. echo.style.fontStyle = "bold";
  136. imePop.appendChild(echo);
  137. var tips = document.createElement('ol');
  138. tips.style.margin = "0px";
  139. tips.style.padding = "0px";
  140. tips.style.color = "black";
  141. var tip = [];
  142. for (var i = 0; i < 5; i++) {
  143. tip[i] = document.createElement('li');
  144. tip[i].style.margin = "0px";
  145. tip[i].style.padding = "0px";
  146. tip[i].style.marginLeft = "2em";
  147. tip[i].style.listStyleType = "decimal";
  148. tips.appendChild(tip[i]);
  149. }
  150. document.body.appendChild(imePop);
  151. var hr = document.createElement('hr')
  152. hr.style.marginTop = "0";
  153. hr.style.marginBottom = "0.2em"
  154. hr.style.color = "grey";
  155. imePop.appendChild(hr);
  156. imePop.appendChild(tips);
  157. }
  158.  
  159. function showImePop(state) {
  160. if (state) {
  161. var coordinates = getCaretCoordinates(tt, tt.selectionEnd);
  162. var textAreaTop = findPos(tt)[1] + 20;
  163. var textAreaLeft = findPos(tt)[0];
  164. imePop.style.left = textAreaLeft + coordinates.left + "px";
  165. imePop.style.top = textAreaTop -tt.scrollTop + coordinates.top + "px";
  166. imePop.style.display = "block";
  167. } else {
  168. imePop.style.display = 'none';
  169. }
  170. }
  171.  
  172. function findPos(obj) {
  173. var curleft = curtop = 0;
  174. if (obj.offsetParent) {
  175. do {
  176. curleft += obj.offsetLeft;
  177. curtop += obj.offsetTop;
  178. } while (obj = obj.offsetParent);
  179. }
  180. return [curleft,curtop];
  181. }
  182.  
  183.  
  184. function intercept(e){
  185. // control keys
  186. if (e.ctrlKey) {
  187. return;
  188. }
  189. if (IME.status == 'POPUP') {
  190. switch (String.fromCharCode(e.which)) {
  191. case " ":
  192. case "1":
  193. case "2":
  194. case "3":
  195. case "4":
  196. case "5":
  197. e.preventDefault();
  198. var index = String.fromCharCode(e.which) == " "?0:parseInt(String.fromCharCode(e.which)) - 1;
  199. console.log(index);
  200. var curStart = tt.selectionStart;
  201. var curEnd = tt.selectionEnd;
  202. var selectedText = imePop.querySelector('ol').children[index].textContent;
  203. tt.value = tt.value.substring(0, curStart) + selectedText + tt.value.substring(curEnd, tt.value.length);
  204. tt.selectionStart = curStart + selectedText.length;
  205. tt.selectionEnd = curStart + selectedText.length;
  206. IME.inputString = "";
  207. IME.status = 'hidden';
  208. showImePop(false);
  209. break;
  210. case "a":
  211. case "b":
  212. case "c":
  213. case "d":
  214. case "e":
  215. case "f":
  216. case "g":
  217. case "h":
  218. case "i":
  219. case "j":
  220. case "k":
  221. case "l":
  222. case "m":
  223. case "n":
  224. case "o":
  225. case "p":
  226. case "q":
  227. case "r":
  228. case "s":
  229. case "t":
  230. case "u":
  231. case "v":
  232. case "w":
  233. case "x":
  234. case "y":
  235. case "z":
  236. case "'":
  237. e.preventDefault();
  238. IME.inputString += String.fromCharCode(e.which);
  239. break;
  240. // {
  241. case "=":
  242. e.preventDefault();
  243. IME.page += 1;
  244. //console.log("[DEBUG]", IME.page);
  245. if (IME.page < IME.TEXTS.length / 5) {
  246. updateList(IME.page);
  247. } else {
  248. IME.page -= 1;
  249. }
  250. return;
  251. case "-":
  252. e.preventDefault();
  253. IME.page = IME.page == 0?IME.page:IME.page - 1;
  254. //console.log("[DEBUG]", IME.page);
  255. updateList(IME.page);
  256. return;
  257. // }
  258. default:
  259. e.preventDefault();
  260. }
  261. } else if (IME.status == 'hidden') {
  262. switch (String.fromCharCode(e.which)) {
  263. case ",":
  264. if (IME.quanjiao) {
  265. e.preventDefault();
  266. var curStart = tt.selectionStart;
  267. var curEnd = tt.selectionEnd;
  268. tt.value = tt.value.substring(0, curStart) + ',' + tt.value.substring(curEnd, tt.value.length);
  269. tt.selectionStart = curStart + ','.length;
  270. tt.selectionEnd = curStart + ','.length;
  271. return;
  272. }
  273. break;
  274. case ".":
  275. if (IME.quanjiao) {
  276. e.preventDefault();
  277. var curStart = tt.selectionStart;
  278. var curEnd = tt.selectionEnd;
  279. tt.value = tt.value.substring(0, curStart) + '。' + tt.value.substring(curEnd, tt.value.length);
  280. tt.selectionStart = curStart + '。'.length;
  281. tt.selectionEnd = curStart + '。'.length;
  282. return;
  283. }
  284. break;
  285. case "a":
  286. case "b":
  287. case "c":
  288. case "d":
  289. case "e":
  290. case "f":
  291. case "g":
  292. case "h":
  293. case "i":
  294. case "j":
  295. case "k":
  296. case "l":
  297. case "m":
  298. case "n":
  299. case "o":
  300. case "p":
  301. case "q":
  302. case "r":
  303. case "s":
  304. case "t":
  305. case "u":
  306. case "v":
  307. case "w":
  308. case "x":
  309. case "y":
  310. case "z":
  311. case "'":
  312. e.preventDefault();
  313. if (IME.inputString.length == 0) {
  314. IME.inputString += String.fromCharCode(e.which);
  315. IME.status = 'POPUP';
  316. showImePop(true);
  317. }
  318. IME.page = 0;
  319. break;
  320. default:
  321. void(0);
  322. }
  323. }
  324. };
  325.  
  326. function printError(err) {
  327. console.log(err);
  328. };
  329.  
  330. function parseRes(resObj) {
  331. // console.log("[resObj]", resObj);
  332. if (resObj['errno'] != 0) {
  333. return;
  334. }
  335. var text = resObj['result'][0];
  336. // console.log("[text]", text[0][0])
  337. for (var i = 0; i < text.length; i++) {
  338. IME.TEXTS[i] = text[i][0];
  339. }
  340. updateList(IME.page);
  341. }
  342.  
  343. function updateList(page) {
  344. for (var i = 0; i < 5; i++) {
  345. imePop.querySelector('ol').children[i].innerHTML = IME.TEXTS[page * 5 + i];
  346. if (page * 5 + i >= IME.TEXTS.length) {
  347. imePop.querySelector('ol').children[i].innerHTML = "--"
  348. }
  349. }
  350. }
  351.  
  352. function parseJSON(text) {
  353. // console.log("JSON response from baidu: ", text);
  354. var resObj = JSON.parse(text);
  355. return resObj;
  356. }
  357.  
  358. // this function comes from https://github.com/component/textarea-caret-position/blob/master/index.js
  359. function getCaretCoordinates(element, position) {
  360. var properties = [
  361. 'direction', // RTL support
  362. 'boxSizing',
  363. 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
  364. 'height',
  365. 'overflowX',
  366. 'overflowY', // copy the scrollbar for IE
  367.  
  368. 'borderTopWidth',
  369. 'borderRightWidth',
  370. 'borderBottomWidth',
  371. 'borderLeftWidth',
  372. 'borderStyle',
  373.  
  374. 'paddingTop',
  375. 'paddingRight',
  376. 'paddingBottom',
  377. 'paddingLeft',
  378.  
  379. // https://developer.mozilla.org/en-US/docs/Web/CSS/font
  380. 'fontStyle',
  381. 'fontVariant',
  382. 'fontWeight',
  383. 'fontStretch',
  384. 'fontSize',
  385. 'fontSizeAdjust',
  386. 'lineHeight',
  387. 'fontFamily',
  388.  
  389. 'textAlign',
  390. 'textTransform',
  391. 'textIndent',
  392. 'textDecoration', // might not make a difference, but better be safe
  393.  
  394. 'letterSpacing',
  395. 'wordSpacing',
  396.  
  397. 'tabSize',
  398. 'MozTabSize'
  399.  
  400. ];
  401. // mirrored div
  402. var div = document.createElement('div');
  403. div.id = 'input-textarea-caret-position-mirror-div';
  404. document.body.appendChild(div);
  405.  
  406. var style = div.style;
  407. var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
  408.  
  409. // default textarea styles
  410. style.whiteSpace = 'pre-wrap';
  411. if (element.nodeName !== 'INPUT')
  412. style.wordWrap = 'break-word'; // only for textarea-s
  413.  
  414. // position off-screen
  415. style.position = 'absolute'; // required to return coordinates properly
  416. style.visibility = 'hidden'; // not 'display: none' because we want rendering
  417.  
  418. // transfer the element's properties to the div
  419. properties.forEach(function (prop) {
  420. style[prop] = computed[prop];
  421. });
  422.  
  423. var isFirefox = window.mozInnerScreenX != null;
  424. if (isFirefox) {
  425. // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
  426. if (element.scrollHeight > parseInt(computed.height))
  427. style.overflowY = 'scroll';
  428. } else {
  429. style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
  430. }
  431.  
  432. div.textContent = element.value.substring(0, position);
  433. // 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
  434. if (element.nodeName === 'INPUT')
  435. div.textContent = div.textContent.replace(/\s/g, "\u00a0");
  436.  
  437. var span = document.createElement('span');
  438. // Wrapping must be replicated *exactly*, including when a long word gets
  439. // onto the next line, with whitespace at the end of the line before (#7).
  440. // The *only* reliable way to do that is to copy the *entire* rest of the
  441. // textarea's content into the <span> created at the caret position.
  442. // for inputs, just '.' would be enough, but why bother?
  443. span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
  444. div.appendChild(span);
  445.  
  446. var coordinates = {
  447. top: span.offsetTop + parseInt(computed['borderTopWidth']),
  448. left: span.offsetLeft + parseInt(computed['borderLeftWidth'])
  449. };
  450.  
  451. document.body.removeChild(div);
  452.  
  453. return coordinates;
  454. }
  455. }
  456.  
  457.