UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

630 lines (511 loc) 18.9 kB
import { DomEventsBuilder } from '../services'; import { DateCategoryAxis, Point } from '../core'; import { MOUSEWHEEL_DELAY, MOUSEWHEEL, SELECT_START, SELECT, SELECT_END } from './constants'; import { LEFT, RIGHT, MIN_VALUE, MAX_VALUE, X } from '../common/constants'; import { addClass, removeClass, eventCoordinates, deepExtend, elementStyles, eventElement, setDefaultOptions, limitValue, round, bindEvents, unbindEvents, mousewheelDelta, hasClasses } from '../common'; import { parseDate } from '../date-utils'; const ZOOM_ACCELERATION = 3; const SELECTOR_HEIGHT_ADJUST = 0.1; function createDiv(classNames) { const element = document.createElement("div"); if (classNames) { element.className = classNames; } return element; } function closestHandle(element) { let current = element; while (current && !hasClasses(current, "k-handle")) { current = current.parentNode; } return current; } class Selection { constructor(chart, categoryAxis, options, observer) { const chartElement = chart.element; this.options = deepExtend({}, this.options, options); this.chart = chart; this.observer = observer; this.chartElement = chartElement; this.categoryAxis = categoryAxis; this._dateAxis = this.categoryAxis instanceof DateCategoryAxis; this.initOptions(); this.visible = this.options.visible && chartElement.offsetHeight; if (this.visible) { this.createElements(); this.set(this._index(this.options.from), this._index(this.options.to)); this.bindEvents(); } } onPane(pane) { return this.categoryAxis.pane === pane; } createElements() { const options = this.options; const wrapper = this.wrapper = createDiv("k-selector k-pointer-events-none"); elementStyles(wrapper, { top: options.offset.top, left: options.offset.left, width: options.width, height: options.height, direction: 'ltr' }); const selection = this.selection = createDiv("k-selection k-pointer-events-none"); this.leftMask = createDiv("k-mask k-pointer-events-none"); this.rightMask = createDiv("k-mask k-pointer-events-none"); wrapper.appendChild(this.leftMask); wrapper.appendChild(this.rightMask); wrapper.appendChild(selection); const body = this.body = createDiv("k-selection-bg k-pointer-events-none"); selection.appendChild(body); const leftHandle = this.leftHandle = createDiv("k-handle k-left-handle k-pointer-events-auto"); const rightHandle = this.rightHandle = createDiv("k-handle k-right-handle k-pointer-events-auto"); leftHandle.appendChild(createDiv()); rightHandle.appendChild(createDiv()); selection.appendChild(leftHandle); selection.appendChild(rightHandle); this.chartElement.appendChild(wrapper); const selectionStyles = elementStyles(selection, [ "borderLeftWidth", "borderRightWidth", "height" ]); const leftHandleHeight = elementStyles(leftHandle, "height").height; const rightHandleHeight = elementStyles(rightHandle, "height").height; options.selection = { border: { left: selectionStyles.borderLeftWidth, right: selectionStyles.borderRightWidth } }; elementStyles(leftHandle, { top: (selectionStyles.height - leftHandleHeight) / 2 }); elementStyles(rightHandle, { top: (selectionStyles.height - rightHandleHeight) / 2 }); /* eslint no-self-assign: "off" */ wrapper.style.cssText = wrapper.style.cssText; } bindEvents() { if (this.options.mousewheel !== false) { this._mousewheelHandler = this._mousewheel.bind(this); bindEvents(this.chartElement, { [ MOUSEWHEEL ]: this._mousewheelHandler }); } this._domEvents = DomEventsBuilder.create(this.chartElement, { stopPropagation: true, // applicable for the jQuery UserEvents start: this._start.bind(this), move: this._move.bind(this), end: this._end.bind(this), tap: this._tap.bind(this), press: this._press.bind(this), gesturestart: this._gesturestart.bind(this), gesturechange: this._gesturechange.bind(this), gestureend: this._gestureend.bind(this) }); } initOptions() { const { options, categoryAxis } = this; const box = categoryAxis.pane.chartsBox(); const intlService = this.chart.chartService.intl; if (this._dateAxis) { deepExtend(options, { min: parseDate(intlService, options.min), max: parseDate(intlService, options.max), from: parseDate(intlService, options.from), to: parseDate(intlService, options.to) }); } const { paddingLeft, paddingTop } = elementStyles(this.chartElement, [ "paddingLeft", "paddingTop" ]); this.options = deepExtend({}, { width: box.width(), height: box.height() + SELECTOR_HEIGHT_ADJUST, //workaround for sub-pixel hover on the paths in chrome padding: { left: paddingLeft, top: paddingTop }, offset: { left: box.x1 + paddingLeft, top: box.y1 + paddingTop }, from: options.min, to: options.max }, options); } destroy() { if (this._domEvents) { this._domEvents.destroy(); delete this._domEvents; } clearTimeout(this._mwTimeout); this._state = null; if (this.wrapper) { if (this._mousewheelHandler) { unbindEvents(this.chartElement, { [ MOUSEWHEEL ]: this._mousewheelHandler }); this._mousewheelHandler = null; } this.chartElement.removeChild(this.wrapper); this.wrapper = null; } } _rangeEventArgs(range) { return { axis: this.categoryAxis.options, from: this._value(range.from), to: this._value(range.to) }; } _pointInPane(x, y) { const paneBox = this.categoryAxis.pane.box; const modelCoords = this.chart._toModelCoordinates(x, y); return paneBox.containsPoint(modelCoords); } _start(e) { const options = this.options; const target = eventElement(e); if (this._state || !target) { return; } const coords = eventCoordinates(e); const inPane = this._pointInPane(coords.x, coords.y); if (!inPane) { return; } const handle = closestHandle(target); const bodyRect = this.body.getBoundingClientRect(); const inBody = !handle && coords.x >= bodyRect.x && coords.x <= bodyRect.x + bodyRect.width && coords.y >= bodyRect.y && coords.y <= bodyRect.y + bodyRect.height; this.chart._unsetActivePoint(); this._state = { moveTarget: handle, startLocation: e.x ? e.x.location : 0, inBody, range: { from: this._index(options.from), to: this._index(options.to) } }; const args = this._rangeEventArgs({ from: this._index(options.from), to: this._index(options.to) }); if (this.trigger(SELECT_START, args)) { this._state = null; } } _press(e) { let handle; if (this._state) { handle = this._state.moveTarget; } else { handle = closestHandle(eventElement(e)); } if (handle) { addClass(handle, "k-handle-active"); } } _move(e) { if (!this._state) { return; } const { _state: state, options, categoryAxis } = this; const { range, moveTarget: target } = state; const reverse = categoryAxis.options.reverse; const from = this._index(options.from); const to = this._index(options.to); const min = this._index(options.min); const max = this._index(options.max); const delta = state.startLocation - e.x.location; const oldRange = { from: range.from, to: range.to }; const span = range.to - range.from; const scale = elementStyles(this.wrapper, "width").width / (categoryAxis.categoriesCount() - 1); const offset = Math.round(delta / scale) * (reverse ? -1 : 1); if (!target && !state.inBody) { return; } const leftHandle = target && hasClasses(target, "k-left-handle"); const rightHandle = target && hasClasses(target, "k-right-handle"); if (state.inBody) { range.from = Math.min( Math.max(min, from - offset), max - span ); range.to = Math.min( range.from + span, max ); } else if ((leftHandle && !reverse) || (rightHandle && reverse)) { range.from = Math.min( Math.max(min, from - offset), max - 1 ); range.to = Math.max(range.from + 1, range.to); } else if ((leftHandle && reverse) || (rightHandle && !reverse)) { range.to = Math.min( Math.max(min + 1, to - offset), max ); range.from = Math.min(range.to - 1, range.from); } if (range.from !== oldRange.from || range.to !== oldRange.to) { this.move(range.from, range.to); this.trigger(SELECT, this._rangeEventArgs(range)); } } _end() { if (this._state) { const moveTarget = this._state.moveTarget; if (moveTarget) { removeClass(moveTarget, "k-handle-active"); } const range = this._state.range; this.set(range.from, range.to); this.trigger(SELECT_END, this._rangeEventArgs(range)); delete this._state; } } _tap(e) { const { options, categoryAxis } = this; const coords = this.chart._eventCoordinates(e); const categoryIx = categoryAxis.pointCategoryIndex(new Point(coords.x, categoryAxis.box.y1)); const from = this._index(options.from); const to = this._index(options.to); const min = this._index(options.min); const max = this._index(options.max); const span = to - from; const mid = from + span / 2; const range = {}; const rightClick = e.event.which === 3; let offset = Math.round(mid - categoryIx); if (this._state || rightClick) { return; } this.chart._unsetActivePoint(); if (!categoryAxis.options.justified) { offset--; } range.from = Math.min( Math.max(min, from - offset), max - span ); range.to = Math.min(range.from + span, max); this._start(e); if (this._state) { this._state.range = range; this.trigger(SELECT, this._rangeEventArgs(range)); this._end(); } } _mousewheel(e) { let delta = mousewheelDelta(e); this._start(e); if (this._state) { const range = this._state.range; e.preventDefault(); e.stopPropagation(); if (Math.abs(delta) > 1) { delta *= ZOOM_ACCELERATION; } if (this.options.mousewheel.reverse) { delta *= -1; } if (this.expand(delta)) { this.trigger(SELECT, { axis: this.categoryAxis.options, delta: delta, originalEvent: e, from: this._value(range.from), to: this._value(range.to) }); } if (this._mwTimeout) { clearTimeout(this._mwTimeout); } this._mwTimeout = setTimeout(() => { this._end(); }, MOUSEWHEEL_DELAY); } } _gesturestart(e) { const options = this.options; const touch = e.touches[0]; const inPane = this._pointInPane(touch.pageX, touch.pageY); if (!inPane) { return; } this._state = { range: { from: this._index(options.from), to: this._index(options.to) } }; const args = this._rangeEventArgs(this._state.range); if (this.trigger(SELECT_START, args)) { this._state = null; } else { e.preventDefault(); } } _gestureend() { if (this._state) { this.trigger(SELECT_END, this._rangeEventArgs(this._state.range)); delete this._state; } } _gesturechange(e) { if (!this._state) { return; } const { chart, _state: state, options, categoryAxis } = this; const range = state.range; const p0 = chart._toModelCoordinates(e.touches[0].x.location).x; const p1 = chart._toModelCoordinates(e.touches[1].x.location).x; const left = Math.min(p0, p1); const right = Math.max(p0, p1); e.preventDefault(); range.from = categoryAxis.pointCategoryIndex(new Point(left)) || options.min; range.to = categoryAxis.pointCategoryIndex(new Point(right)) || options.max; this.move(range.from, range.to); this.trigger(SELECT, this._rangeEventArgs(range)); } _index(value) { let index = value; if (value instanceof Date) { index = this.categoryAxis.categoryIndex(value); } return index; } _value(index) { let value = index; if (this._dateAxis) { value = this.categoryAxis.categoryAt(index); if (value > this.options.max) { value = this.options.max; } } return value; } _slot(value) { const categoryAxis = this.categoryAxis; const index = this._index(value); return categoryAxis.getSlot(index, index, true); } move(from, to) { const options = this.options; const reverse = this.categoryAxis.options.reverse; const { offset, padding, selection: { border } } = options; const left = reverse ? to : from; const right = reverse ? from : to; const edge = 'x' + (reverse ? 2 : 1); let box = this._slot(left); const leftMaskWidth = round(box[edge] - offset.left + padding.left); elementStyles(this.leftMask, { width: leftMaskWidth }); elementStyles(this.selection, { left: leftMaskWidth }); box = this._slot(right); const rightMaskWidth = round(options.width - (box[edge] - offset.left + padding.left)); elementStyles(this.rightMask, { width: rightMaskWidth }); let distance = options.width - rightMaskWidth; if (distance !== options.width) { distance += border.right; } elementStyles(this.rightMask, { left: distance }); elementStyles(this.selection, { width: Math.max(options.width - (leftMaskWidth + rightMaskWidth) - border.right, 0) }); } set(from, to) { const options = this.options; const min = this._index(options.min); const max = this._index(options.max); const fromValue = limitValue(this._index(from), min, max); const toValue = limitValue(this._index(to), fromValue + 1, max); if (options.visible) { this.move(fromValue, toValue); } options.from = this._value(fromValue); options.to = this._value(toValue); } expand(delta) { const options = this.options; const min = this._index(options.min); const max = this._index(options.max); const zDir = options.mousewheel.zoom; const from = this._index(options.from); const to = this._index(options.to); let range = { from: from, to: to }; const oldRange = deepExtend({}, range); if (this._state) { range = this._state.range; } if (zDir !== RIGHT) { range.from = limitValue( limitValue(from - delta, 0, to - 1), min, max ); } if (zDir !== LEFT) { range.to = limitValue( limitValue(to + delta, range.from + 1, max), min, max ); } if (range.from !== oldRange.from || range.to !== oldRange.to) { this.set(range.from, range.to); return true; } } zoom(delta, coords) { const options = this.options; const min = this._index(options.min); const max = this._index(options.max); const from = this._index(options.from); const to = this._index(options.to); let range = { from: from, to: to }; const oldRange = deepExtend({}, range); const { reverse } = this.categoryAxis.options; const origin = X + (reverse ? '2' : '1'); const lineBox = this.categoryAxis.lineBox(); const relative = Math.abs(lineBox[origin] - coords[X]); const size = lineBox.width(); const position = round(relative / size, 2); const minDelta = round(position * delta); const maxDelta = round((1 - position) * delta); if (this._state) { range = this._state.range; } range.from = limitValue( limitValue(from - minDelta, 0, to - 1), min, max ); range.to = limitValue( limitValue(to + maxDelta, range.from + 1, max), min, max ); if (range.from !== oldRange.from || range.to !== oldRange.to) { this.set(range.from, range.to); return true; } } trigger(name, args) { return (this.observer || this.chart).trigger(name, args); } } setDefaultOptions(Selection, { visible: true, mousewheel: { zoom: "both" }, min: MIN_VALUE, max: MAX_VALUE }); export default Selection;