events.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. /* global API */// msg.js
  2. /* global changeQueue installed newUI */// manage.js
  3. /* global checkUpdate handleUpdateInstalled */// updater-ui.js
  4. /* global createStyleElement createTargetsElement getFaviconSrc */// render.js
  5. /* global debounce getOwnTab openURL sessionStore */// toolbox.js
  6. /* global filterAndAppend showFiltersStats */// filters.js
  7. /* global sorter */
  8. /* global t */// localization.js
  9. /* global
  10. $
  11. $$
  12. $entry
  13. animateElement
  14. getEventKeyName
  15. messageBoxProxy
  16. scrollElementIntoView
  17. */// dom.js
  18. 'use strict';
  19. const Events = {
  20. addEntryTitle(link) {
  21. const style = link.closest('.entry').styleMeta;
  22. const ucd = style.usercssData;
  23. link.title =
  24. `${t('dateInstalled')}: ${t.formatDate(style.installDate, true) || '—'}\n` +
  25. `${t('dateUpdated')}: ${t.formatDate(style.updateDate, true) || '—'}\n` +
  26. (ucd ? `UserCSS, v.${ucd.version}` : '');
  27. },
  28. check(event, entry) {
  29. checkUpdate(entry, {single: true});
  30. },
  31. async config(event, {styleMeta}) {
  32. await require(['/js/dlg/config-dialog']); /* global configDialog */
  33. configDialog(styleMeta);
  34. },
  35. async delete(event, entry) {
  36. const id = entry.styleId;
  37. animateElement(entry);
  38. const {button} = await messageBoxProxy.show({
  39. title: t('deleteStyleConfirm'),
  40. contents: entry.styleMeta.customName || entry.styleMeta.name,
  41. className: 'danger center',
  42. buttons: [t('confirmDelete'), t('confirmCancel')],
  43. });
  44. if (button === 0) {
  45. API.styles.delete(id);
  46. }
  47. const deleteButton = $('#message-box-buttons > button');
  48. if (deleteButton) deleteButton.removeAttribute('data-focused-via-click');
  49. },
  50. async edit(event, entry) {
  51. if (event.altKey) {
  52. return;
  53. }
  54. event.preventDefault();
  55. event.stopPropagation();
  56. const key = getEventKeyName(event);
  57. const url = $('[href]', entry).href;
  58. const ownTab = await getOwnTab();
  59. if (key === 'MouseL') {
  60. sessionStore['manageStylesHistory' + ownTab.id] = url;
  61. location.href = url;
  62. } else if (chrome.windows && key === 'Shift-MouseL') {
  63. API.openEditor({id: entry.styleId});
  64. } else {
  65. openURL({
  66. url,
  67. index: ownTab.index + 1,
  68. active: key === 'Shift-MouseM' || key === 'Shift-Ctrl-MouseL',
  69. });
  70. }
  71. },
  72. expandTargets(event, entry) {
  73. if (!entry._allTargetsRendered) {
  74. createTargetsElement({entry, expanded: true});
  75. setTimeout(getFaviconSrc, 0, entry);
  76. }
  77. this.closest('.applies-to').classList.toggle('expanded');
  78. },
  79. async external(event) {
  80. // Not handling Shift-click - the built-in 'open in a new window' command
  81. if (getEventKeyName(event) !== 'Shift-MouseL') {
  82. event.preventDefault(); // Prevent FF from double-handling the event
  83. const {index} = await getOwnTab();
  84. openURL({
  85. url: event.target.closest('a').href,
  86. index: index + 1,
  87. active: !event.ctrlKey || event.shiftKey,
  88. });
  89. }
  90. },
  91. entryClicked(event) {
  92. const target = event.target;
  93. const entry = target.closest('.entry');
  94. for (const selector in Events.ENTRY_ROUTES) {
  95. for (let el = target; el && el !== entry; el = el.parentElement) {
  96. if (el.matches(selector)) {
  97. return Events.ENTRY_ROUTES[selector].call(el, event, entry);
  98. }
  99. }
  100. }
  101. },
  102. lazyAddEntryTitle({type, target}) {
  103. const cell = target.closest('h2.style-name, [data-type=age]');
  104. if (cell) {
  105. const link = $('.style-name-link', cell) || cell;
  106. if (type === 'mouseover' && !link.title) {
  107. debounce(Events.addEntryTitle, 50, link);
  108. } else {
  109. debounce.unregister(Events.addEntryTitle);
  110. }
  111. }
  112. },
  113. name(event, entry) {
  114. if (newUI.enabled) Events.edit(event, entry);
  115. },
  116. toggle(event, entry) {
  117. API.styles.toggle(entry.styleId, this.matches('.enable') || this.checked);
  118. },
  119. update(event, entry) {
  120. const json = entry.updatedCode;
  121. json.id = entry.styleId;
  122. (json.usercssData ? API.usercss.install : API.styles.install)(json);
  123. },
  124. };
  125. Events.ENTRY_ROUTES = {
  126. 'input, .enable, .disable': Events.toggle,
  127. '.style-name': Events.name,
  128. '.homepage': Events.external,
  129. '.check-update': Events.check,
  130. '.update': Events.update,
  131. '.delete': Events.delete,
  132. '.applies-to .expander': Events.expandTargets,
  133. '.configure-usercss': Events.config,
  134. };
  135. /* exported handleBulkChange */
  136. function handleBulkChange() {
  137. for (const msg of changeQueue) {
  138. const {id} = msg.style;
  139. if (msg.method === 'styleDeleted') {
  140. handleDelete(id);
  141. changeQueue.time = performance.now();
  142. } else {
  143. handleUpdateForId(id, msg);
  144. }
  145. }
  146. changeQueue.length = 0;
  147. }
  148. function handleDelete(id) {
  149. const node = $entry(id);
  150. if (node) {
  151. node.remove();
  152. if (node.matches('.can-update')) {
  153. const btnApply = $('#apply-all-updates');
  154. btnApply.dataset.value = Number(btnApply.dataset.value) - 1;
  155. }
  156. showFiltersStats();
  157. }
  158. }
  159. function handleUpdate(style, {reason, method} = {}) {
  160. if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
  161. let entry;
  162. let oldEntry = $entry(style);
  163. if (oldEntry && method === 'styleUpdated') {
  164. handleToggledOrCodeOnly();
  165. }
  166. entry = entry || createStyleElement({style});
  167. if (oldEntry) {
  168. if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
  169. installed.replaceChild(entry, oldEntry);
  170. } else {
  171. oldEntry.remove();
  172. }
  173. }
  174. if ((reason === 'update' || reason === 'install') && entry.matches('.updatable')) {
  175. handleUpdateInstalled(entry, reason);
  176. }
  177. filterAndAppend({entry}).then(sorter.update);
  178. if (!entry.matches('.hidden') && reason !== 'import' && reason !== 'sync') {
  179. animateElement(entry);
  180. requestAnimationFrame(() => scrollElementIntoView(entry));
  181. }
  182. getFaviconSrc(entry);
  183. function handleToggledOrCodeOnly() {
  184. style.sections.forEach(s => (s.code = null));
  185. style.sourceCode = null;
  186. const diff = objectDiff(oldEntry.styleMeta, style)
  187. .filter(({key, path}) => path || (!key.startsWith('original') && !key.endsWith('Date')));
  188. if (diff.length === 0) {
  189. // only code was modified
  190. entry = oldEntry;
  191. oldEntry = null;
  192. }
  193. if (diff.length === 1 && diff[0].key === 'enabled') {
  194. oldEntry.classList.toggle('enabled', style.enabled);
  195. oldEntry.classList.toggle('disabled', !style.enabled);
  196. $$('input', oldEntry).forEach(el => (el.checked = style.enabled));
  197. oldEntry.styleMeta = style;
  198. entry = oldEntry;
  199. oldEntry = null;
  200. }
  201. }
  202. }
  203. async function handleUpdateForId(id, opts) {
  204. handleUpdate(await API.styles.get(id), opts);
  205. changeQueue.time = performance.now();
  206. }
  207. /* exported handleVisibilityChange */
  208. function handleVisibilityChange() {
  209. switch (document.visibilityState) {
  210. // page restored without reloading via history navigation (currently only in FF)
  211. // the catch here is that DOM may be outdated so we'll at least refresh the just edited style
  212. // assuming other changes aren't important enough to justify making a complicated DOM sync
  213. case 'visible': {
  214. const id = sessionStore.justEditedStyleId;
  215. if (id) {
  216. handleUpdateForId(Number(id), {method: 'styleUpdated'});
  217. delete sessionStore.justEditedStyleId;
  218. }
  219. break;
  220. }
  221. // going away
  222. case 'hidden':
  223. history.replaceState({scrollY: window.scrollY}, document.title);
  224. break;
  225. }
  226. }
  227. function objectDiff(first, second, path = '') {
  228. const diff = [];
  229. for (const key in first) {
  230. const a = first[key];
  231. const b = second[key];
  232. if (a === b) {
  233. continue;
  234. }
  235. if (b === undefined) {
  236. diff.push({path, key, values: [a], type: 'removed'});
  237. continue;
  238. }
  239. if (a && typeof a.filter === 'function' && b && typeof b.filter === 'function') {
  240. if (
  241. a.length !== b.length ||
  242. a.some((el, i) => {
  243. const result = !el || typeof el !== 'object'
  244. ? el !== b[i]
  245. : objectDiff(el, b[i], path + key + '[' + i + '].').length;
  246. return result;
  247. })
  248. ) {
  249. diff.push({path, key, values: [a, b], type: 'changed'});
  250. }
  251. } else if (a && b && typeof a === 'object' && typeof b === 'object') {
  252. diff.push(...objectDiff(a, b, path + key + '.'));
  253. } else {
  254. diff.push({path, key, values: [a, b], type: 'changed'});
  255. }
  256. }
  257. for (const key in second) {
  258. if (!(key in first)) {
  259. diff.push({path, key, values: [second[key]], type: 'added'});
  260. }
  261. }
  262. return diff;
  263. }