@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
706 lines (598 loc) • 18.8 kB
JavaScript
// todo: extract to a separate place
import {
deepExtend,
addClass,
Observable,
isFunction,
setDefaultOptions,
on,
off,
UserEvents
} from '../../common';
import {
convertToHtml,
prepend,
wrapInner,
hasNativeScrolling,
wheelDeltaY,
proxy,
setDefaultEvents
} from '../utils';
import {
Transition,
Animation
} from './fx';
import {
Pane,
PaneDimensions,
Movable,
TapCapture
} from './draggable';
let
extend = Object.assign,
abs = Math.abs,
SNAPBACK_DURATION = 500,
SCROLLBAR_OPACITY = 0.7,
FRICTION = 0.96,
VELOCITY_MULTIPLIER = 10,
MAX_VELOCITY = 55,
OUT_OF_BOUNDS_FRICTION = 0.5,
ANIMATED_SCROLLER_PRECISION = 5,
// SCROLLER_RELEASE_CLASS = 'km-scroller-release',
// SCROLLER_REFRESH_CLASS = 'km-scroller-refresh',
PULL = 'pull',
CHANGE = 'change',
RESIZE = 'resize',
SCROLL = 'scroll',
MOUSE_WHEEL_ID = 2;
class ZoomSnapBack extends Animation {
constructor(options) {
super(options);
let that = this;
extend(that, options);
that.userEvents.bind('gestureend', that.start.bind(this));
that.tapCapture.bind('press', that.cancel.bind(this));
}
enabled() {
return this.movable.scale < this.dimensions.minScale;
}
done() {
return this.dimensions.minScale - this.movable.scale < 0.01;
}
tick() {
let movable = this.movable;
movable.scaleWith(1.1);
this.dimensions.rescale(movable.scale);
}
onEnd() {
let movable = this.movable;
movable.scaleTo(this.dimensions.minScale);
this.dimensions.rescale(movable.scale);
}
}
class DragInertia extends Animation {
constructor(options) {
super();
let that = this;
extend(that, options, {
transition: new Transition({
axis: options.axis,
movable: options.movable,
onEnd() {
that._end();
}
})
});
that.tapCapture.bind('press', function() {
that.cancel();
});
that.userEvents.bind('end', proxy(that.start, that));
that.userEvents.bind('gestureend', proxy(that.start, that));
that.userEvents.bind('tap', proxy(that.onEnd, that));
}
onCancel() {
this.transition.cancel();
}
freeze(location) {
let that = this;
that.cancel();
that._moveTo(location);
}
onEnd() {
let that = this;
if (that.paneAxis.outOfBounds()) {
that._snapBack();
} else {
that._end();
}
}
done() {
return abs(this.velocity) < 1;
}
start(e) {
let that = this,
velocity;
if (!that.dimension.enabled) {
return;
}
if (that.paneAxis.outOfBounds()) {
if (that.transition._started) {
that.transition.cancel();
that.velocity = Math.min(e.touch[that.axis].velocity * that.velocityMultiplier, MAX_VELOCITY);
super.start();
} else {
that._snapBack();
}
} else {
velocity = e.touch.id === MOUSE_WHEEL_ID ? 0 : e.touch[that.axis].velocity;
that.velocity = Math.max(Math.min(velocity * that.velocityMultiplier, MAX_VELOCITY), -MAX_VELOCITY);
that.tapCapture.captureNext();
super.start();
}
}
tick() {
let that = this,
dimension = that.dimension,
friction = that.paneAxis.outOfBounds() ? OUT_OF_BOUNDS_FRICTION : that.friction,
delta = that.velocity *= friction,
location = that.movable[that.axis] + delta;
if (!that.elastic && dimension.outOfBounds(location)) {
location = Math.max(Math.min(location, dimension.max), dimension.min);
that.velocity = 0;
}
that.movable.moveAxis(that.axis, location);
}
_end() {
this.tapCapture.cancelCapture();
this.end();
}
_snapBack() {
let that = this,
dimension = that.dimension,
snapBack = that.movable[that.axis] > dimension.max ? dimension.max : dimension.min;
that._moveTo(snapBack);
}
_moveTo(location) {
this.transition.moveTo({
location: location,
duration: SNAPBACK_DURATION,
ease: Transition.easeOutExpo
});
}
}
class AnimatedScroller extends Animation {
constructor(options) {
super(options);
let that = this;
extend(that, options, {
origin: {},
destination: {},
offset: {}
});
}
tick() {
this._updateCoordinates();
this.moveTo(this.origin);
}
done() {
return abs(this.offset.y) < ANIMATED_SCROLLER_PRECISION && abs(this.offset.x) < ANIMATED_SCROLLER_PRECISION;
}
onEnd() {
this.moveTo(this.destination);
if (this.callback) {
this.callback.call();
}
}
setCoordinates(from, to) {
this.offset = {};
this.origin = from;
this.destination = to;
}
/* eslint-disable no-param-reassign */
setCallback(callback) {
if (callback && isFunction(callback)) {
this.callback = callback;
} else {
callback = undefined;
}
}
/* eslint-enable no-param-reassign */
_updateCoordinates() {
this.offset = {
x: (this.destination.x - this.origin.x) / 4,
y: (this.destination.y - this.origin.y) / 4
};
this.origin = {
y: this.origin.y + this.offset.y,
x: this.origin.x + this.offset.x
};
}
}
class ScrollBar {
constructor(options) {
let that = this,
horizontal = options.axis === 'x';
const orientation = (horizontal ? 'horizontal' : 'vertical');
const element = convertToHtml('<div class="km-touch-scrollbar km-' + orientation + '-scrollbar" />');
extend(that, options, {
element: element,
elementSize: 0,
movable: new Movable(element),
scrollMovable: options.movable,
alwaysVisible: options.alwaysVisible,
size: horizontal ? 'width' : 'height'
});
that.scrollMovable.bind(CHANGE, that.refresh.bind(that));
that.container.appendChild(element);
if (options.alwaysVisible) {
that.show();
}
}
refresh() {
let that = this,
axis = that.axis,
dimension = that.dimension,
paneSize = dimension.size,
scrollMovable = that.scrollMovable,
sizeRatio = paneSize / dimension.total,
position = Math.round(-scrollMovable[axis] * sizeRatio),
size = Math.round(paneSize * sizeRatio);
if (sizeRatio >= 1) {
this.element.style.display = "none";
} else {
this.element.style.display = "";
}
if (position + size > paneSize) {
size = paneSize - position;
} else if (position < 0) {
size += position;
position = 0;
}
if (that.elementSize !== size) {
that.element.style[that.size] = size + 'px';
that.elementSize = size;
}
that.movable.moveAxis(axis, position);
}
show() {
this.element.style.opacity = SCROLLBAR_OPACITY;
this.element.style.visibility = "visible";
}
hide() {
if (!this.alwaysVisible) {
this.element.style.opacity = 0;
}
}
}
export class Scroller extends Observable {
constructor(element, options) {
super();
let that = this;
this.element = element;
this._initOptions(options);
const hasScrolling = hasNativeScrolling(navigator.userAgent);
that._native = that.options.useNative && hasScrolling;
const scrollHeader = convertToHtml('<div class="km-scroll-header"/>');
if (that._native) {
addClass(element, 'km-native-scroller');
prepend(scrollHeader, element);
extend(that, {
scrollElement: element,
fixedContainer: element.children[0]
});
return;
}
element.style.overflow = "hidden";
addClass(element, 'km-scroll-wrapper');
const scrollContainer = convertToHtml('<div class="km-scroll-container"/>');
wrapInner(element, scrollContainer);
prepend(scrollHeader, element);
let inner = element.children[1],
tapCapture = new TapCapture(element),
movable = new Movable(inner),
dimensions = new PaneDimensions({
element: inner,
container: element,
forcedEnabled: that.options.zoom
}),
avoidScrolling = this.options.avoidScrolling,
userEvents = new UserEvents(element, {
touchAction: 'none',
allowSelection: true,
preventDragEvent: true,
captureUpIfMoved: true,
multiTouch: that.options.zoom,
supportDoubleTap: that.options.supportDoubleTap,
start(e) {
dimensions.refresh();
let velocityX = abs(e.x.velocity),
velocityY = abs(e.y.velocity),
horizontalSwipe = velocityX * 2 >= velocityY,
originatedFromFixedContainer = that.fixedContainer.contains(e.event.target),
verticalSwipe = velocityY * 2 >= velocityX;
if (!originatedFromFixedContainer && !avoidScrolling(e) && that.enabled && (dimensions.x.enabled && horizontalSwipe || dimensions.y.enabled && verticalSwipe)) {
userEvents.capture();
} else {
userEvents.cancel();
}
}
}),
pane = new Pane({
movable: movable,
dimensions: dimensions,
userEvents: userEvents,
elastic: that.options.elastic
}),
zoomSnapBack = new ZoomSnapBack({
movable: movable,
dimensions: dimensions,
userEvents: userEvents,
tapCapture: tapCapture
}),
animatedScroller = new AnimatedScroller({
moveTo(coordinates) {
that.scrollTo(coordinates.x, coordinates.y);
}
});
movable.bind(CHANGE, function() {
that.scrollTop = -movable.y;
that.scrollLeft = -movable.x;
that.trigger(SCROLL, {
scrollTop: that.scrollTop,
scrollLeft: that.scrollLeft
});
});
if (that.options.mousewheelScrolling) {
this._wheelScrollHandler = this._wheelScroll.bind(this);
on(element, 'wheel', this._wheelScrollHandler);
}
extend(that, {
movable: movable,
dimensions: dimensions,
zoomSnapBack: zoomSnapBack,
animatedScroller: animatedScroller,
userEvents: userEvents,
pane: pane,
tapCapture: tapCapture,
pulled: false,
enabled: true,
scrollElement: inner,
scrollTop: 0,
scrollLeft: 0,
fixedContainer: element.children[0]
});
that._initAxis('x');
that._initAxis('y');
that._wheelEnd = function() {
that._wheel = false;
that.userEvents.end(0, that._wheelY);
};
dimensions.refresh();
if (that.options.pullToRefresh) {
that._initPullToRefresh();
}
}
_initOptions(options) {
this.options = deepExtend({}, this.options, options);
}
_wheelScroll(e) {
if (e.ctrlKey) {
return;
}
if (!this._wheel) {
this._wheel = true;
this._wheelY = 0;
this.userEvents.press(0, this._wheelY);
}
clearTimeout(this._wheelTimeout);
this._wheelTimeout = setTimeout(this._wheelEnd, 50);
let delta = wheelDeltaY(e);
if (delta) {
this._wheelY += delta;
this.userEvents.move(0, this._wheelY);
}
e.preventDefault();
}
makeVirtual() {
this.dimensions.y.makeVirtual();
}
virtualSize(min, max) {
this.dimensions.y.virtualSize(min, max);
}
height() {
return this.dimensions.y.size;
}
scrollHeight() {
return this.scrollElement.scrollHeight;
}
scrollWidth() {
return this.scrollElement.scrollWidth;
}
_resize() {
if (!this._native) {
this.contentResized();
}
}
setOptions(options) {
let that = this;
this._initOptions(options);
if (options.pullToRefresh) {
that._initPullToRefresh();
}
}
reset() {
if (this._native) {
this.scrollElement.scrollTop(0);
} else {
this.movable.moveTo({
x: 0,
y: 0
});
this._scale(1);
}
}
contentResized() {
this.dimensions.refresh();
if (this.pane.x.outOfBounds()) {
this.movable.moveAxis('x', this.dimensions.x.min);
}
if (this.pane.y.outOfBounds()) {
this.movable.moveAxis('y', this.dimensions.y.min);
}
}
zoomOut() {
let dimensions = this.dimensions;
dimensions.refresh();
this._scale(dimensions.fitScale);
this.movable.moveTo(dimensions.centerCoordinates());
}
enable() {
this.enabled = true;
}
disable() {
this.enabled = false;
}
scrollTo(x, y) {
if (this._native) {
this.scrollElement.scrollLeft(abs(x));
this.scrollElement.scrollTop(abs(y));
} else {
this.dimensions.refresh();
this.movable.moveTo({
x: x,
y: y
});
}
}
animatedScrollTo(x, y, callback) {
let from, to;
if (this._native) {
this.scrollTo(x, y);
} else {
from = {
x: this.movable.x,
y: this.movable.y
};
to = {
x: x,
y: y
};
this.animatedScroller.setCoordinates(from, to);
this.animatedScroller.setCallback(callback);
this.animatedScroller.start();
}
}
// kept for API compatibility, not used
pullHandled() {
// let that = this;
// removeClass(that.refreshHint, SCROLLER_REFRESH_CLASS);
// that.hintContainer.innerHTML = that.pullTemplate({}));
// that.yinertia.onEnd();
// that.xinertia.onEnd();
// that.userEvents.cancel();
}
destroy() {
const element = this.element;
off(element, 'wheel', this._wheelScrollHandler);
if (this.userEvents) {
this.userEvents.destroy();
}
}
_scale(scale) {
this.dimensions.rescale(scale);
this.movable.scaleTo(scale);
}
_initPullToRefresh() {
}
// kept for API compatibility, not used
_dragEnd() {
// let that = this;
// if (!that.pulled) {
// return;
// }
// that.pulled = false;
// removeClass(that.refreshHint, SCROLLER_RELEASE_CLASS);
// addClass(that.refreshHint, SCROLLER_REFRESH_CLASS);
// that.hintContainer.innerHTML = that.refreshTemplate({});
// that.yinertia.freeze(that.options.pullOffset / 2);
// that.trigger('pull');
}
// kept for API compatibility, not used
_paneChange() {
// let that = this;
// if (that.movable.y / OUT_OF_BOUNDS_FRICTION > that.options.pullOffset) {
// if (!that.pulled) {
// that.pulled = true;
// that.refreshHint.removeClass(SCROLLER_REFRESH_CLASS).addClass(SCROLLER_RELEASE_CLASS);
// that.hintContainer.html(that.releaseTemplate({}));
// that.hintContainer.html(that.releaseTemplate({}));
// }
// } else if (that.pulled) {
// that.pulled = false;
// that.refreshHint.removeClass(SCROLLER_RELEASE_CLASS);
// that.hintContainer.html(that.pullTemplate({}));
// }
}
_initAxis(axis) {
let that = this,
movable = that.movable,
dimension = that.dimensions[axis],
tapCapture = that.tapCapture,
paneAxis = that.pane[axis],
scrollBar = new ScrollBar({
axis: axis,
movable: movable,
dimension: dimension,
container: that.element,
alwaysVisible: that.options.visibleScrollHints
});
dimension.bind(CHANGE, function() {
scrollBar.refresh();
});
paneAxis.bind(CHANGE, function() {
scrollBar.show();
});
that[axis + 'inertia'] = new DragInertia({
axis: axis,
paneAxis: paneAxis,
movable: movable,
tapCapture: tapCapture,
userEvents: that.userEvents,
dimension: dimension,
elastic: that.options.elastic,
friction: that.options.friction || FRICTION,
velocityMultiplier: that.options.velocityMultiplier || VELOCITY_MULTIPLIER,
end() {
scrollBar.hide();
that.trigger('scrollEnd', {
axis: axis,
scrollTop: that.scrollTop,
scrollLeft: that.scrollLeft
});
}
});
}
}
setDefaultOptions(Scroller, {
name: 'Scroller',
zoom: false,
pullOffset: 140,
visibleScrollHints: false,
elastic: true,
useNative: false,
mousewheelScrolling: true,
avoidScrolling() {
return false;
},
pullToRefresh: false,
messages: {
pullTemplate: 'Pull to refresh',
releaseTemplate: 'Release to refresh',
refreshTemplate: 'Refreshing'
}
});
setDefaultEvents(Scroller, [
PULL,
SCROLL,
RESIZE
]);