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