DistributeElements.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import {
  2. sortBy,
  3. forEach,
  4. isArray
  5. } from 'min-dash';
  6. var AXIS_DIMENSIONS = {
  7. horizontal: [ 'x', 'width' ],
  8. vertical: [ 'y', 'height' ]
  9. };
  10. var THRESHOLD = 5;
  11. /**
  12. * Groups and filters elements and then trigger even distribution.
  13. */
  14. export default function DistributeElements(modeling, rules) {
  15. this._modeling = modeling;
  16. this._filters = [];
  17. this.registerFilter(function(elements) {
  18. var allowed = rules.allowed('elements.distribute', { elements: elements });
  19. if (isArray(allowed)) {
  20. return allowed;
  21. }
  22. return allowed ? elements : [];
  23. });
  24. }
  25. DistributeElements.$inject = [ 'modeling', 'rules' ];
  26. /**
  27. * Registers filter functions that allow external parties to filter
  28. * out certain elements.
  29. *
  30. * @param {Function} filterFn
  31. */
  32. DistributeElements.prototype.registerFilter = function(filterFn) {
  33. if (typeof filterFn !== 'function') {
  34. throw new Error('the filter has to be a function');
  35. }
  36. this._filters.push(filterFn);
  37. };
  38. /**
  39. * Distributes the elements with a given orientation
  40. *
  41. * @param {Array} elements
  42. * @param {string} orientation
  43. */
  44. DistributeElements.prototype.trigger = function(elements, orientation) {
  45. var modeling = this._modeling;
  46. var groups,
  47. distributableElements;
  48. if (elements.length < 3) {
  49. return;
  50. }
  51. this._setOrientation(orientation);
  52. distributableElements = this._filterElements(elements);
  53. groups = this._createGroups(distributableElements);
  54. // nothing to distribute
  55. if (groups.length <= 2) {
  56. return;
  57. }
  58. modeling.distributeElements(groups, this._axis, this._dimension);
  59. return groups;
  60. };
  61. /**
  62. * Filters the elements with provided filters by external parties
  63. *
  64. * @param {Array[Elements]} elements
  65. *
  66. * @return {Array[Elements]}
  67. */
  68. DistributeElements.prototype._filterElements = function(elements) {
  69. var filters = this._filters,
  70. axis = this._axis,
  71. dimension = this._dimension,
  72. distributableElements = [].concat(elements);
  73. if (!filters.length) {
  74. return elements;
  75. }
  76. forEach(filters, function(filterFn) {
  77. distributableElements = filterFn(distributableElements, axis, dimension);
  78. });
  79. return distributableElements;
  80. };
  81. /**
  82. * Create range (min, max) groups. Also tries to group elements
  83. * together that share the same range.
  84. *
  85. * @example
  86. * var distributableElements = [
  87. * {
  88. * range: {
  89. * min: 100,
  90. * max: 200
  91. * },
  92. * elements: [ { id: 'shape1', .. }]
  93. * }
  94. * ]
  95. *
  96. * @param {Array} elements
  97. *
  98. * @return {Array[Objects]}
  99. */
  100. DistributeElements.prototype._createGroups = function(elements) {
  101. var rangeGroups = [],
  102. self = this,
  103. axis = this._axis,
  104. dimension = this._dimension;
  105. if (!axis) {
  106. throw new Error('must have a defined "axis" and "dimension"');
  107. }
  108. // sort by 'left->right' or 'top->bottom'
  109. var sortedElements = sortBy(elements, axis);
  110. forEach(sortedElements, function(element, idx) {
  111. var elementRange = self._findRange(element, axis, dimension),
  112. range;
  113. var previous = rangeGroups[rangeGroups.length - 1];
  114. if (previous && self._hasIntersection(previous.range, elementRange)) {
  115. rangeGroups[rangeGroups.length - 1].elements.push(element);
  116. } else {
  117. range = { range: elementRange, elements: [ element ] };
  118. rangeGroups.push(range);
  119. }
  120. });
  121. return rangeGroups;
  122. };
  123. /**
  124. * Maps a direction to the according axis and dimension
  125. *
  126. * @param {string} direction 'horizontal' or 'vertical'
  127. */
  128. DistributeElements.prototype._setOrientation = function(direction) {
  129. var orientation = AXIS_DIMENSIONS[direction];
  130. this._axis = orientation[0];
  131. this._dimension = orientation[1];
  132. };
  133. /**
  134. * Checks if the two ranges intercept each other
  135. *
  136. * @param {Object} rangeA {min, max}
  137. * @param {Object} rangeB {min, max}
  138. *
  139. * @return {boolean}
  140. */
  141. DistributeElements.prototype._hasIntersection = function(rangeA, rangeB) {
  142. return Math.max(rangeA.min, rangeA.max) >= Math.min(rangeB.min, rangeB.max) &&
  143. Math.min(rangeA.min, rangeA.max) <= Math.max(rangeB.min, rangeB.max);
  144. };
  145. /**
  146. * Returns the min and max values for an element
  147. *
  148. * @param {Bounds} element
  149. * @param {string} axis
  150. * @param {string} dimension
  151. *
  152. * @return {{ min: number, max: number }}
  153. */
  154. DistributeElements.prototype._findRange = function(element) {
  155. var axis = element[this._axis],
  156. dimension = element[this._dimension];
  157. return {
  158. min: axis + THRESHOLD,
  159. max: axis + dimension - THRESHOLD
  160. };
  161. };