biofriction-wp-theme/node_modules/foundation-sites/js/foundation.positionable.js

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};