devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
456 lines (411 loc) • 21.4 kB
JavaScript
var noop = require("../../core/utils/common").noop,
commonModule = require("./common"),
animationSettings = commonModule.utils.animationSettings,
emptySliderMarkerText = commonModule.consts.emptySliderMarkerText,
Slider = require("./slider"),
_normalizeEnum = require("../core/utils").normalizeEnum,
isNumeric = require("../../core/utils/type").isNumeric,
adjust = require("../../core/utils/math").adjust;
function buildRectPoints(left, top, right, bottom) {
return [left, top, right, top, right, bottom, left, bottom];
}
function valueOf(value) {
return value && value.valueOf();
}
function isLess(a, b) {
return a < b;
}
function isGreater(a, b) {
return a > b;
}
function selectClosestValue(target, values) {
var start = 0,
end = values ? values.length - 1 : 0,
middle,
val = target;
while (end - start > 1) {
middle = start + end >> 1;
val = values[middle];
if (val === target) {
return target;
} else if (target < val) {
end = middle;
} else {
start = middle;
}
}
if (values) {
val = values[target - values[start] <= values[end] - target ? start : end];
}
return val;
}
function dummyProcessSelectionChanged() {
this._lastSelectedRange = this.getSelectedRange();
delete this._processSelectionChanged;
}
// See tests in "rangeSelectorWithAssertion.html", "'onSelectedRangeChanged' event" module
function suppressSetSelectedRange(controller) {
controller.setSelectedRange = noop;
if (controller._processSelectionChanged === dummyProcessSelectionChanged) {
controller._processSelectionChanged();
}
}
function restoreSetSelectedRange(controller) {
delete controller.setSelectedRange;
}
function SlidersController(params) {
var that = this,
sliderParams = { renderer: params.renderer, root: params.root, trackersGroup: params.trackersGroup, translator: params.translator };
that._params = params;
that._areaTracker = params.renderer.path(null, "area").attr({ "class": "area-tracker", fill: "#000000", opacity: 0.0001 }).append(params.trackersGroup);
that._selectedAreaTracker = params.renderer.path(null, "area").attr({ "class": "selected-area-tracker", fill: "#000000", opacity: 0.0001 }).append(params.trackersGroup);
// Shutter is appended before sliders because later (when they will be foregrounded) it will be at any case located before them.
that._shutter = params.renderer.path(null, "area").append(params.root);
that._sliders = [new Slider(sliderParams, 0), new Slider(sliderParams, 1)];
// It seems that there is no special reasons to suppress first event - it was accidentally suppressed.
// Let it stay so for now.
that._processSelectionChanged = dummyProcessSelectionChanged;
}
SlidersController.prototype = {
constructor: SlidersController,
dispose: function dispose() {
this._sliders[0].dispose();
this._sliders[1].dispose();
},
getTrackerTargets: function getTrackerTargets() {
return {
area: this._areaTracker,
selectedArea: this._selectedAreaTracker,
sliders: this._sliders
};
},
_processSelectionChanged: function _processSelectionChanged() {
var that = this,
selectedRange = that.getSelectedRange();
if (valueOf(selectedRange.startValue) !== valueOf(that._lastSelectedRange.startValue) || valueOf(selectedRange.endValue) !== valueOf(that._lastSelectedRange.endValue)) {
that._params.updateSelectedRange(selectedRange, that._lastSelectedRange);
that._lastSelectedRange = selectedRange;
}
},
update: function update(verticalRange, behavior, isCompactMode, sliderHandleOptions, sliderMarkerOptions, shutterOptions, rangeBounds, fullTicks, selectedRangeColor) {
var that = this,
callValueChanged = behavior.callValueChanged || behavior.callSelectedRangeChanged,
screenRange = that._params.translator.getScreenRange();
that._verticalRange = verticalRange;
that._minRange = rangeBounds.minRange;
that._maxRange = rangeBounds.maxRange;
// TODO: Investigate reasons of "renderer.animationEnabled" usage - it seems to be useless (if only for vml somehow)
that._animationEnabled = behavior.animationEnabled && that._params.renderer.animationEnabled();
that._allowSlidersSwap = behavior.allowSlidersSwap;
that._sliders[0].update(verticalRange, sliderHandleOptions, sliderMarkerOptions);
that._sliders[1].update(verticalRange, sliderHandleOptions, sliderMarkerOptions);
// This is required for placing sliders and shutter into initial position from which initial animation will be going.
that._sliders[0]._position = that._sliders[1]._position = screenRange[0];
that._values = !that._params.translator.isValueProlonged && behavior.snapToTicks ? fullTicks : null;
that._areaTracker.attr({ points: buildRectPoints(screenRange[0], verticalRange[0], screenRange[1], verticalRange[1]) });
// SlidersContainer
that._isCompactMode = isCompactMode;
that._shutterOffset = sliderHandleOptions.width / 2;
that._updateSelectedView(shutterOptions, selectedRangeColor);
that._isOnMoving = _normalizeEnum(callValueChanged) === "onmoving";
that._updateSelectedRange();
// This is placing sliders and shutter into initial position. They all will be animated from that position when "setSelectedRange" is called.
that._applyTotalPosition(false);
},
_updateSelectedView: function _updateSelectedView(shutterOptions, selectedRangeColor) {
var settings = { fill: null, "fill-opacity": null, stroke: null, "stroke-width": null };
if (this._isCompactMode) {
settings.stroke = selectedRangeColor;
settings["stroke-width"] = 3;
settings.sharp = "v";
} else {
settings.fill = shutterOptions.color;
settings["fill-opacity"] = shutterOptions.opacity;
}
this._shutter.attr(settings);
},
_updateSelectedRange: function _updateSelectedRange() {
var that = this,
sliders = that._sliders;
sliders[0].cancelAnimation();
sliders[1].cancelAnimation();
that._shutter.stopAnimation();
if (that._params.translator.isEmptyValueRange()) {
sliders[0]._setText(emptySliderMarkerText);
sliders[1]._setText(emptySliderMarkerText);
sliders[0]._value = sliders[1]._value = undefined;
sliders[0]._position = that._params.translator.getScreenRange()[0];
sliders[1]._position = that._params.translator.getScreenRange()[1];
that._applyTotalPosition(false);
suppressSetSelectedRange(that);
} else {
restoreSetSelectedRange(that);
}
},
_applyTotalPosition: function _applyTotalPosition(isAnimated) {
var sliders = this._sliders,
areOverlapped;
isAnimated = this._animationEnabled && isAnimated;
sliders[0].applyPosition(isAnimated);
sliders[1].applyPosition(isAnimated);
areOverlapped = sliders[0].getCloudBorder() > sliders[1].getCloudBorder();
sliders[0].setOverlapped(areOverlapped);
sliders[1].setOverlapped(areOverlapped);
this._applyAreaTrackersPosition();
this._applySelectedRangePosition(isAnimated);
},
_applyAreaTrackersPosition: function _applyAreaTrackersPosition() {
var that = this,
position1 = that._sliders[0].getPosition(),
position2 = that._sliders[1].getPosition();
that._selectedAreaTracker.attr({ points: buildRectPoints(position1, that._verticalRange[0], position2, that._verticalRange[1]) }).css({
cursor: Math.abs(that._params.translator.getScreenRange()[1] - that._params.translator.getScreenRange()[0] - position2 + position1) < 0.001 ? "default" : "pointer"
});
},
_applySelectedRangePosition: function _applySelectedRangePosition(isAnimated) {
var that = this,
verticalRange = that._verticalRange,
pos1 = that._sliders[0].getPosition(),
pos2 = that._sliders[1].getPosition(),
screenRange,
points;
if (that._isCompactMode) {
points = [pos1 + Math.ceil(that._shutterOffset), (verticalRange[0] + verticalRange[1]) / 2, pos2 - Math.floor(that._shutterOffset), (verticalRange[0] + verticalRange[1]) / 2];
} else {
screenRange = that._params.translator.getScreenRange();
points = [buildRectPoints(screenRange[0], verticalRange[0], Math.max(pos1 - Math.floor(that._shutterOffset), screenRange[0]), verticalRange[1]), buildRectPoints(screenRange[1], verticalRange[0], Math.min(pos2 + Math.ceil(that._shutterOffset), screenRange[1]), verticalRange[1])];
}
if (isAnimated) {
that._shutter.animate({ points: points }, animationSettings);
} else {
that._shutter.attr({ points: points });
}
},
getSelectedRange: function getSelectedRange() {
return { startValue: this._sliders[0].getValue(), endValue: this._sliders[1].getValue() };
},
setSelectedRange: function setSelectedRange(arg) {
arg = arg || {};
var that = this,
translator = that._params.translator,
startValue = translator.isValid(arg.startValue) ? translator.getCorrectValue(arg.startValue, +1) : translator.getRange()[0],
endValue = translator.isValid(arg.endValue) ? translator.getCorrectValue(arg.endValue, -1) : translator.getRange()[1],
values;
startValue = isNumeric(startValue) ? adjust(startValue) : startValue;
endValue = isNumeric(endValue) ? adjust(endValue) : endValue;
values = translator.to(startValue, -1) < translator.to(endValue, +1) ? [startValue, endValue] : [endValue, startValue];
that._sliders[0].setDisplayValue(values[0]);
that._sliders[1].setDisplayValue(values[1]);
that._sliders[0]._position = translator.to(values[0], -1);
that._sliders[1]._position = translator.to(values[1], +1);
that._applyTotalPosition(true);
that._processSelectionChanged();
},
beginSelectedAreaMoving: function beginSelectedAreaMoving(initialPosition) {
var that = this,
sliders = that._sliders,
offset = (sliders[0].getPosition() + sliders[1].getPosition()) / 2 - initialPosition,
currentPosition = initialPosition;
move.complete = function () {
that._dockSelectedArea();
};
return move;
function move(position) {
if (position !== currentPosition && position > currentPosition === position > (sliders[0].getPosition() + sliders[1].getPosition()) / 2 - offset) {
that._moveSelectedArea(position + offset, false);
}
currentPosition = position;
}
},
_dockSelectedArea: function _dockSelectedArea() {
var translator = this._params.translator,
sliders = this._sliders;
sliders[0]._position = translator.to(sliders[0].getValue(), -1);
sliders[1]._position = translator.to(sliders[1].getValue(), +1);
this._applyTotalPosition(true);
this._processSelectionChanged();
},
moveSelectedArea: function moveSelectedArea(screenPosition) {
this._moveSelectedArea(screenPosition, true);
this._dockSelectedArea();
},
_moveSelectedArea: function _moveSelectedArea(screenPosition, isAnimated) {
var that = this,
translator = that._params.translator,
sliders = that._sliders,
interval = sliders[1].getPosition() - sliders[0].getPosition(),
startPosition = screenPosition - interval / 2,
endPosition = screenPosition + interval / 2,
startValue;
if (startPosition < translator.getScreenRange()[0]) {
startPosition = translator.getScreenRange()[0];
endPosition = startPosition + interval;
}
if (endPosition > translator.getScreenRange()[1]) {
endPosition = translator.getScreenRange()[1];
startPosition = endPosition - interval;
}
// Check for "minRange" and "maxRange" is not performed because it was not performed in the previous code, though I find it strange.
startValue = selectClosestValue(translator.from(startPosition, -1), that._values);
sliders[0].setDisplayValue(startValue);
sliders[1].setDisplayValue(selectClosestValue(translator.from(translator.to(startValue, -1) + interval, +1), that._values));
sliders[0]._position = startPosition;
sliders[1]._position = endPosition;
that._applyTotalPosition(isAnimated);
if (that._isOnMoving) {
that._processSelectionChanged();
}
},
placeSliderAndBeginMoving: function placeSliderAndBeginMoving(firstPosition, secondPosition) {
var that = this,
translator = that._params.translator,
sliders = that._sliders,
index = firstPosition < secondPosition ? 0 : 1,
dir = index > 0 ? +1 : -1,
compare = index > 0 ? isGreater : isLess,
antiCompare = index > 0 ? isLess : isGreater,
thresholdPosition,
positions = [],
values = [],
handler;
values[index] = translator.from(firstPosition, dir);
values[1 - index] = translator.from(secondPosition, -dir);
positions[1 - index] = secondPosition;
if (translator.isValueProlonged) {
// Ensure that first value is strictly to the outer side from the "firstPosition".
if (compare(firstPosition, translator.to(values[index], dir))) {
values[index] = translator.from(firstPosition, -dir);
}
// Check - if "secondPosition" is closer to "firstPosition" than a span of a single category.
if (compare(secondPosition, translator.to(values[index], -dir))) {
values[1 - index] = values[index];
}
}
if (that._minRange) {
thresholdPosition = translator.to(translator.add(selectClosestValue(values[index], that._values), that._minRange, -dir), -dir);
// Check - if "secondPosition" is closer to "firstPosition" than it is allowed by "minRange".
if (compare(secondPosition, thresholdPosition)) {
values[1 - index] = translator.add(values[index], that._minRange, -dir);
}
thresholdPosition = translator.to(translator.add(translator.getRange()[1 - index], that._minRange, dir), -dir);
// Check - if "firstPosition" is closer to an end than it is allowed by "minRange".
// So there is definitely not enough space for both sliders - the first (as the one which is farther from the end) has to be moved away by "minRange".
if (antiCompare(firstPosition, thresholdPosition)) {
values[1 - index] = translator.getRange()[1 - index];
values[index] = translator.add(values[1 - index], that._minRange, dir);
positions[1 - index] = firstPosition;
}
}
values[0] = selectClosestValue(values[0], that._values);
values[1] = selectClosestValue(values[1], that._values);
positions[index] = translator.to(values[index], dir);
sliders[0].setDisplayValue(values[0]);
sliders[1].setDisplayValue(values[1]);
sliders[0]._position = positions[0];
sliders[1]._position = positions[1];
that._applyTotalPosition(true);
if (that._isOnMoving) {
that._processSelectionChanged();
}
handler = that.beginSliderMoving(1 - index, secondPosition);
sliders[1 - index]._sliderGroup.stopAnimation();
that._shutter.stopAnimation();
handler(secondPosition);
return handler;
},
beginSliderMoving: function beginSliderMoving(initialIndex, initialPosition) {
var that = this,
translator = that._params.translator,
sliders = that._sliders,
minPosition = translator.getScreenRange()[0],
maxPosition = translator.getScreenRange()[1],
index = initialIndex,
staticPosition = sliders[1 - index].getPosition(),
currentPosition = initialPosition,
dir = index > 0 ? +1 : -1,
compareMin = index > 0 ? isLess : isGreater,
compareMax = index > 0 ? isGreater : isLess,
moveOffset = sliders[index].getPosition() - initialPosition,
swapOffset = compareMin(sliders[index].getPosition(), initialPosition) ? -moveOffset : moveOffset;
move.complete = function () {
sliders[index]._setValid(true);
that._dockSelectedArea();
};
return move;
function move(position) {
var isValid, temp, pos, slider, value;
if (position !== currentPosition) {
if (compareMin(position + swapOffset, staticPosition)) {
isValid = that._allowSlidersSwap;
// TODO: Validate "_minRange" so that for discrete translator it is always null - that will allow to split "isValueProlonged" and "_minRange" checks
if (isValid && !translator.isValueProlonged && that._minRange) {
isValid = translator.isValid(translator.add(sliders[1 - index].getValue(), that._minRange, -dir));
}
if (isValid) {
that._changeMovingSlider(index);
index = 1 - index;
dir = -dir;
temp = compareMin;
compareMin = compareMax;
compareMax = temp;
moveOffset = -dir * Math.abs(moveOffset);
swapOffset = -moveOffset;
}
}
if (compareMax(position + moveOffset, staticPosition)) {
isValid = true;
slider = sliders[index];
value = sliders[1 - index].getValue();
pos = Math.max(Math.min(position + moveOffset, maxPosition), minPosition);
// TODO: Write it as single operation (isValid = ... && ... && ...) when code is stable.
// Check - if moving slider is closer to static slider than a span of a single category.
if (isValid && translator.isValueProlonged) {
isValid = !compareMin(pos, translator.to(value, dir));
}
// Check - if moving slider is closer to static slider than it is allowed "minRange".
if (isValid && that._minRange) {
isValid = !compareMin(pos, translator.to(translator.add(value, that._minRange, dir), dir));
}
// Check - if moving slider is farther from static slider than it is allowed by "maxRange"
if (isValid && that._maxRange) {
isValid = !compareMax(pos, translator.to(translator.add(value, that._maxRange, dir), dir));
}
slider._setValid(isValid);
slider.setDisplayValue(isValid ? selectClosestValue(translator.from(pos, dir), that._values) : slider.getValue());
slider._position = pos;
that._applyTotalPosition(false);
slider.toForeground();
if (that._isOnMoving) {
that._processSelectionChanged();
}
}
}
currentPosition = position;
}
},
_changeMovingSlider: function _changeMovingSlider(index) {
var that = this,
translator = that._params.translator,
sliders = that._sliders,
position = sliders[1 - index].getPosition(),
dir = index > 0 ? +1 : -1,
newValue;
sliders[index].setDisplayValue(selectClosestValue(translator.from(position, dir), that._values));
newValue = translator.from(position, -dir);
if (translator.isValueProlonged) {
newValue = translator.from(position, dir);
} else if (that._minRange) {
// TODO: Consider adding "translator.isValid" check - that will allow to split "if-else" into two "if"
newValue = translator.add(newValue, that._minRange, -dir);
}
sliders[1 - index].setDisplayValue(selectClosestValue(newValue, that._values));
sliders[index]._setValid(true);
sliders[index]._marker._update(); // This is to update "text" element
sliders[0]._position = sliders[1]._position = position;
},
foregroundSlider: function foregroundSlider(index) {
this._sliders[index].toForeground();
}
};
exports.SlidersController = SlidersController;
;