/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var App = require('app');
var validator = require('utils/validator');
/**
* Slider-view for configs
* Used to numeric values
* Config value attributes should contain minimum and maximum limits for value
* @type {App.ConfigWidgetView}
*/
App.SliderConfigWidgetView = App.ConfigWidgetView.extend({
classNames: ['widget-config'],
templateName: require('templates/common/configs/widgets/slider_config_widget'),
supportSwitchToTextBox: true,
/**
* Slider-object created on the initSlider
* @type {Object}
*/
slider: null,
/**
* Mirror of the config-value shown in the input on the left of the slider
* @type {number}
*/
mirrorValue: 0,
/**
* Previous mirrorValue
* @type {number}
*/
prevMirrorValue: 0,
/**
* Determines if used-input mirrorValue
is valid
* Calculated on the mirrorValueObs
* @type {boolean}
*/
isMirrorValueValid: true,
/**
* Unit label to display.
* @type {String}
*/
unitLabel: '',
/**
* List of widget's properties which changeBoundaries
-method should observe
* @type {string[]}
*/
changeBoundariesProperties: ['maxMirrorValue', 'widgetRecommendedValue','minMirrorValue', 'mirrorStep'],
/**
* Flag to check if value should be changed to recommended or saved.
* @type {boolean}
*/
isRestoring: false,
/**
* max allowed value transformed form config unit to widget unit
* @type {Number}
*/
maxMirrorValue: function() {
var parseFunction = this.get('mirrorValueParseFunction');
var maximum = this.getValueAttributeByGroup('maximum');
var max = this.widgetValueByConfigAttributes(maximum);
return parseFunction(max);
}.property('config.stackConfigProperty.valueAttributes.maximum', 'controller.forceUpdateBoundaries'),
/**
* min allowed value transformed form config unit to widget unit
* @type {Number}
*/
minMirrorValue: function() {
var parseFunction = this.get('mirrorValueParseFunction');
var minimum = this.getValueAttributeByGroup('minimum');
var min = this.widgetValueByConfigAttributes(minimum);
return parseFunction(min);
}.property('config.stackConfigProperty.valueAttributes.minimum', 'controller.forceUpdateBoundaries'),
/**
*
* if group is default look into (ex. for maximum)
* config.stackConfigProperty.valueAttributes.maximum
* if group is not default look into
* config.stackConfigProperty.valueAttributes.{group.name}.maximum
* @param {String} attribute - name of attribute, for current moment
* can be ["maximum","minimum","increment_step"] but allows to use other it there will be available
* @returns {string}
*/
getValueAttributeByGroup: function(attribute) {
var parseFunction = this.get('parseFunction');
var configValue = this.get('config.value');
var defaultGroupAttr = this.get('config.stackConfigProperty.valueAttributes');
var groupAttr = this.get('configGroup') && defaultGroupAttr[this.get('configGroup.name')];
var boundary = (groupAttr && !Em.isNone(groupAttr[attribute])) ? groupAttr[attribute] : defaultGroupAttr[attribute];
if (!this.get('referToSelectedGroup')) {
if (attribute === 'minimum') {
if (parseFunction(configValue) < parseFunction(boundary)) {
return configValue;
}
} else if (attribute === 'maximum') {
if (parseFunction(configValue) > parseFunction(boundary)) {
return configValue;
}
}
}
return boundary;
},
/**
* step transformed form config units to widget units
* @type {Number}
*/
mirrorStep: function() {
var parseFunction = this.get('mirrorValueParseFunction');
var step = this.widgetValueByConfigAttributes(this.get('config.stackConfigProperty.valueAttributes.increment_step'));
return step ? parseFunction(step) : this.get('unitType') === 'int' ? 1 : 0.1;
}.property('config.stackConfigProperty.valueAttributes.increment_step'),
/**
* Default value of config property transformed according widget format
* @returns {Number}
*/
widgetDefaultValue: function () {
var parseFunction = this.get('mirrorValueParseFunction');
return parseFunction(this.widgetValueByConfigAttributes(this.get('config.savedValue')));
}.property('config.savedValue'),
/**
* Default value of config property transformed according widget format
* @returns {Number}
*/
widgetRecommendedValue: function () {
var parseFunction = this.get('mirrorValueParseFunction');
return parseFunction(this.widgetValueByConfigAttributes(this.get('config.recommendedValue')));
}.property('config.recommendedValue'),
/**
* unit type of widget
* @type {String}
*/
unitType: function () {
var widgetUnit = this.get('config.stackConfigProperty.widget.units.length') && this.get('config.stackConfigProperty.widget.units')[0]['unit-name'].toLowerCase();
var configUnit = this.get('config.stackConfigProperty.valueAttributes.type').toLowerCase();
if (widgetUnit) {
return this.get('units').indexOf(widgetUnit) > this.get('units').indexOf(configUnit) ? 'float' : this.get('config.stackConfigProperty.valueAttributes.type')
} else {
return 'float';
}
}.property('config.stackConfigProperty.widget.units.@each.unit-name'),
/**
* Function used to parse widget mirror value
* For integer - parseInt, for float - parseFloat
* @type {Function}
*/
mirrorValueParseFunction: function () {
return this.get('unitType') === 'int' ? parseInt : parseFloat;
}.property('unitType'),
/**
* Function used to validate widget mirror value
* For integer - validator.isValidInt, for float - validator.isValidFloat
* @type {Function}
*/
mirrorValueValidateFunction: function () {
return this.get('unitType') === 'int' ? validator.isValidInt : validator.isValidFloat;
}.property('unitType'),
/**
* Function used to parse config value (based on config.stackConfigProperty.valueAttributes.type
)
* For integer - parseInt, for float - parseFloat
* @type {Function}
*/
parseFunction: function () {
return this.get('config.stackConfigProperty.valueAttributes.type') === 'int' ? parseInt : parseFloat;
}.property('config.stackConfigProperty.valueAttributes.type'),
/**
* Function used to validate config value (based on config.stackConfigProperty.valueAttributes.type
)
* For integer - validator.isValidInt, for float - validator.isValidFloat
* @type {Function}
*/
validateFunction: function () {
return this.get('config.stackConfigProperty.valueAttributes.type') === 'int' ? validator.isValidInt : validator.isValidFloat;
}.property('config.stackConfigProperty.valueAttributes.type'),
/**
* Enable/disable slider state
* @method toggleWidgetState
*/
toggleWidgetState: function () {
var slider = this.get('slider');
this.get('config.isEditable') ? slider.enable() : slider.disable();
this._super();
}.observes('config.isEditable'),
willInsertElement: function () {
this._super();
this.prepareValueConverter();
this.addObserver('mirrorValue', this, this.mirrorValueObs);
},
didInsertElement: function () {
this._super();
this.setValue();
this.initSlider();
this.toggleWidgetState();
this.initPopover();
var self = this;
this.get('changeBoundariesProperties').forEach(function(property) {
self.addObserver(property, self, self.changeBoundaries);
});
},
willDestroyElement: function() {
this.$('[data-toggle=tooltip]').tooltip('destroy');
var self = this;
this.get('changeBoundariesProperties').forEach(function(property) {
self.removeObserver(property, self, self.changeBoundaries);
});
this.removeObserver('mirrorValue', this, this.mirrorValueObs);
if (this.get('slider')) {
try {
if (self.get('slider')) {
self.get('slider').destroy();
}
} catch (e) {
console.error('error while clearing slider for config: ' + self.get('config.name'));
}
}
},
/**
* Check if mirrorValue
was updated by user
* Validate it. If value is correct, set it to slider and config.value
* @method mirrorValueObs
*/
mirrorValueObs: function () {
var mirrorValue = this.get('mirrorValue'),
slider = this.get('slider'),
min = this.get('minMirrorValue'),
max = this.get('maxMirrorValue'),
validationFunction = this.get('mirrorValueValidateFunction'),
parseFunction = this.get('mirrorValueParseFunction');
if (validationFunction(mirrorValue)) {
var parsed = parseFunction(mirrorValue);
if (parsed > max) {
this.set('isMirrorValueValid', false);
this.get('config').setProperties({
warnMessage: Em.I18n.t('config.warnMessage.outOfBoundaries.greater').format(max + this.get('unitLabel')),
warn: true
});
} else if (parsed < min) {
this.set('isMirrorValueValid', false);
this.get('config').setProperties({
warnMessage: Em.I18n.t('config.warnMessage.outOfBoundaries.less').format(min + this.get('unitLabel')),
warn: true
});
} else {
this.set('isMirrorValueValid', !this.get('config.error'));
this.set('config.value', '' + this.configValueByWidget(parsed));
if (slider) {
slider.setValue(parsed);
}
}
// avoid precision during restore value
if (!Em.isNone(this.get('config.savedValue')) && parsed == parseFunction(this.widgetValueByConfigAttributes(this.get('config.savedValue')))) {
this.set('config.value', this.get('config.savedValue'));
}
// ignore precision during set recommended value
if (!Em.isNone(this.get('config.recommendedValue')) && parsed == parseFunction(this.widgetValueByConfigAttributes(this.get('config.recommendedValue')))) {
this.set('config.value', this.get('config.recommendedValue'));
}
} else {
this.set('isMirrorValueValid', false);
this.set('config.errorMessage', 'Invalid value');
}
},
/**
* set widget value same as config value
* @override
* @method setValue
*/
setValue: function(value) {
var parseFunction = this.get('parseFunction');
value = value || parseFunction(this.get('config.value'));
this.set('mirrorValue', this.widgetValueByConfigAttributes(value));
this.set('prevMirrorValue', this.get('mirrorValue'));
},
/**
* Setup convert table according to widget unit-name and property type.
* Set label for unit to display.
* @method prepareValueConverter
*/
prepareValueConverter: function() {
var widgetUnit = this._converterGetWidgetUnits();
if (['int', 'float'].contains(this._converterGetPropertyAttributes()) && widgetUnit == 'percent') {
this.set('currentDimensionType', 'percent.percent_' + this._converterGetPropertyAttributes());
}
this.set('unitLabel', Em.getWithDefault(this.get('unitLabelMap'), widgetUnit, widgetUnit));
},
/**
* Draw slider for current config
* @method initSlider
*/
initSlider: function () {
var self = this,
config = this.get('config'),
valueAttributes = config.get('stackConfigProperty.valueAttributes'),
parseFunction = this.get('parseFunction'),
ticks = [this.valueForTick(this.get('minMirrorValue'))],
ticksLabels = [],
maxMirrorValue = this.get('maxMirrorValue'),
minMirrorValue = this.get('minMirrorValue'),
mirrorStep = this.get('mirrorStep'),
recommendedValue = this.valueForTick(+this.get('widgetRecommendedValue')),
range = Math.floor((maxMirrorValue - minMirrorValue) / mirrorStep) * mirrorStep,
isOriginalSCP = config.get('isOriginalSCP'),
// for little odd numbers in range 4..23 and widget type 'int' use always 4 ticks
isSmallInt = this.get('unitType') == 'int' && range > 4 && range < 23 && range % 2 == 1,
recommendedValueMirroredId,
recommendedValueId;
// ticks and labels
for (var i = 1; i <= 3; i++) {
var val = minMirrorValue + this.valueForTickProportionalToStep(range * (i / (isSmallInt ? 3 : 4)));
// if value's type is float, ticks may be float too
ticks.push(this._extraRound(val));
}
ticks.push(this.valueForTick(maxMirrorValue));
ticks = ticks.uniq();
ticks.forEach(function (tick, index, items) {
var label = '';
if ((items.length < 5 || index % 2 === 0 || items.length - 1 == index)) {
label = this.formatTickLabel(tick, ' ');
}
ticksLabels.push(label);
}, this);
ticks = ticks.uniq();
if (!(this.get('controller.isCompareMode') && !isOriginalSCP)) {
// default marker should be added only if recommendedValue is in range [min, max]
if (recommendedValue <= maxMirrorValue && recommendedValue >= minMirrorValue && recommendedValue != '') {
// process additional tick for default value if it not defined in previous computation
if (!ticks.contains(recommendedValue)) {
// push default value
ticks.push(recommendedValue);
// and resort array
ticks = ticks.sort(function (a, b) {
return a - b;
});
recommendedValueId = ticks.indexOf(recommendedValue);
// to save nice tick labels layout we should add new tick value which is mirrored by index to default value
recommendedValueMirroredId = ticks.length - recommendedValueId;
// push mirrored default value behind default
if (recommendedValueId == recommendedValueMirroredId) {
recommendedValueMirroredId--;
}
// push empty label for default value tick
ticksLabels.insertAt(recommendedValueId, '');
// push empty to mirrored position
ticksLabels.insertAt(recommendedValueMirroredId, '');
// for saving correct sliding need to add value to mirrored position which is average between previous
// and next value
ticks.insertAt(recommendedValueMirroredId, this.valueForTick((ticks[recommendedValueMirroredId] + ticks[recommendedValueMirroredId - 1]) / 2));
// get new index for default value
recommendedValueId = ticks.indexOf(recommendedValue);
}
else {
recommendedValueId = ticks.indexOf(recommendedValue);
}
}
}
/**
* Slider some times change config value while being created,
* this may happens when slider recreating couple times during small period.
* To cover this situation need to reset config value after slider initializing
* @type {String}
*/
var correctConfigValue = this.get('config.value');
var slider = new Slider(this.$('input.slider-input')[0], {
value: this.get('mirrorValue'),
ticks: ticks,
tooltip: 'always',
ticks_labels: ticksLabels,
step: mirrorStep,
formatter: function (val) {
var labelValue = Em.isArray(val) ? val[0] : val;
return self.formatTickLabel(labelValue);
}
});
/**
* Resetting config value, look for correctConfigValue
* for more info
*/
this.set('config.value', correctConfigValue);
slider.on('change', this.onSliderChange.bind(this))
.on('slideStop', function() {
/**
* action to run sendRequestRorDependentConfigs when
* we have changed config value within slider
*/
if (self.get('prevMirrorValue') != self.get('mirrorValue')) {
self.sendRequestRorDependentConfigs(self.get('config'));
}
});
this.set('slider', slider);
var sliderTicks = this.$('.ui-slider-wrapper:eq(0) .slider-tick');
if (recommendedValueId) {
sliderTicks.eq(recommendedValueId).addClass('slider-tick-default').on('mousedown', function(e) {
if (self.get('disabled')) return false;
self.setRecommendedValue();
e.stopPropagation();
return false;
});
// create label for default value and align it
// defaultSliderTick.append('{0}'.format(recommendedValue + this.get('unitLabel')));
// defaultSliderTick.find('span').css('marginLeft', -defaultSliderTick.find('span').width()/2 + 'px');
// if mirrored value was added need to hide the tick for it
if (recommendedValueMirroredId) {
sliderTicks.eq(recommendedValueMirroredId).hide();
}
}
// mark last tick to fix it style
sliderTicks.last().addClass('last');
},
/**
* Callback function triggered on slider change event.
* Set config property and widget value with new one, or ignore changes in case value restoration executed by
* restoreValue
, setRecommendedValue
.
*
* @param {Object} e - object that contains oldValue
and newValue
attributes.
* @method onSliderChange
*/
onSliderChange: function(e) {
if (!this.get('isRestoring')) {
var val = this.get('mirrorValueParseFunction')(e.newValue);
this.set('config.value', '' + this.configValueByWidget(val));
this.set('mirrorValue', val);
} else {
this.set('isRestoring', false);
}
},
/**
* Convert value according to property attribute unit.
*
* @method valueForTick
* @param {Number} val
* @private
* @returns {Number}
*/
valueForTick: function(val) {
return this.get('unitType') === 'int' ? Math.round(val) : this._extraRound(val);
},
/**
* Convert value according to property attribute unit
* Also returned value is proportional to the mirrorStep
*
* @param {Number} val
* @private
* @returns {Number}
*/
valueForTickProportionalToStep: function (val) {
if (this.get('unitType') === 'int') {
return Math.round(val);
}
var mirrorStep = this.get('mirrorStep');
var r = Math.round(val / mirrorStep);
return this._extraRound(r * mirrorStep);
},
/**
* Round number to 3 digits after "."
* Used for all slider's ticks
* @param {Number} v
* @returns {Number} number with 3 digits after "."
* @private
* @method _extraRound
*/
_extraRound: function(v) {
return parseFloat(v.toFixed(3));
},
/**
* Restore savedValue
for config
* Restore mirrorValue
too
* @method restoreValue
*/
restoreValue: function () {
this._super();
this.set('isRestoring', true);
this.get('slider').setValue(this.get('widgetDefaultValue'));
if (this.get('config.value') === this.get('config.savedValue')) {
this.set('isRestoring', false);
}
},
/**
* @method setRecommendedValue
*/
setRecommendedValue: function () {
this._super();
this.set('isRestoring', true);
this.get('slider').setValue(this.get('widgetRecommendedValue'));
if (this.get('config.value') === this.get('config.recommendedValue')) {
this.set('isRestoring', false);
}
},
/**
* Determines if config-value was changed
* @type {boolean}
*/
valueIsChanged: function () {
return !Em.isNone(this.get('config.savedValue')) && this.get('parseFunction')(this.get('config.value')) != this.get('parseFunction')(this.get('config.savedValue'));
}.property('config.value', 'config.savedValue'),
/**
* Run changeBoundariesOnce only once
* @method changeBoundaries
*/
changeBoundaries: function() {
if (this.get('config.stackConfigProperty.widget')) {
Em.run.once(this, 'changeBoundariesOnce');
}
},
/**
* Recreate widget in case max or min values were changed
*
* @method changeBoundariesOnce
*/
changeBoundariesOnce: function () {
if ($.mocho) {
//temp fix as it can broke test that doesn't have any connection with this method
return;
}
if (this.get('config')) {
try {
if (this.get('slider')) {
this.get('slider').destroy();
}
this.initIncompatibleWidgetAsTextBox();
this.initSlider();
this.toggleWidgetState();
// arguments exists - means method is called as observer and not directly from other method
// so, no need to call refreshSliderObserver
and this prevent recursive calls
// like changeBoundariesOnce -> refreshSliderObserver -> changeBoundariesOnce -> ...
if (arguments.length) {
this.refreshSliderObserver();
}
} catch (e) {
console.error('error while rebuilding slider for config: ' + this.get('config.name'));
}
}
},
/**
* Method used for initializing sliders in the next Ember run-loop
* It's useful for overrides, because they are redrawn a little bit later than origin config
*
* If this method is called without arguments, it will call itself recursively using changeBoundariesOnce
* and refreshSliderObserver
* If not - it will just call changeBoundariesOnce
in the next run-loop
* @method changeBoundariesOnceLater
*/
_changeBoundariesOnceLater: function() {
var self = this;
Em.run.later('sync', function() {
self.changeBoundariesOnce();
}, 10);
},
/**
* Workaround for bootstrap-slider widget that was initiated inside hidden container.
* @method refreshSliderObserver
*/
refreshSliderObserver: function() {
var self = this;
var sliderTickLabel = this.$('.ui-slider-wrapper:eq(0) .slider-tick-label:first');
if (sliderTickLabel.width() == 0 && this.isValueCompatibleWithWidget()) {
Em.run.next(function() {
self._changeBoundariesOnceLater();
});
}
}.observes('parentView.content.isActive', 'parentView.parentView.tab.isActive'),
/**
* Check if value provided by user in the textbox may be used in the slider
* @returns {boolean}
* @method isValueCompatibleWithWidget
*/
isValueCompatibleWithWidget: function() {
if (this._super()) {
if (!this.get('validateFunction')(this.get('config.value'))) {
return false;
}
var configValue = this.get('parseFunction')(this.get('config.value'));
if (this.get('config.stackConfigProperty.valueAttributes.minimum')) {
var min = this.get('parseFunction')(this.getValueAttributeByGroup('minimum'));
if (configValue < min) {
min = this.widgetValueByConfigAttributes(min);
this.updateWarningsForCompatibilityWithWidget(Em.I18n.t('config.warnMessage.outOfBoundaries.less').format(min + this.get('unitLabel')));
return false;
}
}
if (this.get('config.stackConfigProperty.valueAttributes.maximum')) {
var max = this.get('parseFunction')(this.getValueAttributeByGroup('maximum'));
if (configValue > max) {
max = this.widgetValueByConfigAttributes(max);
this.updateWarningsForCompatibilityWithWidget(Em.I18n.t('config.warnMessage.outOfBoundaries.greater').format(max + this.get('unitLabel')));
return false;
}
}
this.updateWarningsForCompatibilityWithWidget('');
return true;
}
return false;
},
/**
* Returns formatted value of slider label
* @param tick - starting value
* @param separator - will be inserted between value and unit
* @returns {string}
*/
formatTickLabel: function (tick, separator) {
var label,
separator = separator || '',
valueLabel = tick,
units = ['B', 'KB', 'MB', 'GB', 'TB'],
unitLabel = this.get('unitLabel'),
unitLabelIndex = units.indexOf(unitLabel);
if (unitLabelIndex > -1) {
while (tick > 9999 && unitLabelIndex < units.length - 1) {
tick /= 1024;
unitLabelIndex++;
}
unitLabel = units[unitLabelIndex];
valueLabel = this._extraRound(tick);
}
label = valueLabel + separator + unitLabel;
return label;
}
});