]> git.wh0rd.org - tt-rss.git/blob - js/FeedTree.js
b37d339c2e5c0be0580ba32c12d8b8b37dca393a
[tt-rss.git] / js / FeedTree.js
1 /* global dijit */
2 define(["dojo/_base/declare", "dojo/dom-construct", "dijit/Tree", "dijit/Menu"], function (declare, domConstruct) {
3
4 return declare("fox.FeedTree", dijit.Tree, {
5 _onKeyPress: function(/* Event */ e) {
6 return; // Stop dijit.Tree from interpreting keystrokes
7 },
8 _createTreeNode: function(args) {
9 const tnode = new dijit._TreeNode(args);
10
11 const icon = dojo.doc.createElement('img');
12 if (args.item.icon && args.item.icon[0]) {
13 icon.src = args.item.icon[0];
14 } else {
15 icon.src = 'images/blank_icon.gif';
16 }
17 icon.className = 'tinyFeedIcon';
18 domConstruct.place(icon, tnode.iconNode, 'only');
19
20 const id = args.item.id[0];
21 const bare_id = parseInt(id.substr(id.indexOf(':')+1));
22
23 if (bare_id < _label_base_index) {
24 const span = dojo.doc.createElement('span');
25 const fg_color = args.item.fg_color[0];
26 const bg_color = args.item.bg_color[0];
27
28 span.innerHTML = "&alpha;";
29 span.className = 'labelColorIndicator';
30 span.setStyle({
31 color: fg_color,
32 backgroundColor: bg_color});
33
34 domConstruct.place(span, tnode.iconNode, 'only');
35 }
36
37 if (id.match("FEED:")) {
38 let menu = new dijit.Menu();
39 menu.row_id = bare_id;
40
41 menu.addChild(new dijit.MenuItem({
42 label: __("Mark as read"),
43 onClick: function() {
44 catchupFeed(this.getParent().row_id);
45 }}));
46
47 if (bare_id > 0) {
48 menu.addChild(new dijit.MenuItem({
49 label: __("Edit feed"),
50 onClick: function() {
51 editFeed(this.getParent().row_id, false);
52 }}));
53
54 /* menu.addChild(new dijit.MenuItem({
55 label: __("Update feed"),
56 onClick: function() {
57 heduleFeedUpdate(this.getParent().row_id, false);
58 }})); */
59 }
60
61 menu.bindDomNode(tnode.domNode);
62 tnode._menu = menu;
63 }
64
65 if (id.match("CAT:") && bare_id >= 0) {
66 let menu = new dijit.Menu();
67 menu.row_id = bare_id;
68
69 menu.addChild(new dijit.MenuItem({
70 label: __("Mark as read"),
71 onClick: function() {
72 catchupFeed(this.getParent().row_id, true);
73 }}));
74
75 menu.addChild(new dijit.MenuItem({
76 label: __("(Un)collapse"),
77 onClick: function() {
78 dijit.byId("feedTree").collapseCat(this.getParent().row_id);
79 }}));
80
81 menu.bindDomNode(tnode.domNode);
82 tnode._menu = menu;
83 }
84
85 if (id.match("CAT:")) {
86 loading = dojo.doc.createElement('img');
87 loading.className = 'loadingNode';
88 loading.src = 'images/blank_icon.gif';
89 domConstruct.place(loading, tnode.labelNode, 'after');
90 tnode.loadingNode = loading;
91 }
92
93 if (id.match("CAT:") && bare_id == -1) {
94 let menu = new dijit.Menu();
95 menu.row_id = bare_id;
96
97 menu.addChild(new dijit.MenuItem({
98 label: __("Mark all feeds as read"),
99 onClick: function() {
100 catchupAllFeeds();
101 }}));
102
103 menu.bindDomNode(tnode.domNode);
104 tnode._menu = menu;
105 }
106
107 ctr = dojo.doc.createElement('span');
108 ctr.className = 'counterNode';
109 ctr.innerHTML = args.item.unread > 0 ? args.item.unread : args.item.auxcounter;
110
111 //args.item.unread > 0 ? ctr.addClassName("unread") : ctr.removeClassName("unread");
112
113 args.item.unread > 0 || args.item.auxcounter > 0 ? Element.show(ctr) : Element.hide(ctr);
114
115 args.item.unread == 0 && args.item.auxcounter > 0 ? ctr.addClassName("aux") : ctr.removeClassName("aux");
116
117 domConstruct.place(ctr, tnode.rowNode, 'first');
118 tnode.counterNode = ctr;
119
120 //tnode.labelNode.innerHTML = args.label;
121 return tnode;
122 },
123 postCreate: function() {
124 this.connect(this.model, "onChange", "updateCounter");
125 this.connect(this, "_expandNode", function() {
126 this.hideRead(getInitParam("hide_read_feeds"), getInitParam("hide_read_shows_special"));
127 });
128
129 this.inherited(arguments);
130 },
131 updateCounter: function (item) {
132 const tree = this;
133
134 //console.log("updateCounter: " + item.id[0] + " " + item.unread + " " + tree);
135
136 let node = tree._itemNodesMap[item.id];
137
138 if (node) {
139 node = node[0];
140
141 if (node.counterNode) {
142 ctr = node.counterNode;
143 ctr.innerHTML = item.unread > 0 ? item.unread : item.auxcounter;
144 item.unread > 0 || item.auxcounter > 0 ?
145 item.unread > 0 ?
146 Effect.Appear(ctr, {duration : 0.3,
147 queue: { position: 'end', scope: 'CAPPEAR-' + item.id, limit: 1 }}) :
148 Element.show(ctr) :
149 Element.hide(ctr);
150
151 item.unread == 0 && item.auxcounter > 0 ? ctr.addClassName("aux") : ctr.removeClassName("aux");
152
153 }
154 }
155
156 },
157 getTooltip: function (item) {
158 if (item.updated)
159 return item.updated;
160 else
161 return "";
162 },
163 getIconClass: function (item, opened) {
164 return (!item || this.model.mayHaveChildren(item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "feedIcon";
165 },
166 getLabelClass: function (item, opened) {
167 return (item.unread == 0) ? "dijitTreeLabel" : "dijitTreeLabel Unread";
168 },
169 getRowClass: function (item, opened) {
170 let rc = (!item.error || item.error == '') ? "dijitTreeRow" :
171 "dijitTreeRow Error";
172
173 if (item.unread > 0) rc += " Unread";
174 if (item.updates_disabled > 0) rc += " UpdatesDisabled";
175
176 return rc;
177 },
178 getLabel: function(item) {
179 let name = String(item.name);
180
181 /* Horrible */
182 name = name.replace(/&quot;/g, "\"");
183 name = name.replace(/&amp;/g, "&");
184 name = name.replace(/&mdash;/g, "-");
185 name = name.replace(/&lt;/g, "<");
186 name = name.replace(/&gt;/g, ">");
187
188 /* var label;
189
190 if (item.unread > 0) {
191 label = name + " (" + item.unread + ")";
192 } else {
193 label = name;
194 } */
195
196 return name;
197 },
198 expandParentNodes: function(feed, is_cat, list) {
199 try {
200 for (let i = 0; i < list.length; i++) {
201 const id = String(list[i].id);
202 let item = this._itemNodesMap[id];
203
204 if (item) {
205 item = item[0];
206 this._expandNode(item);
207 }
208 }
209 } catch (e) {
210 exception_error(e);
211 }
212 },
213 findNodeParentsAndExpandThem: function(feed, is_cat, root, parents) {
214 // expands all parents of specified feed to properly mark it as active
215 // my fav thing about frameworks is doing everything myself
216 try {
217 const test_id = is_cat ? 'CAT:' + feed : 'FEED:' + feed;
218
219 if (!root) {
220 if (!this.model || !this.model.store) return false;
221
222 const items = this.model.store._arrayOfTopLevelItems;
223
224 for (let i = 0; i < items.length; i++) {
225 if (String(items[i].id) == test_id) {
226 this.expandParentNodes(feed, is_cat, parents);
227 } else {
228 this.findNodeParentsAndExpandThem(feed, is_cat, items[i], []);
229 }
230 }
231 } else if (root.items) {
232 parents.push(root);
233
234 for (let i = 0; i < root.items.length; i++) {
235 if (String(root.items[i].id) == test_id) {
236 this.expandParentNodes(feed, is_cat, parents);
237 } else {
238 this.findNodeParentsAndExpandThem(feed, is_cat, root.items[i], parents.slice(0));
239 }
240 }
241 } else if (String(root.id) == test_id) {
242 this.expandParentNodes(feed, is_cat, parents.slice(0));
243 }
244 } catch (e) {
245 exception_error(e);
246 }
247 },
248 selectFeed: function(feed, is_cat) {
249 this.findNodeParentsAndExpandThem(feed, is_cat, false, false);
250
251 if (is_cat)
252 treeNode = this._itemNodesMap['CAT:' + feed];
253 else
254 treeNode = this._itemNodesMap['FEED:' + feed];
255
256 if (treeNode) {
257 treeNode = treeNode[0];
258 if (!is_cat) this._expandNode(treeNode);
259 this.set("selectedNodes", [treeNode]);
260 this.focusNode(treeNode);
261
262 // focus headlines to route key events there
263 setTimeout(function() {
264 $("headlines-frame").focus();
265 }, 0);
266 }
267 },
268 setFeedIcon: function(feed, is_cat, src) {
269 if (is_cat)
270 treeNode = this._itemNodesMap['CAT:' + feed];
271 else
272 treeNode = this._itemNodesMap['FEED:' + feed];
273
274 if (treeNode) {
275 treeNode = treeNode[0];
276 const icon = dojo.doc.createElement('img');
277 icon.src = src;
278 icon.className = 'tinyFeedIcon';
279 domConstruct.place(icon, treeNode.iconNode, 'only');
280 return true;
281 }
282 return false;
283 },
284 setFeedExpandoIcon: function(feed, is_cat, src) {
285 if (is_cat)
286 treeNode = this._itemNodesMap['CAT:' + feed];
287 else
288 treeNode = this._itemNodesMap['FEED:' + feed];
289
290 if (treeNode) {
291 treeNode = treeNode[0];
292 if (treeNode.loadingNode) {
293 treeNode.loadingNode.src = src;
294 return true;
295 } else {
296 const icon = dojo.doc.createElement('img');
297 icon.src = src;
298 icon.className = 'loadingExpando';
299 domConstruct.place(icon, treeNode.expandoNode, 'only');
300 return true;
301 }
302 }
303
304 return false;
305 },
306 hasCats: function() {
307 return this.model.hasCats();
308 },
309 hideReadCat: function (cat, hide, show_special) {
310 if (this.hasCats()) {
311 const tree = this;
312
313 if (cat && cat.items) {
314 let cat_unread = tree.hideReadFeeds(cat.items, hide, show_special);
315
316 const id = String(cat.id);
317 const node = tree._itemNodesMap[id];
318 const bare_id = parseInt(id.substr(id.indexOf(":")+1));
319
320 if (node) {
321 const check_unread = tree.model.getFeedUnread(bare_id, true);
322
323 if (hide && cat_unread == 0 && check_unread == 0 && (id != "CAT:-1" || !show_special)) {
324 Effect.Fade(node[0].rowNode, {duration : 0.3,
325 queue: { position: 'end', scope: 'FFADE-' + id, limit: 1 }});
326 } else {
327 Element.show(node[0].rowNode);
328 ++cat_unread;
329 }
330 }
331 }
332 }
333 },
334 hideRead: function (hide, show_special) {
335 if (this.hasCats()) {
336
337 const tree = this;
338 const cats = this.model.store._arrayOfTopLevelItems;
339
340 cats.each(function(cat) {
341 tree.hideReadCat(cat, hide, show_special);
342 });
343
344 } else {
345 this.hideReadFeeds(this.model.store._arrayOfTopLevelItems, hide,
346 show_special);
347 }
348 },
349 hideReadFeeds: function (items, hide, show_special) {
350 const tree = this;
351 let cat_unread = 0;
352
353 items.each(function(feed) {
354 const id = String(feed.id);
355
356 // it's a subcategory
357 if (feed.items) {
358 tree.hideReadCat(feed, hide, show_special);
359 } else { // it's a feed
360 const bare_id = parseInt(feed.bare_id);
361
362 const unread = feed.unread[0];
363 const has_error = feed.error[0] != '';
364 const node = tree._itemNodesMap[id];
365
366 if (node) {
367 if (hide && unread == 0 && !has_error && (bare_id > 0 || bare_id < _label_base_index || !show_special)) {
368 Effect.Fade(node[0].rowNode, {duration : 0.3,
369 queue: { position: 'end', scope: 'FFADE-' + id, limit: 1 }});
370 } else {
371 Element.show(node[0].rowNode);
372 ++cat_unread;
373 }
374 }
375 }
376 });
377
378 return cat_unread;
379 },
380 collapseCat: function(id) {
381 if (!this.model.hasCats()) return;
382
383 const tree = this;
384
385 const node = tree._itemNodesMap['CAT:' + id][0];
386 const item = tree.model.store._itemsByIdentity['CAT:' + id];
387
388 if (node && item) {
389 if (!node.isExpanded)
390 tree._expandNode(node);
391 else
392 tree._collapseNode(node);
393
394 }
395 },
396 getVisibleUnreadFeeds: function() {
397 const items = this.model.store._arrayOfAllItems;
398 const rv = [];
399
400 for (let i = 0; i < items.length; i++) {
401 const id = String(items[i].id);
402 const box = this._itemNodesMap[id];
403
404 if (box) {
405 const row = box[0].rowNode;
406 let cat = false;
407
408 try {
409 cat = box[0].rowNode.parentNode.parentNode;
410 } catch (e) { }
411
412 if (row) {
413 if (Element.visible(row) && (!cat || Element.visible(cat))) {
414 const feed_id = String(items[i].bare_id);
415 const is_cat = !id.match('FEED:');
416 const unread = this.model.getFeedUnread(feed_id, is_cat);
417
418 if (unread > 0)
419 rv.push([feed_id, is_cat]);
420
421 }
422 }
423 }
424 }
425
426 return rv;
427 },
428 getNextFeed: function (feed, is_cat) {
429 if (is_cat) {
430 treeItem = this.model.store._itemsByIdentity['CAT:' + feed];
431 } else {
432 treeItem = this.model.store._itemsByIdentity['FEED:' + feed];
433 }
434
435 const items = this.model.store._arrayOfAllItems;
436 let item = items[0];
437
438 for (let i = 0; i < items.length; i++) {
439 if (items[i] == treeItem) {
440
441 for (let j = i+1; j < items.length; j++) {
442 const id = String(items[j].id);
443 const box = this._itemNodesMap[id];
444
445 if (box) {
446 const row = box[0].rowNode;
447 const cat = box[0].rowNode.parentNode.parentNode;
448
449 if (Element.visible(cat) && Element.visible(row)) {
450 item = items[j];
451 break;
452 }
453 }
454 }
455 break;
456 }
457 }
458
459 if (item) {
460 return [this.model.store.getValue(item, 'bare_id'),
461 !this.model.store.getValue(item, 'id').match('FEED:')];
462 } else {
463 return false;
464 }
465 },
466 getPreviousFeed: function (feed, is_cat) {
467 if (is_cat) {
468 treeItem = this.model.store._itemsByIdentity['CAT:' + feed];
469 } else {
470 treeItem = this.model.store._itemsByIdentity['FEED:' + feed];
471 }
472
473 const items = this.model.store._arrayOfAllItems;
474 let item = items[0] == treeItem ? items[items.length-1] : items[0];
475
476 for (let i = 0; i < items.length; i++) {
477 if (items[i] == treeItem) {
478
479 for (let j = i-1; j > 0; j--) {
480 const id = String(items[j].id);
481 const box = this._itemNodesMap[id];
482
483 if (box) {
484 const row = box[0].rowNode;
485 const cat = box[0].rowNode.parentNode.parentNode;
486
487 if (Element.visible(cat) && Element.visible(row)) {
488 item = items[j];
489 break;
490 }
491 }
492
493 }
494 break;
495 }
496 }
497
498 if (item) {
499 return [this.model.store.getValue(item, 'bare_id'),
500 !this.model.store.getValue(item, 'id').match('FEED:')];
501 } else {
502 return false;
503 }
504
505 },
506 getFeedCategory: function(feed) {
507 try {
508 return this.getNodesByItem(this.model.store.
509 _itemsByIdentity["FEED:" + feed])[0].
510 getParent().item.bare_id[0];
511
512 } catch (e) {
513 return false;
514 }
515 },
516 });
517 });
518