digest.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. var last_feeds = [];
  2. var init_params = {};
  3. var _active_feed_id = false;
  4. var _update_timeout = false;
  5. var _view_update_timeout = false;
  6. var _feedlist_expanded = false;
  7. var _update_seq = 1;
  8. function article_appear(article_id) {
  9. try {
  10. new Effect.Appear('A-' + article_id);
  11. } catch (e) {
  12. exception_error("article_appear", e);
  13. }
  14. }
  15. function catchup_feed(feed_id, callback) {
  16. try {
  17. var fn = find_feed(last_feeds, feed_id).title;
  18. if (confirm(__("Mark all articles in %s as read?").replace("%s", fn))) {
  19. var is_cat = "";
  20. if (feed_id < 0) is_cat = "true"; // KLUDGE
  21. var query = "?op=rpc&method=catchupFeed&feed_id=" +
  22. feed_id + "&is_cat=" + is_cat;
  23. new Ajax.Request("backend.php", {
  24. parameters: query,
  25. onComplete: function(transport) {
  26. if (callback) callback(transport);
  27. update();
  28. } });
  29. }
  30. } catch (e) {
  31. exception_error("catchup_article", e);
  32. }
  33. }
  34. function get_visible_article_ids() {
  35. try {
  36. var elems = $("headlines-content").getElementsByTagName("LI");
  37. var ids = [];
  38. for (var i = 0; i < elems.length; i++) {
  39. if (elems[i].id && elems[i].id.match("A-")) {
  40. ids.push(elems[i].id.replace("A-", ""));
  41. }
  42. }
  43. return ids;
  44. } catch (e) {
  45. exception_error("get_visible_article_ids", e);
  46. }
  47. }
  48. function catchup_visible_articles(callback) {
  49. try {
  50. var ids = get_visible_article_ids();
  51. if (confirm(__("Mark %d displayed articles as read?").replace("%d", ids.length))) {
  52. var query = "?op=rpc&method=catchupSelected" +
  53. "&cmode=0&ids=" + param_escape(ids);
  54. new Ajax.Request("backend.php", {
  55. parameters: query,
  56. onComplete: function(transport) {
  57. if (callback) callback(transport);
  58. viewfeed(_active_feed_id, 0);
  59. } });
  60. }
  61. } catch (e) {
  62. exception_error("catchup_visible_articles", e);
  63. }
  64. }
  65. function catchup_article(article_id, callback) {
  66. try {
  67. var query = "?op=rpc&method=catchupSelected" +
  68. "&cmode=0&ids=" + article_id;
  69. new Ajax.Request("backend.php", {
  70. parameters: query,
  71. onComplete: function(transport) {
  72. if (callback) callback(transport);
  73. } });
  74. } catch (e) {
  75. exception_error("catchup_article", e);
  76. }
  77. }
  78. function set_selected_article(article_id) {
  79. try {
  80. $$("#headlines-content > li[id*=A-]").each(function(article) {
  81. var id = article.id.replace("A-", "");
  82. var cb = article.getElementsByTagName("INPUT")[0];
  83. if (id == article_id) {
  84. article.addClassName("selected");
  85. cb.checked = true;
  86. } else {
  87. article.removeClassName("selected");
  88. cb.checked = false;
  89. }
  90. });
  91. } catch (e) {
  92. exception_error("mark_selected_feed", e);
  93. }
  94. }
  95. function set_selected_feed(feed_id) {
  96. try {
  97. var feeds = $("feeds-content").getElementsByTagName("LI");
  98. for (var i = 0; i < feeds.length; i++) {
  99. if (feeds[i].id == "F-" + feed_id)
  100. feeds[i].className = "selected";
  101. else
  102. feeds[i].className = "";
  103. }
  104. _active_feed_id = feed_id;
  105. } catch (e) {
  106. exception_error("mark_selected_feed", e);
  107. }
  108. }
  109. function load_more() {
  110. try {
  111. var pr = $("H-LOADING-IMG");
  112. if (pr) Element.show(pr);
  113. var offset = $$("#headlines-content > li[id*=A-][class*=fresh],li[id*=A-][class*=unread]").length;
  114. viewfeed(false, offset, false, false, true,
  115. function() {
  116. var pr = $("H-LOADING-IMG");
  117. if (pr) Element.hide(pr);
  118. });
  119. } catch (e) {
  120. exception_error("load_more", e);
  121. }
  122. }
  123. function update(callback) {
  124. try {
  125. console.log('updating feeds...');
  126. window.clearTimeout(_update_timeout);
  127. new Ajax.Request("backend.php", {
  128. parameters: "?op=digest&method=digestinit",
  129. onComplete: function(transport) {
  130. fatal_error_check(transport);
  131. parse_feeds(transport);
  132. set_selected_feed(_active_feed_id);
  133. if (callback) callback(transport);
  134. } });
  135. _update_timeout = window.setTimeout('update()', 5*1000);
  136. } catch (e) {
  137. exception_error("update", e);
  138. }
  139. }
  140. function remove_headline_entry(article_id) {
  141. try {
  142. var elem = $('A-' + article_id);
  143. if (elem) {
  144. elem.parentNode.removeChild(elem);
  145. }
  146. } catch (e) {
  147. exception_error("remove_headline_entry", e);
  148. }
  149. }
  150. function view_update() {
  151. try {
  152. viewfeed(_active_feed_id, _active_feed_offset, false, true, true);
  153. update();
  154. } catch (e) {
  155. exception_error("view_update", e);
  156. }
  157. }
  158. function view(article_id) {
  159. try {
  160. $("content").addClassName("move");
  161. var a = $("A-" + article_id);
  162. var h = $("headlines");
  163. setTimeout(function() {
  164. // below or above viewport, reposition headline
  165. if (a.offsetTop > h.scrollTop + h.offsetHeight || a.offsetTop+a.offsetHeight < h.scrollTop+a.offsetHeight)
  166. h.scrollTop = a.offsetTop - (h.offsetHeight/2 - a.offsetHeight/2);
  167. }, 500);
  168. new Ajax.Request("backend.php", {
  169. parameters: "?op=digest&method=digestgetcontents&article_id=" +
  170. article_id,
  171. onComplete: function(transport) {
  172. fatal_error_check(transport);
  173. var reply = JSON.parse(transport.responseText);
  174. if (reply) {
  175. var article = reply['article'];
  176. var mark_part = "";
  177. var publ_part = "";
  178. var tags_part = "";
  179. if (article.tags.length > 0) {
  180. tags_part = " " + __("in") + " ";
  181. for (var i = 0; i < Math.min(5, article.tags.length); i++) {
  182. //tags_part += "<a href=\"#\" onclick=\"viewfeed('" +
  183. // article.tags[i] + "')\">" +
  184. // article.tags[i] + "</a>, ";
  185. tags_part += article.tags[i] + ", ";
  186. }
  187. tags_part = tags_part.replace(/, $/, "");
  188. tags_part = "<span class=\"tags\">" + tags_part + "</span>";
  189. }
  190. if (article.marked)
  191. mark_part = "<img title='"+ __("Unstar article")+"' onclick=\"toggle_mark(this, "+article.id+")\" src='images/mark_set.svg'>";
  192. else
  193. mark_part = "<img title='"+__("Star article")+"' onclick=\"toggle_mark(this, "+article.id+")\" src='images/mark_unset.svg'>";
  194. if (article.published)
  195. publ_part = "<img title='"+__("Unpublish article")+"' onclick=\"toggle_pub(this, "+article.id+")\" src='images/pub_set.svg'>";
  196. else
  197. publ_part = "<img title='"+__("Publish article")+"' onclick=\"toggle_pub(this, "+article.id+")\" src='images/pub_unset.svg'>";
  198. var tmp = "<div id=\"inner\">" +
  199. "<div id=\"ops\">" +
  200. mark_part +
  201. publ_part +
  202. "</div>" +
  203. "<h1>" + "<a target=\"_blank\" href=\""+article.url+"\">" +
  204. article.title + "</a>" + "</h1>" +
  205. "<div id=\"tags\">" +
  206. tags_part +
  207. "</div>" +
  208. article.content + "</div>";
  209. $("article-content").innerHTML = tmp;
  210. $("article").addClassName("visible");
  211. set_selected_article(article.id);
  212. catchup_article(article_id,
  213. function() {
  214. $("A-" + article_id).addClassName("read");
  215. });
  216. } else {
  217. elem.innerHTML = __("Error: unable to load article.");
  218. }
  219. }
  220. });
  221. return false;
  222. } catch (e) {
  223. exception_error("view", e);
  224. }
  225. }
  226. function close_article() {
  227. $("content").removeClassName("move");
  228. $("article").removeClassName("visible");
  229. }
  230. function viewfeed(feed_id, offset, replace, no_effects, no_indicator, callback) {
  231. try {
  232. if (!feed_id) feed_id = _active_feed_id;
  233. if (offset == undefined) offset = 0;
  234. if (replace == undefined) replace = (offset == 0);
  235. _update_seq = _update_seq + 1;
  236. if (!offset) $("headlines").scrollTop = 0;
  237. var query = "backend.php?op=digest&method=digestupdate&feed_id=" +
  238. param_escape(feed_id) + "&offset=" + offset +
  239. "&seq=" + _update_seq;
  240. console.log(query);
  241. var img = false;
  242. if ($("F-" + feed_id)) {
  243. img = $("F-" + feed_id).getElementsByTagName("IMG")[0];
  244. if (img && !no_indicator) {
  245. img.setAttribute("orig_src", img.src);
  246. img.src = 'images/indicator_tiny.gif';
  247. }
  248. }
  249. new Ajax.Request("backend.php", {
  250. parameters: query,
  251. onComplete: function(transport) {
  252. Element.hide("overlay");
  253. fatal_error_check(transport);
  254. parse_headlines(transport, replace, no_effects);
  255. set_selected_feed(feed_id);
  256. _active_feed_offset = offset;
  257. if (img && !no_indicator)
  258. img.src = img.getAttribute("orig_src");
  259. if (callback) callback(transport);
  260. } });
  261. } catch (e) {
  262. exception_error("view", e);
  263. }
  264. }
  265. function find_article(articles, article_id) {
  266. try {
  267. for (var i = 0; i < articles.length; i++) {
  268. if (articles[i].id == article_id)
  269. return articles[i];
  270. }
  271. return false;
  272. } catch (e) {
  273. exception_error("find_article", e);
  274. }
  275. }
  276. function find_feed(feeds, feed_id) {
  277. try {
  278. for (var i = 0; i < feeds.length; i++) {
  279. if (feeds[i].id == feed_id)
  280. return feeds[i];
  281. }
  282. return false;
  283. } catch (e) {
  284. exception_error("find_feed", e);
  285. }
  286. }
  287. function get_feed_icon(feed) {
  288. try {
  289. if (feed.has_icon)
  290. return getInitParam('icons_url') + "/" + feed.id + '.ico';
  291. if (feed.id == -1)
  292. return 'images/mark_set.svg';
  293. if (feed.id == -2)
  294. return 'images/pub_set.svg';
  295. if (feed.id == -3)
  296. return 'images/fresh.png';
  297. if (feed.id == -4)
  298. return 'images/tag.png';
  299. if (feed.id < -10)
  300. return 'images/label.png';
  301. return 'images/blank_icon.gif';
  302. } catch (e) {
  303. exception_error("get_feed_icon", e);
  304. }
  305. }
  306. function add_feed_entry(feed) {
  307. try {
  308. var icon_part = "";
  309. icon_part = "<img src='" + get_feed_icon(feed) + "'/>";
  310. var title = (feed.title.length > 30) ?
  311. feed.title.substring(0, 30) + "&hellip;" :
  312. feed.title;
  313. var tmp_html = "<li id=\"F-"+feed.id+"\" onclick=\"viewfeed("+feed.id+")\">" +
  314. icon_part + title +
  315. "<div class='unread-ctr'>" + "<span class=\"unread\">" + feed.unread + "</span>" +
  316. "</div>" + "</li>";
  317. $("feeds-content").innerHTML += tmp_html;
  318. } catch (e) {
  319. exception_error("add_feed_entry", e);
  320. }
  321. }
  322. function add_headline_entry(article, feed, no_effects) {
  323. try {
  324. var icon_part = "";
  325. icon_part = "<img class='icon' src='" + get_feed_icon(feed) + "'/>";
  326. var style = "";
  327. //if (!no_effects) style = "style=\"display : none\"";
  328. if (article.excerpt.trim() == "")
  329. article.excerpt = __("Click to expand article.");
  330. var li_class = "unread";
  331. var fresh_max = getInitParam("fresh_article_max_age") * 60 * 60;
  332. var d = new Date();
  333. if (d.getTime() / 1000 - article.updated < fresh_max)
  334. li_class = "fresh";
  335. //"<img title='" + __("Mark as read") + "' onclick=\"view("+article.id+", true)\" src='images/digest_checkbox.png'>" +
  336. var checkbox_part = "<input type=\"checkbox\" class=\"cb\" onclick=\"toggle_select_article(this)\"/>";
  337. var date = new Date(article.updated * 1000);
  338. var date_part = date.toString().substring(0,21);
  339. var tmp_html = "<li id=\"A-"+article.id+"\" "+style+" class=\""+li_class+"\">" +
  340. checkbox_part +
  341. icon_part +
  342. "<a target=\"_blank\" href=\""+article.link+"\""+
  343. "onclick=\"return view("+article.id+")\" class='title'>" +
  344. article.title + "</a>" +
  345. "<div class='body'>" +
  346. "<div onclick=\"view("+article.id+")\" class='excerpt'>" +
  347. article.excerpt + "</div>" +
  348. "<div onclick=\"view("+article.id+")\" class='info'>";
  349. /* tmp_html += "<a href=\#\" onclick=\"viewfeed("+feed.id+")\">" +
  350. feed.title + "</a> " + " @ "; */
  351. tmp_html += date_part + "</div>" +
  352. "</div></li>";
  353. $("headlines-content").innerHTML += tmp_html;
  354. if (!no_effects)
  355. window.setTimeout('article_appear(' + article.id + ')', 100);
  356. } catch (e) {
  357. exception_error("add_headline_entry", e);
  358. }
  359. }
  360. function expand_feeds() {
  361. try {
  362. _feedlist_expanded = true;
  363. redraw_feedlist(last_feeds);
  364. } catch (e) {
  365. exception_error("expand_feeds", e);
  366. }
  367. }
  368. function redraw_feedlist(feeds) {
  369. try {
  370. $('feeds-content').innerHTML = "";
  371. var limit = 10;
  372. if (_feedlist_expanded) limit = feeds.length;
  373. for (var i = 0; i < Math.min(limit, feeds.length); i++) {
  374. add_feed_entry(feeds[i]);
  375. }
  376. if (feeds.length > limit) {
  377. $('feeds-content').innerHTML += "<li id='F-MORE-PROMPT'>" +
  378. "<img src='images/blank_icon.gif'>" +
  379. "<a href=\"#\" onclick=\"expand_feeds()\">" +
  380. __("%d more...").replace("%d", feeds.length-10) +
  381. "</a>" + "</li>";
  382. }
  383. if (feeds.length == 0) {
  384. $('feeds-content').innerHTML =
  385. "<div class='insensitive' style='text-align : center'>" +
  386. __("No unread feeds.") + "</div>";
  387. }
  388. if (_active_feed_id)
  389. set_selected_feed(_active_feed_id);
  390. } catch (e) {
  391. exception_error("redraw_feedlist", e);
  392. }
  393. }
  394. function parse_feeds(transport) {
  395. try {
  396. var reply = JSON.parse(transport.responseText);
  397. if (!reply) return;
  398. var feeds = reply['feeds'];
  399. if (feeds) {
  400. feeds.sort( function (a,b)
  401. {
  402. if (b.unread != a.unread)
  403. return (b.unread - a.unread);
  404. else
  405. if (a.title > b.title)
  406. return 1;
  407. else if (a.title < b.title)
  408. return -1;
  409. else
  410. return 0;
  411. });
  412. var all_articles = find_feed(feeds, -4);
  413. update_title(all_articles.unread);
  414. last_feeds = feeds;
  415. redraw_feedlist(feeds);
  416. }
  417. } catch (e) {
  418. exception_error("parse_feeds", e);
  419. }
  420. }
  421. function parse_headlines(transport, replace, no_effects) {
  422. try {
  423. var reply = JSON.parse(transport.responseText);
  424. if (!reply) return;
  425. var seq = reply['seq'];
  426. if (seq) {
  427. if (seq != _update_seq) {
  428. console.log("parse_headlines: wrong sequence received.");
  429. return;
  430. }
  431. } else {
  432. return;
  433. }
  434. var headlines = reply['headlines']['content'];
  435. var headlines_title = reply['headlines']['title'];
  436. if (headlines && headlines_title) {
  437. if (replace) {
  438. $('headlines-content').innerHTML = '';
  439. }
  440. var pr = $('H-MORE-PROMPT');
  441. if (pr) pr.parentNode.removeChild(pr);
  442. var inserted = false;
  443. for (var i = 0; i < headlines.length; i++) {
  444. if (!$('A-' + headlines[i].id)) {
  445. add_headline_entry(headlines[i],
  446. find_feed(last_feeds, headlines[i].feed_id), !no_effects);
  447. }
  448. }
  449. console.log(inserted.id);
  450. var ids = get_visible_article_ids();
  451. if (ids.length > 0) {
  452. if (pr) {
  453. $('headlines-content').appendChild(pr);
  454. } else {
  455. $('headlines-content').innerHTML += "<li id='H-MORE-PROMPT'>" +
  456. "<div class='body'>" +
  457. "<a href=\"#\" onclick=\"catchup_visible_articles()\">" +
  458. __("Mark as read") + "</a> | " +
  459. "<a href=\"javascript:load_more()\">" +
  460. __("Load more...") + "</a>" +
  461. "<img style=\"display : none\" "+
  462. "id=\"H-LOADING-IMG\" src='images/indicator_tiny.gif'>" +
  463. "</div></li>";
  464. }
  465. } else {
  466. // FIXME : display some kind of "nothing to see here" prompt here
  467. }
  468. // if (replace && !no_effects)
  469. // new Effect.Appear('headlines-content', {duration : 0.3});
  470. //new Effect.Appear('headlines-content');
  471. }
  472. } catch (e) {
  473. exception_error("parse_headlines", e);
  474. }
  475. }
  476. function init_second_stage() {
  477. try {
  478. new Ajax.Request("backend.php", {
  479. parameters: "backend.php?op=digest&method=digestinit",
  480. onComplete: function(transport) {
  481. parse_feeds(transport);
  482. Element.hide("overlay");
  483. document.onkeydown = hotkey_handler;
  484. window.setTimeout('viewfeed(-4)', 100);
  485. _update_timeout = window.setTimeout('update()', 5*1000);
  486. } });
  487. } catch (e) {
  488. exception_error("init_second_stage", e);
  489. }
  490. }
  491. function init() {
  492. try {
  493. dojo.require("dijit.Dialog");
  494. new Ajax.Request("backend.php", {
  495. parameters: {op: "rpc", method: "sanityCheck"},
  496. onComplete: function(transport) {
  497. backend_sanity_check_callback(transport);
  498. } });
  499. } catch (e) {
  500. exception_error("digest_init", e);
  501. }
  502. }
  503. function toggle_mark(img, id) {
  504. try {
  505. var query = "?op=rpc&id=" + id + "&method=mark";
  506. if (!img) return;
  507. if (img.src.match("mark_unset")) {
  508. img.src = img.src.replace("mark_unset", "mark_set");
  509. img.alt = __("Unstar article");
  510. query = query + "&mark=1";
  511. } else {
  512. img.src = img.src.replace("mark_set", "mark_unset");
  513. img.alt = __("Star article");
  514. query = query + "&mark=0";
  515. }
  516. new Ajax.Request("backend.php", {
  517. parameters: query,
  518. onComplete: function(transport) {
  519. update();
  520. } });
  521. } catch (e) {
  522. exception_error("toggle_mark", e);
  523. }
  524. }
  525. function toggle_pub(img, id, note) {
  526. try {
  527. var query = "?op=rpc&id=" + id + "&method=publ";
  528. if (note != undefined) {
  529. query = query + "&note=" + param_escape(note);
  530. } else {
  531. query = query + "&note=undefined";
  532. }
  533. if (!img) return;
  534. if (img.src.match("pub_unset") || note != undefined) {
  535. img.src = img.src.replace("pub_unset", "pub_set");
  536. img.alt = __("Unpublish article");
  537. query = query + "&pub=1";
  538. } else {
  539. img.src = img.src.replace("pub_set", "pub_unset");
  540. img.alt = __("Publish article");
  541. query = query + "&pub=0";
  542. }
  543. new Ajax.Request("backend.php", {
  544. parameters: query,
  545. onComplete: function(transport) {
  546. update();
  547. } });
  548. } catch (e) {
  549. exception_error("toggle_pub", e);
  550. }
  551. }
  552. function fatal_error(code, msg) {
  553. try {
  554. if (code == 6) {
  555. window.location.href = "digest.php";
  556. } else if (code == 5) {
  557. window.location.href = "db-updater.php";
  558. } else {
  559. if (msg == "") msg = "Unknown error";
  560. console.error("Fatal error: " + code + "\n" +
  561. msg);
  562. }
  563. } catch (e) {
  564. exception_error("fatalError", e);
  565. }
  566. }
  567. function fatal_error_check(transport) {
  568. try {
  569. if (transport.responseXML) {
  570. var error = transport.responseXML.getElementsByTagName("error")[0];
  571. if (error) {
  572. var code = error.getAttribute("error-code");
  573. var msg = error.getAttribute("error-msg");
  574. if (code != 0) {
  575. fatal_error(code, msg);
  576. return false;
  577. }
  578. }
  579. }
  580. } catch (e) {
  581. exception_error("fatal_error_check", e);
  582. }
  583. return true;
  584. }
  585. function update_title(unread) {
  586. try {
  587. document.title = "Tiny Tiny RSS";
  588. if (unread > 0)
  589. document.title += " (" + unread + ")";
  590. } catch (e) {
  591. exception_error("update_title", e);
  592. }
  593. }
  594. function toggle_select_article(elem) {
  595. try {
  596. var article = elem.parentNode;
  597. if (article.hasClassName("selected"))
  598. article.removeClassName("selected");
  599. else
  600. article.addClassName("selected");
  601. } catch (e) {
  602. exception_error("toggle_select_article", e);
  603. }
  604. }
  605. function hotkey_handler(e) {
  606. try {
  607. if (e.target.nodeName == "INPUT" || e.target.nodeName == "TEXTAREA") return;
  608. var keycode = false;
  609. var shift_key = false;
  610. var cmdline = $('cmdline');
  611. try {
  612. shift_key = e.shiftKey;
  613. } catch (e) {
  614. }
  615. if (window.event) {
  616. keycode = window.event.keyCode;
  617. } else if (e) {
  618. keycode = e.which;
  619. }
  620. var keychar = String.fromCharCode(keycode);
  621. if (keycode == 16) return; // ignore lone shift
  622. if (keycode == 17) return; // ignore lone ctrl
  623. switch (keycode) {
  624. case 27: // esc
  625. close_article();
  626. break;
  627. default:
  628. console.log("KP: CODE=" + keycode + " CHAR=" + keychar);
  629. }
  630. } catch (e) {
  631. exception_error("hotkey_handler", e);
  632. }
  633. }