sorter.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /* global $ $create messageBoxProxy onDOMready */// dom.js
  2. /* global installed */// manage.js
  3. /* global prefs */
  4. /* global t */// localization.js
  5. 'use strict';
  6. const sorter = (() => {
  7. const sorterType = {
  8. alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
  9. number: (a, b) => (a || 0) - (b || 0),
  10. };
  11. const tagData = {
  12. title: {
  13. text: t('genericTitle'),
  14. parse: ({name}) => name,
  15. sorter: sorterType.alpha,
  16. },
  17. usercss: {
  18. text: 'Usercss',
  19. parse: ({style}) => style.usercssData ? 0 : 1,
  20. sorter: sorterType.number,
  21. },
  22. disabled: {
  23. text: '', // added as either "enabled" or "disabled" by the addOptions function
  24. parse: ({style}) => style.enabled ? 1 : 0,
  25. sorter: sorterType.number,
  26. },
  27. dateInstalled: {
  28. text: t('dateInstalled'),
  29. parse: ({style}) => style.installDate,
  30. sorter: sorterType.number,
  31. },
  32. dateUpdated: {
  33. text: t('dateUpdated'),
  34. parse: ({style}) => style.updateDate || style.installDate,
  35. sorter: sorterType.number,
  36. },
  37. };
  38. // Adding (assumed) most commonly used ('title,asc' should always be first)
  39. // whitespace before & after the comma is ignored
  40. const selectOptions = [
  41. '{groupAsc}',
  42. 'title,asc',
  43. 'dateInstalled,desc, title,asc',
  44. 'dateInstalled,asc, title,asc',
  45. 'dateUpdated,desc, title,asc',
  46. 'dateUpdated,asc, title,asc',
  47. 'usercss,asc, title,asc',
  48. 'usercss,desc, title,asc',
  49. 'disabled,asc, title,asc',
  50. 'disabled,desc, title,asc',
  51. 'disabled,desc, usercss,asc, title,asc',
  52. '{groupDesc}',
  53. 'title,desc',
  54. 'usercss,asc, title,desc',
  55. 'usercss,desc, title,desc',
  56. 'disabled,desc, title,desc',
  57. 'disabled,desc, usercss,asc, title,desc',
  58. ];
  59. const splitRegex = /\s*,\s*/;
  60. let columns = 1;
  61. onDOMready().then(() => {
  62. prefs.subscribe('manage.newUI.sort', sorter.update);
  63. $('#sorter-help').onclick = showHelp;
  64. addOptions();
  65. updateColumnCount();
  66. });
  67. function addOptions() {
  68. let container;
  69. const select = $('#manage.newUI.sort');
  70. const renderBin = document.createDocumentFragment();
  71. const option = $create('option');
  72. const optgroup = $create('optgroup');
  73. const meta = {
  74. desc: ' \u21E9',
  75. enabled: t('genericEnabledLabel'),
  76. disabled: t('genericDisabledLabel'),
  77. dateNew: ` (${t('sortDateNewestFirst')})`,
  78. dateOld: ` (${t('sortDateOldestFirst')})`,
  79. groupAsc: t('sortLabelTitleAsc'),
  80. groupDesc: t('sortLabelTitleDesc'),
  81. };
  82. selectOptions.forEach(sort => {
  83. if (/{\w+}/.test(sort)) {
  84. if (container) {
  85. renderBin.appendChild(container);
  86. }
  87. container = optgroup.cloneNode();
  88. container.label = meta[sort.substring(1, sort.length - 1)];
  89. return;
  90. }
  91. let lastTag = '';
  92. const opt = option.cloneNode();
  93. opt.textContent = sort.split(splitRegex).reduce((acc, val) => {
  94. if (tagData[val]) {
  95. lastTag = val;
  96. return acc + (acc !== '' ? ' + ' : '') + tagData[val].text;
  97. }
  98. if (lastTag.indexOf('date') > -1) return acc + meta[val === 'desc' ? 'dateNew' : 'dateOld'];
  99. if (lastTag === 'disabled') return acc + meta[val === 'desc' ? 'enabled' : 'disabled'];
  100. return acc + (meta[val] || '');
  101. }, '');
  102. opt.value = sort;
  103. container.appendChild(opt);
  104. });
  105. renderBin.appendChild(container);
  106. select.appendChild(renderBin);
  107. select.value = prefs.get('manage.newUI.sort');
  108. }
  109. return {
  110. sort({styles}) {
  111. const sortBy = prefs.get('manage.newUI.sort').split(splitRegex);
  112. const len = sortBy.length;
  113. return styles.sort((a, b) => {
  114. let types, direction;
  115. let result = 0;
  116. let index = 0;
  117. // multi-sort
  118. while (result === 0 && index < len) {
  119. types = tagData[sortBy[index++]];
  120. direction = sortBy[index++] === 'asc' ? 1 : -1;
  121. result = types.sorter(types.parse(a), types.parse(b)) * direction;
  122. }
  123. return result;
  124. });
  125. },
  126. update() {
  127. if (!installed) return;
  128. const current = [...installed.children];
  129. const sorted = sorter.sort({
  130. styles: current.map(entry => ({
  131. entry,
  132. name: entry.styleNameLowerCase,
  133. style: entry.styleMeta,
  134. })),
  135. });
  136. if (current.some((entry, index) => entry !== sorted[index].entry)) {
  137. const renderBin = document.createDocumentFragment();
  138. sorted.forEach(({entry}) => renderBin.appendChild(entry));
  139. installed.appendChild(renderBin);
  140. }
  141. sorter.updateStripes();
  142. },
  143. updateStripes({onlyWhenColumnsChanged} = {}) {
  144. if (onlyWhenColumnsChanged && !updateColumnCount()) return;
  145. let index = 0;
  146. let isOdd = false;
  147. const flipRows = columns % 2 === 0;
  148. for (const {classList} of installed.children) {
  149. if (classList.contains('hidden')) continue;
  150. classList.toggle('odd', isOdd);
  151. classList.toggle('even', !isOdd);
  152. if (flipRows && ++index >= columns) {
  153. index = 0;
  154. } else {
  155. isOdd = !isOdd;
  156. }
  157. }
  158. },
  159. };
  160. function updateColumnCount() {
  161. let newValue = 1;
  162. for (let el = document.documentElement.lastElementChild;
  163. el.localName === 'style';
  164. el = el.previousElementSibling) {
  165. if (el.textContent.includes('--columns:')) {
  166. newValue = Math.max(1, getComputedStyle(document.documentElement).getPropertyValue('--columns') | 0);
  167. break;
  168. }
  169. }
  170. if (columns !== newValue) {
  171. columns = newValue;
  172. return true;
  173. }
  174. }
  175. async function showHelp(event) {
  176. event.preventDefault();
  177. messageBoxProxy.show({
  178. className: 'help-text center-dialog',
  179. title: t('sortStylesHelpTitle'),
  180. contents:
  181. $create('div',
  182. t('sortStylesHelp').split('\n').map(line =>
  183. $create('p', line))),
  184. buttons: [t('confirmOK')],
  185. });
  186. }
  187. })();