WebOrbiton
v3.0.0.2

FlatlyPage

840 lines · 27.8 KB
  1. (function () {
  2. const style = document.createElement('style');
  3. style.textContent = `
  4. .sas-floating-toolbar {
  5. position: absolute;
  6. background: linear-gradient(135deg, rgba(30, 30, 35, 0.98), rgba(20, 20, 25, 0.98));
  7. backdrop-filter: blur(20px) saturate(180%);
  8. -webkit-backdrop-filter: blur(20px) saturate(180%);
  9. border: 1px solid rgba(255, 255, 255, 0.18);
  10. border-radius: 14px;
  11. padding: 6px;
  12. display: flex;
  13. gap: 3px;
  14. z-index: 100000;
  15. box-shadow:
  16. 0 8px 32px rgba(0, 0, 0, 0.4),
  17. 0 2px 8px rgba(0, 0, 0, 0.2),
  18. inset 0 1px 0 rgba(255, 255, 255, 0.1);
  19. opacity: 0;
  20. visibility: hidden;
  21. transform: translateY(12px) scale(0.92);
  22. transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  23. pointer-events: none;
  24. font-family: var(--font-interface, 'Space Grotesk'), -apple-system, BlinkMacSystemFont, sans-serif;
  25. }
  26. .sas-floating-toolbar.active {
  27. opacity: 1;
  28. visibility: visible;
  29. transform: translateY(0) scale(1);
  30. pointer-events: all;
  31. animation: toolbarEntrance 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  32. }
  33. @keyframes toolbarEntrance {
  34. 0% {
  35. opacity: 0;
  36. transform: translateY(12px) scale(0.92);
  37. }
  38. 60% {
  39. transform: translateY(-2px) scale(1.02);
  40. }
  41. 100% {
  42. opacity: 1;
  43. transform: translateY(0) scale(1);
  44. }
  45. }
  46. .sas-floating-toolbar button {
  47. position: relative;
  48. background: rgba(255, 255, 255, 0.05);
  49. border: 1px solid rgba(255, 255, 255, 0.08);
  50. color: rgba(255, 255, 255, 0.85);
  51. width: 38px;
  52. height: 38px;
  53. cursor: pointer;
  54. border-radius: 10px;
  55. display: flex;
  56. align-items: center;
  57. justify-content: center;
  58. transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  59. overflow: hidden;
  60. padding: 0;
  61. }
  62. .sas-floating-toolbar button.format-active {
  63. background: rgba(96, 165, 250, 0.25);
  64. border-color: rgba(96, 165, 250, 0.5);
  65. color: #60a5fa;
  66. box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.15);
  67. }
  68. .sas-floating-toolbar button.format-active::after {
  69. content: '';
  70. position: absolute;
  71. top: 3px;
  72. right: 3px;
  73. width: 6px;
  74. height: 6px;
  75. background: #60a5fa;
  76. border-radius: 50%;
  77. box-shadow: 0 0 8px rgba(96, 165, 250, 0.8);
  78. animation: pulse 2s infinite;
  79. }
  80. @keyframes pulse {
  81. 0%, 100% { opacity: 1; transform: scale(1); }
  82. 50% { opacity: 0.6; transform: scale(0.9); }
  83. }
  84. .sas-floating-toolbar button svg {
  85. width: 18px;
  86. height: 18px;
  87. transition: all 0.2s;
  88. }
  89. .sas-floating-toolbar button::before {
  90. content: '';
  91. position: absolute;
  92. top: 50%;
  93. left: 50%;
  94. width: 0;
  95. height: 0;
  96. border-radius: 50%;
  97. background: rgba(255, 255, 255, 0.15);
  98. transform: translate(-50%, -50%);
  99. transition: width 0.4s, height 0.4s;
  100. }
  101. .sas-floating-toolbar button:hover {
  102. background: rgba(255, 255, 255, 0.12);
  103. border-color: rgba(255, 255, 255, 0.2);
  104. color: #ffffff;
  105. transform: translateY(-1px);
  106. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  107. }
  108. .sas-floating-toolbar button.format-active:hover {
  109. background: rgba(96, 165, 250, 0.35);
  110. border-color: rgba(96, 165, 250, 0.6);
  111. }
  112. .sas-floating-toolbar button:hover svg {
  113. transform: scale(1.1);
  114. }
  115. .sas-floating-toolbar button:active::before {
  116. width: 100px;
  117. height: 100px;
  118. }
  119. .sas-floating-toolbar button:active {
  120. transform: scale(0.95);
  121. }
  122. .sas-floating-toolbar .divider {
  123. width: 1px;
  124. background: linear-gradient(
  125. to bottom,
  126. rgba(255, 255, 255, 0),
  127. rgba(255, 255, 255, 0.15),
  128. rgba(255, 255, 255, 0)
  129. );
  130. margin: 8px 4px;
  131. align-self: stretch;
  132. }
  133. .sas-floating-toolbar::after {
  134. content: '';
  135. position: absolute;
  136. bottom: -7px;
  137. left: 50%;
  138. transform: translateX(-50%);
  139. border-left: 7px solid transparent;
  140. border-right: 7px solid transparent;
  141. border-top: 7px solid rgba(30, 30, 35, 0.98);
  142. filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
  143. }
  144. .sas-floating-toolbar.arrow-left::after {
  145. left: 20px;
  146. transform: translateX(0);
  147. }
  148. .sas-floating-toolbar.arrow-right::after {
  149. left: auto;
  150. right: 20px;
  151. transform: translateX(0);
  152. }
  153. .sas-floating-toolbar button .tooltip {
  154. position: absolute;
  155. bottom: -36px;
  156. left: 50%;
  157. transform: translateX(-50%);
  158. background: rgba(0, 0, 0, 0.95);
  159. color: white;
  160. padding: 6px 10px;
  161. border-radius: 6px;
  162. font-size: 11px;
  163. font-weight: 500;
  164. white-space: nowrap;
  165. pointer-events: none;
  166. opacity: 0;
  167. transition: opacity 0.2s;
  168. z-index: 1;
  169. }
  170. .sas-floating-toolbar button:hover .tooltip {
  171. opacity: 1;
  172. animation: tooltipFade 0.2s ease;
  173. }
  174. .sas-floating-toolbar button .tooltip.with-shortcut {
  175. display: flex;
  176. gap: 8px;
  177. align-items: center;
  178. }
  179. .sas-floating-toolbar button .tooltip .shortcut {
  180. background: rgba(255, 255, 255, 0.15);
  181. padding: 2px 5px;
  182. border-radius: 3px;
  183. font-size: 10px;
  184. font-family: monospace;
  185. }
  186. @keyframes tooltipFade {
  187. from { opacity: 0; transform: translateX(-50%) translateY(-4px); }
  188. to { opacity: 1; transform: translateX(-50%) translateY(0); }
  189. }
  190. .sas-toolbar-status {
  191. position: fixed;
  192. bottom: 20px;
  193. right: 20px;
  194. background: rgba(0, 0, 0, 0.9);
  195. color: white;
  196. padding: 8px 14px;
  197. border-radius: 8px;
  198. font-size: 12px;
  199. font-family: var(--font-interface, 'Space Grotesk'), -apple-system, BlinkMacSystemFont, sans-serif;
  200. z-index: 99999;
  201. opacity: 0;
  202. transform: translateY(10px);
  203. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  204. pointer-events: none;
  205. }
  206. .sas-toolbar-status.show {
  207. opacity: 1;
  208. transform: translateY(0);
  209. }
  210. .sas-toolbar-status.success {
  211. background: rgba(16, 185, 129, 0.95);
  212. }
  213. .sas-toolbar-status.removed {
  214. background: rgba(239, 68, 68, 0.95);
  215. }
  216. `;
  217. document.head.appendChild(style);
  218. ​
  219. const icons = {
  220. bold: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  221. <path d="M6 4h8a4 4 0 0 1 0 8H6z"/>
  222. <path d="M6 12h9a4 4 0 0 1 0 8H6z"/>
  223. </svg>`,
  224. ​
  225. italic: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  226. <line x1="19" y1="4" x2="10" y2="4"/>
  227. <line x1="14" y1="20" x2="5" y2="20"/>
  228. <line x1="15" y1="4" x2="9" y2="20"/>
  229. </svg>`,
  230. ​
  231. underline: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  232. <path d="M6 3v7a6 6 0 0 0 12 0V3"/>
  233. <line x1="4" y1="21" x2="20" y2="21"/>
  234. </svg>`,
  235. ​
  236. strikethrough: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  237. <path d="M16 4H9a3 3 0 0 0-2.83 4"/>
  238. <path d="M14 12a4 4 0 0 1 0 8H6"/>
  239. <line x1="4" y1="12" x2="20" y2="12"/>
  240. </svg>`,
  241. ​
  242. quote: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  243. <path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/>
  244. <path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/>
  245. </svg>`,
  246. ​
  247. undo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  248. <polyline points="1 4 1 10 7 10"/>
  249. <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
  250. /svg>`,
  251. ​
  252. redo: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  253. <polyline points="23 4 23 10 17 10"/>
  254. <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
  255. </svg>`
  256. };
  257. const toolbar = document.createElement('div');
  258. toolbar.className = 'sas-floating-toolbar';
  259. const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
  260. const modKey = isMac ? '⌘' : 'Ctrl';
  261. toolbar.innerHTML = `
  262. <button data-tag="b">
  263. ${icons.bold}
  264. <span class="tooltip with-shortcut">
  265. <span>Bold</span>
  266. <span class="shortcut">${modKey}+B</span>
  267. </span>
  268. </button>
  269. <button data-tag="i">
  270. ${icons.italic}
  271. <span class="tooltip with-shortcut">
  272. <span>Italic</span>
  273. <span class="shortcut">${modKey}+I</span>
  274. </span>
  275. </button>
  276. <div class="divider"></div>
  277. <button data-tag="u">
  278. ${icons.underline}
  279. <span class="tooltip with-shortcut">
  280. <span>Underline</span>
  281. <span class="shortcut">${modKey}+U</span>
  282. </span>
  283. </button>
  284. <button data-tag="s">
  285. ${icons.strikethrough}
  286. <span class="tooltip">
  287. <span>Strikethrough</span>
  288. </span>
  289. </button>
  290. <div class="divider"></div>
  291. <button data-tag="quote">
  292. ${icons.quote}
  293. <span class="tooltip">
  294. <span>Quote</span>
  295. </span>
  296. </button>
  297. <div class="divider"></div>
  298. <button data-action="undo">
  299. ${icons.undo}
  300. <span class="tooltip with-shortcut">
  301. <span>Undo</span>
  302. <span class="shortcut">${modKey}+Z</span>
  303. </span>
  304. </button>
  305. <button data-action="redo">
  306. ${icons.redo}
  307. <span class="tooltip with-shortcut">
  308. <span>Redo</span>
  309. <span class="shortcut">${modKey}+${isMac ? '⇧+Z' : 'Y'}</span>
  310. </span>
  311. </button>
  312. `;
  313. document.body.appendChild(toolbar);
  314. ​
  315. const statusDiv = document.createElement('div');
  316. statusDiv.className = 'sas-toolbar-status';
  317. document.body.appendChild(statusDiv);
  318. ​
  319. const isRichField = (element) => !!element && !!element.dataset && element.dataset.rich === '1' &&
  320. (element.tagName === 'TEXTAREA' || (element.tagName === 'INPUT' && element.type === 'text'));
  321. ​
  322. let activeInput = null;
  323. let hideTimeout = null;
  324. let statusTimeout = null;
  325. let undoStack = new Map();
  326. let isUndoRedo = false;
  327. ​
  328. const showStatus = (message, type = 'success') => {
  329. clearTimeout(statusTimeout);
  330. statusDiv.textContent = message;
  331. statusDiv.className = `sas-toolbar-status show ${type}`;
  332. statusTimeout = setTimeout(() => {
  333. statusDiv.classList.remove('show');
  334. }, 2000);
  335. };
  336. ​
  337. const getInputHistory = (input) => {
  338. if (!undoStack.has(input)) {
  339. undoStack.set(input, {
  340. history: [{
  341. value: input.value,
  342. start: input.selectionStart,
  343. end: input.selectionEnd
  344. }],
  345. position: 0
  346. });
  347. }
  348. return undoStack.get(input);
  349. };
  350. ​
  351. const saveToHistory = (input) => {
  352. if (isUndoRedo) return;
  353. const history = getInputHistory(input);
  354. const currentValue = input.value;
  355. const lastEntry = history.history[history.position];
  356. if (lastEntry && lastEntry.value === currentValue) return;
  357. history.history = history.history.slice(0, history.position + 1);
  358. history.history.push({
  359. value: currentValue,
  360. start: input.selectionStart,
  361. end: input.selectionEnd
  362. });
  363. if (history.history.length > 100) {
  364. history.history.shift();
  365. } else {
  366. history.position++;
  367. }
  368. };
  369. ​
  370. const performUndo = () => {
  371. if (!activeInput) return;
  372. const history = getInputHistory(activeInput);
  373. if (history.position <= 0) {
  374. showStatus('Nothing to undo', 'removed');
  375. return;
  376. }
  377. isUndoRedo = true;
  378. history.position--;
  379. const state = history.history[history.position];
  380. activeInput.value = state.value;
  381. activeInput.setSelectionRange(state.start, state.end);
  382. activeInput.dispatchEvent(new Event('input', { bubbles: true }));
  383. activeInput.dispatchEvent(new Event('change', { bubbles: true }));
  384. setTimeout(() => {
  385. isUndoRedo = false;
  386. }, 50);
  387. showStatus('Undo', 'removed');
  388. updateButtonStates();
  389. };
  390. ​
  391. const performRedo = () => {
  392. if (!activeInput) return;
  393. const history = getInputHistory(activeInput);
  394. if (history.position >= history.history.length - 1) {
  395. showStatus('Nothing to redo', 'removed');
  396. return;
  397. }
  398. isUndoRedo = true;
  399. history.position++;
  400. const state = history.history[history.position];
  401. activeInput.value = state.value;
  402. activeInput.setSelectionRange(state.start, state.end);
  403. activeInput.dispatchEvent(new Event('input', { bubbles: true }));
  404. activeInput.dispatchEvent(new Event('change', { bubbles: true }));
  405. setTimeout(() => {
  406. isUndoRedo = false;
  407. }, 50);
  408. showStatus('Redo', 'success');
  409. updateButtonStates();
  410. };
  411. ​
  412. const detectActiveTags = (text, start, end) => {
  413. const selectedText = text.substring(start, end);
  414. const activeTags = new Set();
  415. const tags = ['b', 'i', 'u', 's', 'quote'];
  416. for (const tag of tags) {
  417. const openTag = `[${tag}]`;
  418. const closeTag = `[/${tag}]`;
  419. if (selectedText.startsWith(openTag) && selectedText.endsWith(closeTag)) {
  420. activeTags.add(tag);
  421. }
  422. const beforeSelection = text.substring(0, start);
  423. const afterSelection = text.substring(end);
  424. const openBefore = (beforeSelection.match(new RegExp(`\\[${tag}\\]`, 'g')) || []).length;
  425. const closeBefore = (beforeSelection.match(new RegExp(`\\[/${tag}\\]`, 'g')) || []).length;
  426. if (openBefore > closeBefore) {
  427. if (afterSelection.includes(closeTag)) {
  428. activeTags.add(tag);
  429. }
  430. }
  431. }
  432. return activeTags;
  433. };
  434. ​
  435. const updateButtonStates = () => {
  436. if (!activeInput) return;
  437. const start = activeInput.selectionStart;
  438. const end = activeInput.selectionEnd;
  439. const value = activeInput.value;
  440. const activeTags = detectActiveTags(value, start, end);
  441. toolbar.querySelectorAll('button[data-tag]').forEach(btn => {
  442. if (activeTags.has(btn.dataset.tag)) {
  443. btn.classList.add('format-active');
  444. } else {
  445. btn.classList.remove('format-active');
  446. }
  447. });
  448. };
  449. ​
  450. const formatText = (tag) => {
  451. if (!activeInput) return;
  452. ​
  453. const start = activeInput.selectionStart;
  454. const end = activeInput.selectionEnd;
  455. const value = activeInput.value;
  456. const selectedText = value.substring(start, end);
  457. ​
  458. if (!selectedText) return;
  459. ​
  460. saveToHistory(activeInput);
  461. ​
  462. const openTag = `[${tag}]`;
  463. const closeTag = `[/${tag}]`;
  464. ​
  465. let replacement;
  466. let newSelectionStart, newSelectionEnd;
  467. let action = 'added';
  468. ​
  469. if (selectedText.startsWith(openTag) && selectedText.endsWith(closeTag)) {
  470. replacement = selectedText.substring(openTag.length, selectedText.length - closeTag.length);
  471. newSelectionStart = start;
  472. newSelectionEnd = start + replacement.length;
  473. action = 'removed';
  474. }
  475. else {
  476. const beforeSelection = value.substring(0, start);
  477. const afterSelection = value.substring(end);
  478. const tagJustBefore = beforeSelection.endsWith(openTag);
  479. const tagJustAfter = afterSelection.startsWith(closeTag);
  480. if (tagJustBefore && tagJustAfter) {
  481. const newValue =
  482. beforeSelection.substring(0, beforeSelection.length - openTag.length) +
  483. selectedText +
  484. afterSelection.substring(closeTag.length);
  485. activeInput.value = newValue;
  486. newSelectionStart = start - openTag.length;
  487. newSelectionEnd = end - openTag.length;
  488. action = 'removed';
  489. activeInput.setSelectionRange(newSelectionStart, newSelectionEnd);
  490. activeInput.dispatchEvent(new Event('input', { bubbles: true }));
  491. activeInput.dispatchEvent(new Event('change', { bubbles: true }));
  492. updateButtonStates();
  493. showStatus(`${tag.toUpperCase()} formatting removed`, 'removed');
  494. return;
  495. }
  496. let searchStart = start;
  497. while (searchStart < end) {
  498. const segmentStart = value.indexOf(openTag, searchStart);
  499. if (segmentStart === -1 || segmentStart >= end) break;
  500. const segmentEnd = value.indexOf(closeTag, segmentStart);
  501. if (segmentEnd === -1 || segmentEnd > end) break;
  502. const beforeTag = value.substring(0, start);
  503. const afterTag = value.substring(end);
  504. let middlePart = selectedText;
  505. const tagRegex = new RegExp(`\\[${tag}\\]([\\s\\S]*?)\\[/${tag}\\]`, 'g');
  506. middlePart = middlePart.replace(tagRegex, '$1');
  507. middlePart = middlePart.replace(new RegExp(`\\[${tag}\\]`, 'g'), '');
  508. middlePart = middlePart.replace(new RegExp(`\\[/${tag}\\]`, 'g'), '');
  509. activeInput.value = beforeTag + middlePart + afterTag;
  510. newSelectionStart = start;
  511. newSelectionEnd = start + middlePart.length;
  512. action = 'removed';
  513. activeInput.setSelectionRange(newSelectionStart, newSelectionEnd);
  514. activeInput.dispatchEvent(new Event('input', { bubbles: true }));
  515. activeInput.dispatchEvent(new Event('change', { bubbles: true }));
  516. updateButtonStates();
  517. showStatus(`${tag.toUpperCase()} formatting removed`, 'removed');
  518. return;
  519. }
  520. replacement = openTag + selectedText + closeTag;
  521. newSelectionStart = start;
  522. newSelectionEnd = end + openTag.length + closeTag.length;
  523. action = 'added';
  524. }
  525. ​
  526. const scrollTop = activeInput.scrollTop;
  527. ​
  528. activeInput.focus();
  529. activeInput.setSelectionRange(start, end);
  530. document.execCommand('insertText', false, replacement);
  531. ​
  532. activeInput.setSelectionRange(newSelectionStart, newSelectionEnd);
  533. activeInput.scrollTop = scrollTop;
  534. ​
  535. activeInput.dispatchEvent(new Event('input', { bubbles: true }));
  536. activeInput.dispatchEvent(new Event('change', { bubbles: true }));
  537. saveToHistory(activeInput);
  538. updateButtonStates();
  539. const actionText = action === 'added' ? 'applied' : 'removed';
  540. const statusType = action === 'added' ? 'success' : 'removed';
  541. showStatus(`${tag.toUpperCase()} formatting ${actionText}`, statusType);
  542. };
  543. ​
  544. const positionToolbar = (x, y) => {
  545. const toolbarWidth = toolbar.offsetWidth;
  546. const toolbarHeight = toolbar.offsetHeight;
  547. const padding = 12;
  548. const arrowOffset = 18;
  549. ​
  550. let left = x - (toolbarWidth / 2);
  551. let top = y - toolbarHeight - arrowOffset;
  552. ​
  553. const viewportWidth = window.innerWidth;
  554. const scrollTop = window.pageYOffset;
  555. ​
  556. toolbar.classList.remove('arrow-left', 'arrow-right');
  557. ​
  558. if (left < padding) {
  559. left = padding;
  560. toolbar.classList.add('arrow-left');
  561. } else if (left + toolbarWidth > viewportWidth - padding) {
  562. left = viewportWidth - toolbarWidth - padding;
  563. toolbar.classList.add('arrow-right');
  564. }
  565. ​
  566. if (top < scrollTop + padding) {
  567. top = y + arrowOffset;
  568. }
  569. ​
  570. toolbar.style.left = `${left}px`;
  571. toolbar.style.top = `${top}px`;
  572. };
  573. ​
  574. const showToolbar = () => {
  575. clearTimeout(hideTimeout);
  576. toolbar.classList.add('active');
  577. updateButtonStates();
  578. };
  579. ​
  580. const hideToolbar = (immediate = false) => {
  581. clearTimeout(hideTimeout);
  582. if (immediate) {
  583. toolbar.classList.remove('active');
  584. } else {
  585. hideTimeout = setTimeout(() => {
  586. toolbar.classList.remove('active');
  587. }, 100);
  588. }
  589. };
  590. ​
  591. let selectionTimeout;
  592. ​
  593. const handleSelection = (e) => {
  594. clearTimeout(selectionTimeout);
  595. ​
  596. selectionTimeout = setTimeout(() => {
  597. const isTextInput = isRichField(e.target);
  598. ​
  599. if (!isTextInput) {
  600. if (!e.target.closest('.sas-floating-toolbar')) {
  601. hideToolbar(true);
  602. }
  603. return;
  604. }
  605. ​
  606. const start = e.target.selectionStart;
  607. const end = e.target.selectionEnd;
  608. const hasSelection = start !== end;
  609. ​
  610. if (hasSelection) {
  611. activeInput = e.target;
  612. const rect = e.target.getBoundingClientRect();
  613. const x = e.pageX || (rect.left + rect.width / 2);
  614. const y = e.pageY || (rect.top + window.pageYOffset);
  615. positionToolbar(x, y);
  616. showToolbar();
  617. } else {
  618. if (!e.target.closest('.sas-floating-toolbar')) {
  619. hideToolbar(true);
  620. }
  621. }
  622. }, 10);
  623. };
  624. ​
  625. document.addEventListener('mouseup', handleSelection);
  626. ​
  627. document.addEventListener('focus', (e) => {
  628. const isTextInput = isRichField(e.target);
  629. if (isTextInput) {
  630. activeInput = e.target;
  631. if (!undoStack.has(e.target)) {
  632. getInputHistory(e.target);
  633. }
  634. }
  635. }, true);
  636. ​
  637. document.addEventListener('input', (e) => {
  638. const isTextInput = isRichField(e.target);
  639. if (isTextInput && !isUndoRedo) {
  640. saveToHistory(e.target);
  641. }
  642. });
  643. ​
  644. document.addEventListener('keyup', (e) => {
  645. if (e.shiftKey) {
  646. const target = document.activeElement;
  647. const isTextInput = isRichField(target);
  648. if (isTextInput && target.selectionStart !== target.selectionEnd) {
  649. activeInput = target;
  650. const rect = target.getBoundingClientRect();
  651. positionToolbar(
  652. rect.left + rect.width / 2,
  653. rect.top + window.pageYOffset
  654. );
  655. showToolbar();
  656. }
  657. }
  658. });
  659. ​
  660. document.addEventListener('contextmenu', (e) => {
  661. const isTextInput = isRichField(e.target);
  662. ​
  663. if (isTextInput) {
  664. setTimeout(() => {
  665. const start = e.target.selectionStart;
  666. const end = e.target.selectionEnd;
  667. if (start !== end) {
  668. e.preventDefault();
  669. handleSelection(e);
  670. }
  671. }, 10);
  672. }
  673. });
  674. ​
  675. toolbar.querySelectorAll('button[data-tag]').forEach(btn => {
  676. btn.addEventListener('mousedown', (e) => {
  677. e.preventDefault();
  678. e.stopPropagation();
  679. formatText(btn.dataset.tag);
  680. });
  681. });
  682. ​
  683. toolbar.querySelectorAll('button[data-action]').forEach(btn => {
  684. btn.addEventListener('mousedown', (e) => {
  685. e.preventDefault();
  686. e.stopPropagation();
  687. if (btn.dataset.action === 'undo') {
  688. performUndo();
  689. } else if (btn.dataset.action === 'redo') {
  690. performRedo();
  691. }
  692. });
  693. });
  694. ​
  695. document.addEventListener('keydown', (e) => {
  696. const target = document.activeElement;
  697. const isTextInput = isRichField(target);
  698. if (!isTextInput) return;
  699. ​
  700. const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
  701. const modKey = isMac ? e.metaKey : e.ctrlKey;
  702. ​
  703. if (!modKey) return;
  704. ​
  705. const key = e.key.toLowerCase();
  706. if (key === 'z' && !e.shiftKey) {
  707. e.preventDefault();
  708. activeInput = target;
  709. performUndo();
  710. return;
  711. }
  712. if ((key === 'y' && !isMac) || (key === 'z' && e.shiftKey && isMac)) {
  713. e.preventDefault();
  714. activeInput = target;
  715. performRedo();
  716. return;
  717. }
  718. ​
  719. let tag = null;
  720. switch (key) {
  721. case 'b': tag = 'b'; break;
  722. case 'i': tag = 'i'; break;
  723. case 'u': tag = 'u'; break;
  724. }
  725. ​
  726. if (tag && target.selectionStart !== target.selectionEnd) {
  727. e.preventDefault();
  728. activeInput = target;
  729. formatText(tag);
  730. }
  731. });
  732. ​
  733. document.addEventListener('selectionchange', () => {
  734. if (activeInput && toolbar.classList.contains('active')) {
  735. updateButtonStates();
  736. }
  737. });
  738. ​
  739. window.addEventListener('scroll', () => hideToolbar(true), { passive: true });
  740. window.addEventListener('resize', () => hideToolbar(true), { passive: true });
  741. ​
  742. toolbar.addEventListener('mousedown', (e) => e.stopPropagation());
  743. ​
  744. document.addEventListener('mousedown', (e) => {
  745. if (!e.target.closest('.sas-floating-toolbar') &&
  746. !e.target.closest('.sas-toolbar-status') &&
  747. e.target !== activeInput) {
  748. hideToolbar(true);
  749. }
  750. });
  751. ​
  752. })();