Hordes UI Mod

Various UI mods for Hordes.io.

目前為 2019-12-23 提交的版本,檢視 最新版本

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