UNPKG

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