Skip to content

Commit 649c3a0

Browse files
authored
Merge pull request #3 from RVenkatesh/likert-scale-support
Likert scale support
2 parents 8b6d52d + 30fc99c commit 649c3a0

File tree

12 files changed

+475
-107
lines changed

12 files changed

+475
-107
lines changed

README.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ember-slider
1+
# ember-slider [![GitHub version](https://badge.fury.io/gh/RVenkatesh%2Fember-slider.svg)](https://badge.fury.io/gh/RVenkatesh%2Fember-slider) [![npm version](https://badge.fury.io/js/%40love-open-source%2Fember-slider.svg)](https://badge.fury.io/js/%40love-open-source%2Fember-slider)
22

33
This slider is built as an addon for ember applications. Follow the instructions in the [demo](https://github.com/RVenkatesh/Ember-slider#demo) section to see it in action.
44

@@ -26,9 +26,7 @@ Include the ember slider component in the template like any other normal ember c
2626
Add configuration options if necessary in the corresponding route's controller or component's object
2727
```
2828
{
29-
type: {
30-
closed: true
31-
},
29+
type: 'closed',
3230
range: {
3331
min: 0,
3432
max: 100
@@ -44,7 +42,7 @@ The component provides different parameters as mentioned below.
4442
| Params | Default value | Description |
4543
| --- | --- | --- |
4644
| initialValue | ```0``` | To load the slider with value other than min value. |
47-
| config | ```null``` | General settings for the slider. Refer [Configurations options](https://github.com/RVenkatesh/Ember-slider#configuration-options) section for details about the different options available. |
45+
| config | ```null``` | General settings for the slider liker min & max, likert and more. Refer [Configurations options](https://github.com/RVenkatesh/Ember-slider#configuration-options) section for details about the different options available. |
4846

4947
##### Callback parameters
5048

@@ -58,6 +56,29 @@ Apart the above parameters, the following are the callback function available fo
5856

5957
| Option | Default | Description |
6058
| --- | --- | --- |
61-
| type | 'sleek' | Changes the look and feel of the slider. Currently it supports only 'sleek' and 'closed' |
59+
| type | 'sleek' | Changes the look and feel of the slider. Currently it supports only 'sleek' and 'closed'. |
6260
| range | { min: 0, max: 100 } | This object can be used to change the min and max of the slider. This can be of following format. ```{ min: `min_value`, max: `max_value` }``` |
63-
| noValue | false | Boolean to show or hide the display of value in the slider. ```true``` hides the value. |
61+
| hideValue | false | Boolean to show or hide the display of value in the slider. ```true``` hides the value. |
62+
| likert | {} | To enable slider as likert scale. Refer section [Likert scale options](https://github.com/RVenkatesh/Ember-Slider/tree/likert-scale-support#likert-scale-options) to know more about likert configuration. |
63+
64+
##### Likert scale options
65+
The 'likert' configuration option allows the slider to function as likert scale. It can be enabled by passing the following to 'config' option
66+
```
67+
{
68+
likert: {
69+
enabled: true
70+
}
71+
}
72+
```
73+
74+
Just enabling likert option would provide a 3-point likert scale slider with values ranging from 0 to 2. Likert scale can be customised by adding more settings inside likert. Following are the different likert config options available.
75+
76+
| Option | Default | Description |
77+
| --- | --- | --- |
78+
| enabled | false | Enable or disable likert scale |
79+
| points | 3 | Number of likert points to be shown. As of now ```ember-slider``` supports 3, 5 and 7 points. |
80+
| labels | [] | Label text to be displayed under each likert point of the slider. If nothing has to be displayed, pass ```null``` |
81+
82+
83+
## Updating ember-slider
84+
If you are updating one version of ember slider to other, please check out the [Release notes](https://github.com/RVenkatesh/Ember-Slider/wiki/Release-notes) page for any breaking changes (which are usually rare).

addon/components/ember-slider.js

Lines changed: 142 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
import Component from '@ember/component';
22
import layout from '../templates/components/ember-slider';
33
import RecognizerMixin from 'ember-gestures/mixins/recognizers';
4+
import { next } from '@ember/runloop';
5+
6+
const LIKERT_DEFAULT = 3;
47

58
export default Component.extend(RecognizerMixin, {
69
layout,
710
classNames: 'ember-slider',
811
recognizers: 'pan tap',
9-
classNameBindings: ['config.type.closed', 'config.noValue', 'sliding', 'leftClosing', 'rightClosing', 'animate'],
12+
classNameBindings: [
13+
'config.type',
14+
'config.type.closed',
15+
'config.noValue',
16+
'config.hideValue',
17+
'sliding',
18+
'leftClosing',
19+
'rightClosing',
20+
'animate',
21+
'likertEnabled'
22+
],
23+
likertPoints: null,
1024
config: null,
1125
initialValue: null,
1226
value: null,
1327
onChange() {},
1428
init() {
1529
this._super(...arguments);
1630

17-
// Copy config for easier use
18-
this.set('min', this.get('config.range.min') || 0);
19-
this.set('max', this.get('config.range.max') || 100);
31+
if (this.get('config.likert.enabled')) {
32+
this.set('likertEnabled', true);
33+
this.generateLikertPoints();
34+
} else {
35+
// Copy config for easier use
36+
this.set('min', this.get('config.range.min') || 0);
37+
this.set('max', this.get('config.range.max') || 100);
38+
}
2039
},
2140
didInsertElement() {
2241
this._super(...arguments);
@@ -28,60 +47,122 @@ export default Component.extend(RecognizerMixin, {
2847
this.set('SLIDER_COLOR_FILLER_CLOSED', this.$('.slider-color-filler-closed'));
2948
this.moveToInitialValue();
3049
},
50+
// Saves the handle position at present to the component variable
51+
// and then reuse it to during the events like sliding to easily
52+
// work with the state of the slider before the event started
3153
lockHandlePosition() {
3254
let handle_left = parseInt(this.get('SLIDER_HANDLE').position().left);
3355
this.set('_LOCKED_HANDLE_POSITION', handle_left);
3456
},
3557

36-
moveHandle(positionInPX, animate) {
58+
// WARNING: DO NOT TOUCH THIS UNLESS NECESSARY
59+
// This is the core function of this component which takes care of moving the handle
60+
// and updating the value based on the required movement percentage
61+
moveToPercentage(percentage, animate) {
3762
if (animate) {
3863
this.set('animate', true);
3964
} else {
4065
this.set('animate', false);
4166
}
42-
let {min,
43-
max,
44-
SLIDER_PATH,
45-
SLIDER_HANDLE,
67+
68+
let { min,
69+
max, SLIDER_HANDLE,
4670
SLIDER_COLOR_FILLER,
4771
SLIDER_COLOR_FILLER_CLOSED
48-
} = this.getProperties('min', 'max', 'SLIDER_PATH', 'SLIDER_HANDLE', 'SLIDER_COLOR_FILLER', 'SLIDER_COLOR_FILLER_CLOSED');
72+
} = this.getProperties('min', 'max','SLIDER_HANDLE', 'SLIDER_COLOR_FILLER', 'SLIDER_COLOR_FILLER_CLOSED');
4973

5074
let difference = max - min;
51-
let pathWidth = SLIDER_PATH.width();
52-
53-
let movedPercentage = (positionInPX / pathWidth) * 100;
54-
55-
// Make sure the percentage value stays within its boundaries
56-
if (movedPercentage <= 0) {
57-
movedPercentage = 0;
58-
} else if (movedPercentage >= 100) {
59-
movedPercentage = 100;
75+
// Make sure the percentage value stays within its boundaries
76+
if (percentage <= 0) {
77+
percentage = 0;
78+
} else if (percentage >= 100) {
79+
percentage = 100;
6080
}
6181

62-
let newValue = Math.round(min + (movedPercentage * difference) / 100);
63-
64-
this.get('onChange')(this.get('value'), newValue);
82+
// Update the value based on the percentage
83+
let newValue = Math.round(min + (percentage * difference) / 100);
6584
this.set('value', newValue);
66-
let percentageString = movedPercentage + '%';
85+
86+
// Store percentage for easy usage
87+
this.set('_percentage', percentage);
88+
// Move the handle to the corresponding percentage
89+
let percentageString = percentage + '%';
6790
SLIDER_HANDLE.css('left', percentageString);
6891
SLIDER_COLOR_FILLER.css('width', percentageString);
6992
SLIDER_COLOR_FILLER_CLOSED.css('width', percentageString);
70-
// this.addClosenessClass(positionInPX, pathWidth);
7193
},
7294

73-
moveToInitialValue() {
74-
// Right now this whole function looks ridiculous,
75-
// because value to percentage and percentage to value conversion happens
76-
// Change this when the time comes
77-
let {initialValue, min, max, SLIDER_PATH} = this.getProperties('initialValue', 'min', 'max', 'SLIDER_PATH');
78-
let difference = max - min;
95+
// Move the handle to a given px value
96+
// Assumption: The pixel value is relative to the slider div
97+
// which means it is the distance in px from the left most point of the slider
98+
moveToPX(positionInPX, animate) {
99+
let SLIDER_PATH = this.get( 'SLIDER_PATH');
79100
let pathWidth = SLIDER_PATH.width();
80-
initialValue = initialValue === null ? min : initialValue;
81-
let percentage = (initialValue - min) * 100 / difference;
82-
let positionInPX = percentage * pathWidth / 100;
101+
// Calculate the percentage corresponding to the position in px
102+
let movedPercentage = (positionInPX / pathWidth) * 100;
103+
104+
this.moveToPercentage(movedPercentage, animate);
105+
},
83106

84-
this.moveHandle(positionInPX);
107+
// Move the handle based on the given slider value
108+
// If the slider is likert, then the automatically move to nearest likert point
109+
moveToValue(value, animate) {
110+
let {min, max} = this.getProperties('min', 'max');
111+
let difference = max - min;
112+
// Calculate the percentage corresponding to the value of the slider
113+
let percentage = (value - min) * 100 / difference;
114+
115+
if (this.get('likertEnabled')) {
116+
this.moveToLikertByPercentage(percentage, animate);
117+
} else {
118+
this.moveToPercentage(percentage, animate);
119+
}
120+
},
121+
122+
// Move the slider to the initialValue passed to the component
123+
moveToInitialValue(animate) {
124+
let {initialValue, min} = this.getProperties('initialValue', 'min');
125+
// Make sure the initial value is properly set otherwise move to the min
126+
let value = typeof initialValue !== 'number' ? min : initialValue;
127+
this.moveToValue(value, animate);
128+
},
129+
130+
moveToLikertPointFromPX(positionInPX, animate) {
131+
let SLIDER_PATH = this.get('SLIDER_PATH');
132+
let pathWidth = SLIDER_PATH.width(),
133+
// Calculate the percentage corresponding to the position in px
134+
movedPercentage = (positionInPX / pathWidth) * 100;
135+
136+
this.moveToLikertByPercentage(movedPercentage, animate);
137+
},
138+
139+
moveToLikertByPercentage(percentage, animate) {
140+
let distance = this.get('likertDistance'),
141+
percentageToMove = Math.round(percentage / distance) * distance;
142+
143+
// Get only the percentage in multiples of distance to make sure
144+
// the handle always lands in one of the likert points
145+
// Move always to the nearest likert point
146+
this.moveToPercentage(percentageToMove, animate);
147+
},
148+
149+
generateLikertPoints() {
150+
let totalPoints = this.get('config.likert.points') || LIKERT_DEFAULT,
151+
labels = this.get('config.likert.labels'),
152+
points = [],
153+
distance = 100 / (totalPoints - 1);
154+
this.set('likertDistance', distance);
155+
this.set('min', 0);
156+
this.set('max', totalPoints - 1);
157+
for(let counter = 0; counter < totalPoints; counter++) {
158+
// Based on the distance between likert points, generate data for likert points
159+
// with amount of 'left' value to given to each of them
160+
points.push({
161+
left: counter * distance,
162+
label: labels ? labels[counter] || '': ''
163+
});
164+
}
165+
this.set('likertPoints', points);
85166
},
86167

87168
// Add classes to the slider based on whether the handle is closer to left end or right end
@@ -101,8 +182,14 @@ export default Component.extend(RecognizerMixin, {
101182
tap(event) {
102183
let tapPosition = event.originalEvent.gesture.srcEvent.pageX;
103184
let sliderPathLeft = this.get('SLIDER_PATH').offset().left;
104-
105-
this.moveHandle(tapPosition - sliderPathLeft, true);
185+
// Get old value to be passed to onchange event
186+
let oldValue = this.get('value');
187+
if (this.get('likertEnabled')) {
188+
this.moveToLikertPointFromPX(tapPosition - sliderPathLeft, true);
189+
} else {
190+
this.moveToPX(tapPosition - sliderPathLeft, true);
191+
}
192+
this.get('onChange')(oldValue, this.get('value'));
106193
},
107194

108195
panStart() {
@@ -113,15 +200,32 @@ export default Component.extend(RecognizerMixin, {
113200
return;
114201
}
115202
let gesture = event.originalEvent.gesture;
203+
// Since hammer through ember-gestures is only giving us the total movement
204+
// for the current event, we need to take only the handle position before the
205+
// event started.
116206
let _LOCKED_HANDLE_POSITION = this.get('_LOCKED_HANDLE_POSITION');
117-
this.moveHandle(_LOCKED_HANDLE_POSITION + gesture.deltaX);
207+
// Get old value to be passed to onchange event
208+
let oldValue = this.get('value');
209+
210+
this.moveToPX(_LOCKED_HANDLE_POSITION + gesture.deltaX);
211+
this.get('onChange')(oldValue, this.get('value'));
118212
},
119213
panEnd() {
120-
this.lockHandlePosition();
214+
let sliding = this.get('sliding');
121215
this.set('sliding', false);
216+
// Do this to get 'sliding' class properly removed before adding animate class
217+
// since both these are not designed in a way to work together
218+
next(this, function() {
219+
if (this.get('likertEnabled') && sliding) {
220+
this.moveToValue(this.get('value'), true);
221+
}
222+
});
223+
this.lockHandlePosition();
122224
},
123225
actions: {
124226
handleMoveStart() {
227+
// Make this boolean 'true', in order to make sure we are capturing pan event
228+
// which originated from the handle
125229
this.set('sliding', true);
126230
}
127231
}

0 commit comments

Comments
 (0)