foundation.positionable.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. 'use strict';
  2. import { Box } from './foundation.util.box';
  3. import { Plugin } from './foundation.core.plugin';
  4. import { rtl as Rtl } from './foundation.core.utils';
  5. const POSITIONS = ['left', 'right', 'top', 'bottom'];
  6. const VERTICAL_ALIGNMENTS = ['top', 'bottom', 'center'];
  7. const HORIZONTAL_ALIGNMENTS = ['left', 'right', 'center'];
  8. const ALIGNMENTS = {
  9. 'left': VERTICAL_ALIGNMENTS,
  10. 'right': VERTICAL_ALIGNMENTS,
  11. 'top': HORIZONTAL_ALIGNMENTS,
  12. 'bottom': HORIZONTAL_ALIGNMENTS
  13. }
  14. function nextItem(item, array) {
  15. var currentIdx = array.indexOf(item);
  16. if(currentIdx === array.length - 1) {
  17. return array[0];
  18. } else {
  19. return array[currentIdx + 1];
  20. }
  21. }
  22. class Positionable extends Plugin {
  23. /**
  24. * Abstract class encapsulating the tether-like explicit positioning logic
  25. * including repositioning based on overlap.
  26. * Expects classes to define defaults for vOffset, hOffset, position,
  27. * alignment, allowOverlap, and allowBottomOverlap. They can do this by
  28. * extending the defaults, or (for now recommended due to the way docs are
  29. * generated) by explicitly declaring them.
  30. *
  31. **/
  32. _init() {
  33. this.triedPositions = {};
  34. this.position = this.options.position === 'auto' ? this._getDefaultPosition() : this.options.position;
  35. this.alignment = this.options.alignment === 'auto' ? this._getDefaultAlignment() : this.options.alignment;
  36. this.originalPosition = this.position;
  37. this.originalAlignment = this.alignment;
  38. }
  39. _getDefaultPosition () {
  40. return 'bottom';
  41. }
  42. _getDefaultAlignment() {
  43. switch(this.position) {
  44. case 'bottom':
  45. case 'top':
  46. return Rtl() ? 'right' : 'left';
  47. case 'left':
  48. case 'right':
  49. return 'bottom';
  50. }
  51. }
  52. /**
  53. * Adjusts the positionable possible positions by iterating through alignments
  54. * and positions.
  55. * @function
  56. * @private
  57. */
  58. _reposition() {
  59. if(this._alignmentsExhausted(this.position)) {
  60. this.position = nextItem(this.position, POSITIONS);
  61. this.alignment = ALIGNMENTS[this.position][0];
  62. } else {
  63. this._realign();
  64. }
  65. }
  66. /**
  67. * Adjusts the dropdown pane possible positions by iterating through alignments
  68. * on the current position.
  69. * @function
  70. * @private
  71. */
  72. _realign() {
  73. this._addTriedPosition(this.position, this.alignment)
  74. this.alignment = nextItem(this.alignment, ALIGNMENTS[this.position])
  75. }
  76. _addTriedPosition(position, alignment) {
  77. this.triedPositions[position] = this.triedPositions[position] || []
  78. this.triedPositions[position].push(alignment);
  79. }
  80. _positionsExhausted() {
  81. var isExhausted = true;
  82. for(var i = 0; i < POSITIONS.length; i++) {
  83. isExhausted = isExhausted && this._alignmentsExhausted(POSITIONS[i]);
  84. }
  85. return isExhausted;
  86. }
  87. _alignmentsExhausted(position) {
  88. return this.triedPositions[position] && this.triedPositions[position].length == ALIGNMENTS[position].length;
  89. }
  90. // When we're trying to center, we don't want to apply offset that's going to
  91. // take us just off center, so wrap around to return 0 for the appropriate
  92. // offset in those alignments. TODO: Figure out if we want to make this
  93. // configurable behavior... it feels more intuitive, especially for tooltips, but
  94. // it's possible someone might actually want to start from center and then nudge
  95. // slightly off.
  96. _getVOffset() {
  97. return this.options.vOffset;
  98. }
  99. _getHOffset() {
  100. return this.options.hOffset;
  101. }
  102. _setPosition($anchor, $element, $parent) {
  103. if($anchor.attr('aria-expanded') === 'false'){ return false; }
  104. var $eleDims = Box.GetDimensions($element),
  105. $anchorDims = Box.GetDimensions($anchor);
  106. if (!this.options.allowOverlap) {
  107. // restore original position & alignment before checking overlap
  108. this.position = this.originalPosition;
  109. this.alignment = this.originalAlignment;
  110. }
  111. $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
  112. if(!this.options.allowOverlap) {
  113. var overlaps = {};
  114. var minOverlap = 100000000;
  115. // default coordinates to how we start, in case we can't figure out better
  116. var minCoordinates = {position: this.position, alignment: this.alignment};
  117. while(!this._positionsExhausted()) {
  118. let overlap = Box.OverlapArea($element, $parent, false, false, this.options.allowBottomOverlap);
  119. if(overlap === 0) {
  120. return;
  121. }
  122. if(overlap < minOverlap) {
  123. minOverlap = overlap;
  124. minCoordinates = {position: this.position, alignment: this.alignment};
  125. }
  126. this._reposition();
  127. $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
  128. }
  129. // If we get through the entire loop, there was no non-overlapping
  130. // position available. Pick the version with least overlap.
  131. this.position = minCoordinates.position;
  132. this.alignment = minCoordinates.alignment;
  133. $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
  134. }
  135. }
  136. }
  137. Positionable.defaults = {
  138. /**
  139. * Position of positionable relative to anchor. Can be left, right, bottom, top, or auto.
  140. * @option
  141. * @type {string}
  142. * @default 'auto'
  143. */
  144. position: 'auto',
  145. /**
  146. * Alignment of positionable relative to anchor. Can be left, right, bottom, top, center, or auto.
  147. * @option
  148. * @type {string}
  149. * @default 'auto'
  150. */
  151. alignment: 'auto',
  152. /**
  153. * Allow overlap of container/window. If false, dropdown positionable first
  154. * try to position as defined by data-position and data-alignment, but
  155. * reposition if it would cause an overflow.
  156. * @option
  157. * @type {boolean}
  158. * @default false
  159. */
  160. allowOverlap: false,
  161. /**
  162. * Allow overlap of only the bottom of the container. This is the most common
  163. * behavior for dropdowns, allowing the dropdown to extend the bottom of the
  164. * screen but not otherwise influence or break out of the container.
  165. * @option
  166. * @type {boolean}
  167. * @default true
  168. */
  169. allowBottomOverlap: true,
  170. /**
  171. * Number of pixels the positionable should be separated vertically from anchor
  172. * @option
  173. * @type {number}
  174. * @default 0
  175. */
  176. vOffset: 0,
  177. /**
  178. * Number of pixels the positionable should be separated horizontally from anchor
  179. * @option
  180. * @type {number}
  181. * @default 0
  182. */
  183. hOffset: 0,
  184. }
  185. export {Positionable};