@odopod/odo-dual-viewer
Version:
A draggable UI component which reveals two panes.
534 lines (409 loc) • 16.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('tiny-emitter'), require('@odopod/odo-helpers'), require('@odopod/odo-draggable'), require('@odopod/odo-window-events'), require('@odopod/odo-object-fit')) :
typeof define === 'function' && define.amd ? define(['tiny-emitter', '@odopod/odo-helpers', '@odopod/odo-draggable', '@odopod/odo-window-events', '@odopod/odo-object-fit'], factory) :
(global.OdoDualViewer = factory(global.TinyEmitter,global.OdoHelpers,global.OdoDraggable,global.OdoWindowEvents,global.OdoObjectFit));
}(this, (function (TinyEmitter,odoHelpers,OdoDraggable,OdoWindowEvents,OdoObjectFit) { 'use strict';
TinyEmitter = TinyEmitter && TinyEmitter.hasOwnProperty('default') ? TinyEmitter['default'] : TinyEmitter;
OdoDraggable = OdoDraggable && OdoDraggable.hasOwnProperty('default') ? OdoDraggable['default'] : OdoDraggable;
OdoWindowEvents = OdoWindowEvents && OdoWindowEvents.hasOwnProperty('default') ? OdoWindowEvents['default'] : OdoWindowEvents;
OdoObjectFit = OdoObjectFit && OdoObjectFit.hasOwnProperty('default') ? OdoObjectFit['default'] : OdoObjectFit;
var settings = {
Position: {
START: 0,
CENTER: 1,
END: 2,
BETWEEN: 3
},
ClassName: {
VERTICAL: 'odo-dual-viewer--vertical',
INNER: 'odo-dual-viewer__inner',
SCRUBBER_CONTAINER: 'odo-dual-viewer__scrubber-bounds',
SCRUBBER: 'odo-dual-viewer__scrubber',
OVERLAY: 'odo-dual-viewer__overlay',
UNDERLAY: 'odo-dual-viewer__underlay',
MEDIA: 'odo-dual-viewer__media',
// States
GRABBING: 'grabbing',
CENTERED: 'is-centered',
START: 'is-start',
END: 'is-end'
},
EventType: {
CAME_TO_REST: 'ododualviewer:handlecametorest'
},
Defaults: {
startPosition: 0.5,
isVertical: false,
animationDuration: 300,
verticalSafeZone: 0.1,
hasZones: true,
zones: [0.33, 0.33, 0.66, 0.66]
}
};
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var inherits = function (subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
var possibleConstructorReturn = function (self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
};
var DualViewer = function (_TinyEmitter) {
inherits(DualViewer, _TinyEmitter);
/**
* Component which has a draggable element in the middle which reveals one or
* the other sides as the user drags.
*
* @constructor
*/
function DualViewer(el, opts) {
classCallCheck(this, DualViewer);
var _this = possibleConstructorReturn(this, _TinyEmitter.call(this));
_this.element = el;
_this.options = Object.assign({}, DualViewer.Defaults, opts);
_this._isVertical = _this.options.isVertical;
/** @private {Element} */
_this._scrubberEl = null;
/** @private {Element} */
_this._overlayEl = null;
/** @private {Element} */
_this._underlayEl = null;
/** @private {Element} */
_this._overlayObjectEl = null;
/**
* Dragger component
* @type {OdoDraggable}
* @private
*/
_this._draggable = null;
/**
* Boundary for the scrubber.
* @type {Rect}
* @private
*/
_this._scrubberLimits = null;
/**
* The axis to drag depends on the carousel direction.
* @type {OdoPointer.Axis}
* @private
*/
_this._dragAxis = _this._isVertical ? 'y' : 'x';
/**
* Height or width.
* @type {string}
* @private
*/
_this._dimensionAttr = _this._isVertical ? 'height' : 'width';
/**
* Previous percentage revealed. Needed for window resizes to reset back to
* correct position.
* @type {number}
* @private
*/
_this._previousPercent = _this.options.startPosition;
/**
* Current position of the dual viewer.
* @type {number}
* @private
*/
_this._position = DualViewer.Position.CENTER;
/** @private {boolean} */
_this._isResting = true;
_this.decorate();
return _this;
}
/**
* Scope the query to the main element.
* @param {string} className Class name of the desired element.
* @return {?Element} The element or null if not found.
*/
DualViewer.prototype.getElementByClass = function getElementByClass(className) {
return this.element.querySelector('.' + className);
};
/**
* Scope the query to the main element.
* @param {string} className Class name of the desired elements.
* @return {NodeList} An array like object of elements.
*/
DualViewer.prototype.getElementsByClass = function getElementsByClass(className) {
return this.element.querySelectorAll('.' + className);
};
DualViewer.prototype.decorate = function decorate() {
this._scrubberEl = this.getElementByClass(DualViewer.ClassName.SCRUBBER);
this._overlayEl = this.getElementByClass(DualViewer.ClassName.OVERLAY);
this._underlayEl = this.getElementByClass(DualViewer.ClassName.UNDERLAY);
this._overlayObjectEl = this._overlayEl.firstElementChild;
this._draggable = new OdoDraggable(this._scrubberEl, {
axis: this._dragAxis
});
// Add vertical class if it's vertical and the is-centered class.
this.element.classList.toggle(DualViewer.ClassName.VERTICAL, this._isVertical);
this.element.classList.add(DualViewer.ClassName.CENTERED);
this._setupHandlers();
this._saveContainerSize();
this._saveScrubberLimits();
OdoObjectFit.cover(this.getElementsByClass(DualViewer.ClassName.MEDIA));
this._draggable.setLimits(this._scrubberLimits);
this.setPosition(this.options.startPosition);
};
DualViewer.prototype._setupHandlers = function _setupHandlers() {
this._resizeId = OdoWindowEvents.onResize(this.reset.bind(this));
this._onMove = this._handleDragMove.bind(this);
this._onEnd = this._handleDragEnd.bind(this);
this._draggable.on(OdoDraggable.EventType.MOVE, this._onMove);
this._draggable.on(OdoDraggable.EventType.END, this._onEnd);
};
/**
* Sets the containing box for the scrubber to stay within.
* @private
*/
DualViewer.prototype._saveScrubberLimits = function _saveScrubberLimits() {
var limits = void 0;
if (this._isVertical) {
var top = this._containerHeight * this.options.verticalSafeZone;
var height = this._containerHeight * (1 - this.options.verticalSafeZone * 2);
limits = new odoHelpers.Rect(0, top, this._containerWidth, height);
} else {
var containingEl = this.getElementByClass(DualViewer.ClassName.SCRUBBER_CONTAINER);
var mainRect = this.element.getBoundingClientRect();
var containingRect = containingEl.getBoundingClientRect();
limits = new odoHelpers.Rect(Math.round(containingRect.left - mainRect.left), containingEl.offsetTop, containingEl.offsetWidth, this._containerHeight);
}
this._scrubberLimits = limits;
};
/**
* Returns the dual viewer's current position.
* @return {number} Position number. Look at the DualViewer.
* Position enum for reference.
*/
DualViewer.prototype.getPosition = function getPosition() {
return this._position;
};
/**
* Set the position of the scrubber and the amount revealed.
* @param {number} position Percentage to be revealed. A number between zero and one.
*/
DualViewer.prototype.setPosition = function setPosition(position) {
// Draggable clamps this based on the axis.
this._draggable.setPosition(position * 100, position * 100);
this._reveal(position);
};
/**
* Clamps a percentage value to the boundaries of the scrubber.
* @param {number} percent Number between zero and one.
* @return {number} Clamped percentage between the left and right boundaries if
* the dual viewer is horizontal, or the top and bottom boundaries if the
* dual viewer is vertical.
* @private
*/
DualViewer.prototype._getLimitedPercent = function _getLimitedPercent(percent) {
var rect = this._scrubberLimits;
var min = void 0;
var max = void 0;
if (this._isVertical) {
min = rect.top / this._containerHeight;
max = (rect.top + rect.height) / this._containerHeight;
} else {
min = rect.left / this._containerWidth;
max = (rect.left + rect.width) / this._containerWidth;
}
return odoHelpers.clamp(percent, min, max);
};
/**
* Caches the main element's dimensions.
* @private
*/
DualViewer.prototype._saveContainerSize = function _saveContainerSize() {
this._containerWidth = this.element.offsetWidth;
this._containerHeight = this.element.offsetHeight;
};
/**
* Toggle state classes.
* @private
*/
DualViewer.prototype._removeStateClasses = function _removeStateClasses() {
this.element.classList.remove(DualViewer.ClassName.START, DualViewer.ClassName.END, DualViewer.ClassName.CENTERED);
this._isResting = false;
};
/**
* Reveals or covers up the second object.
* @param {number} percent Number between zero and one.
* @private
*/
DualViewer.prototype._reveal = function _reveal(percent) {
this._overlayEl.style[this._dimensionAttr] = percent * 100 + '%';
this._overlayObjectEl.style[this._dimensionAttr] = 100 / percent + '%';
};
/**
* Resets the scrubber position, scrubber limits, draggable position, and amount
* revealed based new width/height measurements and the previous position of the
* scrubber.
*/
DualViewer.prototype.reset = function reset() {
this._saveContainerSize();
this._saveScrubberLimits();
OdoObjectFit.cover(this.getElementsByClass(DualViewer.ClassName.MEDIA));
this._draggable.setLimits(this._scrubberLimits);
var percent = this._previousPercent * 100;
this._draggable.setPosition(percent, percent);
this._reveal(this._getLimitedPercent(this._previousPercent));
};
/**
* Dragged.
* @param {Event} evt Custom event object.
* @private
*/
DualViewer.prototype._handleDragMove = function _handleDragMove(evt) {
if (this._isResting) {
this._removeStateClasses();
this._position = DualViewer.Position.BETWEEN;
}
var percentRevealed = evt.position.percent[this._dragAxis] / 100;
// Save value because if the window is resized, the scrubber will be way off.
this._previousPercent = percentRevealed;
this._reveal(percentRevealed);
};
/**
* Dragging ended.
*
* Default zones shown below.
* L = lower zone.
* M = middle zone.
* U = upper zone.
*
* +-------------------------------------+
* | | | | | |
* | | | | | |
* | L | | M | | U |
* | | | | | |
* | | | | | |
* +-------------------------------------+
* | | |
* 0.25 0.5 0.75
*
* @param {Event} evt Custom event object.
* @private
*/
DualViewer.prototype._handleDragEnd = function _handleDragEnd(evt) {
if (!this.options.hasZones) {
return;
}
// Percentage revealed.
var revealed = evt.position.percent[this._dragAxis] / 100;
this._position = this._getZone(revealed);
var percent = null;
switch (this._position) {
case DualViewer.Position.START:
percent = 0;
break;
case DualViewer.Position.CENTER:
percent = 0.5;
break;
case DualViewer.Position.END:
percent = 1;
break;
// no default
}
if (percent !== null) {
this.animateTo(percent);
}
};
DualViewer.prototype._getZone = function _getZone(revealed) {
if (revealed > 0 && revealed < this.options.zones[0]) {
return DualViewer.Position.START;
}
if (revealed < 1 && revealed > this.options.zones[3]) {
return DualViewer.Position.END;
}
if (revealed >= this.options.zones[1] && revealed <= this.options.zones[2]) {
return DualViewer.Position.CENTER;
}
return DualViewer.Position.BETWEEN;
};
/**
* Animate the dual viewer to a percentage.
* @param {number} toPercent Percent to animate to. It will get clamped to the
* scrubber's limits. This is a number between zero and one.
*/
DualViewer.prototype.animateTo = function animateTo(toPercent) {
// Clamp to boundaries.
var percent = this._getLimitedPercent(toPercent);
// Don't animate if the handle isn't going anywhere.
if (percent === this._previousPercent) {
return;
}
this._position = this._getZone(percent);
this._removeStateClasses();
var stepper = new odoHelpers.Stepper({
start: this._previousPercent,
end: percent,
duration: this.options.animationDuration,
step: this._animateStep,
context: this
});
stepper.onfinish = this._didComeToRest;
};
/**
* The tick function. Sets the handle and revealer.
* @param {number} position Applied percentage added to the starting point.
* @private
*/
DualViewer.prototype._animateStep = function _animateStep(position) {
// Move handle.
this._draggable.setPosition(position * 100, position * 100);
// Move reveals.
this._reveal(position);
};
/**
* Dual Viewer animation came to rest. Toggle classes and states.
* @private
*/
DualViewer.prototype._didComeToRest = function _didComeToRest() {
this._previousPercent = this._draggable.getPosition(true)[this._dragAxis] / 100;
this._isResting = true;
this.element.classList.toggle(DualViewer.ClassName.START, this._position === DualViewer.Position.START);
this.element.classList.toggle(DualViewer.ClassName.END, this._position === DualViewer.Position.END);
this.element.classList.toggle(DualViewer.ClassName.CENTERED, this._position === DualViewer.Position.CENTER);
// Emit event.
this.emit(DualViewer.EventType.CAME_TO_REST, {
position: this.getPosition()
});
};
/**
* Reset the style attribute for the properties we might have manipulated.
*/
DualViewer.prototype.dispose = function dispose() {
OdoWindowEvents.remove(this._resizeId);
this._draggable.off(OdoDraggable.EventType.MOVE, this._onMove);
this._draggable.off(OdoDraggable.EventType.END, this._onEnd);
this._draggable.dispose();
this.element.classList.remove(DualViewer.ClassName.CENTERED, DualViewer.ClassName.VERTICAL);
this.element = null;
this._scrubberEl = null;
this._overlayObjectEl = null;
this._overlayEl = null;
this._underlayEl = null;
};
return DualViewer;
}(TinyEmitter);
Object.assign(DualViewer, settings);
return DualViewer;
})));
//# sourceMappingURL=odo-dual-viewer.js.map