math-keyboard.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. // 数学虚拟键盘实现
  2. document.addEventListener('DOMContentLoaded', function() {
  3. // 初始化虚拟键盘
  4. initMathKeyboard();
  5. });
  6. /**
  7. * 初始化数学虚拟键盘
  8. */
  9. function initMathKeyboard() {
  10. // 创建键盘切换按钮
  11. createKeyboardToggle();
  12. // 创建键盘容器
  13. createKeyboardContainer();
  14. // 绑定输入框事件
  15. bindInputEvents();
  16. }
  17. /**
  18. * 创建键盘切换按钮
  19. */
  20. function createKeyboardToggle() {
  21. const toggleButton = document.createElement('button');
  22. toggleButton.className = 'keyboard-toggle';
  23. toggleButton.innerHTML = '⌨';
  24. toggleButton.title = '显示/隐藏数学键盘';
  25. toggleButton.addEventListener('click', function() {
  26. toggleKeyboard();
  27. });
  28. document.body.appendChild(toggleButton);
  29. }
  30. /**
  31. * 创建键盘容器
  32. */
  33. function createKeyboardContainer() {
  34. // 创建键盘主容器
  35. const keyboard = document.createElement('div');
  36. keyboard.className = 'math-keyboard';
  37. keyboard.id = 'math-keyboard';
  38. // 设置键盘固定在窗口底部
  39. keyboard.style.position = 'fixed';
  40. keyboard.style.bottom = '0';
  41. keyboard.style.left = '0';
  42. keyboard.style.width = '100%';
  43. keyboard.style.zIndex = '1000';
  44. // 创建关闭按钮
  45. const closeButton = document.createElement('button');
  46. closeButton.className = 'keyboard-close';
  47. closeButton.innerHTML = '×';
  48. closeButton.title = '关闭键盘';
  49. closeButton.addEventListener('click', function() {
  50. toggleKeyboard(false);
  51. });
  52. keyboard.appendChild(closeButton);
  53. // 创建键盘标签页
  54. const tabsContainer = document.createElement('div');
  55. tabsContainer.className = 'keyboard-tabs';
  56. const tabs = [
  57. { id: 'basic', name: '基础' },
  58. { id: 'functions', name: '函数' },
  59. { id: 'symbols', name: '符号' },
  60. { id: 'greek', name: '希腊字母' }
  61. ];
  62. tabs.forEach((tab, index) => {
  63. const tabElement = document.createElement('div');
  64. tabElement.className = 'keyboard-tab' + (index === 0 ? ' active' : '');
  65. tabElement.dataset.tab = tab.id;
  66. tabElement.textContent = tab.name;
  67. tabElement.addEventListener('click', function() {
  68. document.querySelectorAll('.keyboard-tab').forEach(t => t.classList.remove('active'));
  69. document.querySelectorAll('.keyboard-panel').forEach(p => p.classList.remove('active'));
  70. this.classList.add('active');
  71. document.getElementById(`keyboard-panel-${tab.id}`).classList.add('active');
  72. });
  73. tabsContainer.appendChild(tabElement);
  74. });
  75. keyboard.appendChild(tabsContainer);
  76. // 创建键盘内容容器
  77. const keyboardContainer = document.createElement('div');
  78. keyboardContainer.className = 'keyboard-container';
  79. // 创建各个面板
  80. createBasicPanel(keyboardContainer);
  81. createFunctionsPanel(keyboardContainer);
  82. createSymbolsPanel(keyboardContainer);
  83. createGreekPanel(keyboardContainer);
  84. keyboard.appendChild(keyboardContainer);
  85. // 添加到文档
  86. document.body.appendChild(keyboard);
  87. }
  88. /**
  89. * 创建功能键行(空格、退格、清除、关闭)
  90. */
  91. function createFunctionKeysRow() {
  92. const specialRow = createKeyboardRow();
  93. addKey(specialRow, '空格', ' ', 'wide');
  94. addKey(specialRow, '退格', 'backspace', 'wide');
  95. addKey(specialRow, '清除', 'clear', 'wide');
  96. addKey(specialRow, '关闭', 'close', 'wide');
  97. return specialRow;
  98. }
  99. /**
  100. * 创建基础面板
  101. */
  102. function createBasicPanel(container) {
  103. const panel = document.createElement('div');
  104. panel.className = 'keyboard-panel active';
  105. panel.id = 'keyboard-panel-basic';
  106. // 数字行
  107. const numberRow = createKeyboardRow();
  108. ['7', '8', '9', '+', '(', ')'].forEach(key => {
  109. addKey(numberRow, key, key, key === '+' ? 'operator' : '');
  110. });
  111. panel.appendChild(numberRow);
  112. const numberRow2 = createKeyboardRow();
  113. ['4', '5', '6', '-', '[', ']'].forEach(key => {
  114. addKey(numberRow2, key, key, key === '-' ? 'operator' : '');
  115. });
  116. panel.appendChild(numberRow2);
  117. const numberRow3 = createKeyboardRow();
  118. ['1', '2', '3', '*', '{', '}'].forEach(key => {
  119. addKey(numberRow3, key, key, key === '*' ? 'operator' : '');
  120. });
  121. panel.appendChild(numberRow3);
  122. const numberRow4 = createKeyboardRow();
  123. ['0', '.', ',', '/', '^', '='].forEach(key => {
  124. addKey(numberRow4, key, key, ['/', '^', '='].includes(key) ? 'operator' : '');
  125. });
  126. panel.appendChild(numberRow4);
  127. // 添加功能键行
  128. panel.appendChild(createFunctionKeysRow());
  129. container.appendChild(panel);
  130. }
  131. /**
  132. * 创建函数面板
  133. */
  134. function createFunctionsPanel(container) {
  135. const panel = document.createElement('div');
  136. panel.className = 'keyboard-panel';
  137. panel.id = 'keyboard-panel-functions';
  138. // 基础函数行
  139. const basicFuncRow = createKeyboardRow();
  140. ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'].forEach(key => {
  141. addKey(basicFuncRow, key, key + '()', 'function');
  142. });
  143. panel.appendChild(basicFuncRow);
  144. // 高级函数行
  145. const advFuncRow = createKeyboardRow();
  146. ['log', 'ln', 'exp', 'sqrt', 'abs', 'floor'].forEach(key => {
  147. addKey(advFuncRow, key, key + '()', 'function');
  148. });
  149. panel.appendChild(advFuncRow);
  150. // 统计函数行
  151. const statFuncRow = createKeyboardRow();
  152. ['max', 'min', 'mean', 'median', 'sum', 'std'].forEach(key => {
  153. addKey(statFuncRow, key, key + '()', 'function');
  154. });
  155. panel.appendChild(statFuncRow);
  156. // 特殊函数行
  157. const specialFuncRow = createKeyboardRow();
  158. ['factorial', 'pow', 'mod', 'round', 'ceil', 'random'].forEach(key => {
  159. addKey(specialFuncRow, key, key + '()', 'function');
  160. });
  161. panel.appendChild(specialFuncRow);
  162. // 添加功能键行
  163. panel.appendChild(createFunctionKeysRow());
  164. container.appendChild(panel);
  165. }
  166. /**
  167. * 创建符号面板
  168. */
  169. function createSymbolsPanel(container) {
  170. const panel = document.createElement('div');
  171. panel.className = 'keyboard-panel';
  172. panel.id = 'keyboard-panel-symbols';
  173. // 数学符号行1
  174. const symbolRow1 = createKeyboardRow();
  175. ['±', '∞', '≈', '≠', '≤', '≥'].forEach(key => {
  176. let insert = key;
  177. if (key === '±') insert = '\\pm';
  178. if (key === '∞') insert = '\\infty';
  179. if (key === '≈') insert = '\\approx';
  180. if (key === '≠') insert = '!=';
  181. if (key === '≤') insert = '<=';
  182. if (key === '≥') insert = '>=';
  183. addKey(symbolRow1, key, insert);
  184. });
  185. panel.appendChild(symbolRow1);
  186. // 数学符号行2
  187. const symbolRow2 = createKeyboardRow();
  188. ['×', '÷', '∂', '∫', '∑', '∏'].forEach(key => {
  189. let insert = key;
  190. if (key === '×') insert = '*';
  191. if (key === '÷') insert = '/';
  192. if (key === '∂') insert = '\\partial';
  193. if (key === '∫') insert = '\\int';
  194. if (key === '∑') insert = '\\sum';
  195. if (key === '∏') insert = '\\prod';
  196. addKey(symbolRow2, key, insert);
  197. });
  198. panel.appendChild(symbolRow2);
  199. // 数学符号行3
  200. const symbolRow3 = createKeyboardRow();
  201. ['√', '∛', '∜', '|x|', '⌊x⌋', '⌈x⌉'].forEach(key => {
  202. let insert = key;
  203. if (key === '√') insert = 'sqrt()';
  204. if (key === '∛') insert = 'cbrt()';
  205. if (key === '∜') insert = 'nthRoot(,4)';
  206. if (key === '|x|') insert = 'abs()';
  207. if (key === '⌊x⌋') insert = 'floor()';
  208. if (key === '⌈x⌉') insert = 'ceil()';
  209. addKey(symbolRow3, key, insert);
  210. });
  211. panel.appendChild(symbolRow3);
  212. // 数学符号行4
  213. const symbolRow4 = createKeyboardRow();
  214. ['lim', '→', '∈', '∉', '⊂', '⊃'].forEach(key => {
  215. let insert = key;
  216. if (key === 'lim') insert = '\\lim';
  217. if (key === '→') insert = '\\to';
  218. if (key === '∈') insert = '\\in';
  219. if (key === '∉') insert = '\\notin';
  220. if (key === '⊂') insert = '\\subset';
  221. if (key === '⊃') insert = '\\supset';
  222. addKey(symbolRow4, key, insert);
  223. });
  224. panel.appendChild(symbolRow4);
  225. // 添加功能键行
  226. panel.appendChild(createFunctionKeysRow());
  227. container.appendChild(panel);
  228. }
  229. /**
  230. * 创建希腊字母面板
  231. */
  232. function createGreekPanel(container) {
  233. const panel = document.createElement('div');
  234. panel.className = 'keyboard-panel';
  235. panel.id = 'keyboard-panel-greek';
  236. // 希腊字母行1
  237. const greekRow1 = createKeyboardRow();
  238. ['α', 'β', 'γ', 'δ', 'ε', 'ζ'].forEach(key => {
  239. addKey(greekRow1, key, '\\' + getGreekName(key));
  240. });
  241. panel.appendChild(greekRow1);
  242. // 希腊字母行2
  243. const greekRow2 = createKeyboardRow();
  244. ['η', 'θ', 'ι', 'κ', 'λ', 'μ'].forEach(key => {
  245. addKey(greekRow2, key, '\\' + getGreekName(key));
  246. });
  247. panel.appendChild(greekRow2);
  248. // 希腊字母行3
  249. const greekRow3 = createKeyboardRow();
  250. ['ν', 'ξ', 'π', 'ρ', 'σ', 'τ'].forEach(key => {
  251. addKey(greekRow3, key, '\\' + getGreekName(key));
  252. });
  253. panel.appendChild(greekRow3);
  254. // 希腊字母行4
  255. const greekRow4 = createKeyboardRow();
  256. ['υ', 'φ', 'χ', 'ψ', 'ω', 'Δ'].forEach(key => {
  257. addKey(greekRow4, key, '\\' + getGreekName(key));
  258. });
  259. panel.appendChild(greekRow4);
  260. // 添加功能键行
  261. panel.appendChild(createFunctionKeysRow());
  262. container.appendChild(panel);
  263. }
  264. /**
  265. * 获取希腊字母的名称
  266. */
  267. function getGreekName(symbol) {
  268. const greekMap = {
  269. 'α': 'alpha', 'β': 'beta', 'γ': 'gamma', 'δ': 'delta', 'ε': 'epsilon', 'ζ': 'zeta',
  270. 'η': 'eta', 'θ': 'theta', 'ι': 'iota', 'κ': 'kappa', 'λ': 'lambda', 'μ': 'mu',
  271. 'ν': 'nu', 'ξ': 'xi', 'π': 'pi', 'ρ': 'rho', 'σ': 'sigma', 'τ': 'tau',
  272. 'υ': 'upsilon', 'φ': 'phi', 'χ': 'chi', 'ψ': 'psi', 'ω': 'omega',
  273. 'Α': 'Alpha', 'Β': 'Beta', 'Γ': 'Gamma', 'Δ': 'Delta', 'Ε': 'Epsilon', 'Ζ': 'Zeta',
  274. 'Η': 'Eta', 'Θ': 'Theta', 'Ι': 'Iota', 'Κ': 'Kappa', 'Λ': 'Lambda', 'Μ': 'Mu',
  275. 'Ν': 'Nu', 'Ξ': 'Xi', 'Π': 'Pi', 'Ρ': 'Rho', 'Σ': 'Sigma', 'Τ': 'Tau',
  276. 'Υ': 'Upsilon', 'Φ': 'Phi', 'Χ': 'Chi', 'Ψ': 'Psi', 'Ω': 'Omega'
  277. };
  278. return greekMap[symbol] || symbol;
  279. }
  280. /**
  281. * 创建键盘行
  282. */
  283. function createKeyboardRow() {
  284. const row = document.createElement('div');
  285. row.className = 'keyboard-row';
  286. return row;
  287. }
  288. /**
  289. * 添加键盘按键
  290. */
  291. function addKey(row, display, value, className = '') {
  292. const key = document.createElement('div');
  293. key.className = 'keyboard-key' + (className ? ' ' + className : '');
  294. key.textContent = display;
  295. key.dataset.value = value;
  296. key.addEventListener('click', function() {
  297. handleKeyPress(this.dataset.value);
  298. });
  299. row.appendChild(key);
  300. return key;
  301. }
  302. /**
  303. * 处理按键点击
  304. */
  305. function handleKeyPress(value) {
  306. // 优先使用记录的活动输入元素,如果没有则使用当前焦点元素
  307. const activeElement = window.activeInputElement || document.activeElement;
  308. // 检查是否有活动的输入元素
  309. if (!activeElement || !isInputElement(activeElement)) {
  310. // 如果没有活动输入元素,尝试查找页面上的可编辑单元格
  311. const excelCells = document.querySelectorAll('.excel-cell');
  312. if (excelCells.length > 0) {
  313. // 使用第一个Excel单元格作为目标
  314. window.activeInputElement = excelCells[0];
  315. excelCells[0].focus();
  316. return handleKeyPress(value); // 重新调用处理函数
  317. }
  318. return;
  319. }
  320. // 处理特殊按键
  321. if (value === 'backspace') {
  322. deleteText(activeElement);
  323. return;
  324. }
  325. if (value === 'clear') {
  326. activeElement.value = '';
  327. return;
  328. }
  329. if (value === 'close') {
  330. toggleKeyboard(false);
  331. return;
  332. }
  333. // 插入文本
  334. insertTextAtCursor(activeElement, value);
  335. // 如果是函数,将光标移动到括号内
  336. if (value.endsWith('()')) {
  337. const cursorPos = activeElement.selectionStart;
  338. activeElement.setSelectionRange(cursorPos - 1, cursorPos - 1);
  339. }
  340. }
  341. /**
  342. * 检查元素是否为输入元素
  343. */
  344. function isInputElement(element) {
  345. if (!element) return false;
  346. return element.tagName === 'INPUT' ||
  347. element.tagName === 'TEXTAREA' ||
  348. element.isContentEditable ||
  349. (element.classList && element.classList.contains('excel-cell'));
  350. }
  351. /**
  352. * 在光标位置插入文本
  353. */
  354. function insertTextAtCursor(input, text) {
  355. if (!input) return;
  356. const isContentEditable = input.isContentEditable ||
  357. (input.classList && input.classList.contains('excel-cell'));
  358. if (isContentEditable) {
  359. // 处理可编辑内容
  360. // 保存当前选区
  361. const selection = window.getSelection();
  362. const range = selection.getRangeAt(0);
  363. // 插入文本
  364. const textNode = document.createTextNode(text);
  365. range.deleteContents();
  366. range.insertNode(textNode);
  367. // 移动光标到插入文本的末尾
  368. range.setStartAfter(textNode);
  369. range.setEndAfter(textNode);
  370. selection.removeAllRanges();
  371. selection.addRange(range);
  372. } else {
  373. // 处理输入框和文本区域
  374. const startPos = input.selectionStart;
  375. const endPos = input.selectionEnd;
  376. const beforeText = input.value.substring(0, startPos);
  377. const afterText = input.value.substring(endPos);
  378. input.value = beforeText + text + afterText;
  379. // 设置光标位置
  380. const newCursorPos = startPos + text.length;
  381. input.setSelectionRange(newCursorPos, newCursorPos);
  382. }
  383. // 触发输入事件
  384. const event = new Event('input', { bubbles: true });
  385. input.dispatchEvent(event);
  386. // 保持焦点
  387. input.focus();
  388. }
  389. /**
  390. * 删除光标前的文本
  391. */
  392. function deleteText(input) {
  393. if (!input) return;
  394. const isContentEditable = input.isContentEditable ||
  395. (input.classList && input.classList.contains('excel-cell'));
  396. if (isContentEditable) {
  397. // 处理可编辑内容
  398. const selection = window.getSelection();
  399. const range = selection.getRangeAt(0);
  400. if (range.collapsed) {
  401. // 光标位置,没有选中文本
  402. // 创建一个新的范围,包含光标前的一个字符
  403. const newRange = range.cloneRange();
  404. newRange.setStart(range.startContainer, Math.max(0, range.startOffset - 1));
  405. newRange.setEnd(range.startContainer, range.startOffset);
  406. newRange.deleteContents();
  407. } else {
  408. // 有选中的文本,直接删除
  409. range.deleteContents();
  410. }
  411. } else {
  412. // 处理输入框和文本区域
  413. const startPos = input.selectionStart;
  414. const endPos = input.selectionEnd;
  415. if (startPos === endPos) {
  416. // 没有选中文本,删除前一个字符
  417. if (startPos > 0) {
  418. const beforeText = input.value.substring(0, startPos - 1);
  419. const afterText = input.value.substring(endPos);
  420. input.value = beforeText + afterText;
  421. input.setSelectionRange(startPos - 1, startPos - 1);
  422. }
  423. } else {
  424. // 删除选中的文本
  425. const beforeText = input.value.substring(0, startPos);
  426. const afterText = input.value.substring(endPos);
  427. input.value = beforeText + afterText;
  428. input.setSelectionRange(startPos, startPos);
  429. }
  430. }
  431. // 触发输入事件
  432. const event = new Event('input', { bubbles: true });
  433. input.dispatchEvent(event);
  434. // 保持焦点
  435. input.focus();
  436. }
  437. /**
  438. * 切换键盘显示/隐藏
  439. * @param {boolean} forceClose - 如果为true,则强制关闭键盘
  440. */
  441. function toggleKeyboard(forceClose = null) {
  442. const keyboard = document.getElementById('math-keyboard');
  443. if (!keyboard) return;
  444. if (forceClose === false) {
  445. keyboard.classList.remove('active');
  446. } else if (forceClose === true) {
  447. keyboard.classList.add('active');
  448. } else {
  449. keyboard.classList.toggle('active');
  450. }
  451. // 更新键盘切换按钮状态
  452. const toggleButton = document.querySelector('.keyboard-toggle');
  453. if (toggleButton) {
  454. if (keyboard.classList.contains('active')) {
  455. toggleButton.classList.add('active');
  456. } else {
  457. toggleButton.classList.remove('active');
  458. }
  459. }
  460. // 如果键盘被激活,尝试聚焦到当前活动的输入元素
  461. if (keyboard.classList.contains('active') && window.activeInputElement) {
  462. window.activeInputElement.focus();
  463. }
  464. }
  465. /**
  466. * 绑定输入框事件
  467. */
  468. function bindInputEvents() {
  469. // 监听文档中所有输入元素的焦点事件
  470. document.addEventListener('focusin', function(e) {
  471. // 检查获得焦点的元素是否为输入元素
  472. if (isInputElement(e.target)) {
  473. // 记录当前活动的输入元素
  474. window.activeInputElement = e.target;
  475. }
  476. });
  477. // 监听Excel表格单元格的焦点事件
  478. document.addEventListener('click', function(e) {
  479. if (e.target.classList && e.target.classList.contains('excel-cell')) {
  480. // 记录当前活动的Excel单元格
  481. window.activeInputElement = e.target;
  482. }
  483. });
  484. }