filter.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. // Filtered view manager
  2. // We define local filter objects for each different type of field (SetFilter,
  3. // RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose
  4. // goal is to filter complete documents by using the local filters for each
  5. // fields.
  6. function showFilterSidebar() {
  7. Sidebar.setView('filter');
  8. }
  9. // Use a "set" filter for a field that is a set of documents uniquely
  10. // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
  11. class SetFilter {
  12. constructor() {
  13. this._dep = new Tracker.Dependency();
  14. this._selectedElements = [];
  15. }
  16. isSelected(val) {
  17. this._dep.depend();
  18. return this._selectedElements.indexOf(val) > -1;
  19. }
  20. add(val) {
  21. if (this._indexOfVal(val) === -1) {
  22. this._selectedElements.push(val);
  23. this._dep.changed();
  24. showFilterSidebar();
  25. }
  26. }
  27. remove(val) {
  28. const indexOfVal = this._indexOfVal(val);
  29. if (this._indexOfVal(val) !== -1) {
  30. this._selectedElements.splice(indexOfVal, 1);
  31. this._dep.changed();
  32. }
  33. }
  34. toggle(val) {
  35. if (this._indexOfVal(val) === -1) {
  36. this.add(val);
  37. } else {
  38. this.remove(val);
  39. }
  40. }
  41. reset() {
  42. this._selectedElements = [];
  43. this._dep.changed();
  44. }
  45. _indexOfVal(val) {
  46. return this._selectedElements.indexOf(val);
  47. }
  48. _isActive() {
  49. this._dep.depend();
  50. return this._selectedElements.length !== 0;
  51. }
  52. _getMongoSelector() {
  53. this._dep.depend();
  54. return { $in: this._selectedElements };
  55. }
  56. _getEmptySelector() {
  57. this._dep.depend();
  58. let includeEmpty = false
  59. this._selectedElements.forEach((el) => {
  60. if (el == undefined) {
  61. includeEmpty = true;
  62. }
  63. });
  64. return includeEmpty ? { $eq: [] } : null;
  65. }
  66. }
  67. // The global Filter object.
  68. // XXX It would be possible to re-write this object more elegantly, and removing
  69. // the need to provide a list of `_fields`. We also should move methods into the
  70. // object prototype.
  71. Filter = {
  72. // XXX I would like to rename this field into `labels` to be consistent with
  73. // the rest of the schema, but we need to set some migrations architecture
  74. // before changing the schema.
  75. labelIds: new SetFilter(),
  76. members: new SetFilter(),
  77. _fields: ['labelIds', 'members'],
  78. // We don't filter cards that have been added after the last filter change. To
  79. // implement this we keep the id of these cards in this `_exceptions` fields
  80. // and use a `$or` condition in the mongo selector we return.
  81. _exceptions: [],
  82. _exceptionsDep: new Tracker.Dependency(),
  83. isActive() {
  84. return _.any(this._fields, (fieldName) => {
  85. return this[fieldName]._isActive();
  86. });
  87. },
  88. _getMongoSelector() {
  89. if (!this.isActive())
  90. return {};
  91. const filterSelector = {};
  92. const emptySelector = {};
  93. let includeEmptySelectors = false;
  94. this._fields.forEach((fieldName) => {
  95. const filter = this[fieldName];
  96. if (filter._isActive()) {
  97. filterSelector[fieldName] = filter._getMongoSelector();
  98. emptySelector[fieldName] = filter._getEmptySelector();
  99. if (emptySelector[fieldName] != null) {
  100. includeEmptySelectors = true;
  101. }
  102. }
  103. });
  104. const exceptionsSelector = {_id: {$in: this._exceptions}};
  105. this._exceptionsDep.depend();
  106. if (includeEmptySelectors)
  107. return {$or: [filterSelector, exceptionsSelector, emptySelector]};
  108. else
  109. return {$or: [filterSelector, exceptionsSelector]};
  110. },
  111. mongoSelector(additionalSelector) {
  112. const filterSelector = this._getMongoSelector();
  113. if (_.isUndefined(additionalSelector))
  114. return filterSelector;
  115. else
  116. return {$and: [filterSelector, additionalSelector]};
  117. },
  118. reset() {
  119. this._fields.forEach((fieldName) => {
  120. const filter = this[fieldName];
  121. filter.reset();
  122. });
  123. this.resetExceptions();
  124. },
  125. addException(_id) {
  126. if (this.isActive()) {
  127. this._exceptions.push(_id);
  128. this._exceptionsDep.changed();
  129. Tracker.flush();
  130. }
  131. },
  132. resetExceptions() {
  133. this._exceptions = [];
  134. this._exceptionsDep.changed();
  135. },
  136. };
  137. Blaze.registerHelper('Filter', Filter);