Habr.Features

Всякое-разное для Habr aka habr.com

当前为 2018-09-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Habr.Features
  3. // @version 3.2.23
  4. // @description Всякое-разное для Habr aka habr.com
  5. // @author AngReload
  6. // @include https://habr.com/*
  7. // @include http://habr.com/*
  8. // @namespace habr_comments
  9. // @run-at document-start
  10. // @grant none
  11. // @icon https://habr.com/favicon.ico
  12. // ==/UserScript==
  13. /* global localStorage, MutationObserver */
  14.  
  15. const FLAGS = {};
  16.  
  17. // остановка гифок
  18. // клик по гифке заменит картинку на заглушку
  19. // повторный клик вернет гифку на место
  20. FLAGS.GIF_STOP = true;
  21. // остановить гифки при загрузке страницы
  22. FLAGS.GIF_STOP_ONLOAD = false;
  23. // цвета заглушки
  24. FLAGS.GIF_STOP_COLOR_FG = 'White'; // White
  25. FLAGS.GIF_STOP_COLOR_BG = 'LightGray'; // LightGray or WhiteSmoke
  26. // менять src вместо создания-удаления нод
  27. FLAGS.GIF_STOP_OVERTYPE = true;
  28.  
  29. // показывать счетчики рейтинга в виде:
  30. // рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
  31. FLAGS.RATING_DETAILS = true;
  32. // клик мышкой по рейтингу меняет вид на простой \ детальный
  33. FLAGS.RATING_DETAILS_ONCLICK = false;
  34. // рейтинг = число_плюсов - число_минусов (как альтернатива процентам)
  35. FLAGS.RATING_DETAILS_PN = false;
  36.  
  37. // карма = число_голосовавших * (процент_товарищей - процент_неприятелей)%
  38. FLAGS.KARMA_DETAILS = true;
  39.  
  40. // показывать метки времени в текущем часовом поясе
  41. // абсолютно, либо относительно текущего времени, либо относительно родительского времени
  42. // меняется по клику, в всплывающей подсказке другие виды времени, автообновляется
  43. FLAGS.TIME_DETAILS = true;
  44.  
  45. // добавить возможность сортировки комментариев
  46. FLAGS.COMMENTS_SORT = true;
  47. // сортировать комменты при загрузке страницы или оставить сортировку по времени
  48. FLAGS.COMMENTS_SORT_ONLOAD = true;
  49. // список доступных сортировок
  50. FLAGS.COMMENTS_SORT_BY_FRESHNESS = true;
  51. FLAGS.COMMENTS_SORT_BY_TREND = true; // самая полезная сортировка
  52. FLAGS.COMMENTS_SORT_BY_QUALITY = false;
  53. FLAGS.COMMENTS_SORT_BY_RATING = false;
  54. FLAGS.COMMENTS_SORT_BY_POPULARITY = false;
  55. FLAGS.COMMENTS_SORT_BY_RANDOM = false;
  56.  
  57. // добавить возможность сворачивать комментарии
  58. FLAGS.COMMENTS_HIDE = true;
  59. // свернуть комментарии если их глубина вложенности равна некому числу
  60. FLAGS.HIDE_LEVEL = 4;
  61. // сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
  62. FLAGS.LINE_LEN = 8;
  63. // отбивать каждый следущий уровень вложенности дополнительным отступом
  64. FLAGS.REDUCE_NEW_LINES = true;
  65. // глубина вложенности при «возврате каретки» комментариев обозначается разным цветом
  66. FLAGS.RAINBOW_NEW_LINE = true;
  67.  
  68. // заменить ссылки ведущие к новым комментариям на дерево комментариев
  69. FLAGS.COMMENTS_LINKS = true;
  70.  
  71. // запоминание галки «Использовать MarkDown» для комментариев
  72. FLAGS.COMMENTS_MD = true;
  73.  
  74. // в огнелисе, в отличии о хрома, при загрузке изображений не сохраняется прокрутка
  75. // меня раздражает когда дерево комментариев скачками пытается уехать вниз
  76. // эта опция зафиксирует высоту публикации пока та находится вне обзора
  77. // надо переделать
  78. FLAGS.FIX_JUMPING_SCROLL = false;
  79.  
  80. // линии вдоль прокрутки для отображения размеров поста и комментариев
  81. FLAGS.SCROLL_LEGEND = true;
  82.  
  83. // добавляет кнопку активации ночного режима
  84. FLAGS.NIGHT_MODE = true;
  85.  
  86. // добавляет диалок настроек
  87. FLAGS.CONFIG_INTERFACE = true;
  88.  
  89. // включить разные стили
  90. FLAGS.USERSTYLE = true;
  91. // показывать гифки только при наведении
  92. FLAGS.USERSTYLE_GIF_HOVER = false;
  93. // лента постов с увеличенными отступами, нижний бар выровнен вправо
  94. FLAGS.USERSTYLE_FEED_DISTANCED = true;
  95. // мелкий фикс для отступа в счетчике ленты
  96. FLAGS.USERSTYLE_COUNTER_NEW_FIX_SPACE = true;
  97. // удаляет правую колонку, там где не жалко
  98. FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT = true;
  99. // аватарки без скруглений
  100. FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true;
  101. // большие аватарки в профиле и карточках
  102. FLAGS.USERSTYLE_USERINFO_BIG_AVATARS = true;
  103. // ограничить размер картинок в комментах
  104. FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE = 0;
  105. // нормальные стили для комментариев
  106. FLAGS.USERSTYLE_COMMENTS_FIX = true;
  107. // нормальные стили для кода
  108. FLAGS.USERSTYLE_CODE_FIX = true;
  109. // окантовка границ спойлеров
  110. FLAGS.USERSTYLE_SPOILER_BORDERS = true;
  111. // делает глючные плавающие блоки статичными
  112. FLAGS.USERSTYLE_STATIC_STICKY = true;
  113. // показывать язык подсветки блоков кода
  114. FLAGS.USERSTYLE_HLJS_LANG = true;
  115. // показывать язык кода только при наведении
  116. FLAGS.USERSTYLE_HLJS_LANG_HOVER = false;
  117. // свой шрифт для блоков кода
  118. FLAGS.USERSTYLE_CODE_FONT = 'PT Mono';
  119. // размер табов в коде
  120. FLAGS.USERSTYLE_CODE_TABSIZE = 2;
  121. // для ночного режима
  122. FLAGS.USERSTYLE_CODE_NIGHT = true;
  123. // для настроек
  124. FLAGS.USERSTYLE_CONFIG_INTERFACE = true;
  125.  
  126. const configOptions = [
  127. ['KARMA_DETAILS', 'карма с плюсами и минусами'],
  128. ['RATING_DETAILS', 'рейтинг с подробностями о плюсах и минусах'],
  129. ['RATING_DETAILS_PN', 'рейтинг в абсолютных еденицах вместо процентов'],
  130. ['TIME_DETAILS', 'отметки о времени с подробностями'],
  131. ['GIF_STOP', 'остановка гифок'],
  132. ['GIF_STOP_ONLOAD', 'остановить гифки при загрузке'],
  133. ['COMMENTS_SORT', 'сортировка комментов'],
  134. ['COMMENTS_SORT_ONLOAD', 'сортировать комменты при загрузке'],
  135. ['COMMENTS_SORT_BY_FRESHNESS', 'сортировка по свежести'],
  136. ['COMMENTS_SORT_BY_TREND', 'сортировка по трендам'],
  137. ['COMMENTS_SORT_BY_QUALITY', 'сортировка по качеству'],
  138. ['COMMENTS_SORT_BY_RATING', 'сортировка по рейтингу'],
  139. ['COMMENTS_SORT_BY_POPULARITY', 'сортировка по популярности'],
  140. ['COMMENTS_SORT_BY_RANDOM', 'перемешивание комментариев'],
  141. ['COMMENTS_HIDE', 'сворачивание комментов'],
  142. ['REDUCE_NEW_LINES', 'возврат каретки с уменьшением ширины'],
  143. ['RAINBOW_NEW_LINE', 'возврат каретки комментов в разных цветах'],
  144. ['COMMENTS_LINKS', 'ссылка на корень комментов вместо новых'],
  145. ['COMMENTS_MD', 'запоминать галку MarkDown'],
  146. ['FIX_JUMPING_SCROLL', 'заморозить высоту статьи при загрузке'],
  147. ['SCROLL_LEGEND', 'ленгенда страницы у скроллбара'],
  148. ['NIGHT_MODE', 'ночной режим'],
  149. ['USERSTYLE', 'стилизация'],
  150. ['USERSTYLE_COMMENTS_FIX', 'стили комментов'],
  151. ['USERSTYLE_HLJS_LANG', 'показать язык для блоков кода'],
  152. ['USERSTYLE_HLJS_LANG_HOVER', 'язык кода скрыт до наведении курсора'],
  153. ['USERSTYLE_GIF_HOVER', 'гифки скрыты до наведения курсора'],
  154. ['USERSTYLE_STATIC_STICKY', 'зафиксировать плавающие блоки'],
  155. ['USERSTYLE_SPOILER_BORDERS', 'видимые границы спойлеров'],
  156. ['USERSTYLE_FEED_DISTANCED', 'свободная лента постов'],
  157. ['USERSTYLE_REMOVE_SIDEBAR_RIGHT', 'удалить правую колонку'],
  158. ['USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS', 'квадратные аватарки'],
  159. ['USERSTYLE_USERINFO_BIG_AVATARS', 'большие аватарки в профилях'],
  160. ];
  161.  
  162. // сохраняяем умолчания для панели настроек
  163. if (!localStorage.getItem('habrafixFlags')) {
  164. localStorage.setItem('habrafixFlags', JSON.stringify(FLAGS));
  165. } else {
  166. const jsonString = localStorage.getItem('habrafixFlags');
  167. const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
  168. const loadedKeys = Object.keys(loadedConfig);
  169. Object.keys(FLAGS).forEach((key) => {
  170. if (
  171. loadedKeys.includes(key) &&
  172. configOptions.find(arr => arr[0] === key)
  173. ) {
  174. FLAGS[key] = loadedConfig[key];
  175. }
  176. });
  177. }
  178.  
  179. // интерфейс для хранения настроек
  180. const userConfig = {
  181. // имя записи в localsorage
  182. key: 'habrafix',
  183. // модель настроек: ключ - возможные значения
  184. model: {
  185. time_publications: ['fromNow', 'absolute'],
  186. time_comments: ['fromParent', 'fromNow', 'absolute'],
  187. comments_order: ['trend', 'time'],
  188. scores_details: [true, false],
  189. comment_markdown: [false, true],
  190. night_mode: [false, true],
  191. },
  192. config: {},
  193. // при старте для конфига берем сохраненные параметры либо по умолчанию
  194. init() {
  195. let jsonString = localStorage.getItem(userConfig.key);
  196. const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
  197. const loadedKeys = Object.keys(loadedConfig);
  198. const config = {};
  199. Object.keys(userConfig.model).forEach((key) => {
  200. const exist = loadedKeys.indexOf(key) >= 0;
  201. config[key] = exist ? loadedConfig[key] : userConfig.model[key][0];
  202. });
  203. jsonString = JSON.stringify(config);
  204. localStorage.setItem(userConfig.key, jsonString);
  205. userConfig.config = config;
  206. },
  207. getItem(key) {
  208. const jsonString = localStorage.getItem(userConfig.key);
  209. const config = JSON.parse(jsonString);
  210. return config[key];
  211. // return userConfig.config[key];
  212. },
  213. setItem(key, value) {
  214. let jsonString = localStorage.getItem(userConfig.key);
  215. const config = JSON.parse(jsonString);
  216. config[key] = value;
  217. jsonString = JSON.stringify(config);
  218. localStorage.setItem(userConfig.key, jsonString);
  219. userConfig.config = config;
  220. },
  221. // каруселит параметр по значения модели
  222. shiftItem(key) {
  223. const currentValue = userConfig.getItem(key);
  224. const availableValues = userConfig.model[key];
  225. const currentIdx = availableValues.indexOf(currentValue);
  226. const nextIdx = (currentIdx + 1) % availableValues.length;
  227. const nextValue = availableValues[nextIdx];
  228. userConfig.setItem(key, nextValue);
  229. return nextValue;
  230. },
  231. };
  232. userConfig.init();
  233.  
  234. // свои стили
  235. const userStyleEl = document.createElement('style');
  236. let userStyle = '';
  237.  
  238. if (FLAGS.USERSTYLE_GIF_HOVER) {
  239. userStyle += `
  240. .post__text img[src$=".gif"],
  241. .comment__message img[src$=".gif"] {
  242. filter: contrast(0) brightness(0.33) !important;
  243. }
  244. .post__text img[src$=".gif"]:hover,
  245. .comment__message img[src$=".gif"]:hover {
  246. filter: none !important;
  247. }
  248. `;
  249. }
  250.  
  251. if (FLAGS.SCROLL_LEGEND) {
  252. userStyle += `
  253. .legend_el {
  254. position: fixed;
  255. width: 4px;
  256. right: 0;
  257. transition: top 1s ease-out, height 1s ease-out;
  258. z-index: 101;
  259. }
  260.  
  261. #xpanel {
  262. right: 4px;
  263. }
  264. `;
  265. }
  266.  
  267. if (FLAGS.USERSTYLE_FEED_DISTANCED) {
  268. userStyle += `
  269. .post__body_crop {
  270. text-align: right;
  271. }
  272.  
  273. .post__body_crop .post__text {
  274. text-align: left;
  275. }
  276.  
  277. .post__footer {
  278. text-align: right;
  279. }
  280.  
  281. .posts_list .content-list__item_post {
  282. padding: 40px 0;
  283. }
  284. `;
  285. }
  286.  
  287. if (FLAGS.USERSTYLE_COUNTER_NEW_FIX_SPACE) {
  288. userStyle += `
  289. .toggle-menu__item-counter_new {
  290. margin-left: 4px;
  291. }
  292. `;
  293. }
  294.  
  295. if (FLAGS.USERSTYLE_REMOVE_SIDEBAR_RIGHT) {
  296. // remove for
  297. // https://habr.com/post/352896/
  298. // https://habr.com/sandbox/
  299. // https://habr.com/sandbox/115216/
  300. // https://habr.com/users/saggid/posts/
  301. // https://habr.com/users/saggid/comments/
  302. // https://habr.com/users/saggid/favorites/
  303. // https://habr.com/users/saggid/favorites/posts/
  304. // https://habr.com/users/saggid/favorites/comments/
  305. // https://habr.com/company/pvs-studio/blog/353640/
  306. // https://habr.com/company/pvs-studio/blog/
  307. // https://habr.com/company/pvs-studio/blog/top/
  308. // https://habr.com/company/pvs-studio/
  309. // https://habr.com/feed/
  310. // https://habr.com/top/
  311. // https://habr.com/top/yearly/
  312. // https://habr.com/all/
  313. // https://habr.com/all/top10/
  314.  
  315. // display for
  316. // https://habr.com/company/pvs-studio/profile/
  317. // https://habr.com/company/pvs-studio/vacancies/
  318. // https://habr.com/company/pvs-studio/fans/all/rating/
  319. // https://habr.com/company/pvs-studio/workers/new/rating/
  320. // https://habr.com/feed/settings/
  321. // https://habr.com/users/
  322. // https://habr.com/hubs/
  323. // https://habr.com/hubs/admin/
  324. // https://habr.com/companies/
  325. // https://habr.com/companies/category/software/
  326. // https://habr.com/companies/new/
  327. // https://habr.com/flows/design/
  328.  
  329. const path = window.location.pathname;
  330. const isPost = /^\/post\/\d+\/$/.test(path);
  331. const isSandbox = /^\/sandbox\//.test(path);
  332. const isUserPosts = /^\/users\/[^/]+\/posts\//.test(path);
  333. const isUserComments = /^\/users\/[^/]+\/comments\//.test(path);
  334. const isUserFavorites = /^\/users\/[^/]+\/favorites\//.test(path);
  335. const isCompanyBlog = /^\/company\/[^/]+\/blog\//.test(path);
  336. const isCompanyBlog2 = /^\/company\/[^/]+\/(page\d+\/)?$/.test(path);
  337. const isFeed = /^\/feed\//.test(path);
  338. const isHome = /^\/$/.test(path);
  339. const isTop = /^\/top\//.test(path);
  340. const isAll = /^\/all\//.test(path);
  341.  
  342. if (
  343. isPost || isSandbox ||
  344. isUserPosts || isUserComments || isUserFavorites ||
  345. isCompanyBlog || isCompanyBlog2 ||
  346. isFeed || isHome || isTop || isAll
  347. ) {
  348. userStyle += `
  349. .sidebar_right {
  350. display: none;
  351. }
  352.  
  353. .content_left {
  354. padding-right: 0;
  355. }
  356.  
  357. .comment_plain {
  358. max-width: 860px;
  359. }
  360. `;
  361. }
  362. }
  363.  
  364. if (FLAGS.USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
  365. userStyle += `
  366. .user-info__image-pic,
  367. .user-pic_popover,
  368. .media-obj__image-pic {
  369. border-radius: 0;
  370. }
  371. `;
  372. }
  373.  
  374. if (FLAGS.USERSTYLE_USERINFO_BIG_AVATARS) {
  375. userStyle += `
  376. .page-header {
  377. height: auto;
  378. }
  379.  
  380. .media-obj__image-pic_hub,
  381. .media-obj__image-pic_user,
  382. .media-obj__image-pic_company {
  383. width: auto;
  384. height: auto;
  385. }
  386. `;
  387. }
  388.  
  389. if (FLAGS.USERSTYLE_COMMENTS_FIX) {
  390. userStyle += `
  391. .comments_order {
  392. color: #333;
  393. font-size: 14px;
  394. font-family: "-apple-system",BlinkMacSystemFont,Arial,sans-serif;
  395. text-rendering: optimizeLegibility;
  396. border-bottom: 1px solid #e3e3e3;
  397. padding: 8px;
  398. text-align: right;
  399. }
  400.  
  401. .comments_order a {
  402. color: #548eaa;
  403. font-style: normal;
  404. text-decoration: none;
  405. }
  406.  
  407. .comments_order a:hover {
  408. color: #487284;
  409. }
  410.  
  411. .content-list_comments {
  412. overflow: visible;
  413. }
  414.  
  415. .comment__folding-dotholder {
  416. display: none !important;
  417. }
  418.  
  419. .content-list_nested-comments {
  420. border-left: 1px solid #e3e3e3;
  421. margin: 0;
  422. padding-top: 20px;
  423. padding-left: 20px !important;
  424. }
  425.  
  426. .content-list_comments {
  427. /*border-left: 1px solid silver;*/
  428. margin: 0;
  429. padding-left: 0;
  430. padding-top: 20px;
  431. /*background: #FCE4EC;*/
  432. }
  433.  
  434. #comments-list .js-form_placeholder {
  435. border-left: 1px solid #e3e3e3;
  436. padding-left: 20px;
  437. }
  438.  
  439. .comments_new-line {
  440. border-left: 1px solid #777;
  441. border-bottom: 1px solid #777;
  442. border-top: 1px solid #777;
  443. margin-left: -${FLAGS.LINE_LEN * 21}px !important;
  444. background: white;
  445. padding-bottom: 4px;
  446. }
  447.  
  448. /* .comment__head_topic-author.comment__head_new-comment */
  449. .comment__head_topic-author .user-info {
  450. text-decoration: underline;
  451. }
  452. `;
  453. }
  454.  
  455. if (FLAGS.RAINBOW_NEW_LINE) {
  456. userStyle += `
  457. .comments_new-line-1 {
  458. border-color: #0caefb;
  459. }
  460.  
  461. .comments_new-line-2 {
  462. border-color: #06feb7;
  463. }
  464.  
  465. .comments_new-line-3 {
  466. border-color: #fbcb02;
  467. }
  468.  
  469. .comments_new-line-0 {
  470. border-color: #fb0543;
  471. }
  472. `;
  473. }
  474.  
  475. if (FLAGS.REDUCE_NEW_LINES) {
  476. userStyle += `
  477. .comments_new-line .comments_new-line {
  478. margin-left: -${(FLAGS.LINE_LEN - 1) * 21}px !important;
  479. }
  480. `;
  481. }
  482.  
  483. if (FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE) {
  484. userStyle += `
  485. .comment__message img {
  486. max-height: ${FLAGS.USERSTYLE_COMMENTS_IMG_MAXSIZE}px;
  487. }
  488.  
  489. .comment__message .spoiler .img {
  490. max-height: auto;
  491. }
  492. `;
  493. }
  494.  
  495. if (FLAGS.USERSTYLE_CODE_FIX) {
  496. let addFont = '';
  497. if (FLAGS.USERSTYLE_CODE_FONT) {
  498. addFont = FLAGS.USERSTYLE_CODE_FONT;
  499. if (addFont.indexOf(' ') >= 0) {
  500. addFont = `"${addFont}"`;
  501. }
  502. addFont += ',';
  503. }
  504.  
  505. const tabSize = FLAGS.USERSTYLE_CODE_TABSIZE || 4;
  506.  
  507. userStyle += `
  508. .editor .text-holder textarea,
  509. .tm-editor__textarea {
  510. font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace;
  511. }
  512.  
  513. code {
  514. font-family: ${addFont} 'Ubuntu Mono', Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace !important;;
  515. -o-tab-size: ${tabSize};
  516. -moz-tab-size: ${tabSize};
  517. tab-size: ${tabSize};
  518. background: #f7f7f7;
  519. border-radius: 3px;
  520. color: #505c66;
  521. display: inline-block;
  522. font-weight: 500;
  523. line-height: 1.29;
  524. padding: 5px 9px;
  525. vertical-align: 1px;
  526. }
  527. `;
  528. }
  529.  
  530. if (FLAGS.USERSTYLE_SPOILER_BORDERS) {
  531. userStyle += `
  532. .spoiler .spoiler_text {
  533. border: 1px dashed rgb(12, 174, 251);
  534. }
  535. `;
  536. }
  537.  
  538. if (FLAGS.USERSTYLE_STATIC_STICKY) {
  539. userStyle += `
  540. .wrapper-sticky,
  541. .js-ad_sticky,
  542. .js-ad_sticky_comments {
  543. position: static !important;
  544. }
  545. `;
  546. }
  547.  
  548. if (FLAGS.USERSTYLE_HLJS_LANG) {
  549. let hover = '';
  550. if (FLAGS.USERSTYLE_HLJS_LANG_HOVER) hover = ':hover';
  551. userStyle += `
  552. pre {
  553. position: relative;
  554. }
  555.  
  556. .hljs${hover}::after {
  557. position: absolute;
  558. font-size: 12px;
  559. content: 'code';
  560. right: 0;
  561. top: 0;
  562. padding: 1px 5px 0 4px;
  563. /*border-bottom: 1px solid #e5e8ec;
  564. border-left: 1px solid #e5e8ec;
  565. border-bottom-left-radius: 3px;
  566. color: #505c66;*/
  567. opacity: .5;
  568. }
  569. `;
  570. userStyle += [
  571. ['1c', '1C:Enterprise (v7, v8)'],
  572. ['abnf', 'Augmented Backus-Naur Form'],
  573. ['accesslog', 'Access log'],
  574. ['actionscript', 'ActionScript'],
  575. ['ada', 'Ada'],
  576. ['apache', 'Apache'],
  577. ['applescript', 'AppleScript'],
  578. ['arduino', 'Arduino'],
  579. ['armasm', 'ARM Assembly'],
  580. ['asciidoc', 'AsciiDoc'],
  581. ['aspectj', 'AspectJ'],
  582. ['autohotkey', 'AutoHotkey'],
  583. ['autoit', 'AutoIt'],
  584. ['avrasm', 'AVR Assembler'],
  585. ['awk', 'Awk'],
  586. ['axapta', 'Axapta'],
  587. ['bash', 'Bash'],
  588. ['basic', 'Basic'],
  589. ['bnf', 'Backus–Naur Form'],
  590. ['brainfuck', 'Brainfuck'],
  591. ['cal', 'C/AL'],
  592. ['capnproto', 'Cap’n Proto'],
  593. ['ceylon', 'Ceylon'],
  594. ['clean', 'Clean'],
  595. ['clojure-repl', 'Clojure REPL'],
  596. ['clojure', 'Clojure'],
  597. ['cmake', 'CMake'],
  598. ['coffeescript', 'CoffeeScript'],
  599. ['coq', 'Coq'],
  600. ['cos', 'Caché Object Script'],
  601. ['cpp', 'C++'],
  602. ['crmsh', 'crmsh'],
  603. ['crystal', 'Crystal'],
  604. ['cs', 'C#'],
  605. ['csp', 'CSP'],
  606. ['css', 'CSS'],
  607. ['d', 'D'],
  608. ['dart', 'Dart'],
  609. ['delphi', 'Delphi'],
  610. ['diff', 'Diff'],
  611. ['django', 'Django'],
  612. ['dns', 'DNS Zone file'],
  613. ['dockerfile', 'Dockerfile'],
  614. ['dos', 'DOS .bat'],
  615. ['dsconfig', 'dsconfig'],
  616. ['dts', 'Device Tree'],
  617. ['dust', 'Dust'],
  618. ['ebnf', 'Extended Backus-Naur Form'],
  619. ['elixir', 'Elixir'],
  620. ['elm', 'Elm'],
  621. ['erb', 'ERB (Embedded Ruby)'],
  622. ['erlang-repl', 'Erlang REPL'],
  623. ['erlang', 'Erlang'],
  624. ['excel', 'Excel'],
  625. ['fix', 'FIX'],
  626. ['flix', 'Flix'],
  627. ['fortran', 'Fortran'],
  628. ['fsharp', 'F#'],
  629. ['gams', 'GAMS'],
  630. ['gauss', 'GAUSS'],
  631. ['gcode', 'G-code (ISO 6983)'],
  632. ['gherkin', 'Gherkin'],
  633. ['glsl', 'GLSL'],
  634. ['go', 'Go'],
  635. ['golo', 'Golo'],
  636. ['gradle', 'Gradle'],
  637. ['groovy', 'Groovy'],
  638. ['haml', 'Haml'],
  639. ['handlebars', 'Handlebars'],
  640. ['haskell', 'Haskell'],
  641. ['haxe', 'Haxe'],
  642. ['hsp', 'HSP'],
  643. ['htmlbars', 'HTMLBars'],
  644. ['http', 'HTTP'],
  645. ['hy', 'Hy'],
  646. ['inform7', 'Inform 7'],
  647. ['ini', 'Ini'],
  648. ['irpf90', 'IRPF90'],
  649. ['java', 'Java'],
  650. ['javascript', 'JavaScript'],
  651. ['jboss-cli', 'jboss-cli'],
  652. ['json', 'JSON'],
  653. ['julia-repl', 'Julia REPL'],
  654. ['julia', 'Julia'],
  655. ['kotlin', 'Kotlin'],
  656. ['lasso', 'Lasso'],
  657. ['ldif', 'LDIF'],
  658. ['leaf', 'Leaf'],
  659. ['less', 'Less'],
  660. ['lisp', 'Lisp'],
  661. ['livecodeserver', 'LiveCode'],
  662. ['livescript', 'LiveScript'],
  663. ['llvm', 'LLVM IR'],
  664. ['lsl', 'Linden Scripting Language'],
  665. ['lua', 'Lua'],
  666. ['makefile', 'Makefile'],
  667. ['markdown', 'Markdown'],
  668. ['mathematica', 'Mathematica'],
  669. ['matlab', 'Matlab'],
  670. ['maxima', 'Maxima'],
  671. ['mel', 'MEL'],
  672. ['mercury', 'Mercury'],
  673. ['mipsasm', 'MIPS Assembly'],
  674. ['mizar', 'Mizar'],
  675. ['mojolicious', 'Mojolicious'],
  676. ['monkey', 'Monkey'],
  677. ['moonscript', 'MoonScript'],
  678. ['n1ql', 'N1QL'],
  679. ['nginx', 'Nginx'],
  680. ['nimrod', 'Nimrod'],
  681. ['nix', 'Nix'],
  682. ['nsis', 'NSIS'],
  683. ['objectivec', 'Objective-C'],
  684. ['ocaml', 'OCaml'],
  685. ['openscad', 'OpenSCAD'],
  686. ['oxygene', 'Oxygene'],
  687. ['parser3', 'Parser3'],
  688. ['perl', 'Perl'],
  689. ['pf', 'pf'],
  690. ['php', 'PHP'],
  691. ['pony', 'Pony'],
  692. ['powershell', 'PowerShell'],
  693. ['processing', 'Processing'],
  694. ['profile', 'Python profile'],
  695. ['prolog', 'Prolog'],
  696. ['protobuf', 'Protocol Buffers'],
  697. ['puppet', 'Puppet'],
  698. ['purebasic', 'PureBASIC'],
  699. ['python', 'Python'],
  700. ['q', 'Q'],
  701. ['qml', 'QML'],
  702. ['r', 'R'],
  703. ['rib', 'RenderMan RIB'],
  704. ['roboconf', 'Roboconf'],
  705. ['routeros', 'Microtik RouterOS script'],
  706. ['rsl', 'RenderMan RSL'],
  707. ['ruby', 'Ruby'],
  708. ['ruleslanguage', 'Oracle Rules Language'],
  709. ['rust', 'Rust'],
  710. ['scala', 'Scala'],
  711. ['scheme', 'Scheme'],
  712. ['scilab', 'Scilab'],
  713. ['scss', 'SCSS'],
  714. ['shell', 'Shell Session'],
  715. ['smali', 'Smali'],
  716. ['smalltalk', 'Smalltalk'],
  717. ['sml', 'SML'],
  718. ['sqf', 'SQF'],
  719. ['sql', 'SQL'],
  720. ['stan', 'Stan'],
  721. ['stata', 'Stata'],
  722. ['step21', 'STEP Part 21'],
  723. ['stylus', 'Stylus'],
  724. ['subunit', 'SubUnit'],
  725. ['swift', 'Swift'],
  726. ['taggerscript', 'Tagger Script'],
  727. ['tap', 'Test Anything Protocol'],
  728. ['tcl', 'Tcl'],
  729. ['tex', 'TeX'],
  730. ['thrift', 'Thrift'],
  731. ['tp', 'TP'],
  732. ['twig', 'Twig'],
  733. ['typescript', 'TypeScript'],
  734. ['vala', 'Vala'],
  735. ['vbnet', 'VB.NET'],
  736. ['vbscript-html', 'VBScript in HTML'],
  737. ['vbscript', 'VBScript'],
  738. ['verilog', 'Verilog'],
  739. ['vhdl', 'VHDL'],
  740. ['vim', 'Vim Script'],
  741. ['x86asm', 'Intel x86 Assembly'],
  742. ['xl', 'XL'],
  743. ['xml', 'HTML, XML'],
  744. ['xquery', 'XQuery'],
  745. ['yaml', 'YAML'],
  746. ['zephir', 'Zephir'],
  747. ].map(([langTag, langName]) => `.hljs.${langTag}${hover}::after{content:'${langName} [${langTag}]'}`).join('');
  748. }
  749.  
  750. if (FLAGS.USERSTYLE_CODE_NIGHT) {
  751. userStyle += `
  752. .night_mode_switcher {
  753. box-sizing: border-box;
  754. position: fixed;
  755. width: 32px;
  756. height: 32px;
  757. right: 32px;
  758. bottom: 32px;
  759. z-index: 101;
  760. background-color: transparent;
  761. border-radius: 50%;
  762. border: 4px solid #aaa;
  763. border-right-width: 16px;
  764. transition: border-color 0.1s ease-out;
  765. }
  766.  
  767. .night_mode_switcher:hover {
  768. border-color: #333;
  769. }
  770.  
  771. .night .night_mode_switcher {
  772. border-color: #515151;
  773. }
  774.  
  775. .night .night_mode_switcher:hover {
  776. border-color: #9e9e9e;
  777. }
  778.  
  779.  
  780. /* bg */
  781. .night .sidebar-block__suggest,
  782. .night .dropdown-container,
  783. .night .poll-result__bar,
  784. .night .comments_new-line,
  785. .night .tm-editor__textarea,
  786. .night .layout,
  787. .night .toggle-menu__most-read,
  788. .night .toggle-menu_most-comments {
  789. background: #171c20;
  790. }
  791.  
  792. /* text */
  793. .night .sidebar-block__suggest,
  794. .night .user-message__body,
  795. .night .promo-block__title_total,
  796. .night .beta-anounce__text,
  797. .night .defination-list__label,
  798. .night .defination-list__value,
  799. .night .search-field__select,
  800. .night .search-field__input[type="text"],
  801. .night .search-form__field,
  802. .night .post-info__title,
  803. .night .dropdown__user-stats,
  804. .night .dropdown-container_white .user-info__special,
  805. .night .n-dropdown-menu__item-link,
  806. .night body,
  807. .night .default-block__polling-title,
  808. .night .poll-result__data-label,
  809. .night code,
  810. .night .user-info__fullname,
  811. .night .user-info__specialization,
  812. .night .page-header__info-title,
  813. .night .page-header__info-desc,
  814. .night .post__title-text,
  815. .night .post__title_link,
  816. .night .checkbox__label,
  817. .night .radio__label,
  818. .night .tm-editor__textarea,
  819. .night .footer-block__title,
  820. .night #TMpanel .container .bmenu > a.current,
  821. .night .post__text-html,
  822. .night .comment__message,
  823. .night .comment-form__preview {
  824. color: #9e9e9e;
  825. }
  826.  
  827. .night .n-dropdown-menu__item-link:hover {
  828. color: white;
  829. }
  830.  
  831. /* top lvl bg */
  832. .night .checkbox__label::before,
  833. .night .radio__label::before,
  834. .night .content-list__item_conversation:hover,
  835. .night .search-field__select,
  836. .night .search-field__input[type="text"],
  837. .night .search-form__field,
  838. .night .dropdown-container,
  839. .night .n-dropdown-menu,
  840. .night .post__translatation,
  841. .night code,
  842. .night .megapost-teasers,
  843. .night .tm-editor_comments,
  844. .night .promo-block__header,
  845. .night .post__text-html blockquote,
  846. .night .default-block,
  847. .night .post-share,
  848. .night .company-info__author,
  849. .night .layout__row_footer-links {
  850. background: #22272B;
  851. }
  852.  
  853. /* not important bg */
  854. .night .btn_blue.disabled,
  855. .night .btn_blue[disabled],
  856. .night .tracker_page table.tracker_folowers tr.new,
  857. .night .dropdown__user-stats,
  858. .night .comment__head_topic-author,
  859. .night .promo-item:hover,
  860. .night .layout__row_navbar,
  861. .night .layout__row_footer,
  862. .night #TMpanel {
  863. background: #1f2327;
  864. }
  865.  
  866. /* borders */
  867. .night #comments-list .js-form_placeholder,
  868. .night .sidebar-block__suggest,
  869. .night .content-list_preview-message,
  870. .night .btn_outline_blue[disabled],
  871. .night .user-message__body_html pre code,
  872. .night .content-list_user-dialog,
  873. .night .wysiwyg-toolbar,
  874. .night .content-list__item_bordered,
  875. .night .promo-block__total,
  876. .night .search-field__select,
  877. .night .search-field__input[type="text"],
  878. .night .search-form__field,
  879. .night .tracker_page table.tracker_folowers tr td,
  880. .night .tracker_page table.tracker_folowers tr th,
  881. .night .stacked-menu__item_devided,
  882. .night .post__text-html table,
  883. .night .post__text-html table td,
  884. .night .post__text-html table th,
  885. .night .n-dropdown-menu__item_border,
  886. .night .dropdown-container,
  887. .night .default-block_bordered,
  888. .night .default_block_polling,
  889. .night .column-wrapper_tabs .sidebar_right,
  890. .night .post__type-label,
  891. .night .promo-block__header,
  892. .night .user-info__contacts,
  893. .night .comment__message pre code,
  894. .night .comment-form__preview pre code,
  895. .night .sandbox-panel,
  896. .night .comment__post-title,
  897. .night .tm-editor__textarea,
  898. .night .promo-block__footer,
  899. .night .author-panel,
  900. .night .promo-block,
  901. .night .post__text-html pre code,
  902. .night .footer-block__title,
  903. .night #TMpanel,
  904. .night .layout__row_navbar,
  905. .night .page-header_bordered,
  906. .night .post-stats,
  907. .night .company-info__about,
  908. .night .company-info_post-additional,
  909. .night .company-info__contacts,
  910. .night .post-share,
  911. .night .content-list__item_devided,
  912. .night .comments_order,
  913. .night .comments-section__head,
  914. .night .content-list_nested-comments,
  915. .night .default-block__header,
  916. .night .column-wrapper_bordered,
  917. .night .tabs-menu,
  918. .night .toggle-menu {
  919. border-color: #393d41;
  920. }
  921.  
  922. .night .poll-result__progress {
  923. background-color: #515151;
  924. }
  925.  
  926. .night .poll-result__progress_winner {
  927. background-color: #5e8eac;
  928. }
  929.  
  930. .night .layout__elevator {
  931. color: #515151;
  932. }
  933.  
  934. .night .layout__elevator:hover {
  935. background-color: #22272B;
  936. }
  937.  
  938. .night .comment__head_topic-author {
  939. background: rgba(145, 120, 21, 0.1);
  940. }
  941.  
  942. .night .comment__head_my-comment {
  943. background: rgba(86, 120, 66, 0.1);
  944. }
  945.  
  946. .night .comment__head_new-comment {
  947. background: rgba(71, 93, 253, 0.1)
  948. }
  949.  
  950. .night .icon-svg_logo-habrahabr {
  951. color: inherit;
  952. }
  953.  
  954. /* img filter */
  955. .night .comment__message img,
  956. .night .comment-form__preview img,
  957. .night .default-block__content #facebook_like_box,
  958. .night .default-block__content #vk_groups,
  959. .night .post img,
  960. .night .page-header__banner img,
  961. .night .company_top_banner img,
  962. .night img .teaser__image,
  963. .night .teaser__image-pic,
  964. .night .article__body img {
  965. filter: brightness(0.5);
  966. transition: all .6s ease-out;
  967. }
  968.  
  969. .night .comment__message img:hover,
  970. .night .comment-form__preview img:hover,
  971. .night .default-block__content #facebook_like_box:hover,
  972. .night .default-block__content #vk_groups:hover,
  973. .night img[alt="en"],
  974. .night img[alt="habr"],
  975. .night img:hover,
  976. .night a.post-author__link img,
  977. .night img.user-info__image-pic,
  978. .night .teaser__image-pic:hover,
  979. .night .teaser__image:hover {
  980. filter: none;
  981. }
  982.  
  983. /* Atelier Cave Dark */
  984. .night .hljs-comment,
  985. .night .hljs-quote {
  986. color:#7e7887 !important
  987. }
  988. .night .hljs-variable,
  989. .night .hljs-template-variable,
  990. .night .hljs-attribute,
  991. .night .hljs-regexp,
  992. .night .hljs-link,
  993. .night .hljs-tag,
  994. .night .hljs-name,
  995. .night .hljs-selector-id,
  996. .night .hljs-selector-class {
  997. color:#be4678 !important
  998. }
  999. .night .hljs-number,
  1000. .night .hljs-meta,
  1001. .night .hljs-built_in,
  1002. .night .hljs-builtin-name,
  1003. .night .hljs-literal,
  1004. .night .hljs-type,
  1005. .night .hljs-params {
  1006. color:#aa573c !important
  1007. }
  1008. .night .hljs-string,
  1009. .night .hljs-symbol,
  1010. .night .hljs-bullet {
  1011. color:#2a9292 !important
  1012. }
  1013. .night .hljs-title,
  1014. .night .hljs-section {
  1015. color:#576ddb !important
  1016. }
  1017. .night .hljs-keyword,
  1018. .night .hljs-selector-tag {
  1019. color:#955ae7 !important
  1020. }
  1021. .night .hljs-deletion,
  1022. .night .hljs-addition {
  1023. color:#19171c !important;
  1024. display:inline-block !important;
  1025. width:100% !important
  1026. }
  1027. .night .hljs-deletion {
  1028. background-color:#be4678 !important
  1029. }
  1030. .night .hljs-addition {
  1031. background-color:#2a9292 !important
  1032. }
  1033. .night .hljs {
  1034. display:block !important;
  1035. overflow-x:auto !important;
  1036. background:#19171c !important;
  1037. color:#8b8792 !important;
  1038. /*padding:0.5em !important*/
  1039. }
  1040. .night .hljs-emphasis {
  1041. font-style:italic !important
  1042. }
  1043. .night .hljs-strong {
  1044. font-weight:bold !important
  1045. }
  1046. `;
  1047. }
  1048.  
  1049. if (FLAGS.USERSTYLE_CONFIG_INTERFACE) {
  1050. userStyle += `
  1051. .config_button {
  1052. box-sizing: border-box;
  1053. position: fixed;
  1054. width: 32px;
  1055. height: 25px;
  1056. right: 32px;
  1057. bottom: ${FLAGS.NIGHT_MODE ? 88 : 32}px;
  1058. z-index: 101;
  1059. background: -webkit-linear-gradient(top, #aaa 50%, transparent 50%);
  1060. background: -moz-linear-gradient(top, #aaa 50%, transparent 50%);
  1061. background: -moz-linear-gradient(top, #aaa 50%, transparent 50%);
  1062. background-size: 10px 10px;
  1063. transition: background 0.1s ease-out;
  1064. }
  1065.  
  1066. .config_button:hover {
  1067. background: -webkit-linear-gradient(top, #333 50%, transparent 50%);
  1068. background: -moz-linear-gradient(top, #333 50%, transparent 50%);
  1069. background: -moz-linear-gradient(top, #333 50%, transparent 50%);
  1070. background-size: 10px 10px;
  1071. }
  1072.  
  1073. .night .config_button {
  1074. background: -webkit-linear-gradient(top, #515151 50%, transparent 50%);
  1075. background: -moz-linear-gradient(top, #515151 50%, transparent 50%);
  1076. background: -moz-linear-gradient(top, #515151 50%, transparent 50%);
  1077. background-size: 10px 10px;
  1078. }
  1079.  
  1080. .night .config_button:hover {
  1081. background: -webkit-linear-gradient(top, #9e9e9e 50%, transparent 50%);
  1082. background: -moz-linear-gradient(top, #9e9e9e 50%, transparent 50%);
  1083. background: -moz-linear-gradient(top, #9e9e9e 50%, transparent 50%);
  1084. background-size: 10px 10px;
  1085. }
  1086.  
  1087. .config_frame {
  1088. box-sizing: border-box;
  1089. position: fixed;
  1090. right: 80px;
  1091. bottom: 32px;
  1092. z-index: 102;
  1093. border: 1px solid #aaa;
  1094. padding: 8px;
  1095. background: #f7f7f7;
  1096. -webkit-user-select: none;
  1097. -moz-user-select: none;
  1098. -ms-user-select: none;
  1099. user-select: none;
  1100. }
  1101. .config_frame label:hover {
  1102. cursor: pointer;
  1103. background: rgba(128, 128, 128, 0.3);
  1104. }
  1105. .night .config_frame {
  1106. background: #22272B;
  1107. border-color: #393d41;
  1108. }
  1109. .config_frame span {
  1110. padding-left: 0.3em;
  1111. }
  1112. `;
  1113. }
  1114.  
  1115.  
  1116. userStyleEl.innerHTML = userStyle;
  1117.  
  1118. function readyHead(fn) {
  1119. if (document.body) { // если есть body, значит head готов
  1120. fn();
  1121. } else if (document.documentElement) {
  1122. const observer = new MutationObserver(() => {
  1123. if (document.body) {
  1124. observer.disconnect();
  1125. fn();
  1126. }
  1127. });
  1128. observer.observe(document.documentElement, { childList: true });
  1129. } else {
  1130. // рекурсивное ожидание появления DOM
  1131. setTimeout(() => readyHead(fn), 10);
  1132. }
  1133. }
  1134.  
  1135. readyHead(() => {
  1136. if (FLAGS.USERSTYLE) document.head.appendChild(userStyleEl);
  1137. if (FLAGS.NIGHT_MODE && userConfig.getItem('night_mode')) {
  1138. document.documentElement.classList.add('night');
  1139. }
  1140. });
  1141.  
  1142. function ready(fn) {
  1143. const { readyState } = document;
  1144. if (readyState === 'loading') {
  1145. document.addEventListener('DOMContentLoaded', () => {
  1146. fn();
  1147. });
  1148. } else {
  1149. fn();
  1150. }
  1151. }
  1152.  
  1153. ready(() => {
  1154. if (FLAGS.COMMENTS_MD) {
  1155. const mdSelectorEl = document.getElementById('comment_markdown');
  1156. if (mdSelectorEl) {
  1157. if (userConfig.getItem('comment_markdown')) mdSelectorEl.checked = true;
  1158. mdSelectorEl.addEventListener('input', () => {
  1159. userConfig.setItem('comment_markdown', mdSelectorEl.checked);
  1160. });
  1161. }
  1162. }
  1163.  
  1164. // надо ли ещё
  1165. [...document.querySelectorAll('iframe[src^="https://codepen.io/"]')]
  1166. .map(el => el.setAttribute('scrolling', 'no'));
  1167.  
  1168. // остановка гифок по клику и воспроизведение при повторном клике
  1169. function toggleGIF(el) {
  1170. // если атрибут со старым линком пуст или отсутствует
  1171. if (!el.dataset.oldSrc) {
  1172. // заменим ссылку на data-url-svg с треугольником в круге
  1173. const w = Math.max(el.clientWidth || 256, 16);
  1174. const h = Math.max(el.clientHeight || 128, 16);
  1175. const cx = w / 2;
  1176. const cy = h / 2;
  1177. const r = Math.min(w, h) / 4;
  1178. const ax = (r * 61) / 128;
  1179. const by = (r * 56) / 128;
  1180. const bx = (r * 35) / 128;
  1181. const svg = `data:image/svg+xml;utf8,
  1182. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  1183. <rect x='0' y='0' width='${w}' height='${h}' fill='${FLAGS.GIF_STOP_COLOR_BG}'/>
  1184. <circle cx='${cx}' cy='${cy}' r='${r}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1185. <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${FLAGS.GIF_STOP_COLOR_BG}' />
  1186. </svg>
  1187. `;
  1188. el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign
  1189. el.setAttribute('src', svg);
  1190. } else if (FLAGS.GIF_STOP_OVERTYPE) {
  1191. // иначе поставим svg с троеточием
  1192. const w = el.clientWidth;
  1193. const h = el.clientHeight;
  1194. const cx = w / 2;
  1195. const cy = h / 2;
  1196. const r = Math.min(w, h) / 4;
  1197. const r2 = r / 4;
  1198. const svg = `data:image/svg+xml;utf8,
  1199. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  1200. <rect x='0' y='0' width='${w}' height='${h}' fill='${FLAGS.GIF_STOP_COLOR_BG}'/>
  1201. <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1202. <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1203. <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${FLAGS.GIF_STOP_COLOR_FG}'/>
  1204. </svg>
  1205. `;
  1206. el.setAttribute('src', svg);
  1207. // когда отрендерится троеточие, можно менять на исходную гифку
  1208. setTimeout(() => {
  1209. if (el.dataset.oldSrc) {
  1210. el.setAttribute('src', el.dataset.oldSrc);
  1211. el.dataset.oldSrc = ''; // eslint-disable-line no-param-reassign
  1212. }
  1213. }, 100);
  1214. } else {
  1215. const img = document.createElement('img');
  1216. img.setAttribute('src', el.dataset.oldSrc);
  1217. if (el.hasAttribute('align')) {
  1218. img.setAttribute('align', el.getAttribute('align'));
  1219. }
  1220. el.parentNode.insertBefore(img, el);
  1221. img.onclick = () => toggleGIF(img); // eslint-disable-line no-param-reassign
  1222. el.parentNode.removeChild(el);
  1223. }
  1224. }
  1225.  
  1226. if (FLAGS.GIF_STOP) {
  1227. [...document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]')]
  1228. .forEach((el) => {
  1229. if (FLAGS.GIF_STOP_ONLOAD) toggleGIF(el);
  1230. el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
  1231. });
  1232. }
  1233.  
  1234. // фиксирование высоты публикации чтобы убрать прыжки прокрутки
  1235. if (FLAGS.FIX_JUMPING_SCROLL) {
  1236. const postBodyEl = document.querySelector('.post__body_full');
  1237. const checkPostBodyInViewport = () => postBodyEl.getBoundingClientRect().bottom > 0;
  1238. const autoHeightPost = () => {
  1239. if (checkPostBodyInViewport()) {
  1240. window.removeEventListener('scroll', autoHeightPost);
  1241. postBodyEl.style.height = 'auto';
  1242. }
  1243. };
  1244. if (postBodyEl && !checkPostBodyInViewport()) {
  1245. const h = postBodyEl.clientHeight;
  1246. postBodyEl.style.height = `${h}px`;
  1247. window.addEventListener('scroll', autoHeightPost);
  1248. }
  1249. }
  1250.  
  1251. // счетчики кармы
  1252. if (FLAGS.KARMA_DETAILS) {
  1253. Array.from(document.querySelectorAll('.user-info__stats-item.stacked-counter')).forEach((itemCounter) => {
  1254. itemCounter.style.marginRight = '16px'; // eslint-disable-line no-param-reassign
  1255. });
  1256. Array.from(document.querySelectorAll('.page-header__stats_karma')).forEach((karmaEl) => {
  1257. karmaEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  1258. karmaEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign
  1259. });
  1260. Array.from(document.querySelectorAll('.stacked-counter[href="/info/help/karma/"]')).forEach((couterEl) => {
  1261. let total = parseInt(couterEl.title, 10);
  1262. const scoreEl = couterEl.querySelector('.stacked-counter__value');
  1263. if (!scoreEl || !total) return;
  1264. couterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  1265. couterEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign
  1266. const score = parseFloat(scoreEl.innerHTML.replace('–', '-').replace(',', '.'), 10);
  1267. if (score > total) total = score;
  1268. const likes = (total + score) / 2;
  1269. const percent = Math.round((100 * likes) / total);
  1270. const details = `&nbsp;= ${total} × (${percent} ${100 - percent})%`;
  1271. const detailsEl = document.createElement('span');
  1272. detailsEl.innerHTML = details;
  1273. detailsEl.style.color = '#545454';
  1274. detailsEl.style.fontFamily = '"-apple-system",BlinkMacSystemFont,Arial,sans-serif';
  1275. detailsEl.style.fontSize = '13px';
  1276. detailsEl.style.fontWeight = 'normal';
  1277. detailsEl.style.verticalAlign = 'middle';
  1278. scoreEl.appendChild(detailsEl);
  1279. couterEl.title += `, ${(likes).toFixed(2)} плюсов и ${(total - likes).toFixed(2)} минусов`; // eslint-disable-line no-param-reassign
  1280. });
  1281. }
  1282.  
  1283. // счетчики рейтинга с подробностями
  1284. const scoresMap = new Map();
  1285.  
  1286. class Score {
  1287. constructor(el) {
  1288. this.el = el;
  1289. const data = this.constructor.parse(el);
  1290. this.rating = data.rating;
  1291. this.total = data.total;
  1292. this.likes = data.likes;
  1293. this.dislikes = data.dislikes;
  1294. this.isDetailed = false;
  1295. this.observer = new MutationObserver(() => this.update());
  1296. }
  1297.  
  1298. setDetails(isDetailed) {
  1299. if (this.isDetailed === isDetailed) return;
  1300. this.isDetailed = isDetailed;
  1301. this.update();
  1302. }
  1303.  
  1304. update() {
  1305. const data = this.constructor.parse(this.el);
  1306. this.rating = data.rating;
  1307. this.total = data.total;
  1308. this.likes = data.likes;
  1309. this.dislikes = data.dislikes;
  1310. this.observer.disconnect();
  1311. if (this.isDetailed) {
  1312. this.details();
  1313. } else {
  1314. this.simply();
  1315. }
  1316. this.observer.observe(this.el, { childList: true });
  1317. }
  1318.  
  1319. static parse(el) {
  1320. let [total, likes, dislikes] = el
  1321. .attributes.title.textContent
  1322. .match(/[0-9]+/g).map(Number);
  1323. let [, sign, rating] = el.innerHTML.match(/([–]?)(\d+)/); // eslint-disable-line prefer-const
  1324. rating = Number(rating);
  1325. if (sign) rating = -rating;
  1326. // не знаю что там происходит при голосовании, так что на всякий случай
  1327. const diff = rating - (likes - dislikes);
  1328. if (diff < 0) {
  1329. total += Math.abs(diff);
  1330. dislikes += Math.abs(diff);
  1331. } else if (diff > 0) {
  1332. total += diff;
  1333. likes += diff;
  1334. }
  1335. return {
  1336. rating,
  1337. total,
  1338. likes,
  1339. dislikes,
  1340. };
  1341. }
  1342.  
  1343. simply() {
  1344. let innerHTML = '';
  1345. if (this.rating > 0) {
  1346. innerHTML = `+${this.rating}`;
  1347. } else if (this.rating < 0) {
  1348. innerHTML = `–${Math.abs(this.rating)}`;
  1349. } else {
  1350. innerHTML = '0';
  1351. }
  1352. this.el.innerHTML = innerHTML;
  1353. }
  1354.  
  1355. details() {
  1356. let innerHTML = '';
  1357. if (this.rating > 0) {
  1358. innerHTML = `+${this.rating}`;
  1359. } else if (this.rating < 0) {
  1360. innerHTML = `–${Math.abs(this.rating)}`;
  1361. } else {
  1362. innerHTML = '0';
  1363. }
  1364. if (this.total !== 0) {
  1365. let details = '';
  1366. if (FLAGS.RATING_DETAILS_PN) {
  1367. details = `&nbsp;= ${this.likes} ${this.dislikes}`;
  1368. } else {
  1369. const percent = Math.round((100 * this.likes) / this.total);
  1370. details = `&nbsp;= ${this.total} × (${percent} ${100 - percent})%`;
  1371. }
  1372. innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`;
  1373. }
  1374. this.el.innerHTML = innerHTML;
  1375. }
  1376. }
  1377.  
  1378. // парсим их
  1379. [...document.querySelectorAll('.voting-wjt__counter')].forEach((el) => {
  1380. scoresMap.set(el, new Score(el));
  1381. });
  1382.  
  1383. // добавляем подробностей
  1384. if (FLAGS.RATING_DETAILS) {
  1385. if (FLAGS.RATING_DETAILS_ONCLICK) {
  1386. const isDetailed = userConfig.getItem('scores_details');
  1387. if (isDetailed) scoresMap.forEach(score => score.setDetails(isDetailed));
  1388. scoresMap.forEach((score) => {
  1389. score.el.onclick = () => { // eslint-disable-line no-param-reassign
  1390. const nowDetailed = userConfig.shiftItem('scores_details');
  1391. scoresMap.forEach(s => s.setDetails(nowDetailed));
  1392. };
  1393. });
  1394. } else {
  1395. scoresMap.forEach(score => score.setDetails(true));
  1396. }
  1397. }
  1398.  
  1399. // метки времени и работа с ними
  1400. const pageLoadTime = new Date();
  1401. const monthNames = [
  1402. 'января', 'февраля', 'марта',
  1403. 'апреля', 'мая', 'июня',
  1404. 'июля', 'августа', 'сентября',
  1405. 'октября', 'ноября', 'декабря',
  1406. ];
  1407.  
  1408. class HabraTime {
  1409. constructor(el, parent) {
  1410. this.el = el;
  1411. this.parent = parent;
  1412. this.attrDatetime = this.constructor.getAttributeDatetime(el);
  1413. this.date = new Date(this.attrDatetime);
  1414. }
  1415.  
  1416. // вот было бы хорошо, если б на хабре были datetime атрибуты
  1417. static getAttributeDatetime(el) {
  1418. const imagination = el.getAttribute('datetime');
  1419. if (imagination) return imagination;
  1420.  
  1421. const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/;
  1422. let [,,
  1423. recently, // eslint-disable-line prefer-const
  1424. day, month, year,
  1425. time, // eslint-disable-line prefer-const
  1426. ] = el.innerHTML.match(re);
  1427.  
  1428. // и местное время
  1429. let moscow;
  1430. if (recently || year === undefined) {
  1431. const offsetMoscow = 3 * 60 * 60 * 1000;
  1432. const yesterdayShift = (recently === 'вчера') ? 24 * 60 * 60 * 1000 : 0;
  1433. const offset = pageLoadTime.getTimezoneOffset() * 60 * 1000;
  1434. const value = (pageLoadTime - yesterdayShift) + offsetMoscow + offset;
  1435. moscow = new Date(value);
  1436. }
  1437.  
  1438. if (recently) {
  1439. day = moscow.getDate();
  1440. month = moscow.getMonth() + 1;
  1441. } else if (month.length !== 2) {
  1442. month = monthNames.indexOf(month) + 1;
  1443. } else {
  1444. month = +month;
  1445. }
  1446.  
  1447. if (day < 10) day = `0${+day}`;
  1448. if (month < 10) month = `0${month}`;
  1449. if (year < 100) year = `20${year}`;
  1450. if (year === undefined) year = moscow.getFullYear();
  1451.  
  1452. return `${year}-${month}-${day}T${time}+03:00`;
  1453. }
  1454.  
  1455. absolute() {
  1456. let result = '';
  1457.  
  1458. const time = this.date;
  1459. const day = time.getDate();
  1460. const month = time.getMonth();
  1461. const monthName = monthNames[month];
  1462. const year = time.getFullYear();
  1463. const hours = time.getHours();
  1464. const minutes = time.getMinutes();
  1465.  
  1466. const now = new Date();
  1467. const nowDay = now.getDate();
  1468. const nowMonth = now.getMonth();
  1469. const nowYear = now.getFullYear();
  1470.  
  1471. const yesterday = new Date((now - 24) * 60 * 60 * 1000);
  1472. const yesterdayDay = yesterday.getDate();
  1473. const yesterdayMonth = yesterday.getMonth();
  1474. const yesterdayYear = yesterday.getFullYear();
  1475.  
  1476. const hhmm = `${hours}:${minutes >= 10 ? minutes : `0${minutes}`}`;
  1477.  
  1478. const isToday =
  1479. day === nowDay &&
  1480. month === nowMonth &&
  1481. year === nowYear;
  1482. const isYesterday =
  1483. day === yesterdayDay &&
  1484. month === yesterdayMonth &&
  1485. year === yesterdayYear;
  1486.  
  1487. if (isToday) {
  1488. result = `сегодня в ${hhmm}`;
  1489. } else if (isYesterday) {
  1490. result = `вчера в ${hhmm}`;
  1491. } else if (nowYear === year) {
  1492. result = `${day} ${monthName} в ${hhmm}`;
  1493. } else {
  1494. result = `${day} ${monthName} ${year} в ${hhmm}`;
  1495. }
  1496.  
  1497. return result;
  1498. }
  1499.  
  1500. static relative(milliseconds) {
  1501. let result = '';
  1502.  
  1503. const pluralForm = (n, forms) => {
  1504. if (n % 10 === 1 && n % 100 !== 11) return forms[0];
  1505. if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return forms[1];
  1506. return forms[2];
  1507. };
  1508.  
  1509. const formats = [
  1510. ['год', 'года', 'лет'],
  1511. ['месяц', 'месяца', 'месяцев'],
  1512. ['день', 'дня', 'дней'],
  1513. ['час', 'часа', 'часов'],
  1514. ['минуту', 'минуты', 'минут'],
  1515. ];
  1516.  
  1517. const minutes = milliseconds / 60000;
  1518. const hours = minutes / 60;
  1519. const days = hours / 24;
  1520. const months = days / 30;
  1521. const years = months / 12;
  1522. const idx = [years, months, days, hours, minutes].findIndex(x => x >= 1);
  1523.  
  1524. if (idx === -1) {
  1525. result = 'несколько секунд';
  1526. } else {
  1527. const value = Math.floor([years, months, days, hours, minutes][idx]);
  1528. const forms = formats[idx];
  1529. const form = pluralForm(value, forms);
  1530. result = `${value} ${form}`;
  1531. }
  1532. return result;
  1533. }
  1534.  
  1535. fromNow() {
  1536. const diff = Math.abs(Date.now() - this.date);
  1537. return `${this.constructor.relative(diff)} назад`;
  1538. }
  1539.  
  1540. fromParent() {
  1541. const diff = Math.abs(this.date - this.parent.date);
  1542. return `через ${this.constructor.relative(diff)}`;
  1543. }
  1544. }
  1545.  
  1546. // собираем метки времени
  1547. const datesMap = new Map();
  1548. const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item');
  1549. (megapostTimeEl ? [megapostTimeEl] : [])
  1550. .concat([...document.querySelectorAll(`
  1551. .post__time,
  1552. .preview-data__time-published,
  1553. time.comment__date-time_published,
  1554. .tm-post__date,
  1555. .user-message__date-time
  1556. `)]).forEach((el) => {
  1557. datesMap.set(el, new HabraTime(el));
  1558. });
  1559.  
  1560. function updateTime() {
  1561. datesMap.forEach((habraTime) => {
  1562. let type;
  1563. let otherTypes;
  1564. if (habraTime.parent) {
  1565. type = userConfig.config.time_comments;
  1566. otherTypes = userConfig.model.time_comments
  1567. .filter(str => str !== type);
  1568. } else {
  1569. type = userConfig.config.time_publications;
  1570. otherTypes = userConfig.model.time_publications
  1571. .filter(str => str !== type);
  1572. }
  1573. const title = otherTypes.map(otherType => habraTime[otherType]()).join(', ');
  1574. habraTime.el.innerHTML = habraTime[type](); // eslint-disable-line no-param-reassign
  1575. habraTime.el.setAttribute('title', title);
  1576. });
  1577. }
  1578.  
  1579. if (FLAGS.TIME_DETAILS) {
  1580. datesMap.forEach((habraTime) => {
  1581. habraTime.el.setAttribute(
  1582. 'style',
  1583. 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
  1584. );
  1585. habraTime.el.onclick = () => { // eslint-disable-line no-param-reassign
  1586. if (habraTime.parent) {
  1587. userConfig.shiftItem('time_comments');
  1588. } else {
  1589. userConfig.shiftItem('time_publications');
  1590. }
  1591. updateTime();
  1592. };
  1593. });
  1594. // подождём, когда дерево комментариев будет построено
  1595. // у некоторых меток времени будут установлены родители
  1596. // тогда и обновим их тексты
  1597. setTimeout(updateTime, 100);
  1598. setInterval(updateTime, 30 * 1000);
  1599. }
  1600.  
  1601. // время публикации, понадобится для корня древа комментариев
  1602. let datePublication = datesMap.get(megapostTimeEl || document.querySelector('.post__time'));
  1603. // если нету публикации поищем самую раннюю метку времени
  1604. if (!datePublication) {
  1605. datePublication = { date: pageLoadTime };
  1606. datesMap.forEach((date) => {
  1607. if (date.date < datePublication.date) datePublication = date;
  1608. });
  1609. }
  1610.  
  1611. // создаем дерево комментариев
  1612. class ItemComment {
  1613. constructor(el, parent) {
  1614. this.parent = parent;
  1615. this.el = el;
  1616. this.lvl = parent.lvl + 1;
  1617. this.id = Number(el.getAttribute('rel'));
  1618. this.commentEl = el.querySelector('.comment');
  1619. if (this.commentEl) {
  1620. this.timeEl = this.commentEl.querySelector('time');
  1621. this.ratingEl = this.commentEl.querySelector('.js-score');
  1622. }
  1623. this.date = datesMap.get(this.timeEl);
  1624. if (this.date) {
  1625. this.date.parent = parent.date;
  1626. } else {
  1627. this.date = parent.date;
  1628. }
  1629. this.votes = scoresMap.get(this.ratingEl) || {
  1630. total: 0, likes: 0, dislikes: 0, rating: 0,
  1631. };
  1632. this.elList = el.querySelector('.content-list_nested-comments');
  1633. }
  1634.  
  1635. existId(id) {
  1636. return !!this.elList.querySelector(id);
  1637. }
  1638.  
  1639. existNew() {
  1640. return !!this.elList.querySelector('.js-comment_new');
  1641. }
  1642.  
  1643. getLength() {
  1644. let { length } = this.list;
  1645. this.list.forEach((node) => {
  1646. length += node.getLength();
  1647. });
  1648. return length;
  1649. }
  1650. }
  1651.  
  1652. class CommentsTree {
  1653. constructor() {
  1654. this.root = {
  1655. isRoot: true,
  1656. date: datePublication,
  1657. lvl: 0,
  1658. elList: document.getElementById('comments-list'),
  1659. list: [],
  1660. };
  1661. }
  1662.  
  1663. static exist() {
  1664. return !!document.getElementById('comments-list');
  1665. }
  1666.  
  1667. update() {
  1668. if (!this.root.elList) return;
  1669. const recAdd = (node) => {
  1670. node.list = Array.from(node.elList.children) // eslint-disable-line no-param-reassign
  1671. .map(el => new ItemComment(el, node));
  1672. node.list.forEach(recAdd);
  1673. };
  1674. recAdd(this.root);
  1675. }
  1676.  
  1677. walkTree(fn) {
  1678. const walk = (tree) => {
  1679. fn(tree);
  1680. tree.list.forEach(walk);
  1681. };
  1682. walk(this.root);
  1683. }
  1684.  
  1685. sort(fn) {
  1686. if (!this.root.elList) return;
  1687. this.walkTree((tree) => {
  1688. tree.list.sort(fn).forEach(subtree => tree.elList.appendChild(subtree.el));
  1689. });
  1690. }
  1691.  
  1692. shuffle() {
  1693. if (!this.root.elList) return;
  1694. const randInt = maximum => Math.floor(Math.random() * (maximum + 1));
  1695. this.walkTree((tree) => {
  1696. const { list } = tree;
  1697. for (let i = 0; i < list.length; i += 1) {
  1698. const j = randInt(i);
  1699. [list[i], list[j]] = [list[j], list[i]];
  1700. }
  1701. list.forEach(subtree => tree.elList.appendChild(subtree.el));
  1702. });
  1703. }
  1704. }
  1705.  
  1706. const commentsTree = new CommentsTree();
  1707. commentsTree.update();
  1708.  
  1709. FLAGS.sortVariants = [
  1710. ['time', 'по времени'],
  1711. ];
  1712.  
  1713. if (FLAGS.COMMENTS_SORT_BY_FRESHNESS) FLAGS.sortVariants.push(['freshness', 'свежести']);
  1714. if (FLAGS.COMMENTS_SORT_BY_TREND) FLAGS.sortVariants.push(['trend', 'трендам']);
  1715. if (FLAGS.COMMENTS_SORT_BY_QUALITY) FLAGS.sortVariants.push(['quality', 'качеству']);
  1716. if (FLAGS.COMMENTS_SORT_BY_RATING) FLAGS.sortVariants.push(['rating', 'рейтингу']);
  1717. if (FLAGS.COMMENTS_SORT_BY_POPULARITY) FLAGS.sortVariants.push(['popularity', 'популярности']);
  1718. if (FLAGS.COMMENTS_SORT_BY_RANDOM) FLAGS.sortVariants.push(['shuffle', 'перемешать']);
  1719.  
  1720. // здесь начинается сортировка комментариев
  1721. const commentsOrderEl = document.createElement('div');
  1722. commentsOrderEl.classList.add('comments_order');
  1723. commentsOrderEl.innerHTML = FLAGS.sortVariants.map(([type, text]) => {
  1724. const underline = (type === 'time') ? '; text-decoration: underline' : '';
  1725. return `<a data-order="${type}" style="cursor: pointer${underline}">${text}</a>`;
  1726. }).join(', ');
  1727.  
  1728. if (FLAGS.COMMENTS_SORT && document.getElementById('comments-list')) {
  1729. const commentsList = document.getElementById('comments-list');
  1730. commentsList.parentElement.insertBefore(commentsOrderEl, commentsList);
  1731. }
  1732.  
  1733. const commentsComparators = {
  1734. time(a, b) {
  1735. return a.id - b.id;
  1736. },
  1737.  
  1738. freshness(a, b) {
  1739. return b.id - a.id;
  1740. },
  1741.  
  1742. rating(a, b) {
  1743. const ascore = a.votes.rating;
  1744. const bscore = b.votes.rating;
  1745. if (bscore !== ascore) return bscore - ascore;
  1746. return b.id - a.id;
  1747. },
  1748.  
  1749. popularity(a, b) {
  1750. const aVotes = a.votes.total;
  1751. const bVotes = b.votes.total;
  1752. if (aVotes !== bVotes) return bVotes - aVotes;
  1753. const aLength = a.getLength();
  1754. const bLength = b.getLength();
  1755. if (aLength !== bLength) return bLength - aLength;
  1756. return b.id - a.id;
  1757. },
  1758.  
  1759. quality(a, b) {
  1760. const aQuality = a.votes.rating / a.votes.total || 0;
  1761. const bQuality = b.votes.rating / b.votes.total || 0;
  1762. if (aQuality !== bQuality) return bQuality - aQuality;
  1763. if (a.votes.rating !== b.votes.rating) return b.votes.rating - a.votes.rating;
  1764. return b.id - a.id;
  1765. },
  1766.  
  1767. trend(a, b) {
  1768. // в первые сутки после публикации статьи число посещений больше чем в остальное время
  1769. const oneDay = 24 * 60 * 60 * 1000;
  1770. const firstDayEnd = +datePublication.date + oneDay;
  1771. // у комментария есть только три дня на голосование с момента его создания
  1772. const threeDays = 3 * oneDay;
  1773. const now = Date.now();
  1774.  
  1775. // прикинем число голосов в первый день
  1776. const aDate = +a.date.date;
  1777. let aViews = 0;
  1778. // в первый день
  1779. if (aDate <= firstDayEnd) {
  1780. aViews += Math.min(firstDayEnd, now) - aDate;
  1781. }
  1782. // и в остальное время
  1783. if (now >= firstDayEnd) {
  1784. const threeDaysEnd = aDate + threeDays;
  1785. // для этого соотношения я собрал статистику
  1786. aViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, aDate)) / 16;
  1787. }
  1788. const aScore = a.votes.rating / aViews;
  1789.  
  1790. // аналогично
  1791. const bDate = +b.date.date;
  1792. let bViews = 0;
  1793. if (bDate <= firstDayEnd) {
  1794. bViews += Math.min(firstDayEnd, now) - bDate;
  1795. }
  1796. if (now >= firstDayEnd) {
  1797. const threeDaysEnd = bDate + threeDays;
  1798. // найти зависимость активности голосования от времени суток не удалось
  1799. bViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, bDate)) / 16;
  1800. }
  1801. const bScore = b.votes.rating / bViews;
  1802.  
  1803. if (bScore === aScore) return b.id - a.id;
  1804. return bScore - aScore;
  1805. },
  1806. };
  1807.  
  1808. const sortComments = () => {
  1809. const order = userConfig.getItem('comments_order');
  1810.  
  1811. Array.from(commentsOrderEl.children).forEach((el) => {
  1812. if (el.dataset.order === order) {
  1813. el.style.textDecoration = 'underline'; // eslint-disable-line no-param-reassign
  1814. } else {
  1815. el.style.textDecoration = ''; // eslint-disable-line no-param-reassign
  1816. }
  1817. });
  1818.  
  1819. if (order === 'shuffle') {
  1820. commentsTree.shuffle();
  1821. } else {
  1822. const compare = commentsComparators[order];
  1823. commentsTree.sort(compare);
  1824. }
  1825. };
  1826.  
  1827. // сортируем комменты при загрузке страницы
  1828. // или не сортируем, если они уже по порядку
  1829. if (FLAGS.COMMENTS_SORT && FLAGS.COMMENTS_SORT_ONLOAD && userConfig.getItem('comments_order') !== 'time') {
  1830. sortComments();
  1831. }
  1832.  
  1833. Array.from(commentsOrderEl.children).forEach((el) => {
  1834. el.onclick = () => { // eslint-disable-line no-param-reassign
  1835. userConfig.setItem('comments_order', el.dataset.order);
  1836. sortComments();
  1837. };
  1838. });
  1839.  
  1840. // меняем ссылки ведущие к новым комментариям на ссылки к началу комментариев
  1841. if (FLAGS.COMMENTS_LINKS) {
  1842. const commentsLinks = document.getElementsByClassName('post-stats__comments-link');
  1843.  
  1844. for (let i = 0; i < commentsLinks.length; i += 1) {
  1845. const iLink = commentsLinks[i];
  1846. const hrefValue = iLink.getAttribute('href');
  1847. const hrefToComments = hrefValue.replace('#first_unread', '#comments');
  1848. iLink.setAttribute('href', hrefToComments);
  1849. }
  1850. }
  1851.  
  1852. // сворачивание комментов
  1853. if (FLAGS.COMMENTS_HIDE) {
  1854. const commentHash = window.location.hash;
  1855.  
  1856. const toggle = (subtree) => {
  1857. const listLength = subtree.list.length;
  1858. if (listLength === 0) return;
  1859. /* eslint-disable */
  1860. if (subtree.switcherEl.dataset.isVisibleList === 'true') {
  1861. subtree.switcherEl.dataset.isVisibleList = 'false';
  1862. subtree.switcherEl.innerHTML = `\u229E раскрыть ветвь ${subtree.getLength()}`;
  1863. subtree.elList.style.display = 'none';
  1864. } else {
  1865. subtree.switcherEl.dataset.isVisibleList = 'true';
  1866. subtree.switcherEl.innerHTML = '\u229F';
  1867. subtree.elList.style.display = 'block';
  1868. }
  1869. /* eslint-enable */
  1870. };
  1871.  
  1872. commentsTree.walkTree((subtree) => {
  1873. // не пытаемся сворачивать корень
  1874. if (subtree.isRoot) return;
  1875. // у похищенных нет футера
  1876. const footerEl = subtree.commentEl.querySelector('.comment__footer');
  1877. if (footerEl === null) return;
  1878. // создаём переключатель
  1879. const switcher = document.createElement('a');
  1880. switcher.classList.add('comment__footer-link');
  1881. switcher.classList.add('comment__switcher');
  1882. switcher.dataset.isVisibleList = 'true';
  1883.  
  1884. switcher.innerHTML = '\u229F';
  1885. if (subtree.list.length === 0) switcher.innerHTML = '\u22A1';
  1886. switcher.style.cursor = 'pointer';
  1887. switcher.style.marginLeft = '-5px';
  1888.  
  1889. footerEl.insertBefore(switcher, footerEl.children[0]);
  1890. subtree.switcherEl = switcher; // eslint-disable-line no-param-reassign
  1891.  
  1892. switcher.onclick = () => toggle(subtree);
  1893.  
  1894. const isHideLvl = subtree.lvl === FLAGS.HIDE_LEVEL;
  1895. const isLineLvl = subtree.lvl % FLAGS.LINE_LEN === 0;
  1896. if (isLineLvl) {
  1897. subtree.elList.classList.add('comments_new-line');
  1898. const lineNumber = subtree.lvl / FLAGS.LINE_LEN;
  1899. subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`);
  1900. }
  1901. // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id
  1902. if (
  1903. (isHideLvl || isLineLvl) && !subtree.existNew() &&
  1904. !(commentHash && subtree.existId(commentHash))
  1905. ) {
  1906. toggle(subtree);
  1907. }
  1908. });
  1909. }
  1910.  
  1911. if (FLAGS.SCROLL_LEGEND) {
  1912. const postBodyEl = document.querySelector('.post__body_full') || document.querySelector('.article__body');
  1913. const commentsEl = document.getElementById('comments-list');
  1914. const getPercents = (el) => {
  1915. if (!el) return { topPercent: 0, heightPercent: 0 };
  1916. const pageHeight = document.documentElement.scrollHeight;
  1917. const top = el.getBoundingClientRect().top + window.pageYOffset;
  1918. const topPercent = ((100 * top) / pageHeight).toFixed(2);
  1919. const height = el.clientHeight;
  1920. const heightPercent = ((100 * height) / pageHeight).toFixed(2);
  1921.  
  1922. return { topPercent, heightPercent };
  1923. };
  1924.  
  1925. const updateLegend = (pageEl, legendEl) => {
  1926. const { topPercent, heightPercent } = getPercents(pageEl);
  1927. legendEl.style.top = `${topPercent}%`; // eslint-disable-line no-param-reassign
  1928. legendEl.style.height = `${heightPercent}%`; // eslint-disable-line no-param-reassign
  1929. };
  1930.  
  1931. const legendPost = document.createElement('div');
  1932. legendPost.classList.add('legend_el');
  1933. legendPost.style.background = 'rgba(84, 142, 170, 0.66)';
  1934. updateLegend(postBodyEl, legendPost);
  1935. document.body.appendChild(legendPost);
  1936.  
  1937. const legendComments = document.createElement('div');
  1938. legendComments.classList.add('legend_el');
  1939. legendComments.style.background = 'rgba(49, 176, 7, 0.66)';
  1940. updateLegend(commentsEl, legendComments);
  1941. document.body.appendChild(legendComments);
  1942.  
  1943. setInterval(() => {
  1944. updateLegend(postBodyEl, legendPost);
  1945. updateLegend(commentsEl, legendComments);
  1946. }, 1000);
  1947. }
  1948.  
  1949. if (FLAGS.NIGHT_MODE) {
  1950. const switcherEl = document.createElement('div');
  1951. switcherEl.classList.add('night_mode_switcher');
  1952. switcherEl.onclick = () => {
  1953. const isNightMode = userConfig.shiftItem('night_mode');
  1954. document.documentElement.classList.toggle('night', isNightMode);
  1955. };
  1956. document.body.appendChild(switcherEl);
  1957. setInterval(() => {
  1958. const boolClass = document.documentElement.classList.contains('night');
  1959. const isNightMode = userConfig.getItem('night_mode');
  1960. if (boolClass !== isNightMode) {
  1961. document.documentElement.classList.toggle('night', isNightMode);
  1962. }
  1963. }, 1000);
  1964. }
  1965.  
  1966. if (FLAGS.CONFIG_INTERFACE) {
  1967. const configFrame = document.createElement('div');
  1968. configOptions.forEach(([key, text]) => {
  1969. if (typeof FLAGS[key] !== 'boolean') return;
  1970. const inputEl = document.createElement('input');
  1971. inputEl.type = 'checkbox';
  1972. inputEl.value = key;
  1973. inputEl.checked = FLAGS[key];
  1974. const labelEl = document.createElement('label');
  1975. labelEl.setAttribute('unselectable', 'on');
  1976. labelEl.setAttribute('onselectstart', 'return false');
  1977. const spanEl = document.createElement('span');
  1978. spanEl.innerHTML = text;
  1979. configFrame.appendChild(labelEl);
  1980. labelEl.appendChild(inputEl);
  1981. labelEl.appendChild(spanEl);
  1982. inputEl.onchange = () => {
  1983. FLAGS[key] = inputEl.checked;
  1984. localStorage.setItem('habrafixFlags', JSON.stringify(FLAGS));
  1985. };
  1986. configFrame.appendChild(document.createElement('br'));
  1987. });
  1988. const reloadText = document.createElement('div');
  1989. reloadText.style.textAlign = 'right';
  1990. reloadText.innerHTML = `
  1991. * чтобы увидеть изменения
  1992. <a href="#" onclick="location.reload(); return false">
  1993. обновите страницу
  1994. </a>`;
  1995. configFrame.appendChild(reloadText);
  1996. configFrame.classList.add('config_frame');
  1997. configFrame.style.display = 'none';
  1998. document.body.appendChild(configFrame);
  1999.  
  2000. const configButton = document.createElement('div');
  2001. configButton.classList.add('config_button');
  2002. document.body.appendChild(configButton);
  2003.  
  2004. configButton.onclick = () => {
  2005. if (configFrame.style.display) {
  2006. configFrame.style.display = '';
  2007. } else {
  2008. configFrame.style.display = 'none';
  2009. }
  2010. };
  2011. }
  2012. });