Version 3.18.1
Show:

File: tree/js/extensions/tree-sortable.js

  1. /*jshint expr:true, onevar:false */
  2. /**
  3. Extension for `Tree` that makes nodes sortable.
  4. @module tree
  5. @submodule tree-sortable
  6. @main tree-sortable
  7. **/
  8. /**
  9. Extension for `Tree` that makes nodes sortable.
  10. @class Tree.Sortable
  11. @constructor
  12. @param {Object} [config] Configuration options.
  13. @param {Function} [config.sortComparator] Default comparator function to use
  14. when sorting a node's children if the node itself doesn't have a custom
  15. comparator function. If not specified, insertion order will be used by
  16. default.
  17. @param {Boolean} [config.sortReverse=false] If `true`, node children will be
  18. sorted in reverse (descending) order by default. Otherwise they'll be sorted
  19. in ascending order.
  20. @extensionfor Tree
  21. **/
  22. /**
  23. Fired after a node's children are re-sorted.
  24. @event sort
  25. @param {Tree.Node} node Node whose children were sorted.
  26. @param {Boolean} reverse `true` if the children were sorted in reverse
  27. (descending) order, `false` otherwise.
  28. @param {String} src Source of the event.
  29. **/
  30. var EVT_SORT = 'sort';
  31. function Sortable() {}
  32. Sortable.prototype = {
  33. // -- Public Properties ----------------------------------------------------
  34. /**
  35. If `true`, node children will be sorted in reverse (descending) order by
  36. default. Otherwise they'll be sorted in ascending order.
  37. @property {Boolean} sortReverse
  38. @default false
  39. **/
  40. sortReverse: false,
  41. // -- Lifecycle ------------------------------------------------------------
  42. initializer: function (config) {
  43. this.nodeExtensions = this.nodeExtensions.concat(Y.Tree.Node.Sortable);
  44. if (config) {
  45. if (config.sortComparator) {
  46. this.sortComparator = config.sortComparator;
  47. }
  48. if ('sortReverse' in config) {
  49. this.sortReverse = config.sortReverse;
  50. }
  51. }
  52. },
  53. // -- Public Methods -------------------------------------------------------
  54. /**
  55. Sorts the children of every node in this tree.
  56. A `sort` event will be fired for each node whose children are sorted, which
  57. can get very noisy. If this is a large tree, you may want to set the
  58. `silent` option to `true` to suppress these events.
  59. @method sort
  60. @param {Object} [options] Options.
  61. @param {Boolean} [options.silent] If `true`, no `sort` events will be
  62. fired.
  63. @param {Function} [options.sortComparator] Custom comparator function to
  64. use. If specified, this will become the new comparator function for
  65. each node, overwriting any previous comparator function that was set
  66. for the node.
  67. @param {Boolean} [options.sortReverse] If `true`, children will be
  68. sorted in reverse (descending) order. Otherwise they'll be sorted in
  69. ascending order. This will become each node's new sort order,
  70. overwriting any previous sort order that was set for the node.
  71. @param {String} [options.src] Source of the sort operation. Will be
  72. passed along to the `sort` event facade.
  73. @chainable
  74. **/
  75. sort: function (options) {
  76. return this.sortNode(this.rootNode, Y.merge(options, {deep: true}));
  77. },
  78. /**
  79. Default comparator function to use when sorting a node's children if the
  80. node itself doesn't have a custom comparator function.
  81. If not specified, insertion order will be used by default.
  82. @method sortComparator
  83. @param {Tree.Node} node Node being sorted.
  84. @return {Number|String} Value by which the node should be sorted relative to
  85. its siblings.
  86. **/
  87. sortComparator: function (node) {
  88. return node.index();
  89. },
  90. /**
  91. Sorts the children of the specified node.
  92. By default, only the node's direct children are sorted. To sort all nodes in
  93. the hierarchy (children, children's children, etc.), set the `deep` option
  94. to `true`. If this is a very deep hierarchy, you may also want to set
  95. `silent` to true to avoid generating a flood of `sort` events.
  96. @method sortNode
  97. @param {Tree.Node} node Node whose children should be sorted.
  98. @param {Object} [options] Options.
  99. @param {Boolean} [options.deep=false] If `true`, all of this node's
  100. children (and their children, and so on) will be traversed and
  101. re-sorted as well.
  102. @param {Boolean} [options.silent] If `true`, no `sort` event will be
  103. fired.
  104. @param {Function} [options.sortComparator] Custom comparator function to
  105. use. If specified, this will become the node's new comparator
  106. function, overwriting any previous comparator function that was set
  107. for the node.
  108. @param {Boolean} [options.sortReverse] If `true`, children will be
  109. sorted in reverse (descending) order. Otherwise they'll be sorted in
  110. ascending order. This will become the node's new sort order,
  111. overwriting any previous sort order that was set for the node.
  112. @param {String} [options.src] Source of the sort operation. Will be
  113. passed along to the `sort` event facade.
  114. @chainable
  115. **/
  116. sortNode: function (node, options) {
  117. // Nothing to do if the node has no children.
  118. if (!node.children.length) {
  119. return this;
  120. }
  121. options || (options = {});
  122. if (options.deep) {
  123. // Unset the `deep` option so we don't cause an infinite loop.
  124. options = Y.merge(options, {deep: false});
  125. var self = this;
  126. // Traverse and sort all nodes (including this one).
  127. this.traverseNode(node, function (nodeToSort) {
  128. self.sortNode(nodeToSort, options);
  129. });
  130. return this;
  131. }
  132. var comparator = this._getSortComparator(node, options),
  133. reverse;
  134. if ('sortReverse' in options) {
  135. reverse = node.sortReverse = options.sortReverse;
  136. } else if ('sortReverse' in node) {
  137. reverse = node.sortReverse;
  138. } else {
  139. reverse = this.sortReverse;
  140. }
  141. node.children.sort(Y.rbind(this._sort, this, comparator, reverse));
  142. node._isIndexStale = true;
  143. if (!options.silent) {
  144. this.fire(EVT_SORT, {
  145. node : node,
  146. reverse: !!reverse,
  147. src : options.src
  148. });
  149. }
  150. return this;
  151. },
  152. // -- Protected Methods ----------------------------------------------------
  153. /**
  154. Compares value _a_ to value _b_ for sorting purposes.
  155. Values are assumed to be the result of calling a sortComparator function.
  156. @method _compare
  157. @param {Mixed} a First value to compare.
  158. @param {Mixed} b Second value to compare.
  159. @return {Number} `-1` if _a_ should come before _b_, `0` if they're
  160. equivalent, `1` if _a_ should come after _b_.
  161. @protected
  162. **/
  163. _compare: function (a, b) {
  164. return a < b ? -1 : (a > b ? 1 : 0);
  165. },
  166. /**
  167. Compares value _a_ to value _b_ for sorting purposes, but sorts them in
  168. reverse (descending) order.
  169. Values are assumed to be the result of calling a sortComparator function.
  170. @method _compareReverse
  171. @param {Mixed} a First value to compare.
  172. @param {Mixed} b Second value to compare.
  173. @return {Number} `-1` if _a_ should come before _b_, `0` if they're
  174. equivalent, `1` if _a_ should come after _b_.
  175. @protected
  176. **/
  177. _compareReverse: function (a, b) {
  178. return b < a ? -1 : (b > a ? 1 : 0);
  179. },
  180. /**
  181. Overrides `Tree#_getDefaultNodeIndex()` to provide insertion-time sorting
  182. for nodes inserted without an explicit index.
  183. @method _getDefaultNodeIndex
  184. @param {Tree.Node} parent Parent node.
  185. @param {Tree.Node} node Node being inserted.
  186. @param {Object} [options] Options passed to `insertNode()`.
  187. @return {Number} Index at which _node_ should be inserted into _parent_'s
  188. `children` array.
  189. @protected
  190. **/
  191. _getDefaultNodeIndex: function (parent, node) {
  192. /*jshint bitwise:false */
  193. var children = parent.children,
  194. comparator = this._getSortComparator(parent),
  195. max = children.length,
  196. min = 0,
  197. reverse = 'sortReverse' in parent ? parent.sortReverse : this.sortReverse;
  198. if (!max) {
  199. return max;
  200. }
  201. // Special case: if the sortComparator is the default sortComparator,
  202. // cheat and just return the first or last index of the children array.
  203. //
  204. // This is necessary because the default sortComparator relies on
  205. // the node's index, which is always -1 for uninserted nodes.
  206. if (comparator._unboundComparator === Sortable.prototype.sortComparator) {
  207. return reverse ? 0 : max;
  208. }
  209. var compare = reverse ? this._compareReverse : this._compare,
  210. needle = comparator(node);
  211. // Perform an iterative binary search to determine the correct position
  212. // for the node based on the return value of the comparator function.
  213. var middle;
  214. while (min < max) {
  215. middle = (min + max) >> 1; // Divide by two and discard remainder.
  216. if (compare(comparator(children[middle]), needle) < 0) {
  217. min = middle + 1;
  218. } else {
  219. max = middle;
  220. }
  221. }
  222. return min;
  223. },
  224. /**
  225. Returns a sort comparator function derived from the given _node_ and
  226. _options_, and bound to the correct `thisObj` based on where it was found.
  227. @method _getSortComparator
  228. @param {Tree.Node} node Node on which to look for a `sortComparator`
  229. function.
  230. @param {Object} [options] Options object on which to look for a
  231. `sortComparator` function.
  232. @return {Function} Properly bound sort comparator function.
  233. @protected
  234. **/
  235. _getSortComparator: function (node, options) {
  236. var boundComparator,
  237. comparator,
  238. thisObj;
  239. if (options && options.sortComparator) {
  240. comparator = node.sortComparator = options.sortComparator;
  241. } else if (node.sortComparator) {
  242. comparator = node.sortComparator;
  243. thisObj = node;
  244. } else {
  245. comparator = this.sortComparator;
  246. thisObj = this;
  247. }
  248. boundComparator = function () {
  249. return comparator.apply(thisObj, arguments);
  250. };
  251. boundComparator._unboundComparator = comparator;
  252. return boundComparator;
  253. },
  254. /**
  255. Array sort function used by `sortNode()` to re-sort a node's children.
  256. @method _sort
  257. @param {Tree.Node} a First node to compare.
  258. @param {Tree.Node} b Second node to compare.
  259. @param {Function} comparator Comparator function.
  260. @param {Boolean} [reverse=false] If `true`, this will be a reverse
  261. (descending) comparison.
  262. @return {Number} `-1` if _a_ is less than _b_, `0` if equal, `1` if greater.
  263. @protected
  264. **/
  265. _sort: function (a, b, comparator, reverse) {
  266. return this[reverse ? '_compareReverse' : '_compare'](
  267. comparator(a), comparator(b));
  268. }
  269. };
  270. Y.Tree.Sortable = Sortable;