123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- 'use strict';
- import { Box } from './foundation.util.box';
- import { Plugin } from './foundation.core.plugin';
- import { rtl as Rtl } from './foundation.core.utils';
- const POSITIONS = ['left', 'right', 'top', 'bottom'];
- const VERTICAL_ALIGNMENTS = ['top', 'bottom', 'center'];
- const HORIZONTAL_ALIGNMENTS = ['left', 'right', 'center'];
- const ALIGNMENTS = {
- 'left': VERTICAL_ALIGNMENTS,
- 'right': VERTICAL_ALIGNMENTS,
- 'top': HORIZONTAL_ALIGNMENTS,
- 'bottom': HORIZONTAL_ALIGNMENTS
- }
- function nextItem(item, array) {
- var currentIdx = array.indexOf(item);
- if(currentIdx === array.length - 1) {
- return array[0];
- } else {
- return array[currentIdx + 1];
- }
- }
- class Positionable extends Plugin {
- /**
- * Abstract class encapsulating the tether-like explicit positioning logic
- * including repositioning based on overlap.
- * Expects classes to define defaults for vOffset, hOffset, position,
- * alignment, allowOverlap, and allowBottomOverlap. They can do this by
- * extending the defaults, or (for now recommended due to the way docs are
- * generated) by explicitly declaring them.
- *
- **/
- _init() {
- this.triedPositions = {};
- this.position = this.options.position === 'auto' ? this._getDefaultPosition() : this.options.position;
- this.alignment = this.options.alignment === 'auto' ? this._getDefaultAlignment() : this.options.alignment;
- this.originalPosition = this.position;
- this.originalAlignment = this.alignment;
- }
- _getDefaultPosition () {
- return 'bottom';
- }
- _getDefaultAlignment() {
- switch(this.position) {
- case 'bottom':
- case 'top':
- return Rtl() ? 'right' : 'left';
- case 'left':
- case 'right':
- return 'bottom';
- }
- }
- /**
- * Adjusts the positionable possible positions by iterating through alignments
- * and positions.
- * @function
- * @private
- */
- _reposition() {
- if(this._alignmentsExhausted(this.position)) {
- this.position = nextItem(this.position, POSITIONS);
- this.alignment = ALIGNMENTS[this.position][0];
- } else {
- this._realign();
- }
- }
- /**
- * Adjusts the dropdown pane possible positions by iterating through alignments
- * on the current position.
- * @function
- * @private
- */
- _realign() {
- this._addTriedPosition(this.position, this.alignment)
- this.alignment = nextItem(this.alignment, ALIGNMENTS[this.position])
- }
- _addTriedPosition(position, alignment) {
- this.triedPositions[position] = this.triedPositions[position] || []
- this.triedPositions[position].push(alignment);
- }
- _positionsExhausted() {
- var isExhausted = true;
- for(var i = 0; i < POSITIONS.length; i++) {
- isExhausted = isExhausted && this._alignmentsExhausted(POSITIONS[i]);
- }
- return isExhausted;
- }
- _alignmentsExhausted(position) {
- return this.triedPositions[position] && this.triedPositions[position].length == ALIGNMENTS[position].length;
- }
- // When we're trying to center, we don't want to apply offset that's going to
- // take us just off center, so wrap around to return 0 for the appropriate
- // offset in those alignments. TODO: Figure out if we want to make this
- // configurable behavior... it feels more intuitive, especially for tooltips, but
- // it's possible someone might actually want to start from center and then nudge
- // slightly off.
- _getVOffset() {
- return this.options.vOffset;
- }
- _getHOffset() {
- return this.options.hOffset;
- }
- _setPosition($anchor, $element, $parent) {
- if($anchor.attr('aria-expanded') === 'false'){ return false; }
- var $eleDims = Box.GetDimensions($element),
- $anchorDims = Box.GetDimensions($anchor);
- if (!this.options.allowOverlap) {
- // restore original position & alignment before checking overlap
- this.position = this.originalPosition;
- this.alignment = this.originalAlignment;
- }
- $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
- if(!this.options.allowOverlap) {
- var overlaps = {};
- var minOverlap = 100000000;
- // default coordinates to how we start, in case we can't figure out better
- var minCoordinates = {position: this.position, alignment: this.alignment};
- while(!this._positionsExhausted()) {
- let overlap = Box.OverlapArea($element, $parent, false, false, this.options.allowBottomOverlap);
- if(overlap === 0) {
- return;
- }
- if(overlap < minOverlap) {
- minOverlap = overlap;
- minCoordinates = {position: this.position, alignment: this.alignment};
- }
- this._reposition();
- $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
- }
- // If we get through the entire loop, there was no non-overlapping
- // position available. Pick the version with least overlap.
- this.position = minCoordinates.position;
- this.alignment = minCoordinates.alignment;
- $element.offset(Box.GetExplicitOffsets($element, $anchor, this.position, this.alignment, this._getVOffset(), this._getHOffset()));
- }
- }
- }
- Positionable.defaults = {
- /**
- * Position of positionable relative to anchor. Can be left, right, bottom, top, or auto.
- * @option
- * @type {string}
- * @default 'auto'
- */
- position: 'auto',
- /**
- * Alignment of positionable relative to anchor. Can be left, right, bottom, top, center, or auto.
- * @option
- * @type {string}
- * @default 'auto'
- */
- alignment: 'auto',
- /**
- * Allow overlap of container/window. If false, dropdown positionable first
- * try to position as defined by data-position and data-alignment, but
- * reposition if it would cause an overflow.
- * @option
- * @type {boolean}
- * @default false
- */
- allowOverlap: false,
- /**
- * Allow overlap of only the bottom of the container. This is the most common
- * behavior for dropdowns, allowing the dropdown to extend the bottom of the
- * screen but not otherwise influence or break out of the container.
- * @option
- * @type {boolean}
- * @default true
- */
- allowBottomOverlap: true,
- /**
- * Number of pixels the positionable should be separated vertically from anchor
- * @option
- * @type {number}
- * @default 0
- */
- vOffset: 0,
- /**
- * Number of pixels the positionable should be separated horizontally from anchor
- * @option
- * @type {number}
- * @default 0
- */
- hOffset: 0,
- }
- export {Positionable};
|