Habr.Features

Всякое-разное для Habr и GeekTimes

当前为 2018-06-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Habr.Features
  3. // @version 3.0.16
  4. // @description Всякое-разное для Habr и GeekTimes
  5. // @author AngReload
  6. // @include https://geektimes.com/*
  7. // @include https://habr.com/*
  8. // @include http://habr.com/*
  9. // @include http://geektimes.com/*
  10. // @include https://tmfeed.ru
  11. // @include http://tmfeed.ru
  12. // @namespace habr_comments
  13. // @run-at document-start
  14. // @grant none
  15. // @icon https://habr.com/favicon.ico
  16. // ==/UserScript==
  17. /* global localStorage, MutationObserver */
  18.  
  19. // остановка гифок
  20. // клик по гифке заменит картинку на заглушку
  21. // повторный клик вернет гифку на место
  22. const GIF_STOP = true;
  23. // остановить гифки при загрузке страницы
  24. const GIF_STOP_ONLOAD = false;
  25. // цвета заглушки
  26. const GIF_STOP_COLOR_FG = 'White';
  27. const GIF_STOP_COLOR_BG = 'LightGray'; // WhiteSmoke
  28. // менять src вместо создания-удаления нод
  29. const GIF_STOP_OVERTYPE = true;
  30.  
  31. // показывать счетчики рейтинга в виде:
  32. // рейтинг = число_голосовавших * (процент_плюсов - процент_минусов)%
  33. const RATING_DETAILS = true;
  34. // клик мышкой по рейтингу меняет вид на простой \ детальный
  35. const RATING_DETAILS_ONCLICK = false;
  36.  
  37. const RATING_DETAILS_PN = false;
  38.  
  39. const KARMA_DETAILS = true;
  40.  
  41. // показывать метки времени в текущем часовом поясе
  42. // абсолютно, либо относительно текущего времени, либо относительно родительского времени
  43. // меняется по клику, в всплывающей подсказке другие виды времени, автообновляется
  44. const TIME_DETAILS = true;
  45.  
  46. // добавить возможность сортировки комментариев
  47. const COMMENTS_SORT = true;
  48. // сортировать комменты при загрузке страницы или оставить сортировку по времени
  49. const COMMENTS_SORT_ONLOAD = true;
  50. // список доступных сортировок
  51. const sortVariants = [
  52. ['time', 'по времени'],
  53. ['freshness', 'свежести'],
  54. ['trend', 'трендам'],
  55. // ['quality', 'качеству'],
  56. // ['rating', 'рейтингу'],
  57. // ['popularity', 'популярности'],
  58. // ['shuffle', 'перемешать'],
  59. ];
  60.  
  61. // добавить возможность сворачивать комментарии
  62. const COMMENTS_HIDE = true;
  63. // свернуть комментарии если их глубина вложенности равна некому числу
  64. const HIDE_LEVEL = 4;
  65. // сделать «возврат каретки» для комментариев чтобы глубина вложенности не превышала некого числа
  66. const LINE_LEN = 8;
  67. // const LINE_LEN = 16;
  68. const REDUCE_NEW_LINES = true;
  69. const RAINBOW_NEW_LINE = true;
  70.  
  71. // заменить ссылки ведущие к новым комментариям на дерево комментариев
  72. const COMMENTS_LINKS = true;
  73.  
  74. // запоминание галки «Использовать MarkDown» для комментариев
  75. const COMMENTS_MD = true;
  76.  
  77. // в огнелисе, в отличии о хрома, при загрузке изображений не сохраняется прокрутка
  78. // меня раздражает когда дерево комментариев скачками пытается уехать вниз
  79. // эта опция зафиксирует высоту публикации пока та находится вне обзора
  80. const FIX_JUMPING_SCROLL = true;
  81.  
  82. const SCROLL_LEGEND = true;
  83.  
  84. // включить разные стили
  85. const USERSTYLE = true;
  86.  
  87. const USERSTYLE_GIF_HOVER = false;
  88. const USERSTYLE_FEED_DISTANCED = true;
  89. const USERSTYLE_COUNTER_NEW_FIX_SPACE = true;
  90. const USERSTYLE_REMOVE_SIDEBAR_RIGHT = true;
  91. const USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS = true;
  92. const USERSTYLE_USERINFO_BIG_AVATARS = true;
  93. const USERSTYLE_COMMENTS_IMG_MAXSIZE = 0;
  94. const USERSTYLE_COMMENTS_FIX = true;
  95. const USERSTYLE_CODE_FIX = true;
  96. const USERSTYLE_SPOILER_BORDERS = true;
  97. const USERSTYLE_STATIC_STICKY = true;
  98. const USERSTYLE_HLJS_LANG = true;
  99. const USERSTYLE_HLJS_LANG_HOVER = false;
  100. const USERSTYLE_CODE_FONT = 'PT Mono';
  101. const USERSTYLE_CODE_TABSIZE = 2;
  102. // свои стили
  103. const userStyleEl = document.createElement('style');
  104. let userStyle = '';
  105.  
  106. if (USERSTYLE_GIF_HOVER) {
  107. userStyle += `
  108. .post__text img[src$=".gif"]:hover,
  109. .comment__message img[src$=".gif"]:hover {
  110. opacity: 0.75;
  111. }
  112. `;
  113. }
  114.  
  115. if (SCROLL_LEGEND) {
  116. userStyle += `
  117. .legend_el {
  118. position: fixed;
  119. width: 4px;
  120. right: 0;
  121. transition: top 1s ease-out, height 1s ease-out;
  122. z-index: 101;
  123. }
  124.  
  125. #xpanel {
  126. right: 4px;
  127. }
  128. `;
  129. }
  130.  
  131. if (USERSTYLE_FEED_DISTANCED) {
  132. userStyle += `
  133. .post__body_crop {
  134. text-align: right;
  135. }
  136.  
  137. .post__body_crop .post__text {
  138. text-align: left;
  139. }
  140.  
  141. .post__footer {
  142. text-align: right;
  143. }
  144.  
  145. .posts_list .content-list__item_post {
  146. padding: 40px 0;
  147. }
  148. `;
  149. }
  150.  
  151. if (USERSTYLE_COUNTER_NEW_FIX_SPACE) {
  152. userStyle += `
  153. .toggle-menu__item-counter_new {
  154. margin-left: 4px;
  155. }
  156. `;
  157. }
  158.  
  159. if (USERSTYLE_REMOVE_SIDEBAR_RIGHT) {
  160. // remove for
  161. // https://habr.com/post/352896/
  162. // https://habr.com/sandbox/
  163. // https://habr.com/sandbox/115216/
  164. // https://habr.com/users/saggid/posts/
  165. // https://habr.com/users/saggid/comments/
  166. // https://habr.com/users/saggid/favorites/
  167. // https://habr.com/users/saggid/favorites/posts/
  168. // https://habr.com/users/saggid/favorites/comments/
  169. // https://habr.com/company/pvs-studio/blog/353640/
  170. // https://habr.com/company/pvs-studio/blog/
  171. // https://habr.com/company/pvs-studio/blog/top/
  172. // https://habr.com/company/pvs-studio/
  173. // https://habr.com/feed/
  174. // https://habr.com/top/
  175. // https://habr.com/top/yearly/
  176. // https://habr.com/all/
  177. // https://habr.com/all/top10/
  178.  
  179. // display for
  180. // https://habr.com/company/pvs-studio/profile/
  181. // https://habr.com/company/pvs-studio/vacancies/
  182. // https://habr.com/company/pvs-studio/fans/all/rating/
  183. // https://habr.com/company/pvs-studio/workers/new/rating/
  184. // https://habr.com/feed/settings/
  185. // https://habr.com/users/
  186. // https://habr.com/hubs/
  187. // https://habr.com/hubs/admin/
  188. // https://habr.com/companies/
  189. // https://habr.com/companies/category/software/
  190. // https://habr.com/companies/new/
  191. // https://habr.com/flows/design/
  192.  
  193. const path = window.location.pathname;
  194. const isPost = /^\/post\/\d+\/$/.test(path);
  195. const isSandbox = /^\/sandbox\//.test(path);
  196. const isUserPosts = /^\/users\/[^/]+\/posts\//.test(path);
  197. const isUserComments = /^\/users\/[^/]+\/comments\//.test(path);
  198. const isUserFavorites = /^\/users\/[^/]+\/favorites\//.test(path);
  199. const isCompanyBlog = /^\/company\/[^/]+\/blog\//.test(path);
  200. const isCompanyBlog2 = /^\/company\/[^/]+\/(page\d+\/)?$/.test(path);
  201. const isFeed = /^\/feed\//.test(path);
  202. const isHome = /^\/$/.test(path);
  203. const isTop = /^\/top\//.test(path);
  204. const isAll = /^\/all\//.test(path);
  205.  
  206. if (
  207. isPost || isSandbox ||
  208. isUserPosts || isUserComments || isUserFavorites ||
  209. isCompanyBlog || isCompanyBlog2 ||
  210. isFeed || isHome || isTop || isAll
  211. ) {
  212. userStyle += `
  213. .sidebar_right {
  214. display: none;
  215. }
  216.  
  217. .content_left {
  218. padding-right: 0;
  219. }
  220.  
  221. .comment_plain {
  222. max-width: 860px;
  223. }
  224. `;
  225. }
  226. }
  227.  
  228. if (USERSTYLE_REMOVE_BORDER_RADIUS_AVATARS) {
  229. userStyle += `
  230. .user-info__image-pic,
  231. .user-pic_popover,
  232. .media-obj__image-pic {
  233. border-radius: 0;
  234. }
  235. `;
  236. }
  237.  
  238. if (USERSTYLE_USERINFO_BIG_AVATARS) {
  239. userStyle += `
  240. .page-header {
  241. height: auto;
  242. }
  243.  
  244. .media-obj__image-pic_hub,
  245. .media-obj__image-pic_user,
  246. .media-obj__image-pic_company {
  247. width: auto;
  248. height: auto;
  249. }
  250. `;
  251. }
  252.  
  253. if (USERSTYLE_COMMENTS_FIX) {
  254. userStyle += `
  255. .comments_order {
  256. color: #333;
  257. font-size: 14px;
  258. font-family: "-apple-system",BlinkMacSystemFont,Arial,sans-serif;
  259. text-rendering: optimizeLegibility;
  260. border-bottom: 1px solid #e3e3e3;
  261. padding: 8px;
  262. text-align: right;
  263. }
  264.  
  265. .comments_order a {
  266. color: #548eaa;
  267. font-style: normal;
  268. text-decoration: none;
  269. }
  270.  
  271. .comments_order a:hover {
  272. color: #487284;
  273. }
  274.  
  275. .content-list_comments {
  276. overflow: visible;
  277. }
  278.  
  279. .comment__folding-dotholder {
  280. display: none !important;
  281. }
  282.  
  283. .content-list_nested-comments {
  284. border-left: 1px solid #e3e3e3;
  285. margin: 0;
  286. padding-top: 20px;
  287. padding-left: 20px !important;
  288. }
  289.  
  290. .content-list_comments {
  291. /*border-left: 1px solid silver;*/
  292. margin: 0;
  293. padding-left: 0;
  294. padding-top: 20px;
  295. /*background: #FCE4EC;*/
  296. }
  297.  
  298. #comments-list .js-form_placeholder {
  299. border-left: 1px solid #e3e3e3;
  300. padding-left: 20px;
  301. }
  302.  
  303. .comments_new-line {
  304. border-left: 1px solid #777;
  305. border-bottom: 1px solid #777;
  306. border-top: 1px solid #777;
  307. margin-left: -${LINE_LEN * 21}px !important;
  308. background: white;
  309. padding-bottom: 4px;
  310. }
  311.  
  312. /* .comment__head_topic-author.comment__head_new-comment */
  313. .comment__head_topic-author .user-info {
  314. text-decoration: underline;
  315. }
  316. `;
  317. }
  318.  
  319. if (RAINBOW_NEW_LINE) {
  320. userStyle += `
  321. .comments_new-line-1 {
  322. border-color: #0caefb;
  323. }
  324.  
  325. .comments_new-line-2 {
  326. border-color: #06feb7;
  327. }
  328.  
  329. .comments_new-line-3 {
  330. border-color: #fbcb02;
  331. }
  332.  
  333. .comments_new-line-0 {
  334. border-color: #fb0543;
  335. }
  336. `;
  337. }
  338.  
  339. if (REDUCE_NEW_LINES) {
  340. userStyle += `
  341. .comments_new-line .comments_new-line {
  342. margin-left: -${(LINE_LEN - 1) * 21}px !important;
  343. }
  344. `;
  345. }
  346.  
  347. if (USERSTYLE_COMMENTS_IMG_MAXSIZE) {
  348. userStyle += `
  349. .comment__message img {
  350. max-height: ${USERSTYLE_COMMENTS_IMG_MAXSIZE}px;
  351. }
  352.  
  353. .comment__message .spoiler .img {
  354. max-height: auto;
  355. }
  356. `;
  357. }
  358.  
  359. if (USERSTYLE_CODE_FIX) {
  360. let addFont = '';
  361. if (USERSTYLE_CODE_FONT) {
  362. addFont = USERSTYLE_CODE_FONT;
  363. if (addFont.indexOf(' ') >= 0) {
  364. addFont = `"${addFont}"`;
  365. }
  366. addFont += ',';
  367. }
  368.  
  369. const tabSize = USERSTYLE_CODE_TABSIZE || 4;
  370.  
  371. userStyle += `
  372. .editor .text-holder textarea,
  373. .tm-editor__textarea {
  374. font-family: ${addFont} Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace;
  375. }
  376.  
  377. code {
  378. font-family: ${addFont} Menlo, Monaco, Consolas, 'Lucida Console', 'Courier New', monospace !important;;
  379. -o-tab-size: ${tabSize};
  380. -moz-tab-size: ${tabSize};
  381. tab-size: ${tabSize};
  382. background: #f7f7f7;
  383. border-radius: 3px;
  384. color: #505c66;
  385. display: inline-block;
  386. font-weight: 500;
  387. line-height: 1.29;
  388. padding: 5px 9px;
  389. vertical-align: 1px;
  390. }
  391. `;
  392. }
  393.  
  394. if (USERSTYLE_SPOILER_BORDERS) {
  395. userStyle += `
  396. .spoiler .spoiler_text {
  397. border: 1px dashed rgb(12, 174, 251);
  398. }
  399. `;
  400. }
  401.  
  402. if (USERSTYLE_STATIC_STICKY) {
  403. userStyle += `
  404. .wrapper-sticky,
  405. .js-ad_sticky,
  406. .js-ad_sticky_comments {
  407. position: static !important;
  408. }
  409. `;
  410. }
  411.  
  412. if (USERSTYLE_HLJS_LANG) {
  413. let hover = '';
  414. if (USERSTYLE_HLJS_LANG_HOVER) hover = ':hover';
  415. userStyle += `
  416. pre {
  417. position: relative;
  418. }
  419.  
  420. .hljs${hover}::after {
  421. position: absolute;
  422. font-size: 12px;
  423. content: 'code';
  424. right: 0;
  425. top: 0;
  426. padding: 1px 5px 0 4px;
  427. /*border-bottom: 1px solid #e5e8ec;
  428. border-left: 1px solid #e5e8ec;
  429. border-bottom-left-radius: 3px;
  430. color: #505c66;*/
  431. opacity: .5;
  432. }
  433. `;
  434. userStyle += [
  435. ['1c', '1C:Enterprise (v7, v8)'],
  436. ['abnf', 'Augmented Backus-Naur Form'],
  437. ['accesslog', 'Access log'],
  438. ['actionscript', 'ActionScript'],
  439. ['ada', 'Ada'],
  440. ['apache', 'Apache'],
  441. ['applescript', 'AppleScript'],
  442. ['arduino', 'Arduino'],
  443. ['armasm', 'ARM Assembly'],
  444. ['asciidoc', 'AsciiDoc'],
  445. ['aspectj', 'AspectJ'],
  446. ['autohotkey', 'AutoHotkey'],
  447. ['autoit', 'AutoIt'],
  448. ['avrasm', 'AVR Assembler'],
  449. ['awk', 'Awk'],
  450. ['axapta', 'Axapta'],
  451. ['bash', 'Bash'],
  452. ['basic', 'Basic'],
  453. ['bnf', 'Backus–Naur Form'],
  454. ['brainfuck', 'Brainfuck'],
  455. ['cal', 'C/AL'],
  456. ['capnproto', 'Cap’n Proto'],
  457. ['ceylon', 'Ceylon'],
  458. ['clean', 'Clean'],
  459. ['clojure-repl', 'Clojure REPL'],
  460. ['clojure', 'Clojure'],
  461. ['cmake', 'CMake'],
  462. ['coffeescript', 'CoffeeScript'],
  463. ['coq', 'Coq'],
  464. ['cos', 'Caché Object Script'],
  465. ['cpp', 'C++'],
  466. ['crmsh', 'crmsh'],
  467. ['crystal', 'Crystal'],
  468. ['cs', 'C#'],
  469. ['csp', 'CSP'],
  470. ['css', 'CSS'],
  471. ['d', 'D'],
  472. ['dart', 'Dart'],
  473. ['delphi', 'Delphi'],
  474. ['diff', 'Diff'],
  475. ['django', 'Django'],
  476. ['dns', 'DNS Zone file'],
  477. ['dockerfile', 'Dockerfile'],
  478. ['dos', 'DOS .bat'],
  479. ['dsconfig', 'dsconfig'],
  480. ['dts', 'Device Tree'],
  481. ['dust', 'Dust'],
  482. ['ebnf', 'Extended Backus-Naur Form'],
  483. ['elixir', 'Elixir'],
  484. ['elm', 'Elm'],
  485. ['erb', 'ERB (Embedded Ruby)'],
  486. ['erlang-repl', 'Erlang REPL'],
  487. ['erlang', 'Erlang'],
  488. ['excel', 'Excel'],
  489. ['fix', 'FIX'],
  490. ['flix', 'Flix'],
  491. ['fortran', 'Fortran'],
  492. ['fsharp', 'F#'],
  493. ['gams', 'GAMS'],
  494. ['gauss', 'GAUSS'],
  495. ['gcode', 'G-code (ISO 6983)'],
  496. ['gherkin', 'Gherkin'],
  497. ['glsl', 'GLSL'],
  498. ['go', 'Go'],
  499. ['golo', 'Golo'],
  500. ['gradle', 'Gradle'],
  501. ['groovy', 'Groovy'],
  502. ['haml', 'Haml'],
  503. ['handlebars', 'Handlebars'],
  504. ['haskell', 'Haskell'],
  505. ['haxe', 'Haxe'],
  506. ['hsp', 'HSP'],
  507. ['htmlbars', 'HTMLBars'],
  508. ['http', 'HTTP'],
  509. ['hy', 'Hy'],
  510. ['inform7', 'Inform 7'],
  511. ['ini', 'Ini'],
  512. ['irpf90', 'IRPF90'],
  513. ['java', 'Java'],
  514. ['javascript', 'JavaScript'],
  515. ['jboss-cli', 'jboss-cli'],
  516. ['json', 'JSON'],
  517. ['julia-repl', 'Julia REPL'],
  518. ['julia', 'Julia'],
  519. ['kotlin', 'Kotlin'],
  520. ['lasso', 'Lasso'],
  521. ['ldif', 'LDIF'],
  522. ['leaf', 'Leaf'],
  523. ['less', 'Less'],
  524. ['lisp', 'Lisp'],
  525. ['livecodeserver', 'LiveCode'],
  526. ['livescript', 'LiveScript'],
  527. ['llvm', 'LLVM IR'],
  528. ['lsl', 'Linden Scripting Language'],
  529. ['lua', 'Lua'],
  530. ['makefile', 'Makefile'],
  531. ['markdown', 'Markdown'],
  532. ['mathematica', 'Mathematica'],
  533. ['matlab', 'Matlab'],
  534. ['maxima', 'Maxima'],
  535. ['mel', 'MEL'],
  536. ['mercury', 'Mercury'],
  537. ['mipsasm', 'MIPS Assembly'],
  538. ['mizar', 'Mizar'],
  539. ['mojolicious', 'Mojolicious'],
  540. ['monkey', 'Monkey'],
  541. ['moonscript', 'MoonScript'],
  542. ['n1ql', 'N1QL'],
  543. ['nginx', 'Nginx'],
  544. ['nimrod', 'Nimrod'],
  545. ['nix', 'Nix'],
  546. ['nsis', 'NSIS'],
  547. ['objectivec', 'Objective-C'],
  548. ['ocaml', 'OCaml'],
  549. ['openscad', 'OpenSCAD'],
  550. ['oxygene', 'Oxygene'],
  551. ['parser3', 'Parser3'],
  552. ['perl', 'Perl'],
  553. ['pf', 'pf'],
  554. ['php', 'PHP'],
  555. ['pony', 'Pony'],
  556. ['powershell', 'PowerShell'],
  557. ['processing', 'Processing'],
  558. ['profile', 'Python profile'],
  559. ['prolog', 'Prolog'],
  560. ['protobuf', 'Protocol Buffers'],
  561. ['puppet', 'Puppet'],
  562. ['purebasic', 'PureBASIC'],
  563. ['python', 'Python'],
  564. ['q', 'Q'],
  565. ['qml', 'QML'],
  566. ['r', 'R'],
  567. ['rib', 'RenderMan RIB'],
  568. ['roboconf', 'Roboconf'],
  569. ['routeros', 'Microtik RouterOS script'],
  570. ['rsl', 'RenderMan RSL'],
  571. ['ruby', 'Ruby'],
  572. ['ruleslanguage', 'Oracle Rules Language'],
  573. ['rust', 'Rust'],
  574. ['scala', 'Scala'],
  575. ['scheme', 'Scheme'],
  576. ['scilab', 'Scilab'],
  577. ['scss', 'SCSS'],
  578. ['shell', 'Shell Session'],
  579. ['smali', 'Smali'],
  580. ['smalltalk', 'Smalltalk'],
  581. ['sml', 'SML'],
  582. ['sqf', 'SQF'],
  583. ['sql', 'SQL'],
  584. ['stan', 'Stan'],
  585. ['stata', 'Stata'],
  586. ['step21', 'STEP Part 21'],
  587. ['stylus', 'Stylus'],
  588. ['subunit', 'SubUnit'],
  589. ['swift', 'Swift'],
  590. ['taggerscript', 'Tagger Script'],
  591. ['tap', 'Test Anything Protocol'],
  592. ['tcl', 'Tcl'],
  593. ['tex', 'TeX'],
  594. ['thrift', 'Thrift'],
  595. ['tp', 'TP'],
  596. ['twig', 'Twig'],
  597. ['typescript', 'TypeScript'],
  598. ['vala', 'Vala'],
  599. ['vbnet', 'VB.NET'],
  600. ['vbscript-html', 'VBScript in HTML'],
  601. ['vbscript', 'VBScript'],
  602. ['verilog', 'Verilog'],
  603. ['vhdl', 'VHDL'],
  604. ['vim', 'Vim Script'],
  605. ['x86asm', 'Intel x86 Assembly'],
  606. ['xl', 'XL'],
  607. ['xml', 'HTML, XML'],
  608. ['xquery', 'XQuery'],
  609. ['yaml', 'YAML'],
  610. ['zephir', 'Zephir'],
  611. ].map(([langTag, langName]) => `.hljs.${langTag}${hover}::after{content:'${langName} [${langTag}]'}`).join('');
  612. }
  613.  
  614. userStyleEl.innerHTML = userStyle;
  615.  
  616. function readyHead(fn) {
  617. if (document.body) { // если есть body, значит head готов
  618. fn();
  619. } else if (document.documentElement) {
  620. const observer = new MutationObserver(() => {
  621. if (document.body) {
  622. observer.disconnect();
  623. fn();
  624. }
  625. });
  626. observer.observe(document.documentElement, { childList: true });
  627. } else {
  628. // рекурсивное ожидание появления DOM
  629. setTimeout(() => readyHead(fn), 10);
  630. }
  631. }
  632.  
  633. readyHead(() => {
  634. if (USERSTYLE) document.head.appendChild(userStyleEl);
  635. });
  636.  
  637. function ready(fn) {
  638. const { readyState } = document;
  639. if (readyState === 'loading') {
  640. document.addEventListener('DOMContentLoaded', () => {
  641. fn();
  642. });
  643. } else {
  644. fn();
  645. }
  646. }
  647.  
  648. ready(() => {
  649. // интерфейс для хранения настроек
  650. const userConfig = {
  651. // имя записи в localsorage
  652. key: 'habrafix',
  653. // модель настроек: ключ - возможные значения
  654. model: {
  655. time_publications: ['fromNow', 'absolute'],
  656. time_comments: ['fromParent', 'fromNow', 'absolute'],
  657. comments_order: ['trend', 'time'],
  658. scores_details: [true, false],
  659. comment_markdown: [false, true],
  660. },
  661. config: {},
  662. // при старте для конфига берем сохраненные параметры либо по умолчанию
  663. init() {
  664. let jsonString = localStorage.getItem(userConfig.key);
  665. const loadedConfig = jsonString ? JSON.parse(jsonString) : {};
  666. const loadedKeys = Object.keys(loadedConfig);
  667. const config = {};
  668. Object.keys(userConfig.model).forEach((key) => {
  669. const exist = loadedKeys.indexOf(key) >= 0;
  670. config[key] = exist ? loadedConfig[key] : userConfig.model[key][0];
  671. });
  672. jsonString = JSON.stringify(config);
  673. localStorage.setItem(userConfig.key, jsonString);
  674. userConfig.config = config;
  675. },
  676. getItem(key) {
  677. return userConfig.config[key];
  678. },
  679. setItem(key, value) {
  680. let jsonString = localStorage.getItem(userConfig.key);
  681. const config = JSON.parse(jsonString);
  682. config[key] = value;
  683. jsonString = JSON.stringify(config);
  684. localStorage.setItem(userConfig.key, jsonString);
  685. userConfig.config = config;
  686. },
  687. // каруселит параметр по значения модели
  688. shiftItem(key) {
  689. const currentValue = userConfig.getItem(key);
  690. const availableValues = userConfig.model[key];
  691. const currentIdx = availableValues.indexOf(currentValue);
  692. const nextIdx = (currentIdx + 1) % availableValues.length;
  693. const nextValue = availableValues[nextIdx];
  694. userConfig.setItem(key, nextValue);
  695. return nextValue;
  696. },
  697. };
  698. userConfig.init();
  699.  
  700. if (COMMENTS_MD) {
  701. const mdSelectorEl = document.getElementById('comment_markdown');
  702. if (mdSelectorEl) {
  703. if (userConfig.getItem('comment_markdown')) mdSelectorEl.checked = true;
  704. mdSelectorEl.addEventListener('input', () => {
  705. userConfig.setItem('comment_markdown', mdSelectorEl.checked);
  706. });
  707. }
  708. }
  709.  
  710. // надо ли ещё
  711. [...document.querySelectorAll('iframe[src^="https://codepen.io/"]')]
  712. .map(el => el.setAttribute('scrolling', 'no'));
  713.  
  714. // остановка гифок по клику и воспроизведение при повторном клике
  715. function toggleGIF(el) {
  716. // если атрибут со старым линком пуст или отсутствует
  717. if (!el.dataset.oldSrc) {
  718. // заменим ссылку на data-url-svg с треугольником в круге
  719. const w = Math.max(el.clientWidth || 256, 16);
  720. const h = Math.max(el.clientHeight || 128, 16);
  721. const cx = w / 2;
  722. const cy = h / 2;
  723. const r = Math.min(w, h) / 4;
  724. const ax = (r * 61) / 128;
  725. const by = (r * 56) / 128;
  726. const bx = (r * 35) / 128;
  727. const svg = `data:image/svg+xml;utf8,
  728. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  729. <rect x='0' y='0' width='${w}' height='${h}' fill='${GIF_STOP_COLOR_BG}'/>
  730. <circle cx='${cx}' cy='${cy}' r='${r}' fill='${GIF_STOP_COLOR_FG}'/>
  731. <polygon points='${cx + ax} ${cy} ${cx - bx} ${cy - by} ${cx - bx} ${cy + by}' fill='${GIF_STOP_COLOR_BG}' />
  732. </svg>
  733. `;
  734. el.dataset.oldSrc = el.getAttribute('src'); // eslint-disable-line no-param-reassign
  735. el.setAttribute('src', svg);
  736. } else if (GIF_STOP_OVERTYPE) {
  737. // иначе поставим svg с троеточием
  738. const w = el.clientWidth;
  739. const h = el.clientHeight;
  740. const cx = w / 2;
  741. const cy = h / 2;
  742. const r = Math.min(w, h) / 4;
  743. const r2 = r / 4;
  744. const svg = `data:image/svg+xml;utf8,
  745. <svg width='${w}' height='${h}' baseProfile='full' xmlns='http://www.w3.org/2000/svg'>
  746. <rect x='0' y='0' width='${w}' height='${h}' fill='${GIF_STOP_COLOR_BG}'/>
  747. <circle cx='${cx - r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
  748. <circle cx='${cx}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
  749. <circle cx='${cx + r}' cy='${cy}' r='${r2}' fill='${GIF_STOP_COLOR_FG}'/>
  750. </svg>
  751. `;
  752. el.setAttribute('src', svg);
  753. // когда отрендерится троеточие, можно менять на исходную гифку
  754. setTimeout(() => {
  755. if (el.dataset.oldSrc) {
  756. el.setAttribute('src', el.dataset.oldSrc);
  757. el.dataset.oldSrc = ''; // eslint-disable-line no-param-reassign
  758. }
  759. }, 100);
  760. } else {
  761. const img = document.createElement('img');
  762. img.setAttribute('src', el.dataset.oldSrc);
  763. if (el.hasAttribute('align')) {
  764. img.setAttribute('align', el.getAttribute('align'));
  765. }
  766. el.parentNode.insertBefore(img, el);
  767. img.onclick = () => toggleGIF(img); // eslint-disable-line no-param-reassign
  768. el.parentNode.removeChild(el);
  769. }
  770. }
  771.  
  772. if (GIF_STOP) {
  773. [...document.querySelectorAll('.post__text img[src$=".gif"], .comment__message img[src$=".gif"]')]
  774. .forEach((el) => {
  775. if (GIF_STOP_ONLOAD) toggleGIF(el);
  776. el.onclick = () => toggleGIF(el); // eslint-disable-line no-param-reassign
  777. });
  778. }
  779.  
  780. // фиксирование высоты публикации чтобы убрать прыжки прокрутки
  781. if (FIX_JUMPING_SCROLL) {
  782. const postBodyEl = document.querySelector('.post__body_full');
  783. const checkPostBodyInViewport = () => postBodyEl.getBoundingClientRect().bottom > 0;
  784. const autoHeightPost = () => {
  785. if (checkPostBodyInViewport()) {
  786. window.removeEventListener('scroll', autoHeightPost);
  787. postBodyEl.style.height = 'auto';
  788. }
  789. };
  790. if (postBodyEl && !checkPostBodyInViewport()) {
  791. const h = postBodyEl.clientHeight;
  792. postBodyEl.style.height = `${h}px`;
  793. window.addEventListener('scroll', autoHeightPost);
  794. }
  795. }
  796.  
  797. // счетчики кармы
  798. if (KARMA_DETAILS) {
  799. Array.from(document.querySelectorAll('.user-info__stats-item.stacked-counter')).forEach((itemCounter) => {
  800. itemCounter.style.marginRight = '16px'; // eslint-disable-line no-param-reassign
  801. });
  802. Array.from(document.querySelectorAll('.page-header__stats_karma')).forEach((karmaEl) => {
  803. karmaEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  804. karmaEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign
  805. });
  806. Array.from(document.querySelectorAll('.stacked-counter[href="/info/help/karma/"]')).forEach((couterEl) => {
  807. let total = parseInt(couterEl.title, 10);
  808. const scoreEl = couterEl.querySelector('.stacked-counter__value');
  809. if (!scoreEl || !total) return;
  810. couterEl.style.width = 'auto'; // eslint-disable-line no-param-reassign
  811. couterEl.style.minWidth = '84px'; // eslint-disable-line no-param-reassign
  812. const score = parseFloat(scoreEl.innerHTML.replace('–', '-').replace(',', '.'), 10);
  813. if (score > total) total = score;
  814. const likes = (total + score) / 2;
  815. const percent = Math.round((100 * likes) / total);
  816. const details = `&nbsp;= ${total} × (${percent} ${100 - percent})%`;
  817. const detailsEl = document.createElement('span');
  818. detailsEl.innerHTML = details;
  819. detailsEl.style.color = '#545454';
  820. detailsEl.style.fontFamily = '"-apple-system",BlinkMacSystemFont,Arial,sans-serif';
  821. detailsEl.style.fontSize = '13px';
  822. detailsEl.style.fontWeight = 'normal';
  823. detailsEl.style.verticalAlign = 'middle';
  824. scoreEl.appendChild(detailsEl);
  825. couterEl.title += `, ${(likes).toFixed(2)} плюсов и ${(total - likes).toFixed(2)} минусов`; // eslint-disable-line no-param-reassign
  826. });
  827. }
  828.  
  829. // счетчики рейтинга с подробностями
  830. const scoresMap = new Map();
  831.  
  832. class Score {
  833. constructor(el) {
  834. this.el = el;
  835. const data = this.constructor.parse(el);
  836. this.rating = data.rating;
  837. this.total = data.total;
  838. this.likes = data.likes;
  839. this.dislikes = data.dislikes;
  840. this.isDetailed = false;
  841. this.observer = new MutationObserver(() => this.update());
  842. }
  843.  
  844. setDetails(isDetailed) {
  845. if (this.isDetailed === isDetailed) return;
  846. this.isDetailed = isDetailed;
  847. this.update();
  848. }
  849.  
  850. update() {
  851. const data = this.constructor.parse(this.el);
  852. this.rating = data.rating;
  853. this.total = data.total;
  854. this.likes = data.likes;
  855. this.dislikes = data.dislikes;
  856. this.observer.disconnect();
  857. if (this.isDetailed) {
  858. this.details();
  859. } else {
  860. this.simply();
  861. }
  862. this.observer.observe(this.el, { childList: true });
  863. }
  864.  
  865. static parse(el) {
  866. let [total, likes, dislikes] = el
  867. .attributes.title.textContent
  868. .match(/[0-9]+/g).map(Number);
  869. let [, sign, rating] = el.innerHTML.match(/([–]?)(\d+)/); // eslint-disable-line prefer-const
  870. rating = Number(rating);
  871. if (sign) rating = -rating;
  872. // не знаю что там происходит при голосовании, так что на всякий случай
  873. const diff = rating - (likes - dislikes);
  874. if (diff < 0) {
  875. total += Math.abs(diff);
  876. dislikes += Math.abs(diff);
  877. } else if (diff > 0) {
  878. total += diff;
  879. likes += diff;
  880. }
  881. return {
  882. rating,
  883. total,
  884. likes,
  885. dislikes,
  886. };
  887. }
  888.  
  889. simply() {
  890. let innerHTML = '';
  891. if (this.rating > 0) {
  892. innerHTML = `+${this.rating}`;
  893. } else if (this.rating < 0) {
  894. innerHTML = `–${Math.abs(this.rating)}`;
  895. } else {
  896. innerHTML = '0';
  897. }
  898. this.el.innerHTML = innerHTML;
  899. }
  900.  
  901. details() {
  902. let innerHTML = '';
  903. if (this.rating > 0) {
  904. innerHTML = `+${this.rating}`;
  905. } else if (this.rating < 0) {
  906. innerHTML = `–${Math.abs(this.rating)}`;
  907. } else {
  908. innerHTML = '0';
  909. }
  910. if (this.total !== 0) {
  911. let details = '';
  912. if (RATING_DETAILS_PN) {
  913. details = `&nbsp;= ${this.likes} ${this.dislikes}`;
  914. } else {
  915. const percent = Math.round((100 * this.likes) / this.total);
  916. details = `&nbsp;= ${this.total} × (${percent} ${100 - percent})%`;
  917. }
  918. innerHTML += ` <span style='color: #545454; font-weight: normal'>${details}</span>`;
  919. }
  920. this.el.innerHTML = innerHTML;
  921. }
  922. }
  923.  
  924. // парсим их
  925. [...document.querySelectorAll('.voting-wjt__counter')].forEach((el) => {
  926. scoresMap.set(el, new Score(el));
  927. });
  928.  
  929. // добавляем подробностей
  930. if (RATING_DETAILS) {
  931. if (RATING_DETAILS_ONCLICK) {
  932. const isDetailed = userConfig.getItem('scores_details');
  933. if (isDetailed) scoresMap.forEach(score => score.setDetails(isDetailed));
  934. scoresMap.forEach((score) => {
  935. score.el.onclick = () => { // eslint-disable-line no-param-reassign
  936. const nowDetailed = userConfig.shiftItem('scores_details');
  937. scoresMap.forEach(s => s.setDetails(nowDetailed));
  938. };
  939. });
  940. } else {
  941. scoresMap.forEach(score => score.setDetails(true));
  942. }
  943. }
  944.  
  945. // метки времени и работа с ними
  946. const pageLoadTime = new Date();
  947. const monthNames = [
  948. 'января', 'февраля', 'марта',
  949. 'апреля', 'мая', 'июня',
  950. 'июля', 'августа', 'сентября',
  951. 'октября', 'ноября', 'декабря',
  952. ];
  953.  
  954. class HabraTime {
  955. constructor(el, parent) {
  956. this.el = el;
  957. this.parent = parent;
  958. this.attrDatetime = this.constructor.getAttributeDatetime(el);
  959. this.date = new Date(this.attrDatetime);
  960. }
  961.  
  962. // вот было бы хорошо, если б на хабре были datetime атрибуты
  963. static getAttributeDatetime(el) {
  964. const imagination = el.getAttribute('datetime');
  965. if (imagination) return imagination;
  966.  
  967. const re = /((сегодня|вчера)|(\d+)[ .]([а-я]+|\d+)[ .]?(\d+)?) в (\d\d:\d\d)/;
  968. let [,,
  969. recently, // eslint-disable-line prefer-const
  970. day, month, year,
  971. time, // eslint-disable-line prefer-const
  972. ] = el.innerHTML.match(re);
  973.  
  974. // и местное время
  975. let moscow;
  976. if (recently || year === undefined) {
  977. const offsetMoscow = 3 * 60 * 60 * 1000;
  978. const yesterdayShift = (recently === 'вчера') ? 24 * 60 * 60 * 1000 : 0;
  979. const offset = pageLoadTime.getTimezoneOffset() * 60 * 1000;
  980. const value = (pageLoadTime - yesterdayShift) + offsetMoscow + offset;
  981. moscow = new Date(value);
  982. }
  983.  
  984. if (recently) {
  985. day = moscow.getDate();
  986. month = moscow.getMonth() + 1;
  987. } else if (month.length !== 2) {
  988. month = monthNames.indexOf(month) + 1;
  989. } else {
  990. month = +month;
  991. }
  992.  
  993. if (day < 10) day = `0${+day}`;
  994. if (month < 10) month = `0${month}`;
  995. if (year < 100) year = `20${year}`;
  996. if (year === undefined) year = moscow.getFullYear();
  997.  
  998. return `${year}-${month}-${day}T${time}+03:00`;
  999. }
  1000.  
  1001. absolute() {
  1002. let result = '';
  1003.  
  1004. const time = this.date;
  1005. const day = time.getDate();
  1006. const month = time.getMonth();
  1007. const monthName = monthNames[month];
  1008. const year = time.getFullYear();
  1009. const hours = time.getHours();
  1010. const minutes = time.getMinutes();
  1011.  
  1012. const now = new Date();
  1013. const nowDay = now.getDate();
  1014. const nowMonth = now.getMonth();
  1015. const nowYear = now.getFullYear();
  1016.  
  1017. const yesterday = new Date((now - 24) * 60 * 60 * 1000);
  1018. const yesterdayDay = yesterday.getDate();
  1019. const yesterdayMonth = yesterday.getMonth();
  1020. const yesterdayYear = yesterday.getFullYear();
  1021.  
  1022. const hhmm = `${hours}:${minutes >= 10 ? minutes : `0${minutes}`}`;
  1023.  
  1024. const isToday =
  1025. day === nowDay &&
  1026. month === nowMonth &&
  1027. year === nowYear;
  1028. const isYesterday =
  1029. day === yesterdayDay &&
  1030. month === yesterdayMonth &&
  1031. year === yesterdayYear;
  1032.  
  1033. if (isToday) {
  1034. result = `сегодня в ${hhmm}`;
  1035. } else if (isYesterday) {
  1036. result = `вчера в ${hhmm}`;
  1037. } else if (nowYear === year) {
  1038. result = `${day} ${monthName} в ${hhmm}`;
  1039. } else {
  1040. result = `${day} ${monthName} ${year} в ${hhmm}`;
  1041. }
  1042.  
  1043. return result;
  1044. }
  1045.  
  1046. static relative(milliseconds) {
  1047. let result = '';
  1048.  
  1049. const pluralForm = (n, forms) => {
  1050. if (n % 10 === 1 && n % 100 !== 11) return forms[0];
  1051. if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) return forms[1];
  1052. return forms[2];
  1053. };
  1054.  
  1055. const formats = [
  1056. ['год', 'года', 'лет'],
  1057. ['месяц', 'месяца', 'месяцев'],
  1058. ['день', 'дня', 'дней'],
  1059. ['час', 'часа', 'часов'],
  1060. ['минуту', 'минуты', 'минут'],
  1061. ];
  1062.  
  1063. const minutes = milliseconds / 60000;
  1064. const hours = minutes / 60;
  1065. const days = hours / 24;
  1066. const months = days / 30;
  1067. const years = months / 12;
  1068. const idx = [years, months, days, hours, minutes].findIndex(x => x >= 1);
  1069.  
  1070. if (idx === -1) {
  1071. result = 'несколько секунд';
  1072. } else {
  1073. const value = Math.floor([years, months, days, hours, minutes][idx]);
  1074. const forms = formats[idx];
  1075. const form = pluralForm(value, forms);
  1076. result = `${value} ${form}`;
  1077. }
  1078. return result;
  1079. }
  1080.  
  1081. fromNow() {
  1082. const diff = Math.abs(Date.now() - this.date);
  1083. return `${this.constructor.relative(diff)} назад`;
  1084. }
  1085.  
  1086. fromParent() {
  1087. const diff = Math.abs(this.date - this.parent.date);
  1088. return `через ${this.constructor.relative(diff)}`;
  1089. }
  1090. }
  1091.  
  1092. // собираем метки времени
  1093. const datesMap = new Map();
  1094. const megapostTimeEl = document.querySelector('.megapost-head__meta > .list_inline > .list__item');
  1095. (megapostTimeEl ? [megapostTimeEl] : [])
  1096. .concat([...document.querySelectorAll(`
  1097. .post__time,
  1098. .preview-data__time-published,
  1099. time.comment__date-time_published,
  1100. .tm-post__date,
  1101. .user-message__date-time
  1102. `)]).forEach((el) => {
  1103. datesMap.set(el, new HabraTime(el));
  1104. });
  1105.  
  1106. function updateTime() {
  1107. datesMap.forEach((habraTime) => {
  1108. let type;
  1109. let otherTypes;
  1110. if (habraTime.parent) {
  1111. type = userConfig.config.time_comments;
  1112. otherTypes = userConfig.model.time_comments
  1113. .filter(str => str !== type);
  1114. } else {
  1115. type = userConfig.config.time_publications;
  1116. otherTypes = userConfig.model.time_publications
  1117. .filter(str => str !== type);
  1118. }
  1119. const title = otherTypes.map(otherType => habraTime[otherType]()).join(', ');
  1120. habraTime.el.innerHTML = habraTime[type](); // eslint-disable-line no-param-reassign
  1121. habraTime.el.setAttribute('title', title);
  1122. });
  1123. }
  1124.  
  1125. if (TIME_DETAILS) {
  1126. datesMap.forEach((habraTime) => {
  1127. habraTime.el.setAttribute(
  1128. 'style',
  1129. 'cursor: pointer; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;',
  1130. );
  1131. habraTime.el.onclick = () => { // eslint-disable-line no-param-reassign
  1132. if (habraTime.parent) {
  1133. userConfig.shiftItem('time_comments');
  1134. } else {
  1135. userConfig.shiftItem('time_publications');
  1136. }
  1137. updateTime();
  1138. };
  1139. });
  1140. // подождём, когда дерево комментариев будет построено
  1141. // у некоторых меток времени будут установлены родители
  1142. // тогда и обновим их тексты
  1143. setTimeout(updateTime, 100);
  1144. setInterval(updateTime, 30 * 1000);
  1145. }
  1146.  
  1147. // время публикации, понадобится для корня древа комментариев
  1148. let datePublication = datesMap.get(megapostTimeEl || document.querySelector('.post__time'));
  1149. // если нету публикации поищем самую раннюю метку времени
  1150. if (!datePublication) {
  1151. datePublication = { date: pageLoadTime };
  1152. datesMap.forEach((date) => {
  1153. if (date.date < datePublication.date) datePublication = date;
  1154. });
  1155. }
  1156.  
  1157. // создаем дерево комментариев
  1158. class ItemComment {
  1159. constructor(el, parent) {
  1160. this.parent = parent;
  1161. this.el = el;
  1162. this.lvl = parent.lvl + 1;
  1163. this.id = Number(el.getAttribute('rel'));
  1164. this.commentEl = el.querySelector('.comment');
  1165. if (this.commentEl) {
  1166. this.timeEl = this.commentEl.querySelector('time');
  1167. this.ratingEl = this.commentEl.querySelector('.js-score');
  1168. }
  1169. this.date = datesMap.get(this.timeEl);
  1170. if (this.date) {
  1171. this.date.parent = parent.date;
  1172. } else {
  1173. this.date = parent.date;
  1174. }
  1175. this.votes = scoresMap.get(this.ratingEl) || {
  1176. total: 0, likes: 0, dislikes: 0, rating: 0,
  1177. };
  1178. this.elList = el.querySelector('.content-list_nested-comments');
  1179. }
  1180.  
  1181. existId(id) {
  1182. return !!this.elList.querySelector(id);
  1183. }
  1184.  
  1185. existNew() {
  1186. return !!this.elList.querySelector('.js-comment_new');
  1187. }
  1188.  
  1189. getLength() {
  1190. let { length } = this.list;
  1191. this.list.forEach((node) => {
  1192. length += node.getLength();
  1193. });
  1194. return length;
  1195. }
  1196. }
  1197.  
  1198. class CommentsTree {
  1199. constructor() {
  1200. this.root = {
  1201. isRoot: true,
  1202. date: datePublication,
  1203. lvl: 0,
  1204. elList: document.getElementById('comments-list'),
  1205. list: [],
  1206. };
  1207. }
  1208.  
  1209. static exist() {
  1210. return !!document.getElementById('comments-list');
  1211. }
  1212.  
  1213. update() {
  1214. if (!this.root.elList) return;
  1215. const recAdd = (node) => {
  1216. node.list = Array.from(node.elList.children) // eslint-disable-line no-param-reassign
  1217. .map(el => new ItemComment(el, node));
  1218. node.list.forEach(recAdd);
  1219. };
  1220. recAdd(this.root);
  1221. }
  1222.  
  1223. walkTree(fn) {
  1224. const walk = (tree) => {
  1225. fn(tree);
  1226. tree.list.forEach(walk);
  1227. };
  1228. walk(this.root);
  1229. }
  1230.  
  1231. sort(fn) {
  1232. if (!this.root.elList) return;
  1233. this.walkTree((tree) => {
  1234. tree.list.sort(fn).forEach(subtree => tree.elList.appendChild(subtree.el));
  1235. });
  1236. }
  1237.  
  1238. shuffle() {
  1239. if (!this.root.elList) return;
  1240. const randInt = maximum => Math.floor(Math.random() * (maximum + 1));
  1241. this.walkTree((tree) => {
  1242. const { list } = tree;
  1243. for (let i = 0; i < list.length; i += 1) {
  1244. const j = randInt(i);
  1245. [list[i], list[j]] = [list[j], list[i]];
  1246. }
  1247. list.forEach(subtree => tree.elList.appendChild(subtree.el));
  1248. });
  1249. }
  1250. }
  1251.  
  1252. const commentsTree = new CommentsTree();
  1253. commentsTree.update();
  1254.  
  1255. // здесь начинается сортировка комментариев
  1256. const commentsOrderEl = document.createElement('div');
  1257. commentsOrderEl.classList.add('comments_order');
  1258. commentsOrderEl.innerHTML = sortVariants.map(([type, text]) => {
  1259. const underline = (type === 'time') ? 'style="text-decoration: underline;"' : '';
  1260. return `<a href='#' data-order="${type}" ${underline}>${text}</a>`;
  1261. }).join(', ');
  1262.  
  1263. if (COMMENTS_SORT && document.getElementById('comments-list')) {
  1264. const commentsList = document.getElementById('comments-list');
  1265. commentsList.parentElement.insertBefore(commentsOrderEl, commentsList);
  1266. }
  1267.  
  1268. const commentsComparators = {
  1269. time(a, b) {
  1270. return a.id - b.id;
  1271. },
  1272.  
  1273. freshness(a, b) {
  1274. return b.id - a.id;
  1275. },
  1276.  
  1277. rating(a, b) {
  1278. const ascore = a.votes.rating;
  1279. const bscore = b.votes.rating;
  1280. if (bscore !== ascore) return bscore - ascore;
  1281. return b.id - a.id;
  1282. },
  1283.  
  1284. popularity(a, b) {
  1285. const aVotes = a.votes.total;
  1286. const bVotes = b.votes.total;
  1287. if (aVotes !== bVotes) return bVotes - aVotes;
  1288. const aLength = a.getLength();
  1289. const bLength = b.getLength();
  1290. if (aLength !== bLength) return bLength - aLength;
  1291. return b.id - a.id;
  1292. },
  1293.  
  1294. quality(a, b) {
  1295. const aQuality = a.votes.rating / a.votes.total || 0;
  1296. const bQuality = b.votes.rating / b.votes.total || 0;
  1297. if (aQuality !== bQuality) return bQuality - aQuality;
  1298. if (a.votes.rating !== b.votes.rating) return b.votes.rating - a.votes.rating;
  1299. return b.id - a.id;
  1300. },
  1301.  
  1302. trend(a, b) {
  1303. // в первые сутки после публикации статьи число посещений больше чем в остальное время
  1304. const oneDay = 24 * 60 * 60 * 1000;
  1305. const firstDayEnd = +datePublication.date + oneDay;
  1306. // у комментария есть только три дня на голосование с момента его создания
  1307. const threeDays = 3 * oneDay;
  1308. const now = Date.now();
  1309.  
  1310. // прикинем число голосов в первый день
  1311. const aDate = +a.date.date;
  1312. let aViews = 0;
  1313. // в первый день
  1314. if (aDate <= firstDayEnd) {
  1315. aViews += Math.min(firstDayEnd, now) - aDate;
  1316. }
  1317. // и в остальное время
  1318. if (now >= firstDayEnd) {
  1319. const threeDaysEnd = aDate + threeDays;
  1320. // для этого соотношения я собрал статистику
  1321. aViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, aDate)) / 16;
  1322. }
  1323. const aScore = a.votes.rating / aViews;
  1324.  
  1325. // аналогично
  1326. const bDate = +b.date.date;
  1327. let bViews = 0;
  1328. if (bDate <= firstDayEnd) {
  1329. bViews += Math.min(firstDayEnd, now) - bDate;
  1330. }
  1331. if (now >= firstDayEnd) {
  1332. const threeDaysEnd = bDate + threeDays;
  1333. // найти зависимость активности голосования от времени суток не удалось
  1334. bViews += (Math.min(threeDaysEnd, now) - Math.max(firstDayEnd, bDate)) / 16;
  1335. }
  1336. const bScore = b.votes.rating / bViews;
  1337.  
  1338. if (bScore === aScore) return b.id - a.id;
  1339. return bScore - aScore;
  1340. },
  1341. };
  1342.  
  1343. const sortComments = () => {
  1344. const order = userConfig.getItem('comments_order');
  1345.  
  1346. Array.from(commentsOrderEl.children).forEach((el) => {
  1347. if (el.dataset.order === order) {
  1348. el.style.textDecoration = 'underline'; // eslint-disable-line no-param-reassign
  1349. } else {
  1350. el.style.textDecoration = ''; // eslint-disable-line no-param-reassign
  1351. }
  1352. });
  1353.  
  1354. if (order === 'shuffle') {
  1355. commentsTree.shuffle();
  1356. } else {
  1357. const compare = commentsComparators[order];
  1358. commentsTree.sort(compare);
  1359. }
  1360. };
  1361.  
  1362. // сортируем комменты при загрузке страницы
  1363. // или не сортируем, если они уже по порядку
  1364. if (COMMENTS_SORT && COMMENTS_SORT_ONLOAD && userConfig.getItem('comments_order') !== 'time') {
  1365. sortComments();
  1366. }
  1367.  
  1368. Array.from(commentsOrderEl.children).forEach((el) => {
  1369. el.onclick = () => { // eslint-disable-line no-param-reassign
  1370. userConfig.setItem('comments_order', el.dataset.order);
  1371. sortComments();
  1372. };
  1373. });
  1374.  
  1375. // меняем ссылки ведущие к новым комментариям на ссылки к началу комментариев
  1376. if (COMMENTS_LINKS) {
  1377. const commentsLinks = document.getElementsByClassName('post-stats__comments-link');
  1378.  
  1379. for (let i = 0; i < commentsLinks.length; i += 1) {
  1380. const iLink = commentsLinks[i];
  1381. const hrefValue = iLink.getAttribute('href');
  1382. const hrefToComments = hrefValue.replace('#first_unread', '#comments');
  1383. iLink.setAttribute('href', hrefToComments);
  1384. }
  1385. }
  1386.  
  1387. // сворачивание комментов
  1388. if (COMMENTS_HIDE) {
  1389. const commentHash = window.location.hash;
  1390.  
  1391. const toggle = (subtree) => {
  1392. const listLength = subtree.list.length;
  1393. if (listLength === 0) return;
  1394. /* eslint-disable */
  1395. if (subtree.switcherEl.dataset.isVisibleList === 'true') {
  1396. subtree.switcherEl.dataset.isVisibleList = 'false';
  1397. subtree.switcherEl.innerHTML = `\u229E раскрыть ветвь ${subtree.getLength()}`;
  1398. subtree.elList.style.display = 'none';
  1399. } else {
  1400. subtree.switcherEl.dataset.isVisibleList = 'true';
  1401. subtree.switcherEl.innerHTML = '\u229F';
  1402. subtree.elList.style.display = 'block';
  1403. }
  1404. /* eslint-enable */
  1405. };
  1406.  
  1407. const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
  1408.  
  1409. commentsTree.walkTree((subtree) => {
  1410. // не пытаемся сворачивать корень
  1411. if (subtree.isRoot) return;
  1412. // у похищенных нет футера
  1413. const footerEl = subtree.commentEl.querySelector('.comment__footer');
  1414. if (footerEl === null) return;
  1415. // создаём переключатель
  1416. const switcher = document.createElement('a');
  1417. switcher.setAttribute('href', '#');
  1418. switcher.classList.add('comment__footer-link');
  1419. switcher.classList.add('comment__switcher');
  1420. switcher.dataset.isVisibleList = 'true';
  1421.  
  1422. switcher.innerHTML = '\u229F';
  1423. if (subtree.list.length === 0) switcher.innerHTML = '\u22A1';
  1424. switcher.style.marginLeft = isFirefox ? '-4px' : '-6px';
  1425.  
  1426. footerEl.insertBefore(switcher, footerEl.children[0]);
  1427. subtree.switcherEl = switcher; // eslint-disable-line no-param-reassign
  1428.  
  1429. switcher.onclick = () => toggle(subtree);
  1430.  
  1431. const isHideLvl = subtree.lvl === HIDE_LEVEL;
  1432. const isLineLvl = subtree.lvl % LINE_LEN === 0;
  1433. if (isLineLvl) {
  1434. subtree.elList.classList.add('comments_new-line');
  1435. const lineNumber = subtree.lvl / LINE_LEN;
  1436. subtree.elList.classList.add(`comments_new-line-${lineNumber % 4}`);
  1437. }
  1438. // при запуске не сворачиваем ветки с новыми комментами, и содержащие целевой id
  1439. if (
  1440. (isHideLvl || isLineLvl) && !subtree.existNew() &&
  1441. !(commentHash && subtree.existId(commentHash))
  1442. ) {
  1443. toggle(subtree);
  1444. }
  1445. });
  1446. }
  1447.  
  1448. if (SCROLL_LEGEND) {
  1449. const postBodyEl = document.querySelector('.post__body_full') || document.querySelector('.article__body');
  1450. const commentsEl = document.getElementById('comments-list');
  1451. const getPercents = (el) => {
  1452. if (!el) return { topPercent: 0, heightPercent: 0 };
  1453. const pageHeight = document.documentElement.scrollHeight;
  1454. const top = el.getBoundingClientRect().top + window.pageYOffset;
  1455. const topPercent = ((100 * top) / pageHeight).toFixed(2);
  1456. const height = el.clientHeight;
  1457. const heightPercent = ((100 * height) / pageHeight).toFixed(2);
  1458.  
  1459. return { topPercent, heightPercent };
  1460. };
  1461.  
  1462. const updateLegend = (pageEl, legendEl) => {
  1463. const { topPercent, heightPercent } = getPercents(pageEl);
  1464. legendEl.style.top = `${topPercent}%`; // eslint-disable-line no-param-reassign
  1465. legendEl.style.height = `${heightPercent}%`; // eslint-disable-line no-param-reassign
  1466. };
  1467.  
  1468. const legendPost = document.createElement('div');
  1469. legendPost.classList.add('legend_el');
  1470. legendPost.style.background = 'rgba(84, 142, 170, 0.66)';
  1471. updateLegend(postBodyEl, legendPost);
  1472. document.body.appendChild(legendPost);
  1473.  
  1474. const legendComments = document.createElement('div');
  1475. legendComments.classList.add('legend_el');
  1476. legendComments.style.background = 'rgba(49, 176, 7, 0.66)';
  1477. updateLegend(commentsEl, legendComments);
  1478. document.body.appendChild(legendComments);
  1479.  
  1480. setInterval(() => {
  1481. updateLegend(postBodyEl, legendPost);
  1482. updateLegend(commentsEl, legendComments);
  1483. }, 1000);
  1484. }
  1485. });