215 lines
6.4 KiB
JavaScript
215 lines
6.4 KiB
JavaScript
|
'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};
|