Hordes UI Mod

Various UI mods for Hordes.io.

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

  1. // ==UserScript==
  2. // @name Hordes UI Mod
  3. // @version 0.190
  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: Add whisper chat filter
  12. * TODO: Implement inventory sorting
  13. * TODO: (Maybe) Improved healer party frames
  14. * TODO: FIX BUG: Add support for resizing map back to saved position after minimizing it, from maximized position
  15. * TODO: (Maybe): Add toggleable option to include chat messages to right of party frame
  16. * TODO: Remove all reliance on svelte- classes, likely breaks with updates
  17. * TODO: Add cooldown on skills (leverage skill icon URL, have a map for each skill icon mapping to its respective cooldown)
  18. * TODO: Clicking names in party to add as friends
  19. * TODO: (MAYBE, Confirm if dev is ok w it) Ctrl clicking item in inventory when Merchant is open to paste item name into search field, and search
  20. * TODO: (MAYBE, confirm if dev is ok w it) Ctrl clicking item to copy details so user can paste in chat
  21. */
  22. (function() {
  23. 'use strict';
  24.  
  25. // If this version is different from the user's stored state,
  26. // e.g. they have upgraded the version of this script and there are breaking changes,
  27. // then their stored state will be deleted.
  28. const BREAKING_VERSION = 1;
  29. const VERSION = '0.190'; // Should match version in UserScript description
  30.  
  31. const DEFAULT_CHAT_TAB_NAME = 'Untitled';
  32. const STORAGE_STATE_KEY = 'hordesio-uimodsakaiyo-state';
  33.  
  34. let state = {
  35. breakingVersion: BREAKING_VERSION,
  36. chat: {
  37. GM: true,
  38. },
  39. windowsPos: {},
  40. blockList: {},
  41. friendsList: {},
  42. mapOpacity: 70, // e.g. 70 = opacity: 0.7
  43. friendNotes: {},
  44. chatTabs: [],
  45. };
  46. // tempState is saved only between page refreshes.
  47. const tempState = {
  48. // The last name clicked in chat
  49. chatName: null,
  50. lastMapWidth: 0,
  51. lastMapHeight: 0,
  52. };
  53.  
  54. // UPDATING STYLES BELOW - Must be invoked in main function
  55. GM_addStyle(`
  56. /* Transparent chat bg color */
  57. .frame.svelte-1vrlsr3 {
  58. background: rgba(0,0,0,0.4);
  59. }
  60.  
  61. /* Our mod's chat message color */
  62. .textuimod {
  63. color: #00dd33;
  64. }
  65.  
  66. /* Allows windows to be moved */
  67. .window {
  68. position: relative;
  69. }
  70.  
  71. /* Allows last clicked window to appear above all other windows */
  72. .js-is-top {
  73. z-index: 9998 !important;
  74. }
  75. .panel.context:not(.commandlist) {
  76. z-index: 9999 !important;
  77. }
  78. /* The item icon being dragged in the inventory */
  79. .container.svelte-120o2pb {
  80. z-index: 9999 !important;
  81. }
  82.  
  83. /* All purpose hidden class */
  84. .js-hidden {
  85. display: none;
  86. }
  87.  
  88. /* Custom chat context menu, invisible by default */
  89. .js-chat-context-menu {
  90. display: none;
  91. }
  92.  
  93. .js-chat-context-menu .name {
  94. color: white;
  95. padding: 2px 4px;
  96. }
  97.  
  98. /* Allow names in chat to be clicked */
  99. #chat .name,
  100. .textwhisper .textf1 {
  101. pointer-events: all !important;
  102. }
  103.  
  104. /* Custom chat filter colors */
  105. .js-chat-gm {
  106. color: #a6dcd5;
  107. }
  108.  
  109. /* Class that hides chat lines */
  110. .js-line-hidden,
  111. .js-line-blocked {
  112. display: none;
  113. }
  114.  
  115. /* Enable chat & map resize */
  116. .js-chat-resize {
  117. resize: both;
  118. overflow: auto;
  119. }
  120. .js-map-resize:hover {
  121. resize: both;
  122. overflow: auto;
  123. direction: rtl;
  124. }
  125.  
  126. /* The browser resize icon */
  127. *::-webkit-resizer {
  128. background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
  129. border-radius: 8px;
  130. box-shadow: 0 1px 1px rgba(0,0,0,1);
  131. }
  132. *::-moz-resizer {
  133. background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
  134. border-radius: 8px;
  135. box-shadow: 0 1px 1px rgba(0,0,0,1);
  136. }
  137.  
  138. .js-map-btns {
  139. position: absolute;
  140. top: 46px;
  141. right: 12px;
  142. z-index: 999;
  143. width: 100px;
  144. height: 100px;
  145. text-align: right;
  146. display: none;
  147. pointer-events: all;
  148. }
  149. .js-map-btns:hover {
  150. display: block;
  151. }
  152. .js-map-btns button {
  153. border-radius: 10px;
  154. font-size: 18px;
  155. padding: 0 5px;
  156. background: rgba(0,0,0,0.4);
  157. border: 0;
  158. color: white;
  159. font-weight: bold;
  160. pointer: cursor;
  161. }
  162. /* On hover of map, show opacity controls */
  163. .js-map:hover .js-map-btns {
  164. display: block;
  165. }
  166.  
  167. /* Custom css for settings page, duplicates preexisting settings pane grid */
  168. .uimod-settings {
  169. display: grid;
  170. grid-template-columns: 2fr 3fr;
  171. grid-gap: 8px;
  172. align-items: center;
  173. max-height: 390px;
  174. margin: 0 20px;
  175. overflow-y: auto;
  176. }
  177. /* Friends list CSS, similar to settings but supports 4 columns */
  178. .uimod-friends {
  179. display: grid;
  180. grid-template-columns: 2fr 1.1fr 1.5fr 0.33fr 3fr;
  181. grid-gap: 8px;
  182. align-items: center;
  183. max-height: 390px;
  184. margin: 0 20px;
  185. overflow-y: auto;
  186. }
  187. /* Our custom window, closely mirrors main settings window */
  188. .uimod-custom-window {
  189. position: absolute;
  190. top: 100px;
  191. left: 50%;
  192. transform: translate(-50%, 0);
  193. min-width: 350px;
  194. max-width: 600px;
  195. width: 90%;
  196. height: 80%;
  197. min-height: 350px;
  198. max-height: 500px;
  199. z-index: 9;
  200. padding: 0px 10px 5px;
  201. }
  202. /* Custom chat tabs */
  203. .uimod-chat-tabs {
  204. position: fixed;
  205. margin-top: -22px;
  206. left: 5px;
  207. pointer-events: all;
  208. color: #5b858e;
  209. font-size: 12px;
  210. font-weight: bold;
  211. }
  212. .uimod-chat-tabs > div {
  213. cursor: pointer;
  214. background-color: rgba(0,0,0,0.4);
  215. border-top-right-radius: 4px;
  216. border-top-left-radius: 4px;
  217. display: inline-block;
  218. border: 1px black solid;
  219. border-bottom: 0;
  220. margin-right: 2px;
  221. padding: 3px 5px;
  222. }
  223. .uimod-chat-tabs > div:not(.js-selected-tab):hover {
  224. border-color: #aaa;
  225. }
  226. .uimod-chat-tabs > .js-selected-tab {
  227. color: #fff;
  228. }
  229.  
  230. /* Chat tab custom config */
  231. .uimod-chat-tab-config {
  232. position: absolute;
  233. z-index: 9999999;
  234. background-color: rgba(0,0,0,0.6);
  235. color: white;
  236. border-radius: 3px;
  237. text-align: center;
  238. padding: 8px 12px 8px 6px;
  239. width: 175px;
  240. font-size: 14px;
  241. border: 1px solid black;
  242. display: none;
  243. }
  244.  
  245. .uimod-chat-tab-config-grid {
  246. grid-template-columns: 35% 65%;
  247. display: grid;
  248. grid-gap: 6px;
  249. align-items: center;
  250. }
  251.  
  252. .uimod-chat-tab-config h1 {
  253. font-size: 16px;
  254. margin-top: 0;
  255. }
  256.  
  257. .uimod-chat-tab-config .btn,
  258. .uimod-chat-tab-config input {
  259. font-size: 12px;
  260. }
  261. `);
  262.  
  263.  
  264. const modHelpers = {
  265. // Automated chat command helpers
  266. // (We've been OK'd to do these by the dev - all automation like this should receive approval from the dev)
  267. whisperPlayer: playerName => {
  268. enterTextIntoChat(`/whisper ${tempState.chatName} `);
  269. },
  270. partyPlayer: playerName => {
  271. enterTextIntoChat(`/partyinvite ${tempState.chatName}`);
  272. submitChat();
  273. },
  274.  
  275. // Filters all chat based on custom filters
  276. filterAllChat: () => {
  277. // Blocked user filter
  278. Object.keys(state.blockList).forEach(blockedName => {
  279. // Get the `.name` elements from the blocked user, if we haven't already hidden their messages
  280. const $blockedChatNames = Array.from(document.querySelectorAll(`[data-chat-name="${blockedName}"]:not(.js-line-blocked)`));
  281. // Hide each of their messages
  282. $blockedChatNames.forEach($name => {
  283. // Add the class name to $name so we can track whether it's been hidden in our CSS selector $blockedChatNames
  284. $name.classList.add('js-line-blocked');
  285. const $line = $name.parentNode.parentNode.parentNode;
  286. // Add the class name to $line so we can visibly hide the entire chat line
  287. $line.classList.add('js-line-blocked');
  288. });
  289. })
  290.  
  291. // Custom channel filter
  292. Object.keys(state.chat).forEach(channel => {
  293. Array.from(document.querySelectorAll(`.text${channel}.content`)).forEach($textItem => {
  294. const $line = $textItem.parentNode.parentNode;
  295. $line.classList.toggle('js-line-hidden', !state.chat[channel]);
  296. });
  297. });
  298. },
  299.  
  300. // Makes chat context menu visible and appear under the mouse
  301. showChatContextMenu: (name, mousePos) => {
  302. // Right before we show the context menu, we want to handle showing/hiding Friend/Unfriend
  303. const $contextMenu = document.querySelector('.js-chat-context-menu');
  304. $contextMenu.querySelector('[name="friend"]').classList.toggle('js-hidden', !!state.friendsList[name]);
  305. $contextMenu.querySelector('[name="unfriend"]').classList.toggle('js-hidden', !state.friendsList[name]);
  306.  
  307. $contextMenu.querySelector('.js-name').textContent = name;
  308. $contextMenu.setAttribute('style', `display: block; left: ${mousePos.x}px; top: ${mousePos.y}px;`);
  309. },
  310.  
  311. // Close chat context menu if clicking outside of it
  312. closeChatContextMenu: clickEvent => {
  313. const $target = clickEvent.target;
  314. // If clicking on name or directly on context menu, don't close it
  315. // Still closes if clicking on context menu item
  316. if ($target.classList.contains('js-is-context-menu-initd')
  317. || $target.classList.contains('js-chat-context-menu')) {
  318. return;
  319. }
  320.  
  321. const $contextMenu = document.querySelector('.js-chat-context-menu');
  322. $contextMenu.style.display = 'none';
  323. },
  324.  
  325. friendPlayer: playerName => {
  326. if (state.friendsList[playerName]) {
  327. return;
  328. }
  329.  
  330. state.friendsList[playerName] = true;
  331. modHelpers.addChatMessage(`${playerName} has been added to your friends list.`);
  332. save();
  333. },
  334.  
  335. unfriendPlayer: playerName => {
  336. if (!state.friendsList[playerName]) {
  337. return;
  338. }
  339.  
  340. delete state.friendsList[playerName];
  341. delete state.friendNotes[playerName];
  342. modHelpers.addChatMessage(`${playerName} is no longer on your friends list.`);
  343. save();
  344. },
  345.  
  346. // Adds player to block list, to be filtered out of chat
  347. blockPlayer: playerName => {
  348. if (state.blockList[playerName]) {
  349. return;
  350. }
  351.  
  352. state.blockList[playerName] = true;
  353. modHelpers.filterAllChat();
  354. modHelpers.addChatMessage(`${playerName} has been blocked.`)
  355. save();
  356. },
  357.  
  358. // Removes player from block list and makes their messages visible again
  359. unblockPlayer: playerName => {
  360. delete state.blockList[playerName];
  361. modHelpers.addChatMessage(`${playerName} has been unblocked.`);
  362. save();
  363.  
  364. // Make messages visible again
  365. const $chatNames = Array.from(document.querySelectorAll(`.js-line-blocked[data-chat-name="${playerName}"]`));
  366. $chatNames.forEach($name => {
  367. $name.classList.remove('js-line-blocked');
  368. const $line = $name.parentNode.parentNode.parentNode;
  369. $line.classList.remove('js-line-blocked');
  370. });
  371. },
  372.  
  373. // Pushes message to chat
  374. // TODO: The margins for the message are off slightly compared to other messages - why?
  375. addChatMessage: text => {
  376. const newMessageHTML = `
  377. <div class="linewrap svelte-1vrlsr3">
  378. <span class="time svelte-1vrlsr3">00.00</span>
  379. <span class="textuimod content svelte-1vrlsr3">
  380. <span class="capitalize channel svelte-1vrlsr3">UIMod</span>
  381. </span>
  382. <span class="svelte-1vrlsr3">${text}</span>
  383. </div>
  384. `;
  385.  
  386. const element = makeElement({
  387. element: 'article',
  388. class: 'line svelte-1vrlsr3',
  389. content: newMessageHTML});
  390. document.querySelector('#chat').appendChild(element);
  391. },
  392.  
  393. // Gets current chat filters as represented in the UI
  394. // filter being true means it's invisible(filtered) in chat
  395. // filter being false means it's visible(unfiltered) in chat
  396. getCurrentChatFilters: () => {
  397. // Saved by the official game client
  398. const gameFilters = JSON.parse(localStorage.getItem('filteredChannels'));
  399. return {
  400. global: gameFilters.includes('global'),
  401. faction: gameFilters.includes('faction'),
  402. party: gameFilters.includes('party'),
  403. clan: gameFilters.includes('clan'),
  404. pvp: gameFilters.includes('pvp'),
  405. inv: gameFilters.includes('inv'),
  406. GM: !state.chat.GM, // state.chat.GM is whether or not GM chat is shown - we want whether or not GM chat should be hidden
  407. };
  408. },
  409.  
  410. // Shows the chat tab config window for a specific tab, displayed in a specific position
  411. showChatTabConfigWindow: (tabId, pos) => {
  412. const $chatTabConfig = document.querySelector('.js-chat-tab-config');
  413. const chatTab = state.chatTabs.find(tab => tab.id === tabId);
  414. // Update position and name in chat tab config
  415. $chatTabConfig.style.left = `${pos.x}px`;
  416. $chatTabConfig.style.top = `${pos.y}px`;
  417. $chatTabConfig.querySelector('.js-chat-tab-name').value = chatTab.name;
  418.  
  419. // Store tabId in state, to be used by the Remove/Add buttons in config window
  420. tempState.editedChatTabId = tabId;
  421.  
  422. // Hide remove button if only one chat tab left - can't remove last one
  423. // Show it if more than one chat tab left
  424. const chatTabCount = Object.keys(state.chatTabs).length;
  425. const $removeChatTabBtn = $chatTabConfig.querySelector('.js-remove-chat-tab');
  426. $removeChatTabBtn.style.display = chatTabCount < 2 ? 'none' : 'block';
  427.  
  428. // Show chat tab config
  429. $chatTabConfig.style.display = 'block';
  430. },
  431.  
  432. // Adds chat tab to DOM, sets it as selected
  433. // If argument chatTab is provided, will use that name+id
  434. // If no argument is provided, will create new tab name/id and add it to state
  435. // isInittingTab is optional boolean, if `true`, will _not_ set added tab as selected. Used when initializing all chat tabs on load
  436. // Returns newly added tabId
  437. addChatTab: (chatTab, isInittingTab) => {
  438. let tabName = DEFAULT_CHAT_TAB_NAME;
  439. let tabId = uuid();
  440. if (chatTab) {
  441. tabName = chatTab.name;
  442. tabId = chatTab.id;
  443. } else {
  444. // If no chat tab was provided, create it in state
  445. state.chatTabs.push({
  446. name: tabName,
  447. id: tabId,
  448. filters: modHelpers.getCurrentChatFilters(),
  449. });
  450. save();
  451. }
  452.  
  453. const $tabs = document.querySelector('.js-chat-tabs');
  454. const $tab = makeElement({
  455. element: 'div',
  456. content: tabName,
  457. });
  458. $tab.setAttribute('data-tab-id', tabId);
  459.  
  460. // Add chat tab to DOM
  461. $tabs.appendChild($tab);
  462.  
  463. // Wire chat tab up to open config on right click
  464. $tab.addEventListener('contextmenu', clickEvent => {
  465. const mousePos = {x: clickEvent.pageX, y: clickEvent.pageY};
  466. modHelpers.showChatTabConfigWindow(tabId, mousePos);
  467. });
  468. // And select chat tab on left click
  469. $tab.addEventListener('click', () => {
  470. modHelpers.selectChatTab(tabId);
  471. });
  472.  
  473. if (!isInittingTab) {
  474. // Select the newly added chat tab
  475. modHelpers.selectChatTab(tabId);
  476. }
  477.  
  478. // Returning tabId to all adding new tab to pass tab ID to `showChatTabConfigWindow`
  479. return tabId;
  480. },
  481.  
  482. // Selects chat tab [on click], updating client chat filters and custom chat filters
  483. selectChatTab: tabId => {
  484. // Remove selected class from everything, then add selected class to clicked tab
  485. Array.from(document.querySelectorAll('[data-tab-id]')).forEach($tab => {
  486. $tab.classList.remove('js-selected-tab');
  487. });
  488. const $tab = document.querySelector(`[data-tab-id="${tabId}"]`);
  489. $tab.classList.add('js-selected-tab');
  490.  
  491. const tabFilters = state.chatTabs.find(tab => tab.id === tabId).filters;
  492. // Simulating clicks on the filters to turn them on/off
  493. const $filterButtons = Array.from(document.querySelectorAll('.channelselect small'));
  494. Object.keys(tabFilters).forEach(filter => {
  495. const $filterButton = $filterButtons.find($btn => $btn.textContent === filter);
  496. const isCurrentlyFiltered = $filterButton.classList.contains('textgrey');
  497.  
  498. // If is currently filtered but filter for this tab is turned off, click it to turn filter off
  499. if (isCurrentlyFiltered && !tabFilters[filter]) {
  500. $filterButton.click();
  501. }
  502. // If it is not currently filtered but filter for this tab is turned on, click it to turn filter on
  503. if (!isCurrentlyFiltered && tabFilters[filter]) {
  504. $filterButton.click();
  505. }
  506. });
  507.  
  508. // Update state for our custom chat filters to match the tab's configuration, then filter chat for it
  509. const isGMChatVisible = !tabFilters.GM;
  510. modHelpers.setGMChatVisibility(isGMChatVisible);
  511.  
  512. // Update the selected tab in state
  513. state.selectedChatTabId = tabId;
  514. save();
  515. },
  516.  
  517. // Updates state.chat.GM and the DOM to make text white/grey depending on if gm chat is visible/filtered
  518. // Then filters chat and saves updated chat state
  519. setGMChatVisibility: isGMChatVisible => {
  520. const $chatGM = document.querySelector(`.js-chat-gm`);
  521. state.chat.GM = isGMChatVisible;
  522. $chatGM.classList.toggle('textgrey', !state.chat.GM);
  523. modHelpers.filterAllChat();
  524. save();
  525. },
  526. };
  527.  
  528. // MAIN MODS BELOW
  529. const mods = [
  530. // Creates DOM elements for custom chat filters
  531. function newChatFilters() {
  532. const $channelselect = document.querySelector('.channelselect');
  533. if (!document.querySelector(`.js-chat-gm`)) {
  534. const $gm = makeElement({
  535. element: 'small',
  536. class: `btn border black js-chat-gm ${state.chat.GM ? '' : 'textgrey'}`,
  537. content: 'GM'
  538. });
  539. $channelselect.appendChild($gm);
  540. }
  541. },
  542.  
  543. // Creates DOM elements and wires them up for custom chat tabs and chat tab config
  544. // Note: Should be done after creating new custom chat filters
  545. function customChatTabs() {
  546. // Create the chat tab configuration DOM
  547. const $chatTabConfigurator = makeElement({
  548. element: 'div',
  549. class: 'uimod-chat-tab-config js-chat-tab-config',
  550. content: `
  551. <h1>Chat Tab Config</h1>
  552. <div class="uimod-chat-tab-config-grid">
  553. <div>Name</div><input type="text" class="js-chat-tab-name" value="untitled"></input>
  554. <div class="btn orange js-remove-chat-tab">Remove</div><div class="btn blue js-save-chat-tab">Ok</div>
  555. </div>
  556. `,
  557. });
  558. document.body.append($chatTabConfigurator);
  559.  
  560. // Wire it up
  561. document.querySelector('.js-remove-chat-tab').addEventListener('click', () => {
  562. // Remove the chat tab from state
  563. const editedChatTab = state.chatTabs.find(tab => tab.id === tempState.editedChatTabId);
  564. const editedChatTabIndex = state.chatTabs.indexOf(editedChatTab);
  565. state.chatTabs.splice(editedChatTabIndex, 1);
  566. // Remove the chat tab from DOM
  567. const $chatTab = document.querySelector(`[data-tab-id="${tempState.editedChatTabId}"]`);
  568. $chatTab.parentNode.removeChild($chatTab);
  569.  
  570. // If we just removed the currently selected chat tab
  571. if (tempState.editedChatTabId === state.selectedChatTabId) {
  572. // Select the chat tab to the left of the removed one
  573. const nextChatTabIndex = editedChatTabIndex === 0 ? 0 : editedChatTabIndex - 1;
  574. modHelpers.selectChatTab(state.chatTabs[nextChatTabIndex].id);
  575. }
  576.  
  577. // Close chat tab config
  578. document.querySelector('.js-chat-tab-config').style.display = 'none';
  579. });
  580.  
  581. document.querySelector('.js-save-chat-tab').addEventListener('click', () => {
  582. // Set new chat tab name in DOM
  583. const $chatTab = document.querySelector(`[data-tab-id="${state.selectedChatTabId}"]`);
  584. const newName = document.querySelector('.js-chat-tab-name').value;
  585. $chatTab.textContent = newName;
  586.  
  587. // Set new chat tab name in state
  588. // `selectedChatTab` is a reference on `state.chatTabs`, so updating it above still updates it in the state - we want to save that
  589. const selectedChatTab = state.chatTabs.find(tab => tab.id === state.selectedChatTabId);
  590. selectedChatTab.name = newName;
  591. save();
  592.  
  593. // Close chat tab config
  594. document.querySelector('.js-chat-tab-config').style.display = 'none';
  595. });
  596.  
  597. // Create the initial chat tabs HTML
  598. const $chat = document.querySelector('#chat');
  599. const $chatTabs = makeElement({
  600. element: 'div',
  601. class: 'uimod-chat-tabs js-chat-tabs',
  602. content: '<div class="js-chat-tab-add">+</div>',
  603. });
  604.  
  605. // Add them to the DOM
  606. $chat.parentNode.insertBefore($chatTabs, $chat);
  607.  
  608. // Add all our chat tabs from state
  609. state.chatTabs.forEach(chatTab => {
  610. const isInittingTab = true;
  611. modHelpers.addChatTab(chatTab, isInittingTab);
  612. });
  613.  
  614. // Wire up the add chat tab button
  615. document.querySelector('.js-chat-tab-add').addEventListener('click', clickEvent => {
  616. const chatTabId = modHelpers.addChatTab();
  617. const mousePos = {x: clickEvent.pageX, y: clickEvent.pageY};
  618. modHelpers.showChatTabConfigWindow(chatTabId, mousePos);
  619. });
  620.  
  621. // If initial chat tab doesn't exist, create it based off current filter settings
  622. if (!Object.keys(state.chatTabs).length) {
  623. const tabId = uuid();
  624. const chatTab = {
  625. name: 'Main',
  626. id: tabId,
  627. filters: modHelpers.getCurrentChatFilters()
  628. };
  629. state.chatTabs.push(chatTab);
  630. save();
  631. modHelpers.addChatTab(chatTab);
  632. }
  633.  
  634. // Wire up click event handlers onto the filters to update the selected chat tab's filters in state
  635. document.querySelector('.channelselect').addEventListener('click', clickEvent => {
  636. const $elementMouseIsOver = document.elementFromPoint(clickEvent.clientX, clickEvent.clientY);
  637. // We only want to change the filters if the user manually clicks the filter button
  638. // If they clicked a chat tab and we programatically set filters, we don't want to update
  639. // the current tab's filter state
  640. if (!$elementMouseIsOver.classList.contains('btn')) {
  641. return;
  642. }
  643. const selectedChatTab = state.chatTabs.find(tab => tab.id === state.selectedChatTabId);
  644. selectedChatTab.filters = modHelpers.getCurrentChatFilters();
  645. save();
  646. });
  647.  
  648. // Select the currently selected tab in state on mod initialization
  649. if (state.selectedChatTabId) {
  650. modHelpers.selectChatTab(state.selectedChatTabId);
  651. }
  652. },
  653.  
  654. // Wire up new chat buttons to toggle in state+ui
  655. function newChatFilterButtons() {
  656. const $chatGM = document.querySelector(`.js-chat-gm`);
  657. $chatGM.addEventListener('click', () => {
  658. modHelpers.setGMChatVisibility(!state.chat.GM);
  659. });
  660. },
  661.  
  662. // Filter out chat in UI based on chat buttons state
  663. function filterChatObserver() {
  664. const chatObserver = new MutationObserver(modHelpers.filterAllChat);
  665. chatObserver.observe(document.querySelector('#chat'), { attributes: true, childList: true });
  666. },
  667.  
  668. // Drag all windows by their header
  669. function draggableUIWindows() {
  670. Array.from(document.querySelectorAll('.window:not(.js-can-move)')).forEach($window => {
  671. $window.classList.add('js-can-move');
  672. dragElement($window, $window.querySelector('.titleframe'));
  673. });
  674. },
  675.  
  676. // Save dragged UI windows position to state
  677. function saveDraggedUIWindows() {
  678. Array.from(document.querySelectorAll('.window:not(.js-window-is-saving)')).forEach($window => {
  679. $window.classList.add('js-window-is-saving');
  680. const $draggableTarget = $window.querySelector('.titleframe');
  681. const windowName = $draggableTarget.querySelector('[name="title"]').textContent;
  682. $draggableTarget.addEventListener('mouseup', () => {
  683. state.windowsPos[windowName] = $window.getAttribute('style');
  684. save();
  685. });
  686. });
  687. },
  688.  
  689. // Loads draggable UI windows position from state
  690. function loadDraggedUIWindowsPositions() {
  691. Array.from(document.querySelectorAll('.window:not(.js-has-loaded-pos)')).forEach($window => {
  692. $window.classList.add('js-has-loaded-pos');
  693. const windowName = $window.querySelector('[name="title"]').textContent;
  694. const pos = state.windowsPos[windowName];
  695. if (pos) {
  696. $window.setAttribute('style', pos);
  697. }
  698. });
  699. },
  700.  
  701. // Makes chat resizable
  702. function resizableChat() {
  703. // Add the appropriate classes
  704. const $chatContainer = document.querySelector('#chat').parentNode;
  705. $chatContainer.classList.add('js-chat-resize');
  706.  
  707. // Load initial chat and map size
  708. if (state.chatWidth && state.chatHeight) {
  709. $chatContainer.style.width = state.chatWidth;
  710. $chatContainer.style.height = state.chatHeight;
  711. }
  712.  
  713. // Save chat size on resize
  714. const resizeObserverChat = new ResizeObserver(() => {
  715. const chatWidthStr = window.getComputedStyle($chatContainer, null).getPropertyValue('width');
  716. const chatHeightStr = window.getComputedStyle($chatContainer, null).getPropertyValue('height');
  717. state.chatWidth = chatWidthStr;
  718. state.chatHeight = chatHeightStr;
  719. save();
  720. });
  721. resizeObserverChat.observe($chatContainer);
  722. },
  723.  
  724. // Makes map resizable
  725. function resizeableMap() {
  726. const $map = document.querySelector('.container canvas').parentNode;
  727. const $canvas = $map.querySelector('canvas');
  728. $map.classList.add('js-map-resize');
  729.  
  730. // Track whether we're clicking (resizing) map or not
  731. // Used to detect if resize changes are manually done, or from minimizing/maximizing map (with [M])
  732. $map.addEventListener('mousedown', () => {
  733. tempState.clickingMap = true;
  734. });
  735. // Sometimes the mouseup event may be registered outside of the map - we account for this
  736. document.body.addEventListener('mouseup', () => {
  737. tempState.clickingMap = false;
  738. });
  739.  
  740. const onMapResize = () => {
  741. // Get real values of map height/width, excluding padding/margin/etc
  742. const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
  743. const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
  744. const mapWidth = Number(mapWidthStr.slice(0, -2));
  745. const mapHeight = Number(mapHeightStr.slice(0, -2));
  746.  
  747. // If height/width are 0 or unset, don't resize canvas
  748. if (!mapWidth || !mapHeight) {
  749. return;
  750. }
  751.  
  752. if ($canvas.width !== mapWidth) {
  753. $canvas.width = mapWidth;
  754. }
  755.  
  756. if ($canvas.height !== mapHeight) {
  757. $canvas.height = mapHeight;
  758. }
  759.  
  760. // If we're clicking map, i.e. manually resizing, then save state
  761. // Don't save state when minimizing/maximizing map via [M]
  762. if (tempState.clickingMap) {
  763. state.mapWidth = mapWidthStr;
  764. state.mapHeight = mapHeightStr;
  765. save();
  766. } else {
  767. const isMaximized = mapWidth > tempState.lastMapWidth && mapHeight > tempState.lastMapHeight;
  768. if (!isMaximized) {
  769. $map.style.width = state.mapWidth;
  770. $map.style.height = state.mapHeight;
  771. }
  772. }
  773.  
  774. // Store last map width/height in temp state, so we know if we've minimized or maximized
  775. tempState.lastMapWidth = mapWidth;
  776. tempState.lastMapHeight = mapHeight;
  777. };
  778.  
  779. if (state.mapWidth && state.mapHeight) {
  780. $map.style.width = state.mapWidth;
  781. $map.style.height = state.mapHeight;
  782. onMapResize(); // Update canvas size on initial load of saved map size
  783. }
  784.  
  785. // On resize of map, resize canvas to match
  786. const resizeObserverMap = new ResizeObserver(onMapResize);
  787. resizeObserverMap.observe($map);
  788.  
  789. // We need to observe canvas resizes to tell when the user presses M to open the big map
  790. // At that point, we resize the map to match the canvas
  791. const triggerResize = () => {
  792. // Get real values of map height/width, excluding padding/margin/etc
  793. const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
  794. const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
  795. const mapWidth = Number(mapWidthStr.slice(0, -2));
  796. const mapHeight = Number(mapHeightStr.slice(0, -2));
  797.  
  798. // If height/width are 0 or unset, we don't care about resizing yet
  799. if (!mapWidth || !mapHeight) {
  800. return;
  801. }
  802.  
  803. if ($canvas.width !== mapWidth) {
  804. $map.style.width = `${$canvas.width}px`;
  805. }
  806.  
  807. if ($canvas.height !== mapHeight) {
  808. $map.style.height = `${$canvas.height}px`;
  809. }
  810. };
  811.  
  812. // We debounce the canvas resize, so it doesn't resize every single
  813. // pixel you move when resizing the DOM. If this were to happen,
  814. // resizing would constantly be interrupted. You'd have to resize a tiny bit,
  815. // lift left click, left click again to resize a tiny bit more, etc.
  816. // Resizing is smooth when we debounce this canvas.
  817. const debouncedTriggerResize = debounce(triggerResize, 50);
  818. const resizeObserverCanvas = new ResizeObserver(debouncedTriggerResize);
  819. resizeObserverCanvas.observe($canvas);
  820. },
  821.  
  822. function mapControls() {
  823. const $map = document.querySelector('.container canvas');
  824. if (!$map.parentNode.classList.contains('js-map')) {
  825. $map.parentNode.classList.add('js-map');
  826. }
  827. const $mapContainer = document.querySelector('.js-map');
  828.  
  829. // On load, update map opacity to match state
  830. // We modify the opacity of the canvas and the background color alpha of the parent container
  831. // We do this to allow our opacity buttons to be visible on hover with 100% opacity
  832. // (A surprisingly difficult enough task to require this implementation)
  833. const updateMapOpacity = () => {
  834. $map.style.opacity = String(state.mapOpacity / 100);
  835. const mapContainerBgColor = window.getComputedStyle($mapContainer, null).getPropertyValue('background-color');
  836. // Credit for this regexp + This opacity+rgba dual implementation: https://stackoverflow.com/questions/16065998/replacing-changing-alpha-in-rgba-javascript
  837. let opacity = state.mapOpacity / 100;
  838. // This is a slightly lazy browser workaround to fix a bug.
  839. // If the opacity is `1`, and it sets `rgba` to `1`, then the browser changes the
  840. // rgba to rgb, dropping the alpha. We could account for that and add the `alpha` back in
  841. // later, but setting the max opacity to very close to 1 makes sure the issue never crops up.
  842. // Fun fact: 0.99 retains the alpha, but setting this to 0.999 still causes the browser to drop the alpha. Rude.
  843. if (opacity === 1) {
  844. opacity = 0.99;
  845. }
  846. const newBgColor = mapContainerBgColor.replace(/[\d\.]+\)$/g, `${opacity})`);
  847. $mapContainer.style['background-color'] = newBgColor;
  848.  
  849. // Update the button opacity
  850. const $addBtn = document.querySelector('.js-map-opacity-add');
  851. const $minusBtn = document.querySelector('.js-map-opacity-minus');
  852. // Hide plus button if the opacity is max
  853. if (state.mapOpacity === 100) {
  854. $addBtn.style.visibility = 'hidden';
  855. } else {
  856. $addBtn.style.visibility = 'visible';
  857. }
  858. // Hide minus button if the opacity is lowest
  859. if (state.mapOpacity === 0) {
  860. $minusBtn.style.visibility = 'hidden';
  861. } else {
  862. $minusBtn.style.visibility = 'visible';
  863. }
  864. };
  865.  
  866. const $mapButtons = makeElement({
  867. element: 'div',
  868. class: 'js-map-btns',
  869. content: `
  870. <button class="js-map-opacity-add">+</button>
  871. <button class="js-map-opacity-minus">-</button>
  872. <button class="js-map-reset">r</button>
  873. `,
  874. });
  875.  
  876. // Add it right before the map container div
  877. $map.parentNode.insertBefore($mapButtons, $map);
  878.  
  879. const $addBtn = document.querySelector('.js-map-opacity-add');
  880. const $minusBtn = document.querySelector('.js-map-opacity-minus');
  881. const $resetBtn = document.querySelector('.js-map-reset');
  882. // Hide the buttons if map opacity is maxed/minimum
  883. if (state.mapOpacity === 100) {
  884. $addBtn.style.visibility = 'hidden';
  885. }
  886. if (state.mapOpacity === 0) {
  887. $minusBtn.style.visibility = 'hidden';
  888. }
  889.  
  890. // Wire it up
  891. $addBtn.addEventListener('click', clickEvent => {
  892. // Update opacity
  893. state.mapOpacity += 10;
  894. save();
  895. updateMapOpacity();
  896. });
  897.  
  898. $minusBtn.addEventListener('click', clickEvent => {
  899. // Update opacity
  900. state.mapOpacity -= 10;
  901. save();
  902. updateMapOpacity();
  903. });
  904.  
  905. $resetBtn.addEventListener('click', clickEvent => {
  906. state.mapOpacity = 70;
  907. state.mapWidth = '174px';
  908. state.mapHeight = '174px';
  909. save();
  910. updateMapOpacity();
  911. $mapContainer.style.width = state.mapWidth;
  912. $mapContainer.style.height = state.mapHeight;
  913. });
  914.  
  915. updateMapOpacity();
  916. },
  917.  
  918. // The last clicked UI window displays above all other UI windows
  919. // This is useful when, for example, your inventory is near the market window,
  920. // and you want the window and the tooltips to display above the market window.
  921. function selectedWindowIsTop() {
  922. Array.from(document.querySelectorAll('.window:not(.js-is-top-initd)')).forEach($window => {
  923. $window.classList.add('js-is-top-initd');
  924.  
  925. $window.addEventListener('mousedown', () => {
  926. // First, make the other is-top window not is-top
  927. const $otherWindowContainer = document.querySelector('.js-is-top');
  928. if ($otherWindowContainer) {
  929. $otherWindowContainer.classList.remove('js-is-top');
  930. }
  931.  
  932. // Then, make our window's container (the z-index container) is-top
  933. $window.parentNode.classList.add('js-is-top');
  934. });
  935. });
  936. },
  937.  
  938. // The F icon and the UI that appears when you click it
  939. function customFriendsList() {
  940. var friendsIconElement = makeElement({
  941. element: 'div',
  942. class: 'btn border black js-friends-list-icon',
  943. content: 'F',
  944. });
  945. // Add the icon to the right of Elixir icon
  946. const $elixirIcon = document.querySelector('#sysgem');
  947. $elixirIcon.parentNode.insertBefore(friendsIconElement, $elixirIcon.nextSibling);
  948.  
  949. // Create the friends list UI
  950. document.querySelector('.js-friends-list-icon').addEventListener('click', () => {
  951. if (document.querySelector('.js-friends-list')) {
  952. // Don't open the friends list twice.
  953. return;
  954. }
  955. let friendsListHTML = '';
  956. Object.keys(state.friendsList).sort().forEach(friendName => {
  957. friendsListHTML += `
  958. <div data-player-name="${friendName}">${friendName}</div>
  959. <div class="btn blue js-whisper-player" data-player-name="${friendName}">Whisper</div>
  960. <div class="btn blue js-party-player" data-player-name="${friendName}">Party invite</div>
  961. <div class="btn orange js-unfriend-player" data-player-name="${friendName}">X</div>
  962. <input type="text" class="js-friend-note" data-player-name="${friendName}" value="${state.friendNotes[friendName] || ''}"></input>
  963. `;
  964. });
  965.  
  966. const customFriendsWindowHTML = `
  967. <h3 class="textprimary">Friends list</h3>
  968. <div class="uimod-friends">${friendsListHTML}</div>
  969. <p></p>
  970. <div class="btn purp js-close-custom-friends-list">Close</div>
  971. `;
  972.  
  973. const $customFriendsList = makeElement({
  974. element: 'div',
  975. class: 'menu panel-black js-friends-list uimod-custom-window',
  976. content: customFriendsWindowHTML,
  977. });
  978. document.body.appendChild($customFriendsList);
  979.  
  980. // Wire up the buttons
  981. Array.from(document.querySelectorAll('.js-whisper-player')).forEach($button => {
  982. $button.addEventListener('click', clickEvent => {
  983. const name = clickEvent.target.getAttribute('data-player-name');
  984. modHelpers.whisperPlayer(name);
  985. });
  986. });
  987. Array.from(document.querySelectorAll('.js-party-player')).forEach($button => {
  988. $button.addEventListener('click', clickEvent => {
  989. const name = clickEvent.target.getAttribute('data-player-name');
  990. modHelpers.partyPlayer(name);
  991. });
  992. });
  993. Array.from(document.querySelectorAll('.js-unfriend-player')).forEach($button => {
  994. $button.addEventListener('click', clickEvent => {
  995. const name = clickEvent.target.getAttribute('data-player-name');
  996. modHelpers.unfriendPlayer(name);
  997.  
  998. // Remove the blocked player from the list
  999. Array.from(document.querySelectorAll(`.js-friends-list [data-player-name="${name}"]`)).forEach($element => {
  1000. $element.parentNode.removeChild($element);
  1001. });
  1002. });
  1003. });
  1004. Array.from(document.querySelectorAll('.js-friend-note')).forEach($element => {
  1005. $element.addEventListener('change', clickEvent => {
  1006. const name = clickEvent.target.getAttribute('data-player-name');
  1007. state.friendNotes[name] = clickEvent.target.value;
  1008. });
  1009. })
  1010.  
  1011. // The close button for our custom UI
  1012. document.querySelector('.js-close-custom-friends-list').addEventListener('click', () => {
  1013. const $friendsListWindow = document.querySelector('.js-friends-list');
  1014. $friendsListWindow.parentNode.removeChild($friendsListWindow);
  1015. });
  1016. });
  1017. },
  1018.  
  1019. // Custom settings UI, currently just Blocked players
  1020. function customSettings() {
  1021. const $settings = document.querySelector('.divide:not(.js-settings-initd)');
  1022. if (!$settings) {
  1023. return;
  1024. }
  1025.  
  1026. $settings.classList.add('js-settings-initd');
  1027. const $settingsChoiceList = $settings.querySelector('.choice').parentNode;
  1028. $settingsChoiceList.appendChild(makeElement({
  1029. element: 'div',
  1030. class: 'choice js-blocked-players',
  1031. content: 'Blocked players',
  1032. }));
  1033.  
  1034. // Upon click, we display our custom settings window UI
  1035. document.querySelector('.js-blocked-players').addEventListener('click', () => {
  1036. let blockedPlayersHTML = '';
  1037. Object.keys(state.blockList).sort().forEach(blockedName => {
  1038. blockedPlayersHTML += `
  1039. <div data-player-name="${blockedName}">${blockedName}</div>
  1040. <div class="btn orange js-unblock-player" data-player-name="${blockedName}">Unblock player</div>
  1041. `;
  1042. });
  1043.  
  1044. const customSettingsHTML = `
  1045. <h3 class="textprimary">Blocked players</h3>
  1046. <div class="settings uimod-settings">${blockedPlayersHTML}</div>
  1047. <p></p>
  1048. <div class="btn purp js-close-custom-settings">Close</div>
  1049. `;
  1050.  
  1051. const $customSettings = makeElement({
  1052. element: 'div',
  1053. class: 'menu panel-black js-custom-settings uimod-custom-window',
  1054. content: customSettingsHTML,
  1055. });
  1056. document.body.appendChild($customSettings);
  1057.  
  1058. // Wire up all the unblock buttons
  1059. Array.from(document.querySelectorAll('.js-unblock-player')).forEach($button => {
  1060. $button.addEventListener('click', clickEvent => {
  1061. const name = clickEvent.target.getAttribute('data-player-name');
  1062. modHelpers.unblockPlayer(name);
  1063.  
  1064. // Remove the blocked player from the list
  1065. Array.from(document.querySelectorAll(`.js-custom-settings [data-player-name="${name}"]`)).forEach($element => {
  1066. $element.parentNode.removeChild($element);
  1067. });
  1068. });
  1069. });
  1070. // And the close button for our custom UI
  1071. document.querySelector('.js-close-custom-settings').addEventListener('click', () => {
  1072. const $customSettingsWindow = document.querySelector('.js-custom-settings');
  1073. $customSettingsWindow.parentNode.removeChild($customSettingsWindow);
  1074. });
  1075. });
  1076. },
  1077.  
  1078. // This creates the initial chat context menu DOM (which starts as hidden)
  1079. function createChatContextMenu() {
  1080. if (document.querySelector('.js-chat-context-menu')) {
  1081. return;
  1082. }
  1083.  
  1084. let contextMenuHTML = `
  1085. <div class="js-name">...</div>
  1086. <div class="choice" name="party">Party invite</div>
  1087. <div class="choice" name="whisper">Whisper</div>
  1088. <div class="choice" name="friend">Friend</div>
  1089. <div class="choice" name="unfriend">Unfriend</div>
  1090. <div class="choice" name="block">Block</div>
  1091. `
  1092. document.body.appendChild(makeElement({
  1093. element: 'div',
  1094. class: 'panel context border grey js-chat-context-menu',
  1095. content: contextMenuHTML,
  1096. }));
  1097.  
  1098. const $chatContextMenu = document.querySelector('.js-chat-context-menu');
  1099. $chatContextMenu.querySelector('[name="party"]').addEventListener('click', () => {
  1100. modHelpers.partyPlayer(tempState.chatName);
  1101. });
  1102. $chatContextMenu.querySelector('[name="whisper"]').addEventListener('click', () => {
  1103. modHelpers.whisperPlayer(tempState.chatName);
  1104. });
  1105. $chatContextMenu.querySelector('[name="friend"]').addEventListener('click', () => {
  1106. modHelpers.friendPlayer(tempState.chatName);
  1107. });
  1108. $chatContextMenu.querySelector('[name="unfriend"]').addEventListener('click', () => {
  1109. modHelpers.unfriendPlayer(tempState.chatName);
  1110. });
  1111. $chatContextMenu.querySelector('[name="block"]').addEventListener('click', () => {
  1112. modHelpers.blockPlayer(tempState.chatName);
  1113. });
  1114. },
  1115.  
  1116. // This opens a context menu when you click a user's name in chat
  1117. function chatContextMenu() {
  1118. const addContextMenu = ($name, name) => {
  1119. $name.classList.add('js-is-context-menu-initd');
  1120. // Add name to element so we can target it in CSS when filtering chat for block list
  1121. $name.setAttribute('data-chat-name', name);
  1122.  
  1123. const showContextMenu = clickEvent => {
  1124. // TODO: Is there a way to pass the name to showChatContextMenumethod, instead of storing in tempState?
  1125. tempState.chatName = name;
  1126. modHelpers.showChatContextMenu(name, {x: clickEvent.pageX, y: clickEvent.pageY});
  1127. };
  1128. $name.addEventListener('click', showContextMenu); // Left click
  1129. $name.addEventListener('contextmenu', showContextMenu); // Right click works too
  1130. };
  1131. Array.from(document.querySelectorAll('.name:not(.js-is-context-menu-initd)')).forEach(($name) => {
  1132. addContextMenu($name, $name.textContent);
  1133. });
  1134. Array.from(document.querySelectorAll('.textwhisper .textf1:not(.js-is-context-menu-initd)')).forEach($whisperName => {
  1135. // $whisperName's textContent is "to [name]" or "from [name]", so we cut off the first word
  1136. let name = $whisperName.textContent.split(' ');
  1137. name.shift(); // Remove the first word
  1138. name = name.join(' ');
  1139. addContextMenu($whisperName, name);
  1140. });
  1141. },
  1142. ];
  1143.  
  1144. // Add new DOM, load our stored state, wire it up, then continuously rerun specific methods whenever UI changes
  1145. function initialize() {
  1146. // If the Hordes.io tab isn't active for long enough, it reloads the entire page, clearing this mod
  1147. // We check for that and reinitialize the mod if that happens
  1148. const $layout = document.querySelector('.layout');
  1149. if ($layout.classList.contains('uimod-initd')) {
  1150. return;
  1151. }
  1152.  
  1153. modHelpers.addChatMessage(`Hordes UI Mod v${VERSION} by Sakaiyo has been initialized.`);
  1154.  
  1155. $layout.classList.add('uimod-initd')
  1156. load();
  1157. mods.forEach(mod => mod());
  1158.  
  1159. // Continuously re-run specific mods methods that need to be executed on UI change
  1160. const rerunObserver = new MutationObserver(() => {
  1161. // If new window appears, e.g. even if window is closed and reopened, we need to rewire it
  1162. // Fun fact: Some windows always exist in the DOM, even when hidden, e.g. Inventory
  1163. // But some windows only exist in the DOM when open, e.g. Interaction
  1164. const modsToRerun = [
  1165. 'saveDraggedUIWindows',
  1166. 'draggableUIWindows',
  1167. 'loadDraggedUIWindowsPositions',
  1168. 'selectedWindowIsTop',
  1169. 'customSettings',
  1170. ];
  1171. modsToRerun.forEach(modName => {
  1172. mods.find(mod => mod.name === modName)();
  1173. });
  1174. });
  1175. rerunObserver.observe(document.querySelector('.layout > .container'), { attributes: false, childList: true, });
  1176.  
  1177. // Rerun only on chat
  1178. const chatRerunObserver = new MutationObserver(() => {
  1179. mods.find(mod => mod.name === 'chatContextMenu')();
  1180. modHelpers.filterAllChat();
  1181.  
  1182. });
  1183. chatRerunObserver.observe(document.querySelector('#chat'), { attributes: false, childList: true, });
  1184.  
  1185. // Event listeners for document.body might be kept when the game reloads, so don't reinitialize them
  1186. if (!document.body.classList.contains('js-uimod-initd')) {
  1187. document.body.classList.add('js-uimod-initd');
  1188.  
  1189. // Close chat context menu when clicking outside of it
  1190. document.body.addEventListener('click', modHelpers.closeChatContextMenu);
  1191. }
  1192. }
  1193.  
  1194. // Initialize mods once UI DOM has loaded
  1195. const pageObserver = new MutationObserver(() => {
  1196. const isUiLoaded = !!document.querySelector('.layout');
  1197. if (isUiLoaded) {
  1198. initialize();
  1199. }
  1200. });
  1201. pageObserver.observe(document.body, { attributes: true, childList: true })
  1202.  
  1203. // UTIL METHODS
  1204. // Save to in-memory state and localStorage to retain on refresh
  1205. function save(items) {
  1206. localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
  1207. }
  1208.  
  1209. // Load localStorage state if it exists
  1210. // NOTE: If user is trying to load unsupported version of stored state,
  1211. // e.g. they just upgraded to breaking version, then we delete their stored state
  1212. function load() {
  1213. const storedStateJson = localStorage.getItem(STORAGE_STATE_KEY)
  1214. if (storedStateJson) {
  1215. const storedState = JSON.parse(storedStateJson);
  1216. if (storedState.breakingVersion !== BREAKING_VERSION) {
  1217. localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
  1218. return;
  1219. }
  1220. state = {
  1221. ...state,
  1222. ...storedState,
  1223. };
  1224. }
  1225. }
  1226.  
  1227. // Nicer impl to create elements in one method call
  1228. function makeElement(args) {
  1229. const $node = document.createElement(args.element);
  1230. if (args.class) { $node.className = args.class; }
  1231. if (args.content) { $node.innerHTML = args.content; }
  1232. if (args.src) { $node.src = args.src; }
  1233. return $node;
  1234. }
  1235.  
  1236. function enterTextIntoChat(text) {
  1237. // Open chat input
  1238. const enterEvent = new KeyboardEvent('keydown', {
  1239. bubbles: true, cancelable: true, keyCode: 13
  1240. });
  1241. document.body.dispatchEvent(enterEvent);
  1242.  
  1243. // Place text into chat
  1244. const $input = document.querySelector('#chatinput input');
  1245. $input.value = text;
  1246.  
  1247. // Get chat input to recognize slash commands and change the channel
  1248. // by triggering the `input` event.
  1249. // (Did some debugging to figure out the channel only changes when the
  1250. // svelte `input` event listener exists.)
  1251. const inputEvent = new KeyboardEvent('input', {
  1252. bubbles: true, cancelable: true
  1253. })
  1254. $input.dispatchEvent(inputEvent);
  1255. }
  1256.  
  1257. function submitChat() {
  1258. const $input = document.querySelector('#chatinput input');
  1259. const kbEvent = new KeyboardEvent('keydown', {
  1260. bubbles: true, cancelable: true, keyCode: 13
  1261. });
  1262. $input.dispatchEvent(kbEvent);
  1263. }
  1264.  
  1265. // Credit: https://stackoverflow.com/a/14234618 (Has been slightly modified)
  1266. // $draggedElement is the item that will be dragged.
  1267. // $dragTrigger is the element that must be held down to drag $draggedElement
  1268. function dragElement($draggedElement, $dragTrigger) {
  1269. let offset = [0,0];
  1270. let isDown = false;
  1271. $dragTrigger.addEventListener('mousedown', function(e) {
  1272. isDown = true;
  1273. offset = [
  1274. $draggedElement.offsetLeft - e.clientX,
  1275. $draggedElement.offsetTop - e.clientY
  1276. ];
  1277. }, true);
  1278. document.addEventListener('mouseup', function() {
  1279. isDown = false;
  1280. }, true);
  1281.  
  1282. document.addEventListener('mousemove', function(e) {
  1283. event.preventDefault();
  1284. if (isDown) {
  1285. $draggedElement.style.left = (e.clientX + offset[0]) + 'px';
  1286. $draggedElement.style.top = (e.clientY + offset[1]) + 'px';
  1287. }
  1288. }, true);
  1289. }
  1290.  
  1291. // Credit: David Walsh
  1292. function debounce(func, wait, immediate) {
  1293. var timeout;
  1294. return function() {
  1295. var context = this, args = arguments;
  1296. var later = function() {
  1297. timeout = null;
  1298. if (!immediate) func.apply(context, args);
  1299. };
  1300. var callNow = immediate && !timeout;
  1301. clearTimeout(timeout);
  1302. timeout = setTimeout(later, wait);
  1303. if (callNow) func.apply(context, args);
  1304. };
  1305. }
  1306.  
  1307.  
  1308. // Credit: https://gist.github.com/jcxplorer/823878
  1309. // Generate random UUID string
  1310. function uuid() {
  1311. var uuid = "", i, random;
  1312. for (i = 0; i < 32; i++) {
  1313. random = Math.random() * 16 | 0;
  1314. if (i == 8 || i == 12 || i == 16 || i == 20) {
  1315. uuid += "-";
  1316. }
  1317. uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16);
  1318. }
  1319. return uuid;
  1320. }
  1321. })();