Hordes UI Mod

Various UI mods for Hordes.io.

当前为 2019-12-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Hordes UI Mod
  3. // @version 0.151
  4. // @description Various UI mods for Hordes.io.
  5. // @author Sakaiyo
  6. // @match https://hordes.io/play
  7. // @grant GM_addStyle
  8. // @namespace https://greasyfork.org/users/160017
  9. // ==/UserScript==
  10. /**
  11. * TODO: Implement chat tabs
  12. * TODO: Implement inventory sorting
  13. * TODO: (Maybe) Improved healer party frames
  14. * TODO: Opacity scaler for map
  15. * Check if it's ok to emulate keypresses before releasing. If not, then maybe copy text to clipboard.
  16. * TODO: FIX BUG: Add support for resizing map back to saved position after minimizing it, from maximized position
  17. * TODO (Maybe): Ability to make GM chat look like normal chat?
  18. * TODO: Add toggleable option to include chat messages to right of party frame
  19. * TODO: Remove all reliance on svelte- classes, likely breaks with updates
  20. */
  21. (function() {
  22. 'use strict';
  23.  
  24. // If this version is different from the user's stored state,
  25. // e.g. they have upgraded the version of this script and there are breaking changes,
  26. // then their stored state will be deleted.
  27. const BREAKING_VERSION = 1;
  28. const VERSION = '0.150'; // Should match version in UserScript description
  29.  
  30. // The width+height of the maximized chat, so we don't save map size when it's maximized
  31. // TODO: FIX BUG: This is NOT everyones max size. INSTEAD OF USING A STATIC SIZE, we should detect large instant resizes
  32. // Should also do this to avoid saving when minimizing menu after maximizing
  33. const CHAT_MAXIMIZED_SIZE = 692;
  34.  
  35. const STORAGE_STATE_KEY = 'hordesio-uimodsakaiyo-state';
  36. const CHAT_GM_CLASS = 'js-chat-gm';
  37.  
  38. let state = {
  39. breakingVersion: BREAKING_VERSION,
  40. chat: {
  41. GM: true,
  42. },
  43. windowsPos: {},
  44. blockList: {},
  45. };
  46. // tempState is saved only between page refreshed.
  47. const tempState = {
  48. // The last name clicked in chat
  49. chatName: null
  50. };
  51.  
  52. // UPDATING STYLES BELOW - Must be invoked in main function
  53. GM_addStyle(`
  54. /* Transparent chat bg color */
  55. .frame.svelte-1vrlsr3 {
  56. background: rgba(0,0,0,0.4);
  57. }
  58.  
  59. /* Transparent map */
  60. .svelte-hiyby7 {
  61. opacity: 0.7;
  62. }
  63.  
  64. /* Our mod's chat message color */
  65. .textuimod {
  66. color: #00dd33;
  67. }
  68.  
  69. /* Allows windows to be moved */
  70. .window {
  71. position: relative;
  72. }
  73.  
  74. /* Allows last clicked window to appear above all other windows */
  75. .js-is-top {
  76. z-index: 9998 !important;
  77. }
  78. .panel.context:not(.commandlist) {
  79. z-index: 9999 !important;
  80. }
  81. /* The item icon being dragged in the inventory */
  82. .container.svelte-120o2pb {
  83. z-index: 9999 !important;
  84. }
  85.  
  86. /* Custom chat context menu, invisible by default */
  87. .js-chat-context-menu {
  88. display: none;
  89. }
  90.  
  91. .js-chat-context-menu .name {
  92. color: white;
  93. padding: 2px 4px;
  94. }
  95.  
  96. /* Allow names in chat to be clicked */
  97. #chat .name {
  98. pointer-events: all !important;
  99. }
  100.  
  101. /* Custom chat filter colors */
  102. .js-chat-gm {
  103. color: #a6dcd5;
  104. }
  105.  
  106. /* Class that hides chat lines */
  107. .js-line-hidden,
  108. .js-line-blocked {
  109. display: none;
  110. }
  111.  
  112. /* Enable chat & map resize */
  113. .js-chat-resize {
  114. resize: both;
  115. overflow: auto;
  116. }
  117. .js-map-resize:hover {
  118. resize: both;
  119. overflow: auto;
  120. direction: rtl;
  121. }
  122.  
  123. /* The browser resize icon */
  124. *::-webkit-resizer {
  125. background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
  126. border-radius: 8px;
  127. box-shadow: 0 1px 1px rgba(0,0,0,1);
  128. }
  129. *::-moz-resizer {
  130. background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
  131. border-radius: 8px;
  132. box-shadow: 0 1px 1px rgba(0,0,0,1);
  133. }
  134.  
  135. /* Custom css for settings page, duplicates preexisting settings pane grid */
  136. .uimod-settings {
  137. display: grid;
  138. grid-template-columns: 2fr 3fr;
  139. grid-gap: 8px;
  140. align-items: center;
  141. max-height: 390px;
  142. margin: 0 20px;
  143. }
  144. /* The custom settings main window, closely mirrors main settings window */
  145. .uimod-custom-settings {
  146. position: absolute;
  147. top: 100px;
  148. left: 50%;
  149. transform: translate(-50%, 0);
  150. min-width: 350px;
  151. max-width: 600px;
  152. width: 90%;
  153. height: 80%;
  154. min-height: 350px;
  155. max-height: 500px;
  156. z-index: 9;
  157. padding: 0px 10px 5px;
  158. }
  159. `);
  160.  
  161.  
  162. const modHelpers = {
  163. // Filters all chat based on custom filters
  164. filterAllChat: () => {
  165. // Blocked user filter
  166. Object.keys(state.blockList).forEach(blockedName => {
  167. // Get the `.name` elements from the blocked user, if we haven't already hidden their messages
  168. const $blockedChatNames = Array.from(document.querySelectorAll(`[data-chat-name="${blockedName}"]:not(.js-line-blocked)`));
  169. // Hide each of their messages
  170. $blockedChatNames.forEach($name => {
  171. // Add the class name to $name so we can track whether it's been hidden in our CSS selector $blockedChatNames
  172. $name.classList.add('js-line-blocked');
  173. const $line = $name.parentNode.parentNode.parentNode;
  174. // Add the class name to $line so we can visibly hide the entire chat line
  175. $line.classList.add('js-line-blocked');
  176. });
  177. })
  178.  
  179. // Custom channel filter
  180. Object.keys(state.chat).forEach(channel => {
  181. Array.from(document.querySelectorAll(`.text${channel}.content`)).forEach($textItem => {
  182. const $line = $textItem.parentNode.parentNode;
  183. $line.classList.toggle('js-line-hidden', !state.chat[channel]);
  184. });
  185. });
  186. },
  187.  
  188. // Makes chat context menu visible and appear under the mouse
  189. showChatContextMenu: (name, mousePos) => {
  190. const $contextMenu = document.querySelector('.js-chat-context-menu');
  191. $contextMenu.querySelector('.js-name').textContent = name;
  192. $contextMenu.setAttribute('style', `display: block; left: ${mousePos.x}px; top: ${mousePos.y}px;`);
  193. },
  194.  
  195. // Close chat context menu if clicking outside of it
  196. closeChatContextMenu: clickEvent => {
  197. const $target = clickEvent.target;
  198. // If clicking on name or directly on context menu, don't close it
  199. // Still closes if clicking on context menu item
  200. if ($target.classList.contains('js-is-context-menu-initd')
  201. || $target.classList.contains('js-chat-context-menu')) {
  202. return;
  203. }
  204.  
  205. const $contextMenu = document.querySelector('.js-chat-context-menu');
  206. $contextMenu.setAttribute('style', 'display: none');
  207. },
  208.  
  209. // Adds player to block list, to be filtered out of chat
  210. blockPlayer: playerName => {
  211. if (state.blockList[playerName]) {
  212. return;
  213. }
  214. state.blockList[playerName] = true;
  215. modHelpers.filterAllChat();
  216. modHelpers.addChatMessage(`${playerName} has been blocked.`)
  217. save({blockList: state.blockList});
  218. },
  219.  
  220. // Removes player from block list and makes their messages visible again
  221. unblockPlayer: playerName => {
  222. delete state.blockList[playerName];
  223. modHelpers.addChatMessage(`${playerName} has been unblocked.`);
  224. save({blockList: state.blockList});
  225.  
  226. // Make messages visible again
  227. const $chatNames = Array.from(document.querySelectorAll(`.js-line-blocked[data-chat-name="${playerName}"]`));
  228. $chatNames.forEach($name => {
  229. $name.classList.remove('js-line-blocked');
  230. const $line = $name.parentNode.parentNode.parentNode;
  231. $line.classList.remove('js-line-blocked');
  232. });
  233. },
  234.  
  235. // Pushes message to chat
  236. // TODO: The margins for the message are off slightly compared to other messages - why?
  237. addChatMessage: text => {
  238. const newMessageHTML = `
  239. <div class="linewrap svelte-1vrlsr3">
  240. <span class="time svelte-1vrlsr3">00.00</span>
  241. <span class="textuimod content svelte-1vrlsr3">
  242. <span class="capitalize channel svelte-1vrlsr3">UIMod</span>
  243. </span>
  244. <span class="svelte-1vrlsr3">${text}</span>
  245. </div>
  246. `;
  247.  
  248. const element = makeElement({
  249. element: 'article',
  250. class: 'line svelte-1vrlsr3',
  251. content: newMessageHTML});
  252. document.querySelector('#chat').appendChild(element);
  253. },
  254. };
  255.  
  256. // MAIN MODS BELOW
  257. const mods = [
  258. // Creates DOM elements for custom chat filters
  259. function newChatFilters() {
  260. const $channelselect = document.querySelector('.channelselect');
  261. if (!document.querySelector(`.${CHAT_GM_CLASS}`)) {
  262. const $gm = makeElement({
  263. element: 'small',
  264. class: `btn border black ${CHAT_GM_CLASS} ${state.chat.GM ? '' : 'textgrey'}`,
  265. content: 'GM'
  266. });
  267. $channelselect.appendChild($gm);
  268. }
  269. },
  270.  
  271. // Wire up new chat buttons to toggle in state+ui
  272. function newChatFilterButtons() {
  273. const $chatGM = document.querySelector(`.${CHAT_GM_CLASS}`);
  274. $chatGM.addEventListener('click', () => {
  275. state.chat.GM = !state.chat.GM;
  276. $chatGM.classList.toggle('textgrey', !state.chat.GM);
  277. modHelpers.filterAllChat();
  278. save({chat: state.chat});
  279. });
  280. },
  281.  
  282. // Filter out chat in UI based on chat buttons state
  283. function filterChatObserver() {
  284. const chatObserver = new MutationObserver(modHelpers.filterAllChat);
  285. chatObserver.observe(document.querySelector('#chat'), { attributes: true, childList: true });
  286. },
  287.  
  288. // Drag all windows by their header
  289. function draggableUIWindows() {
  290. Array.from(document.querySelectorAll('.window:not(.js-can-move)')).forEach($window => {
  291. $window.classList.add('js-can-move');
  292. dragElement($window, $window.querySelector('.titleframe'));
  293. });
  294. },
  295.  
  296. // Save dragged UI windows position to state
  297. function saveDraggedUIWindows() {
  298. Array.from(document.querySelectorAll('.window:not(.js-window-is-saving)')).forEach($window => {
  299. $window.classList.add('js-window-is-saving');
  300. const $draggableTarget = $window.querySelector('.titleframe');
  301. const windowName = $draggableTarget.querySelector('[name="title"]').textContent;
  302. $draggableTarget.addEventListener('mouseup', () => {
  303. state.windowsPos[windowName] = $window.getAttribute('style');
  304. save({windowsPos: state.windowsPos});
  305. });
  306. });
  307. },
  308.  
  309. // Loads draggable UI windows position from state
  310. function loadDraggedUIWindowsPositions() {
  311. Array.from(document.querySelectorAll('.window:not(.js-has-loaded-pos)')).forEach($window => {
  312. $window.classList.add('js-has-loaded-pos');
  313. const windowName = $window.querySelector('[name="title"]').textContent;
  314. const pos = state.windowsPos[windowName];
  315. if (pos) {
  316. $window.setAttribute('style', pos);
  317. }
  318. });
  319. },
  320.  
  321. // Makes chat resizable
  322. function resizableChat() {
  323. // Add the appropriate classes
  324. const $chatContainer = document.querySelector('#chat').parentNode;
  325. $chatContainer.classList.add('js-chat-resize');
  326.  
  327. // Load initial chat and map size
  328. if (state.chatWidth && state.chatHeight) {
  329. $chatContainer.style.width = state.chatWidth;
  330. $chatContainer.style.height = state.chatHeight;
  331. }
  332.  
  333. // Save chat size on resize
  334. const resizeObserverChat = new ResizeObserver(() => {
  335. const chatWidthStr = window.getComputedStyle($chatContainer, null).getPropertyValue('width');
  336. const chatHeightStr = window.getComputedStyle($chatContainer, null).getPropertyValue('height');
  337. save({
  338. chatWidth: chatWidthStr,
  339. chatHeight: chatHeightStr,
  340. });
  341. });
  342. resizeObserverChat.observe($chatContainer);
  343. },
  344.  
  345. // Makes map resizable
  346. function resizeableMap() {
  347. const $map = document.querySelector('.svelte-hiyby7');
  348. const $canvas = $map.querySelector('canvas');
  349. $map.classList.add('js-map-resize');
  350.  
  351. const onMapResize = () => {
  352. // Get real values of map height/width, excluding padding/margin/etc
  353. const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
  354. const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
  355. const mapWidth = Number(mapWidthStr.slice(0, -2));
  356. const mapHeight = Number(mapHeightStr.slice(0, -2));
  357.  
  358. // If height/width are 0 or unset, don't resize canvas
  359. if (!mapWidth || !mapHeight) {
  360. return;
  361. }
  362.  
  363. if ($canvas.width !== mapWidth) {
  364. $canvas.width = mapWidth;
  365. }
  366.  
  367. if ($canvas.height !== mapHeight) {
  368. $canvas.height = mapHeight;
  369. }
  370.  
  371. // Save map size on resize, unless map has been maximized by user
  372. if (mapWidth !== CHAT_MAXIMIZED_SIZE && mapHeight !== CHAT_MAXIMIZED_SIZE) {
  373. save({
  374. mapWidth: mapWidthStr,
  375. mapHeight: mapHeightStr,
  376. });
  377. }
  378. };
  379.  
  380. if (state.mapWidth && state.mapHeight) {
  381. $map.style.width = state.mapWidth;
  382. $map.style.height = state.mapHeight;
  383. onMapResize(); // Update canvas size on initial load of saved map size
  384. }
  385.  
  386. // On resize of map, resize canvas to match
  387. const resizeObserverMap = new ResizeObserver(onMapResize);
  388. resizeObserverMap.observe($map);
  389.  
  390. // We need to observe canvas resizes to tell when the user presses M to open the big map
  391. // At that point, we resize the map to match the canvas
  392. const triggerResize = () => {
  393. // Get real values of map height/width, excluding padding/margin/etc
  394. const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
  395. const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
  396. const mapWidth = Number(mapWidthStr.slice(0, -2));
  397. const mapHeight = Number(mapHeightStr.slice(0, -2));
  398.  
  399. // If height/width are 0 or unset, we don't care about resizing yet
  400. if (!mapWidth || !mapHeight) {
  401. return;
  402. }
  403.  
  404. if ($canvas.width !== mapWidth) {
  405. $map.style.width = `${$canvas.width}px`;
  406. }
  407.  
  408. if ($canvas.height !== mapHeight) {
  409. $map.style.height = `${$canvas.height}px`;
  410. }
  411. };
  412.  
  413. // We debounce the canvas resize, so it doesn't resize every single
  414. // pixel you move when resizing the DOM. If this were to happen,
  415. // resizing would constantly be interrupted. You'd have to resize a tiny bit,
  416. // lift left click, left click again to resize a tiny bit more, etc.
  417. // Resizing is smooth when we debounce this canvas.
  418. const debouncedTriggerResize = debounce(triggerResize, 200);
  419. const resizeObserverCanvas = new ResizeObserver(debouncedTriggerResize);
  420. resizeObserverCanvas.observe($canvas);
  421. },
  422.  
  423. // The last clicked UI window displays above all other UI windows
  424. // This is useful when, for example, your inventory is near the market window,
  425. // and you want the window and the tooltips to display above the market window.
  426. function selectedWindowIsTop() {
  427. Array.from(document.querySelectorAll('.window:not(.js-is-top-initd)')).forEach($window => {
  428. $window.classList.add('js-is-top-initd');
  429.  
  430. $window.addEventListener('mousedown', () => {
  431. // First, make the other is-top window not is-top
  432. const $otherWindowContainer = document.querySelector('.js-is-top');
  433. if ($otherWindowContainer) {
  434. $otherWindowContainer.classList.remove('js-is-top');
  435. }
  436.  
  437. // Then, make our window's container (the z-index container) is-top
  438. $window.parentNode.classList.add('js-is-top');
  439. });
  440. });
  441. },
  442.  
  443. function customSettings() {
  444. const $settings = document.querySelector('.divide:not(.js-settings-initd)');
  445. if (!$settings) {
  446. return;
  447. }
  448.  
  449. $settings.classList.add('js-settings-initd');
  450. const $settingsChoiceList = $settings.querySelector('.choice').parentNode;
  451. $settingsChoiceList.appendChild(makeElement({
  452. element: 'div',
  453. class: 'choice js-blocked-players',
  454. content: 'Blocked players',
  455. }));
  456.  
  457. // Upon click, we display our custom settings window UI
  458. document.querySelector('.js-blocked-players').addEventListener('click', () => {
  459. let blockedPlayersHTML = '';
  460. Object.keys(state.blockList).sort().forEach(blockedName => {
  461. blockedPlayersHTML += `
  462. <div data-player-name="${blockedName}">${blockedName}</div>
  463. <div class="btn orange js-unblock-player" data-player-name="${blockedName}">Unblock player</div>
  464. `;
  465. });
  466.  
  467. const customSettingsHTML = `
  468. <h3 class="textprimary">Blocked players</h3>
  469. <div class="settings uimod-settings">${blockedPlayersHTML}</div>
  470. <p></p>
  471. <div class="btn blue js-close-custom-settings">Close</div>
  472. `;
  473.  
  474. const $customSettings = makeElement({
  475. element: 'div',
  476. class: 'menu panel-black js-custom-settings uimod-custom-settings',
  477. content: customSettingsHTML,
  478. });
  479. document.body.appendChild($customSettings);
  480.  
  481. // Wire up all the unblock buttons
  482. Array.from(document.querySelectorAll('.js-unblock-player')).forEach($button => {
  483. $button.addEventListener('click', clickEvent => {
  484. const name = clickEvent.target.getAttribute('data-player-name');
  485. modHelpers.unblockPlayer(name);
  486.  
  487. // Remove the blocked player from the list
  488. Array.from(document.querySelectorAll(`[data-player-name="${name}"]`)).forEach($element => {
  489. $element.parentNode.removeChild($element);
  490. });
  491. });
  492. });
  493. // And the close button for our custom UI
  494. document.querySelector('.js-close-custom-settings').addEventListener('click', () => {
  495. const $customSettingsWindow = document.querySelector('.js-custom-settings');
  496. $customSettingsWindow.parentNode.removeChild($customSettings);
  497. });
  498. });
  499. },
  500.  
  501. // This creates the initial chat context menu (which starts as hidden)
  502. function createChatContextMenu() {
  503. if (document.querySelector('.js-chat-context-menu')) {
  504. return;
  505. }
  506.  
  507. document.body.appendChild(makeElement({
  508. element: 'div',
  509. class: 'panel context border grey js-chat-context-menu',
  510. content: `
  511. <div class="js-name">...</div>
  512. <div class="choice" name="party">Party invite</div>
  513. <div class="choice" name="whisper">Whisper</div>
  514. <div class="choice" name="block">Block</div>
  515. `,
  516. }));
  517.  
  518. const $chatContextMenu = document.querySelector('.js-chat-context-menu');
  519. $chatContextMenu.querySelector('[name="party"]').addEventListener('click', () => {
  520. enterTextIntoChat(`/partyinvite ${tempState.chatName}`);
  521. submitChat();
  522. });
  523. $chatContextMenu.querySelector('[name="whisper"]').addEventListener('click', () => {
  524. enterTextIntoChat(`/whisper ${tempState.chatName} `);
  525. });
  526. $chatContextMenu.querySelector('[name="block"]').addEventListener('click', () => {
  527. modHelpers.blockPlayer(tempState.chatName);
  528. })
  529. },
  530.  
  531. // This opens a context menu when you click a user's name in chat
  532. function chatContextMenu() {
  533. Array.from(document.querySelectorAll('.name:not(.js-is-context-menu-initd)')).forEach($name => {
  534. $name.classList.add('js-is-context-menu-initd');
  535. // Add name to element so we can target it in CSS when filtering chat for block list
  536. $name.setAttribute('data-chat-name', $name.textContent);
  537.  
  538. const showContextMenu = clickEvent => {
  539. // TODO: Is there a way to pass the name to showChatContextMenumethod, instead of storing in tempState?
  540. tempState.chatName = $name.textContent;
  541. modHelpers.showChatContextMenu($name.textContent, {x: clickEvent.pageX, y: clickEvent.pageY});
  542. };
  543. $name.addEventListener('click', showContextMenu);
  544. });
  545. },
  546. ];
  547.  
  548. // Add new DOM, load our stored state, wire it up, then continuously rerun specific methods whenever UI changes
  549. function initialize() {
  550. // If the Hordes.io tab isn't active for long enough, it reloads the entire page, clearing this mod
  551. // We check for that and reinitialize the mod if that happens
  552. const $layout = document.querySelector('.layout');
  553. if ($layout.classList.contains('uimod-initd')) {
  554. return;
  555. }
  556.  
  557. modHelpers.addChatMessage(`Hordes UI Mod v${VERSION} by Sakaiyo has been initialized.`);
  558.  
  559. $layout.classList.add('uimod-initd')
  560. load();
  561. mods.forEach(mod => mod());
  562.  
  563. // Continuously re-run specific mods methods that need to be executed on UI change
  564. const rerunObserver = new MutationObserver(() => {
  565. // If new window appears, e.g. even if window is closed and reopened, we need to rewire it
  566. // Fun fact: Some windows always exist in the DOM, even when hidden, e.g. Inventory
  567. // But some windows only exist in the DOM when open, e.g. Interaction
  568. const modsToRerun = [
  569. 'saveDraggedUIWindows',
  570. 'draggableUIWindows',
  571. 'loadDraggedUIWindowsPositions',
  572. 'selectedWindowIsTop',
  573. 'customSettings',
  574. ];
  575. modsToRerun.forEach(modName => {
  576. mods.find(mod => mod.name === modName)();
  577. });
  578. });
  579. rerunObserver.observe(document.querySelector('.layout > .container'), { attributes: false, childList: true, });
  580.  
  581. // Rerun only on chat
  582. const chatRerunObserver = new MutationObserver(() => {
  583. mods.find(mod => mod.name === 'chatContextMenu')();
  584. modHelpers.filterAllChat();
  585.  
  586. });
  587. chatRerunObserver.observe(document.querySelector('#chat'), { attributes: false, childList: true, });
  588.  
  589. // Event listeners for document.body might be kept when the game reloads, so don't reinitialize them
  590. if (!document.body.classList.contains('js-uimod-initd')) {
  591. document.body.classList.add('js-uimod-initd');
  592.  
  593. // Close chat context menu when clicking outside of it
  594. document.body.addEventListener('click', modHelpers.closeChatContextMenu);
  595. }
  596. }
  597.  
  598. // Initialize mods once UI DOM has loaded
  599. const pageObserver = new MutationObserver(() => {
  600. const isUiLoaded = !!document.querySelector('.layout');
  601. if (isUiLoaded) {
  602. initialize();
  603. }
  604. });
  605. pageObserver.observe(document.body, { attributes: true, childList: true })
  606.  
  607. // UTIL METHODS
  608. // Save to in-memory state and localStorage to retain on refresh
  609. // TODO: Can use this solely to save to storage - dont need to update state, we already do that ourselves a lot
  610. function save(items) {
  611. state = {
  612. ...state,
  613. ...items,
  614. };
  615. localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
  616. }
  617.  
  618. // Load localStorage state if it exists
  619. // NOTE: If user is trying to load unsupported version of stored state,
  620. // e.g. they just upgraded to breaking version, then we delete their stored state
  621. function load() {
  622. const storedStateJson = localStorage.getItem(STORAGE_STATE_KEY)
  623. if (storedStateJson) {
  624. const storedState = JSON.parse(storedStateJson);
  625. if (storedState.breakingVersion !== BREAKING_VERSION) {
  626. localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
  627. return;
  628. }
  629. state = {
  630. ...state,
  631. ...storedState,
  632. };
  633. }
  634. }
  635.  
  636. // Nicer impl to create elements in one method call
  637. function makeElement(args) {
  638. const $node = document.createElement(args.element);
  639. if (args.class) { $node.className = args.class; }
  640. if (args.content) { $node.innerHTML = args.content; }
  641. if (args.src) { $node.src = args.src; }
  642. return $node;
  643. }
  644.  
  645. function enterTextIntoChat(text) {
  646. // Open chat input
  647. const enterEvent = new KeyboardEvent('keydown', {
  648. bubbles: true, cancelable: true, keyCode: 13
  649. });
  650. document.body.dispatchEvent(enterEvent);
  651.  
  652. // Place text into chat
  653. const $input = document.querySelector('#chatinput input');
  654. $input.value = text;
  655.  
  656. // Get chat input to recognize slash commands and change the channel
  657. // by triggering the `input` event.
  658. // (Did some debugging to figure out the channel only changes when the
  659. // svelte `input` event listener exists.)
  660. const inputEvent = new KeyboardEvent('input', {
  661. bubbles: true, cancelable: true
  662. })
  663. $input.dispatchEvent(inputEvent);
  664. }
  665.  
  666. function submitChat() {
  667. const $input = document.querySelector('#chatinput input');
  668. const kbEvent = new KeyboardEvent('keydown', {
  669. bubbles: true, cancelable: true, keyCode: 13
  670. });
  671. $input.dispatchEvent(kbEvent);
  672. }
  673.  
  674. // Credit: https://stackoverflow.com/a/14234618 (Has been slightly modified)
  675. // $draggedElement is the item that will be dragged.
  676. // $dragTrigger is the element that must be held down to drag $draggedElement
  677. function dragElement($draggedElement, $dragTrigger) {
  678. let offset = [0,0];
  679. let isDown = false;
  680. $dragTrigger.addEventListener('mousedown', function(e) {
  681. isDown = true;
  682. offset = [
  683. $draggedElement.offsetLeft - e.clientX,
  684. $draggedElement.offsetTop - e.clientY
  685. ];
  686. }, true);
  687. document.addEventListener('mouseup', function() {
  688. isDown = false;
  689. }, true);
  690.  
  691. document.addEventListener('mousemove', function(e) {
  692. event.preventDefault();
  693. if (isDown) {
  694. $draggedElement.style.left = (e.clientX + offset[0]) + 'px';
  695. $draggedElement.style.top = (e.clientY + offset[1]) + 'px';
  696. }
  697. }, true);
  698. }
  699.  
  700. // Credit: David Walsh
  701. function debounce(func, wait, immediate) {
  702. var timeout;
  703. return function() {
  704. var context = this, args = arguments;
  705. var later = function() {
  706. timeout = null;
  707. if (!immediate) func.apply(context, args);
  708. };
  709. var callNow = immediate && !timeout;
  710. clearTimeout(timeout);
  711. timeout = setTimeout(later, wait);
  712. if (callNow) func.apply(context, args);
  713. };
  714. }
  715. })();