main.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620
  1. "use strict";
  2. /* globals $, jQuery, shortcut */
  3. /* jshint esversion:6, strict:global */
  4. //<Polyfills>
  5. if (!NodeList.prototype.forEach) NodeList.prototype.forEach = Array.prototype.forEach;
  6. if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.webkitMatchesSelector;
  7. if (!Element.prototype.closest) {
  8. Element.prototype.closest = function (s) {
  9. let el = this;
  10. if (!document.documentElement.contains(el)) return null;
  11. do {
  12. if (el.matches(s)) return el;
  13. el = el.parentElement || el.parentNode;
  14. } while (el !== null && el.nodeType == 1);
  15. return null;
  16. };
  17. }
  18. //</Polyfills>
  19. //<Global variables>
  20. var context, i18n, icons, shortcuts, urls;
  21. (function parseJsonVars() {
  22. const jsonVars = document.getElementById('jsonVars'),
  23. json = JSON.parse(jsonVars.innerHTML);
  24. jsonVars.outerHTML = '';
  25. context = json.context;
  26. i18n = json.i18n;
  27. shortcuts = json.shortcuts;
  28. urls = json.urls;
  29. icons = json.icons;
  30. icons.read = decodeURIComponent(icons.read);
  31. icons.unread = decodeURIComponent(icons.unread);
  32. }());
  33. var $stream = null,
  34. ajax_loading = false,
  35. isCollapsed = true,
  36. $nav_entries = null;
  37. //</Global variables>
  38. function redirect(url, new_tab) {
  39. if (url) {
  40. if (new_tab) {
  41. window.open(url);
  42. } else {
  43. location.href = url;
  44. }
  45. }
  46. }
  47. function needsScroll($elem) {
  48. const $win = $(window),
  49. winTop = $win.scrollTop(),
  50. winHeight = $win.height(),
  51. winBottom = winTop + winHeight,
  52. elemTop = $elem.offset().top,
  53. elemBottom = elemTop + $elem.outerHeight();
  54. return (elemTop < winTop || elemBottom > winBottom) ? elemTop - (winHeight / 2) : 0;
  55. }
  56. function str2int(str) {
  57. if (!str) {
  58. return 0;
  59. }
  60. return parseInt(str.replace(/\D/g, ''), 10) || 0;
  61. }
  62. function numberFormat(nStr) {
  63. if (nStr < 0) {
  64. return 0;
  65. }
  66. // http://www.mredkj.com/javascript/numberFormat.html
  67. nStr += '';
  68. const x = nStr.split('.'),
  69. x2 = x.length > 1 ? '.' + x[1] : '',
  70. rgx = /(\d+)(\d{3})/;
  71. let x1 = x[0];
  72. while (rgx.test(x1)) {
  73. x1 = x1.replace(rgx, '$1' + ' ' + '$2');
  74. }
  75. return x1 + x2;
  76. }
  77. function incLabel(p, inc, spaceAfter) {
  78. const i = str2int(p) + inc;
  79. return i > 0 ? ((spaceAfter ? '' : ' ') + '(' + numberFormat(i) + ')' + (spaceAfter ? ' ' : '')) : '';
  80. }
  81. function incUnreadsFeed(article, feed_id, nb) {
  82. //Update unread: feed
  83. let elem = document.getElementById(feed_id),
  84. feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0,
  85. feed_priority = elem ? str2int(elem.getAttribute('data-priority')) : 0;
  86. if (elem) {
  87. elem.setAttribute('data-unread', feed_unreads + nb);
  88. elem = elem.querySelector('.item-title');
  89. if (elem) {
  90. elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
  91. }
  92. }
  93. //Update unread: category
  94. elem = document.getElementById(feed_id).closest('.category');
  95. feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
  96. if (elem) {
  97. elem.setAttribute('data-unread', feed_unreads + nb);
  98. elem = elem.querySelector('.title');
  99. if (elem) {
  100. elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
  101. }
  102. }
  103. //Update unread: all
  104. if (feed_priority > 0) {
  105. elem = document.querySelector('#aside_feed .all .title');
  106. if (elem) {
  107. feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
  108. elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
  109. }
  110. }
  111. //Update unread: favourites
  112. if (article && article.closest('div').classList.contains('favorite')) {
  113. elem = document.querySelector('#aside_feed .favorites .title');
  114. if (elem) {
  115. feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
  116. elem.setAttribute('data-unread', numberFormat(feed_unreads + nb));
  117. }
  118. }
  119. let isCurrentView = false;
  120. // Update unread: title
  121. document.title = document.title.replace(/^((?:\([ 0-9]+\) )?)/, function (m, p1) {
  122. const feed = document.getElementById(feed_id);
  123. if (article || (feed.closest('.active') && $(feed).siblings('.active').length === 0)) {
  124. isCurrentView = true;
  125. return incLabel(p1, nb, true);
  126. } else if (document.querySelector('.all.active')) {
  127. isCurrentView = feed_priority > 0;
  128. return incLabel(p1, feed_priority > 0 ? nb : 0, true);
  129. } else {
  130. return p1;
  131. }
  132. });
  133. return isCurrentView;
  134. }
  135. function incUnreadsTag(tag_id, nb) {
  136. let t = document.getElementById(tag_id);
  137. if (t) {
  138. let unreads = str2int(t.getAttribute('data-unread'));
  139. t.setAttribute('data-unread', unreads + nb);
  140. t.querySelector('.item-title').setAttribute('data-unread', numberFormat(unreads + nb));
  141. }
  142. t = document.querySelector('.category.tags .title');
  143. if (t) {
  144. let unreads = str2int(t.getAttribute('data-unread'));
  145. t.setAttribute('data-unread', numberFormat(unreads + nb));
  146. }
  147. }
  148. var pending_entries = {},
  149. mark_read_queue = [];
  150. function send_mark_read_queue(queue, asRead) {
  151. $.ajax({
  152. type: 'POST',
  153. url: '.?c=entry&a=read' + (asRead ? '' : '&is_read=0'),
  154. data: {
  155. ajax: true,
  156. _csrf: context.csrf,
  157. 'id[]': queue,
  158. },
  159. }).done(function (data) {
  160. for (let i = queue.length - 1; i >= 0; i--) {
  161. const div = document.getElementById('flux_' + queue[i]),
  162. myIcons = icons;
  163. let inc = 0;
  164. if (div.classList.contains('not_read')) {
  165. div.classList.remove('not_read');
  166. div.querySelectorAll('a.read').forEach(function (a) { a.setAttribute('href', a.getAttribute('href').replace('&is_read=0', '') + '&is_read=1'); });
  167. div.querySelectorAll('a.read > .icon').forEach(function (img) { img.outerHTML = myIcons.read; });
  168. inc--;
  169. } else {
  170. div.classList.add('not_read', 'keep_unread');
  171. div.querySelectorAll('a.read').forEach(function (a) { a.setAttribute('href', a.getAttribute('href').replace('&is_read=1', '')); });
  172. div.querySelectorAll('a.read > .icon').forEach(function (img) { img.outerHTML = myIcons.unread; });
  173. inc++;
  174. }
  175. let feed_link = div.querySelector('.website > a');
  176. if (feed_link) {
  177. let feed_url = feed_link.getAttribute('href');
  178. let feed_id = feed_url.substr(feed_url.lastIndexOf('f_'));
  179. incUnreadsFeed(div, feed_id, inc);
  180. }
  181. delete pending_entries[queue[i]];
  182. }
  183. faviconNbUnread();
  184. if (data.tags) {
  185. let tagIds = Object.keys(data.tags);
  186. for (let i = tagIds.length - 1; i >= 0; i--) {
  187. let tagId = tagIds[i];
  188. incUnreadsTag(tagId, (asRead ? -1 : 1) * data.tags[tagId].length);
  189. }
  190. }
  191. }).fail(function (data) {
  192. openNotification(i18n.notif_request_failed, 'bad');
  193. for (let i = queue.length - 1; i >= 0; i--) {
  194. delete pending_entries[queue[i]];
  195. }
  196. });
  197. }
  198. var send_mark_read_queue_timeout = 0;
  199. function mark_read(div, only_not_read) {
  200. if (!div || !div.id || context.anonymous ||
  201. (only_not_read && !div.classList.contains('not_read'))) {
  202. return false;
  203. }
  204. const entryId = div.id.replace(/^flux_/, '');
  205. if (pending_entries[entryId]) {
  206. return false;
  207. }
  208. pending_entries[entryId] = true;
  209. const asRead = div.classList.contains('not_read');
  210. if (asRead) {
  211. mark_read_queue.push(entryId);
  212. if (send_mark_read_queue_timeout == 0) {
  213. send_mark_read_queue_timeout = setTimeout(function () {
  214. send_mark_read_queue_timeout = 0;
  215. const queue = mark_read_queue.slice(0);
  216. mark_read_queue = [];
  217. send_mark_read_queue(queue, asRead);
  218. }, 1000);
  219. }
  220. } else {
  221. const queue = [ entryId ];
  222. send_mark_read_queue(queue, asRead);
  223. }
  224. }
  225. function mark_favorite(div) {
  226. if (!div) {
  227. return false;
  228. }
  229. let a = div.querySelector('a.bookmark'),
  230. url = a ? a.getAttribute('href') : '';
  231. if (!url) {
  232. return false;
  233. }
  234. if (pending_entries[div.id]) {
  235. return false;
  236. }
  237. pending_entries[div.id] = true;
  238. $.ajax({
  239. type: 'POST',
  240. url: url,
  241. data: {
  242. ajax: true,
  243. _csrf: context.csrf,
  244. },
  245. }).done(function (data) {
  246. let inc = 0;
  247. if (div.classList.contains('favorite')) {
  248. div.classList.remove('favorite');
  249. inc--;
  250. } else {
  251. div.classList.add('favorite');
  252. inc++;
  253. }
  254. div.querySelectorAll('a.bookmark').forEach(function (a) { a.setAttribute('href', data.url); });
  255. div.querySelectorAll('a.bookmark > .icon').forEach(function (img) { img.outerHTML = data.icon; });
  256. const favourites = $('#aside_feed .favorites .title').contents().last().get(0);
  257. if (favourites && favourites.textContent) {
  258. favourites.textContent = favourites.textContent.replace(/((?: \([ 0-9]+\))?\s*)$/, function (m, p1) {
  259. return incLabel(p1, inc, false);
  260. });
  261. }
  262. if (div.classList.contains('not_read')) {
  263. const elem = document.querySelector('#aside_feed .favorites .title'),
  264. feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
  265. if (elem) {
  266. elem.setAttribute('data-unread', numberFormat(feed_unreads + inc));
  267. }
  268. }
  269. delete pending_entries[div.id];
  270. }).fail(function (data) {
  271. openNotification(i18n.notif_request_failed, 'bad');
  272. delete pending_entries[div.id];
  273. });
  274. }
  275. function toggleContent($new_active, $old_active, skipping) {
  276. // If skipping, move current without activating or marking as read
  277. if ($new_active.length === 0) {
  278. return;
  279. }
  280. if (context.does_lazyload && !skipping) {
  281. $new_active.find('img[data-original], iframe[data-original]').each(function () {
  282. this.setAttribute('src', this.getAttribute('data-original'));
  283. this.removeAttribute('data-original');
  284. });
  285. }
  286. if ($old_active[0] !== $new_active[0]) {
  287. if (isCollapsed && !skipping) { // BUG?: isCollapsed can only ever be true
  288. $new_active.addClass('active');
  289. }
  290. $old_active.removeClass('active current');
  291. $new_active.addClass('current');
  292. if (context.auto_remove_article && !$old_active.hasClass('not_read') && !skipping) {
  293. auto_remove($old_active);
  294. }
  295. } else { // collapse_entry calls toggleContent(flux_current, flux_current, false)
  296. $new_active.toggleClass('active');
  297. }
  298. const relative_move = context.current_view === 'global',
  299. $box_to_move = $(relative_move ? '#panel' : 'html,body');
  300. if (context.sticky_post) {
  301. let prev_article = $new_active.prevAll('.flux'),
  302. new_pos = $new_active.offset().top,
  303. old_scroll = $box_to_move.scrollTop();
  304. if (prev_article.length > 0 && new_pos - prev_article.offset().top <= 150) {
  305. new_pos = prev_article.offset().top;
  306. if (relative_move) {
  307. new_pos -= $box_to_move.offset().top;
  308. }
  309. }
  310. if (skipping) {
  311. // when skipping, this feels more natural if it's not so near the top
  312. new_pos -= $(window).height() / 4;
  313. }
  314. if (context.hide_posts) {
  315. if (relative_move) {
  316. new_pos += old_scroll;
  317. }
  318. $new_active.children('.flux_content').first().each(function () {
  319. $box_to_move.scrollTop(new_pos).scrollTop();
  320. });
  321. } else {
  322. if (relative_move) {
  323. new_pos += old_scroll;
  324. }
  325. $box_to_move.scrollTop(new_pos).scrollTop();
  326. }
  327. }
  328. if (context.auto_mark_article && $new_active.hasClass('active') && !skipping) {
  329. mark_read($new_active[0], true);
  330. }
  331. }
  332. function auto_remove($element) {
  333. let $p = $element.prev(),
  334. $n = $element.next();
  335. if ($p.hasClass('day') && $n.hasClass('day')) {
  336. $p.remove();
  337. }
  338. $element.remove();
  339. $('#stream > .flux:not(.not_read):not(.active)').remove();
  340. }
  341. function prev_entry() {
  342. let $old_active = $('.flux.current'),
  343. $new_active = $old_active.length === 0 ? $('.flux:last') : $old_active.prevAll('.flux:first');
  344. toggleContent($new_active, $old_active, false);
  345. }
  346. function next_entry() {
  347. let $old_active = $('.flux.current'),
  348. $new_active = $old_active.length === 0 ? $('.flux:first') : $old_active.nextAll('.flux:first');
  349. toggleContent($new_active, $old_active, false);
  350. if ($new_active.nextAll().length < 3) {
  351. load_more_posts();
  352. }
  353. }
  354. function skip_prev_entry() {
  355. let $old_active = $('.flux.current'),
  356. $new_active = $old_active.length === 0 ? $('.flux:last') : $old_active.prevAll('.flux:first');
  357. toggleContent($new_active, $old_active, true);
  358. }
  359. function skip_next_entry() {
  360. let $old_active = $('.flux.current'),
  361. $new_active = $old_active.length === 0 ? $('.flux:first') : $old_active.nextAll('.flux:first');
  362. toggleContent($new_active, $old_active, true);
  363. if ($new_active.nextAll().length < 3) {
  364. load_more_posts();
  365. }
  366. }
  367. function prev_feed() {
  368. let $active_feed = $('#aside_feed .tree-folder-items .item.active');
  369. if ($active_feed.length > 0) {
  370. $active_feed.prevAll(':visible:first').find('a').each(function () { this.click(); });
  371. } else {
  372. last_feed();
  373. }
  374. }
  375. function next_feed() {
  376. let $active_feed = $('#aside_feed .tree-folder-items .item.active');
  377. if ($active_feed.length > 0) {
  378. $active_feed.nextAll(':visible:first').find('a').each(function () { this.click(); });
  379. } else {
  380. first_feed();
  381. }
  382. }
  383. function first_feed() {
  384. let $feed = $('#aside_feed .tree-folder-items.active .item:visible:first');
  385. if ($feed.length > 0) {
  386. $feed.find('a')[1].click();
  387. }
  388. }
  389. function last_feed() {
  390. let $feed = $('#aside_feed .tree-folder-items.active .item:visible:last');
  391. if ($feed.length > 0) {
  392. $feed.find('a')[1].click();
  393. }
  394. }
  395. function prev_category() {
  396. let $active_cat = $('#aside_feed .tree-folder.active');
  397. if ($active_cat.length > 0) {
  398. let $prev_cat = $active_cat.prevAll(':visible:first').find('.tree-folder-title .title');
  399. if ($prev_cat.length > 0) {
  400. $prev_cat[0].click();
  401. }
  402. } else {
  403. last_category();
  404. }
  405. return;
  406. }
  407. function next_category() {
  408. let $active_cat = $('#aside_feed .tree-folder.active');
  409. if ($active_cat.length > 0) {
  410. let $next_cat = $active_cat.nextAll(':visible:first').find('.tree-folder-title .title');
  411. if ($next_cat.length > 0) {
  412. $next_cat[0].click();
  413. }
  414. } else {
  415. first_category();
  416. }
  417. return;
  418. }
  419. function first_category() {
  420. let $cat = $('#aside_feed .tree-folder:visible:first');
  421. if ($cat.length > 0) {
  422. $cat.find('.tree-folder-title .title')[0].click();
  423. }
  424. }
  425. function last_category() {
  426. let $cat = $('#aside_feed .tree-folder:visible:last');
  427. if ($cat.length > 0) {
  428. $cat.find('.tree-folder-title .title')[0].click();
  429. }
  430. }
  431. function collapse_entry() {
  432. let $flux_current = $('.flux.current');
  433. toggleContent($flux_current, $flux_current, false);
  434. }
  435. function user_filter(key) {
  436. const filter = $('#dropdown-query'),
  437. filters = filter.siblings('.dropdown-menu').find('.item.query a');
  438. if (typeof key === 'undefined') {
  439. if (!filter.length) {
  440. return;
  441. }
  442. // Display the filter div
  443. location.hash = filter.attr('id');
  444. // Force scrolling to the filter div
  445. const scroll = needsScroll($('.header'));
  446. if (scroll !== 0) {
  447. $('html,body').scrollTop(scroll);
  448. }
  449. // Force the key value if there is only one action, so we can trigger it automatically
  450. if (filters.length === 1) {
  451. key = 1;
  452. } else {
  453. return;
  454. }
  455. }
  456. // Trigger selected share action
  457. key = parseInt(key);
  458. if (key <= filters.length) {
  459. filters[key - 1].click();
  460. }
  461. }
  462. function auto_share(key) {
  463. const $share = $('.flux.current.active').find('.dropdown-target[id^="dropdown-share"]'),
  464. $shares = $share.siblings('.dropdown-menu').find('.item a');
  465. if (typeof key === 'undefined') {
  466. if (!$share.length) {
  467. return;
  468. }
  469. // Display the share div
  470. location.hash = $share.attr('id');
  471. // Force scrolling to the share div
  472. const scroll = needsScroll($share.closest('.bottom'));
  473. if (scroll !== 0) {
  474. $('html,body').scrollTop(scroll);
  475. }
  476. // Force the key value if there is only one action, so we can trigger it automatically
  477. if ($shares.length === 1) {
  478. key = 1;
  479. } else {
  480. return;
  481. }
  482. }
  483. // Trigger selected share action and hide the share div
  484. key = parseInt(key);
  485. if (key <= $shares.length) {
  486. $shares[key - 1].click();
  487. $share.siblings('.dropdown-menu').find('.dropdown-close a')[0].click();
  488. }
  489. }
  490. function scrollAsRead($box_to_follow) {
  491. const minTop = 40 + (context.current_view === 'global' ? $box_to_follow.offset().top : $box_to_follow.scrollTop());
  492. $('.not_read:not(.keep_unread):visible').each(function () {
  493. const $this = $(this);
  494. if ($this.offset().top + $this.height() < minTop) {
  495. mark_read(this, true);
  496. }
  497. });
  498. }
  499. function init_posts() {
  500. let $box_to_follow = context.current_view === 'global' ? $('#panel') : $(window);
  501. if (context.auto_mark_scroll) {
  502. let lastScroll = 0, //Throttle
  503. timerId = 0;
  504. $box_to_follow.scroll(function () {
  505. clearTimeout(timerId);
  506. if (lastScroll + 500 < Date.now()) {
  507. lastScroll = Date.now();
  508. scrollAsRead($box_to_follow);
  509. } else {
  510. timerId = setTimeout(function () {
  511. scrollAsRead($box_to_follow);
  512. }, 500);
  513. }
  514. });
  515. }
  516. if (context.auto_load_more) {
  517. $box_to_follow.scroll(function () {
  518. const $load_more = $('#load_more');
  519. if (!$load_more.is(':visible')) {
  520. return;
  521. }
  522. const boxBot = $box_to_follow.scrollTop() + $box_to_follow.height(),
  523. load_more_top = $load_more.offset().top;
  524. if (boxBot >= load_more_top) {
  525. load_more_posts();
  526. }
  527. });
  528. $box_to_follow.scroll();
  529. }
  530. }
  531. function init_column_categories() {
  532. if (context.current_view !== 'normal' && context.current_view !== 'reader') {
  533. return;
  534. }
  535. $('#aside_feed').on('click', '.tree-folder>.tree-folder-title>a.dropdown-toggle', function () {
  536. $(this).children().each(function () {
  537. if (this.alt === '▽') {
  538. this.src = this.src.replace('/icons/down.', '/icons/up.');
  539. this.alt = '△';
  540. } else {
  541. this.src = this.src.replace('/icons/up.', '/icons/down.');
  542. this.alt = '▽';
  543. }
  544. });
  545. $(this).parent().next('.tree-folder-items').slideToggle(300, function () {
  546. //Workaround for Gecko bug in Firefox 64-65(+?):
  547. const sidebar = document.getElementById('sidebar');
  548. if (sidebar && sidebar.scrollHeight > sidebar.clientHeight && //if needs scrollbar
  549. sidebar.scrollWidth >= sidebar.offsetWidth) { //but no scrollbar
  550. sidebar.style['overflow-y'] = 'scroll'; //then force scrollbar
  551. setTimeout(function () { sidebar.style['overflow-y'] = ''; }, 0);
  552. }
  553. });
  554. return false;
  555. });
  556. $('#aside_feed').on('click', '.tree-folder-items .feed .dropdown-toggle', function () {
  557. const itemId = $(this).closest('.item').attr('id'),
  558. templateId = itemId.substring(0, 2) === 't_' ? 'tag_config_template' : 'feed_config_template',
  559. id = itemId.substr(2),
  560. feed_web = $(this).data('fweb'),
  561. template = $('#' + templateId)
  562. .html().replace(/------/g, id).replace('http://example.net/', feed_web);
  563. if ($(this).next('.dropdown-menu').length === 0) {
  564. $(this).attr('href', '#dropdown-' + id).prev('.dropdown-target').attr('id', 'dropdown-' + id).parent()
  565. .append(template).find('button.confirm').removeAttr('disabled');
  566. } else {
  567. if ($(this).next('.dropdown-menu').css('display') === 'none') {
  568. const id2 = $(this).closest('.item').attr('id').substr(2);
  569. $(this).attr('href', '#dropdown-' + id2);
  570. } else {
  571. $(this).attr('href', '#close');
  572. }
  573. }
  574. });
  575. }
  576. function init_shortcuts() {
  577. if (!(window.shortcut)) {
  578. if (window.console) {
  579. console.log('FreshRSS waiting for shortcut.js…');
  580. }
  581. setTimeout(init_shortcuts, 200);
  582. return;
  583. }
  584. // Manipulation shortcuts
  585. shortcut.add(shortcuts.mark_read, function () {
  586. // Toggle the read state
  587. mark_read(document.querySelector('.flux.current'), false);
  588. }, {
  589. 'disable_in_input': true
  590. });
  591. shortcut.add('shift+' + shortcuts.mark_read, function () {
  592. // Mark everything as read
  593. $('.nav_menu .read_all').click();
  594. }, {
  595. 'disable_in_input': true
  596. });
  597. shortcut.add(shortcuts.mark_favorite, function () {
  598. // Toggle the favorite state
  599. mark_favorite(document.querySelector('.flux.current'));
  600. }, {
  601. 'disable_in_input': true
  602. });
  603. shortcut.add(shortcuts.collapse_entry, function () {
  604. // Toggle the collapse state
  605. collapse_entry();
  606. }, {
  607. 'disable_in_input': true
  608. });
  609. shortcut.add(shortcuts.auto_share, function () {
  610. // Display the share options
  611. auto_share();
  612. }, {
  613. 'disable_in_input': true
  614. });
  615. shortcut.add(shortcuts.user_filter, function () {
  616. // Display the user filters
  617. user_filter();
  618. }, {
  619. 'disable_in_input': true
  620. });
  621. function addShortcut(evt) {
  622. if ($('#dropdown-query').siblings('.dropdown-menu').is(':visible')) {
  623. user_filter(String.fromCharCode(evt.keyCode));
  624. } else {
  625. auto_share(String.fromCharCode(evt.keyCode));
  626. }
  627. }
  628. for (let i = 1; i < 10; i++) {
  629. shortcut.add(i.toString(), addShortcut, {
  630. 'disable_in_input': true
  631. });
  632. }
  633. // Entry navigation shortcuts
  634. shortcut.add(shortcuts.prev_entry, prev_entry, {
  635. 'disable_in_input': true
  636. });
  637. shortcut.add(shortcuts.skip_prev_entry, skip_prev_entry, {
  638. 'disable_in_input': true
  639. });
  640. shortcut.add(shortcuts.first_entry, function () {
  641. const $old_active = $('.flux.current'),
  642. $first = $('.flux:first');
  643. if ($first.hasClass('flux')) {
  644. toggleContent($first, $old_active, false);
  645. }
  646. }, {
  647. 'disable_in_input': true
  648. });
  649. shortcut.add(shortcuts.next_entry, next_entry, {
  650. 'disable_in_input': true
  651. });
  652. shortcut.add(shortcuts.skip_next_entry, skip_next_entry, {
  653. 'disable_in_input': true
  654. });
  655. shortcut.add(shortcuts.last_entry, function () {
  656. const $old_active = $('.flux.current'),
  657. $last = $('.flux:last');
  658. if ($last.hasClass('flux')) {
  659. toggleContent($last, $old_active, false);
  660. }
  661. }, {
  662. 'disable_in_input': true
  663. });
  664. // Feed navigation shortcuts
  665. shortcut.add('shift+' + shortcuts.prev_entry, prev_feed, {
  666. 'disable_in_input': true
  667. });
  668. shortcut.add('shift+' + shortcuts.next_entry, next_feed, {
  669. 'disable_in_input': true
  670. });
  671. shortcut.add('shift+' + shortcuts.first_entry, first_feed, {
  672. 'disable_in_input': true
  673. });
  674. shortcut.add('shift+' + shortcuts.last_entry, last_feed, {
  675. 'disable_in_input': true
  676. });
  677. // Category navigation shortcuts
  678. shortcut.add('alt+' + shortcuts.prev_entry, prev_category, {
  679. 'disable_in_input': true
  680. });
  681. shortcut.add('alt+' + shortcuts.next_entry, next_category, {
  682. 'disable_in_input': true
  683. });
  684. shortcut.add('alt+' + shortcuts.first_entry, first_category, {
  685. 'disable_in_input': true
  686. });
  687. shortcut.add('alt+' + shortcuts.last_entry, last_category, {
  688. 'disable_in_input': true
  689. });
  690. shortcut.add(shortcuts.go_website, function () {
  691. const url_website = $('.flux.current a.go_website').attr('href');
  692. if (context.auto_mark_site) {
  693. $('.flux.current').each(function () {
  694. mark_read(this, true);
  695. });
  696. }
  697. redirect(url_website, true);
  698. }, {
  699. 'disable_in_input': true
  700. });
  701. shortcut.add(shortcuts.load_more, function () {
  702. load_more_posts();
  703. }, {
  704. 'disable_in_input': true
  705. });
  706. shortcut.add(shortcuts.focus_search, function () {
  707. focus_search();
  708. }, {
  709. 'disable_in_input': true
  710. });
  711. shortcut.add(shortcuts.help, function () {
  712. redirect(urls.help, true);
  713. }, {
  714. 'disable_in_input': true
  715. });
  716. shortcut.add(shortcuts.close_dropdown, function () {
  717. location.hash = null;
  718. }, {
  719. 'disable_in_input': true
  720. });
  721. shortcut.add(shortcuts.normal_view, function () {
  722. $('#nav_menu_views .view-normal').get(0).click();
  723. }, {
  724. 'disable_in_input': true
  725. });
  726. shortcut.add(shortcuts.global_view, function () {
  727. $('#nav_menu_views .view-global').get(0).click();
  728. }, {
  729. 'disable_in_input': true
  730. });
  731. shortcut.add(shortcuts.reading_view, function () {
  732. $('#nav_menu_views .view-reader').get(0).click();
  733. }, {
  734. 'disable_in_input': true
  735. });
  736. shortcut.add(shortcuts.rss_view, function () {
  737. $('#nav_menu_views .view-rss').get(0).click();
  738. }, {
  739. 'disable_in_input': true
  740. });
  741. }
  742. function init_stream(divStream) {
  743. divStream.on('click', '.flux_header,.flux_content', function (e) { //flux_toggle
  744. if ($(e.target).closest('.content, .item.website, .item.link, .dropdown-menu').length > 0) {
  745. return;
  746. }
  747. if (!context.sides_close_article && $(e.target).is('div.flux_content')) {
  748. // setting for not-closing after clicking outside article area
  749. return;
  750. }
  751. const old_active = document.querySelector('.flux.current'),
  752. new_active = this.parentNode;
  753. isCollapsed = true;
  754. if (e.target.tagName.toUpperCase() === 'A') { //Leave real links alone
  755. if (context.auto_mark_article) {
  756. mark_read(new_active, true);
  757. }
  758. return true;
  759. }
  760. toggleContent($(new_active), $(old_active), false);
  761. });
  762. divStream.on('click', '.flux a.read', function () {
  763. const $active = $(this).parents('.flux');
  764. if (context.auto_remove_article && $active.hasClass('not_read')) {
  765. auto_remove($active);
  766. }
  767. mark_read($active[0], false);
  768. return false;
  769. });
  770. divStream.on('click', '.flux a.bookmark', function () {
  771. mark_favorite(this.closest('.flux'));
  772. return false;
  773. });
  774. divStream.on('click', '.item.title > a', function (e) {
  775. // Allow default control-click behaviour such as open in backround-tab.
  776. return e.ctrlKey;
  777. });
  778. divStream.on('mouseup', '.item.title > a', function (e) {
  779. // Mouseup enables us to catch middle click.
  780. if (e.ctrlKey) {
  781. // CTRL+click, it will be manage by previous rule.
  782. return;
  783. }
  784. if (e.which == 2) {
  785. // If middle click, we want same behaviour as CTRL+click.
  786. const ev = jQuery.Event('click');
  787. ev.ctrlKey = true;
  788. $(this).trigger(ev);
  789. } else if(e.which == 1) {
  790. // Normal click, just toggle article.
  791. $(this).parent().click();
  792. }
  793. });
  794. divStream.on('click', '.flux .content a', function () {
  795. if (!$(this).closest('div').hasClass('author')) {
  796. $(this).attr('target', '_blank').attr('rel', 'noreferrer');
  797. }
  798. });
  799. if (context.auto_mark_site) {
  800. // catch mouseup instead of click so we can have the correct behaviour
  801. // with middle button click (scroll button).
  802. divStream.on('mouseup', '.flux .link > a', function (e) {
  803. if (e.which == 3) {
  804. return;
  805. }
  806. mark_read(this.closest('.flux'), true);
  807. });
  808. }
  809. }
  810. function init_nav_entries() {
  811. $nav_entries = $('#nav_entries');
  812. $nav_entries.find('.previous_entry').click(function () {
  813. prev_entry();
  814. return false;
  815. });
  816. $nav_entries.find('.next_entry').click(function () {
  817. next_entry();
  818. return false;
  819. });
  820. $nav_entries.find('.up').click(function () {
  821. const $active_item = $('.flux.current'),
  822. windowTop = $(window).scrollTop(),
  823. item_top = $active_item.offset().top;
  824. if (windowTop > item_top) {
  825. $('html,body').scrollTop(item_top);
  826. } else {
  827. $('html,body').scrollTop(0);
  828. }
  829. return false;
  830. });
  831. }
  832. function loadDynamicTags($div) {
  833. $div.removeClass('dynamictags');
  834. $div.find('li.item').remove();
  835. const entryId = $div.closest('div.flux').attr('id').replace(/^flux_/, '');
  836. $.getJSON('./?c=tag&a=getTagsForEntry&id_entry=' + entryId)
  837. .done(function (data) {
  838. const $ul = $div.find('.dropdown-menu');
  839. $ul.append('<li class="item"><label><input class="checkboxTag" name="t_0" type="checkbox" /> <input type="text" name="newTag" /></label></li>');
  840. if (data && data.length) {
  841. for (let i = 0; i < data.length; i++) {
  842. const tag = data[i];
  843. $ul.append('<li class="item"><label><input class="checkboxTag" name="t_' + tag.id + '" type="checkbox"' +
  844. (tag.checked ? ' checked="checked"' : '') + '> ' + tag.name + '</label></li>');
  845. }
  846. }
  847. })
  848. .fail(function () {
  849. $div.find('li.item').remove();
  850. $div.addClass('dynamictags');
  851. });
  852. }
  853. function init_dynamic_tags() {
  854. $stream.on('click', '.dynamictags', function () {
  855. loadDynamicTags($(this));
  856. });
  857. $stream.on('change', '.checkboxTag', function (ev) {
  858. const $checkbox = $(this),
  859. isChecked = $checkbox.prop('checked'),
  860. tagId = $checkbox.attr('name').replace(/^t_/, ''),
  861. tagName = $checkbox.siblings('input[name]').val(),
  862. $entry = $checkbox.closest('div.flux'),
  863. entryId = $entry.attr('id').replace(/^flux_/, '');
  864. $checkbox.prop('disabled', true);
  865. $.ajax({
  866. type: 'POST',
  867. url: './?c=tag&a=tagEntry',
  868. data: {
  869. _csrf: context.csrf,
  870. id_tag: tagId,
  871. name_tag: tagId == 0 ? tagName : '',
  872. id_entry: entryId,
  873. checked: isChecked,
  874. },
  875. })
  876. .done(function () {
  877. if ($entry.hasClass('not_read')) {
  878. incUnreadsTag('t_' + tagId, isChecked ? 1 : -1);
  879. }
  880. })
  881. .fail(function () {
  882. $checkbox.prop('checked', !isChecked);
  883. })
  884. .always(function () {
  885. $checkbox.prop('disabled', false);
  886. if (tagId == 0) {
  887. loadDynamicTags($checkbox.closest('div.dropdown'));
  888. }
  889. });
  890. });
  891. }
  892. // <actualize>
  893. var feed_processed = 0;
  894. function updateFeed(feeds, feeds_count) {
  895. const feed = feeds.pop();
  896. if (!feed) {
  897. return;
  898. }
  899. $.ajax({
  900. type: 'POST',
  901. url: feed.url,
  902. data: {
  903. _csrf: context.csrf,
  904. noCommit: 1,
  905. },
  906. }).always(function (data) {
  907. feed_processed++;
  908. $('#actualizeProgress .progress').html(feed_processed + ' / ' + feeds_count);
  909. $('#actualizeProgress .title').html(feed.title);
  910. if (feed_processed === feeds_count) {
  911. $.ajax({ //Empty request to commit new articles
  912. type: 'POST',
  913. url: './?c=feed&a=actualize&id=-1&ajax=1',
  914. data: {
  915. _csrf: context.csrf,
  916. noCommit: 0,
  917. },
  918. }).always(function (data) {
  919. location.reload();
  920. });
  921. } else {
  922. updateFeed(feeds, feeds_count);
  923. }
  924. });
  925. }
  926. function init_actualize() {
  927. let auto = false;
  928. $('#actualize').click(function () {
  929. if (ajax_loading) {
  930. return false;
  931. }
  932. ajax_loading = true;
  933. $.getJSON('./?c=javascript&a=actualize').done(function (data) {
  934. if (auto && data.feeds.length < 1) {
  935. auto = false;
  936. ajax_loading = false;
  937. return false;
  938. }
  939. if (data.feeds.length === 0) {
  940. openNotification(data.feedback_no_refresh, 'good');
  941. $.ajax({ //Empty request to force refresh server database cache
  942. type: 'POST',
  943. url: './?c=feed&a=actualize&id=-1&ajax=1',
  944. data: {
  945. _csrf: context.csrf,
  946. noCommit: 0,
  947. },
  948. }).always(function (data) {
  949. ajax_loading = false;
  950. });
  951. return;
  952. }
  953. //Progress bar
  954. const feeds_count = data.feeds.length;
  955. $('body').after('<div id="actualizeProgress" class="notification good">' + data.feedback_actualize +
  956. '<br /><span class="title">/</span><br /><span class="progress">0 / ' + feeds_count +
  957. '</span></div>');
  958. for (let i = 10; i > 0; i--) {
  959. updateFeed(data.feeds, feeds_count);
  960. }
  961. });
  962. return false;
  963. });
  964. if (context.auto_actualize_feeds) {
  965. auto = true;
  966. $('#actualize').click();
  967. }
  968. }
  969. // </actualize>
  970. // <notification>
  971. var $notification = null,
  972. notification_interval = null,
  973. notification_working = false;
  974. function openNotification(msg, status) {
  975. if (notification_working === true) {
  976. return false;
  977. }
  978. notification_working = true;
  979. $notification.removeClass();
  980. $notification.addClass('notification');
  981. $notification.addClass(status);
  982. $notification.find('.msg').html(msg);
  983. $notification.fadeIn(300);
  984. notification_interval = setTimeout(closeNotification, 4000);
  985. }
  986. function closeNotification() {
  987. $notification.fadeOut(600, function () {
  988. $notification.removeClass();
  989. $notification.addClass('closed');
  990. clearInterval(notification_interval);
  991. notification_working = false;
  992. });
  993. }
  994. function init_notifications() {
  995. $notification = $('#notification');
  996. $notification.find('a.close').click(function () {
  997. closeNotification();
  998. return false;
  999. });
  1000. if ($notification.find('.msg').html().length > 0) {
  1001. notification_working = true;
  1002. notification_interval = setTimeout(closeNotification, 4000);
  1003. }
  1004. }
  1005. // </notification>
  1006. // <notifs html5>
  1007. var notifs_html5_permission = 'denied';
  1008. function notifs_html5_is_supported() {
  1009. return window.Notification !== undefined;
  1010. }
  1011. function notifs_html5_ask_permission() {
  1012. window.Notification.requestPermission(function () {
  1013. notifs_html5_permission = window.Notification.permission;
  1014. });
  1015. }
  1016. function notifs_html5_show(nb) {
  1017. if (notifs_html5_permission !== 'granted') {
  1018. return;
  1019. }
  1020. const notification = new window.Notification(i18n.notif_title_articles, {
  1021. icon: '../themes/icons/favicon-256.png',
  1022. body: i18n.notif_body_articles.replace('%d', nb),
  1023. tag: 'freshRssNewArticles',
  1024. });
  1025. notification.onclick = function () {
  1026. location.reload();
  1027. window.focus();
  1028. notification.close();
  1029. };
  1030. if (context.html5_notif_timeout !== 0) {
  1031. setTimeout(function () {
  1032. notification.close();
  1033. }, context.html5_notif_timeout * 1000);
  1034. }
  1035. }
  1036. function init_notifs_html5() {
  1037. if (!notifs_html5_is_supported()) {
  1038. return;
  1039. }
  1040. notifs_html5_permission = notifs_html5_ask_permission();
  1041. }
  1042. // </notifs html5>
  1043. function refreshUnreads() {
  1044. $.getJSON('./?c=javascript&a=nbUnreadsPerFeed').done(function (data) {
  1045. const isAll = document.querySelector('.category.all.active');
  1046. let new_articles = false;
  1047. $.each(data.feeds, function (feed_id, nbUnreads) {
  1048. feed_id = 'f_' + feed_id;
  1049. const elem = document.getElementById(feed_id),
  1050. feed_unreads = elem ? str2int(elem.getAttribute('data-unread')) : 0;
  1051. if ((incUnreadsFeed(null, feed_id, nbUnreads - feed_unreads) || isAll) && //Update of current view?
  1052. (nbUnreads - feed_unreads > 0)) {
  1053. $('#new-article').attr('aria-hidden', 'false').show();
  1054. new_articles = true;
  1055. }
  1056. });
  1057. let nbUnreadTags = 0;
  1058. $.each(data.tags, function (tag_id, nbUnreads) {
  1059. nbUnreadTags += nbUnreads;
  1060. $('#t_' + tag_id).attr('data-unread', nbUnreads)
  1061. .children('.item-title').attr('data-unread', numberFormat(nbUnreads));
  1062. });
  1063. $('.category.tags').attr('data-unread', nbUnreadTags)
  1064. .find('.title').attr('data-unread', numberFormat(nbUnreadTags));
  1065. const nb_unreads = str2int($('.category.all .title').attr('data-unread'));
  1066. if (nb_unreads > 0 && new_articles) {
  1067. faviconNbUnread(nb_unreads);
  1068. notifs_html5_show(nb_unreads);
  1069. }
  1070. });
  1071. }
  1072. //<endless_mode>
  1073. var url_load_more = '',
  1074. load_more = false,
  1075. box_load_more = null;
  1076. function load_more_posts() {
  1077. if (load_more || url_load_more === '' || box_load_more === null) {
  1078. return;
  1079. }
  1080. load_more = true;
  1081. document.getElementById('load_more').classList.add('loading');
  1082. $.get(url_load_more, function (data) {
  1083. box_load_more.children('.flux:last').after($('#stream', data).children('.flux, .day'));
  1084. $('.pagination').replaceWith($('.pagination', data));
  1085. if (context.display_order === 'ASC') {
  1086. $('#nav_menu_read_all .read_all').attr(
  1087. 'formaction', $('#bigMarkAsRead').attr('formaction')
  1088. );
  1089. } else {
  1090. $('#bigMarkAsRead').attr(
  1091. 'formaction', $('#nav_menu_read_all .read_all').attr('formaction')
  1092. );
  1093. }
  1094. $('[id^=day_]').each(function (i) {
  1095. const ids = $('[id="' + this.id + '"]');
  1096. if (ids.length > 1) {
  1097. $('[id="' + this.id + '"]:gt(0)').remove();
  1098. }
  1099. });
  1100. init_load_more(box_load_more);
  1101. const bigMarkAsRead = document.getElementById('bigMarkAsRead'),
  1102. div_load_more = document.getElementById('load_more');
  1103. if (bigMarkAsRead) {
  1104. bigMarkAsRead.removeAttribute('disabled');
  1105. }
  1106. if (div_load_more) {
  1107. div_load_more.classList.remove('loading');
  1108. }
  1109. load_more = false;
  1110. });
  1111. }
  1112. function focus_search() {
  1113. $('#search').focus();
  1114. }
  1115. var freshrssLoadMoreEvent = document.createEvent('Event');
  1116. freshrssLoadMoreEvent.initEvent('freshrss:load-more', true, true);
  1117. function init_load_more(box) {
  1118. box_load_more = box;
  1119. document.body.dispatchEvent(freshrssLoadMoreEvent);
  1120. const $next_link = $('#load_more');
  1121. if (!$next_link.length) {
  1122. // no more article to load
  1123. url_load_more = '';
  1124. return;
  1125. }
  1126. url_load_more = $next_link.attr('href');
  1127. $next_link.click(function () {
  1128. load_more_posts();
  1129. return false;
  1130. });
  1131. }
  1132. //</endless_mode>
  1133. //<crypto form (Web login)>
  1134. function poormanSalt() { //If crypto.getRandomValues is not available
  1135. const base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz';
  1136. let text = '$2a$04$';
  1137. for (let i = 22; i > 0; i--) {
  1138. text += base.charAt(Math.floor(Math.random() * 64));
  1139. }
  1140. return text;
  1141. }
  1142. function init_crypto_form() {
  1143. /* globals dcodeIO */
  1144. const $crypto_form = $('#crypto-form');
  1145. if ($crypto_form.length === 0) {
  1146. return;
  1147. }
  1148. if (!(window.dcodeIO)) {
  1149. if (window.console) {
  1150. console.log('FreshRSS waiting for bcrypt.js…');
  1151. }
  1152. setTimeout(init_crypto_form, 100);
  1153. return;
  1154. }
  1155. $crypto_form.on('submit', function () {
  1156. const $submit_button = $(this).find('button[type="submit"]');
  1157. $submit_button.attr('disabled', '');
  1158. let success = false;
  1159. $.ajax({
  1160. url: './?c=javascript&a=nonce&user=' + $('#username').val(),
  1161. dataType: 'json',
  1162. async: false
  1163. }).done(function (data) {
  1164. if (!data.salt1 || !data.nonce) {
  1165. openNotification('Invalid user!', 'bad');
  1166. } else {
  1167. try {
  1168. const strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'),
  1169. s = dcodeIO.bcrypt.hashSync($('#passwordPlain').val(), data.salt1),
  1170. c = dcodeIO.bcrypt.hashSync(data.nonce + s, strong ? dcodeIO.bcrypt.genSaltSync(4) : poormanSalt());
  1171. $('#challenge').val(c);
  1172. if (!s || !c) {
  1173. openNotification('Crypto error!', 'bad');
  1174. } else {
  1175. success = true;
  1176. }
  1177. } catch (e) {
  1178. openNotification('Crypto exception! ' + e, 'bad');
  1179. }
  1180. }
  1181. }).fail(function () {
  1182. openNotification('Communication error!', 'bad');
  1183. });
  1184. $submit_button.removeAttr('disabled');
  1185. return success;
  1186. });
  1187. }
  1188. //</crypto form (Web login)>
  1189. function init_confirm_action() {
  1190. $('body').on('click', '.confirm', function () {
  1191. let str_confirmation = $(this).attr('data-str-confirm');
  1192. if (!str_confirmation) {
  1193. str_confirmation = i18n.confirmation_default;
  1194. }
  1195. return confirm(str_confirmation);
  1196. });
  1197. $('button.confirm').removeAttr('disabled');
  1198. }
  1199. function init_print_action() {
  1200. $('.item.share > a[href="#"]').click(function (e) {
  1201. const content = '<html><head><style>' +
  1202. 'body { font-family: Serif; text-align: justify; }' +
  1203. 'a { color: #000; text-decoration: none; }' +
  1204. 'a:after { content: " [" attr(href) "]"}' +
  1205. '</style></head><body>' +
  1206. $(e.target).closest('.flux_content').find('.content').html() +
  1207. '</body></html>';
  1208. const tmp_window = window.open();
  1209. tmp_window.document.writeln(content);
  1210. tmp_window.document.close();
  1211. tmp_window.focus();
  1212. tmp_window.print();
  1213. tmp_window.close();
  1214. return false;
  1215. });
  1216. }
  1217. function init_post_action() {
  1218. $('.item.share > a[href="POST"]').click(function (e) {
  1219. e.preventDefault();
  1220. const $form = $(this).next('form');
  1221. $.post($form.data('url'), $form.serialize());
  1222. });
  1223. }
  1224. var shares = 0;
  1225. function init_share_observers() {
  1226. shares = $('.group-share').length;
  1227. $('.share.add').on('click', function (e) {
  1228. const $opt = $(this).siblings('select').find(':selected');
  1229. let row = $(this).parents('form').data($opt.data('form'));
  1230. row = row.replace(/##label##/g, $opt.html().trim());
  1231. row = row.replace(/##type##/g, $opt.val());
  1232. row = row.replace(/##help##/g, $opt.data('help'));
  1233. row = row.replace(/##key##/g, shares);
  1234. row = row.replace(/##method##/g, $opt.data('method'));
  1235. row = row.replace(/##field##/g, $opt.data('field'));
  1236. $(this).parents('.form-group').before(row);
  1237. shares++;
  1238. return false;
  1239. });
  1240. }
  1241. function init_stats_observers() {
  1242. $('.select-change').on('change', function (e) {
  1243. redirect($(this).find(':selected').data('url'));
  1244. });
  1245. }
  1246. function init_remove_observers() {
  1247. $('.post').on('click', 'a.remove', function (e) {
  1248. const remove_what = $(this).attr('data-remove');
  1249. if (remove_what !== undefined) {
  1250. $('#' + remove_what).remove();
  1251. }
  1252. return false;
  1253. });
  1254. }
  1255. function init_feed_observers() {
  1256. $('select[id="category"]').on('change', function () {
  1257. const $detail = $('#new_category_name').parent();
  1258. if ($(this).val() === 'nc') {
  1259. $detail.attr('aria-hidden', 'false').show();
  1260. $detail.find('input').focus();
  1261. } else {
  1262. $detail.attr('aria-hidden', 'true').hide();
  1263. }
  1264. });
  1265. }
  1266. function init_password_observers() {
  1267. $('.toggle-password').on('mousedown', function (e) {
  1268. const $button = $(this),
  1269. $passwordField = $('#' + $button.attr('data-toggle'));
  1270. $passwordField.attr('type', 'text');
  1271. $button.addClass('active');
  1272. return false;
  1273. }).on('mouseup', function (e) {
  1274. const $button = $(this),
  1275. $passwordField = $('#' + $button.attr('data-toggle'));
  1276. $passwordField.attr('type', 'password');
  1277. $button.removeClass('active');
  1278. return false;
  1279. });
  1280. }
  1281. function faviconNbUnread(n) {
  1282. if (typeof n === 'undefined') {
  1283. n = str2int($('.category.all .title').attr('data-unread'));
  1284. }
  1285. //http://remysharp.com/2010/08/24/dynamic-favicons/
  1286. const canvas = document.createElement('canvas'),
  1287. link = document.getElementById('favicon').cloneNode(true);
  1288. if (canvas.getContext && link) {
  1289. canvas.height = canvas.width = 16;
  1290. const img = document.createElement('img');
  1291. img.onload = function () {
  1292. const ctx = canvas.getContext('2d');
  1293. ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
  1294. if (n > 0) {
  1295. let text = '';
  1296. if (n < 1000) {
  1297. text = n;
  1298. } else if (n < 100000) {
  1299. text = Math.floor(n / 1000) + 'k';
  1300. } else {
  1301. text = 'E' + Math.floor(Math.log10(n));
  1302. }
  1303. ctx.font = 'bold 9px "Arial", sans-serif';
  1304. ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
  1305. ctx.fillRect(0, 7, ctx.measureText(text).width, 9);
  1306. ctx.fillStyle = '#F00';
  1307. ctx.fillText(text, 0, canvas.height - 1);
  1308. }
  1309. link.href = canvas.toDataURL('image/png');
  1310. $('link[rel~=icon]').remove();
  1311. document.head.appendChild(link);
  1312. };
  1313. img.src = '../favicon.ico';
  1314. }
  1315. }
  1316. function init_slider_observers() {
  1317. const $slider = $('#slider'),
  1318. $closer = $('#close-slider');
  1319. if ($slider.length < 1) {
  1320. return;
  1321. }
  1322. $('.post').on('click', '.open-slider', function () {
  1323. if (ajax_loading) {
  1324. return false;
  1325. }
  1326. ajax_loading = true;
  1327. $.ajax({
  1328. type: 'GET',
  1329. url: $(this).attr('href'),
  1330. data: { ajax: true }
  1331. }).done(function (data) {
  1332. $slider.html(data);
  1333. $closer.addClass('active');
  1334. $slider.addClass('active');
  1335. ajax_loading = false;
  1336. });
  1337. return false;
  1338. });
  1339. $closer.on('click', function () {
  1340. $closer.removeClass('active');
  1341. $slider.removeClass('active');
  1342. return false;
  1343. });
  1344. }
  1345. function init_configuration_alert() {
  1346. $(window).on('submit', function (e) {
  1347. window.hasSubmit = true;
  1348. });
  1349. $(window).on('beforeunload', function (e) {
  1350. if (window.hasSubmit) {
  1351. return;
  1352. }
  1353. const inputs = document.querySelectorAll('[data-leave-validation]');
  1354. for (let i = inputs.length - 1; i >= 0; i--) {
  1355. const input = inputs[i];
  1356. if (input.type === 'checkbox' || input.type === 'radio') {
  1357. if (input.checked != input.getAttribute('data-leave-validation')) {
  1358. return false;
  1359. }
  1360. } else if (input.value != input.getAttribute('data-leave-validation')) {
  1361. return false;
  1362. }
  1363. }
  1364. });
  1365. }
  1366. function init_subscription() {
  1367. $('body').on('click', '.bookmarkClick', function (e) {
  1368. return false;
  1369. });
  1370. }
  1371. function init_normal() {
  1372. $stream = $('#stream');
  1373. if ($stream.length < 1) {
  1374. if (window.console) {
  1375. console.log('FreshRSS waiting for content…');
  1376. }
  1377. setTimeout(init_normal, 100);
  1378. return;
  1379. }
  1380. init_column_categories();
  1381. init_stream($stream);
  1382. init_shortcuts();
  1383. init_actualize();
  1384. faviconNbUnread();
  1385. }
  1386. function init_beforeDOM() {
  1387. if (!window.$) {
  1388. if (window.console) {
  1389. console.log('FreshRSS waiting for jQuery…');
  1390. }
  1391. setTimeout(init_beforeDOM, 100);
  1392. return;
  1393. }
  1394. if (['normal', 'reader', 'global'].indexOf(context.current_view) >= 0) {
  1395. init_normal();
  1396. }
  1397. }
  1398. function init_afterDOM() {
  1399. if (!window.$) {
  1400. if (window.console) {
  1401. console.log('FreshRSS waiting again for jQuery…');
  1402. }
  1403. setTimeout(init_afterDOM, 100);
  1404. return;
  1405. }
  1406. init_notifications();
  1407. init_confirm_action();
  1408. $stream = $('#stream');
  1409. if ($stream.length > 0) {
  1410. init_load_more($stream);
  1411. init_posts();
  1412. init_nav_entries();
  1413. init_dynamic_tags();
  1414. init_print_action();
  1415. init_post_action();
  1416. init_notifs_html5();
  1417. setInterval(refreshUnreads, 120000);
  1418. } else {
  1419. init_subscription();
  1420. init_crypto_form();
  1421. init_share_observers();
  1422. init_remove_observers();
  1423. init_feed_observers();
  1424. init_password_observers();
  1425. init_stats_observers();
  1426. init_slider_observers();
  1427. init_configuration_alert();
  1428. }
  1429. if (window.console) {
  1430. console.log('FreshRSS init done.');
  1431. }
  1432. }
  1433. init_beforeDOM(); //Can be called before DOM is fully loaded
  1434. if (document.readyState && document.readyState !== 'loading') {
  1435. init_afterDOM();
  1436. } else if (document.addEventListener) {
  1437. document.addEventListener('DOMContentLoaded', function () {
  1438. if (window.console) {
  1439. console.log('FreshRSS waiting for DOMContentLoaded…');
  1440. }
  1441. init_afterDOM();
  1442. }, false);
  1443. }