|
8 | 8 | and sets `selected` as user clicks on it. It doesn't require any variable but |
9 | 9 | it's higly recommended to provide `itemsCount` (as number) or `items` (as array). |
10 | 10 |
|
| 11 | +**This element needs to be transpiled to run in older (ES6 incompatible) browsers** |
| 12 | +
|
11 | 13 | Example: |
12 | 14 |
|
13 | 15 | <paper-pager items-count="3" selected="{{selected}}"></paper-pager> |
|
20 | 22 | ----------------|-------------|---------- |
21 | 23 | `--paper-pager-color` | Color of dots | `white` |
22 | 24 | `--paper-pager-opacity` | Opacity of not selected dots | `0.7` |
| 25 | +`--paper-pager-dots-margin` | Margin of dots | `5px` |
23 | 26 | If you quickly need to switch to dark theme you can use `dark` attribute. |
24 | 27 |
|
25 | 28 | @demo demo/index.html |
|
31 | 34 | :host { |
32 | 35 | margin: 5px; |
33 | 36 | position: relative; |
| 37 | + display: inline-block; |
34 | 38 | } |
35 | 39 |
|
36 | 40 | :host([dark]) { |
|
44 | 48 | display: inline-flex; |
45 | 49 | } |
46 | 50 |
|
47 | | - iron-selector div { |
48 | | - margin: 5px; |
| 51 | + iron-selector > div { |
| 52 | + width: calc(var(--paper-pager-dots-margin, 5px) * 2 + 10px); |
| 53 | + height: calc(var(--paper-pager-dots-margin, 5px) * 2 + 10px); |
| 54 | + position: relative; |
| 55 | + } |
| 56 | + |
| 57 | + iron-selector .dot { |
| 58 | + margin: var(--paper-pager-dots-margin, 5px); |
49 | 59 | border-radius: 5px; |
50 | 60 | width: 10px; |
51 | 61 | height: 10px; |
|
54 | 64 | opacity: var(--paper-pager-opacity, 0.7); |
55 | 65 | } |
56 | 66 |
|
57 | | - .dot { |
58 | | - will-change: transform; |
59 | | - display: none; |
| 67 | + iron-selector .iron-selected.ready .dot { |
| 68 | + opacity: 1; |
| 69 | + } |
| 70 | + |
| 71 | + :host([accessible]) iron-selector > div:focus::after { |
| 72 | + content: ''; |
60 | 73 | position: absolute; |
61 | | - border-radius: 5px; |
| 74 | + top: 0; |
| 75 | + bottom: 0; |
| 76 | + left: 0; |
| 77 | + right: 0; |
| 78 | + border-radius: 50%; |
| 79 | + opacity: 0.2; |
62 | 80 | background-color: var(--paper-pager-color, white); |
63 | | - transition: all 300ms cubic-bezier(0.4, 0.0, 0.2, 1); |
64 | 81 | } |
65 | 82 |
|
66 | | - div { |
67 | | - transition: background-color 150ms cubic-bezier(0.4, 0.0, 0.2, 1); |
| 83 | + iron-selector > div:focus { |
| 84 | + outline: none; |
| 85 | + } |
| 86 | + |
| 87 | + #canvas { |
| 88 | + position: absolute; |
| 89 | + top: 0; |
| 90 | + bottom: 0; |
| 91 | + right: 0; |
| 92 | + left: 0; |
| 93 | + pointer-events: none; |
68 | 94 | } |
69 | 95 | </style> |
70 | 96 | <iron-selector selected="[[selected]]"> |
71 | | - <dom-repeat items="[[items]]"> |
72 | | - <template is="dom-repeat" items="[[items]]"> |
73 | | - <div index="[[index]]" on-tap="_onTap"></div> |
74 | | - </template> |
75 | | - </dom-repeat> |
| 97 | + <template is="dom-repeat" items="[[items]]"> |
| 98 | + <div index="[[index]]" on-tap="_onTap"> |
| 99 | + <div class="dot"></div> |
| 100 | + </div> |
| 101 | + </template> |
76 | 102 | </iron-selector> |
77 | | - <div class="dot"></div> |
| 103 | + <canvas id="canvas"></canvas> |
78 | 104 | </template> |
79 | 105 | <script> |
80 | 106 | Polymer({ |
|
99 | 125 | * leave itemsCount empty. |
100 | 126 | */ |
101 | 127 | items: { |
102 | | - type: Array |
| 128 | + type: Array, |
| 129 | + observer: '_changeSize' |
103 | 130 | }, |
104 | 131 |
|
105 | 132 | /** |
|
120 | 147 | type: Boolean, |
121 | 148 | value: false, |
122 | 149 | observer: '_updateStyles' |
| 150 | + }, |
| 151 | + |
| 152 | + /** |
| 153 | + * Time in ms for the animation between two dots |
| 154 | + */ |
| 155 | + transitionDuration: { |
| 156 | + type: Number, |
| 157 | + value: 200 |
| 158 | + }, |
| 159 | + |
| 160 | + /** |
| 161 | + * Time in ms for the transition to pause (when two dots connect) |
| 162 | + */ |
| 163 | + pauseDuration: { |
| 164 | + type: Number, |
| 165 | + value: 200 |
| 166 | + }, |
| 167 | + |
| 168 | + /** |
| 169 | + * Turn on accessibility features (keyboard navigation, focus ring); |
| 170 | + */ |
| 171 | + accessible: { |
| 172 | + type: Boolean, |
| 173 | + reflectToAttribute: true, |
| 174 | + observer: '_setupAccessibility' |
123 | 175 | } |
124 | 176 |
|
125 | 177 | }, |
126 | 178 |
|
| 179 | + /*keyBindings: { |
| 180 | + 'down' : '_previous', |
| 181 | + 'up' : '_next', |
| 182 | + 'left' : '_previous', |
| 183 | + 'right' : '_next', |
| 184 | + 'space' : '_enterSelected', |
| 185 | + 'enter' : '_enterSelected', |
| 186 | + },*/ |
| 187 | + |
127 | 188 | attached: function() { |
128 | | - if (this.selected == 0) { |
129 | | - this._selectedChanged(this.selected, 2); |
130 | | - } else { |
131 | | - this._selectedChanged(this.selected, 0); |
132 | | - } |
| 189 | + this._draw = this.$.canvas.getContext('2d'); |
| 190 | + Polymer.RenderStatus.afterNextRender(this, () => { |
| 191 | + this.$$('.iron-selected').classList.add('ready'); |
| 192 | + }); |
133 | 193 | }, |
134 | 194 |
|
135 | 195 | _onTap: function(e) { |
136 | | - this.selected = e.target.index; |
| 196 | + this.selected = e.currentTarget.index; |
137 | 197 | }, |
138 | 198 |
|
139 | 199 | _computeItems: function(count) { |
140 | 200 | this.items = new Array(count); |
141 | 201 | }, |
142 | 202 |
|
143 | | - _selectedChanged: function(selected, lastSelected) { |
144 | | - if(selected === undefined || lastSelected === undefined) return; |
145 | | - var dot = this.$$('.dot').style; |
146 | | - if (this.items.length && selected != lastSelected) { |
147 | | - dot.display = 'block'; |
148 | | - var selectedN = selected + 1; |
149 | | - var lastSelectedN = lastSelected ? lastSelected + 1 : 1; |
150 | | - var selectedItem = this.$$('div:nth-child(' + selectedN + ')'); |
151 | | - var lastSelectedItem = this.$$('div:nth-child(' + lastSelectedN + ')'); |
152 | | - if(!selectedItem || !lastSelectedItem) return; |
153 | | - var lastSelRect = lastSelectedItem.getBoundingClientRect(); |
154 | | - var selectedRect = selectedItem.getBoundingClientRect(); |
155 | | - var elRect = this.getBoundingClientRect(); |
156 | | - selectedRect = this._processRelativeRect(selectedRect, elRect); |
157 | | - lastSelRect = this._processRelativeRect(lastSelRect, elRect); |
158 | | - dot.top = lastSelRect.top + 'px'; |
159 | | - dot.bottom = lastSelRect.bottom + 'px'; |
160 | | - dot.left = lastSelRect.left + 'px'; |
161 | | - dot.right = lastSelRect.right + 'px'; |
162 | | - if (lastSelected > selected) { |
163 | | - dot.left = selectedRect.left + 'px'; |
164 | | - dot.right = lastSelRect.right + 'px'; |
165 | | - } else { |
166 | | - dot.left = lastSelRect.left + 'px'; |
167 | | - dot.right = selectedRect.right + 'px'; |
168 | | - } |
169 | | - setTimeout(function() { |
170 | | - dot.left = selectedRect.left + 'px'; |
171 | | - dot.right = selectedRect.right + 'px'; |
172 | | - }, 400); |
173 | | - } |
| 203 | + _changeSize: function(items) { |
| 204 | + const marginPx = this.getComputedStyleValue('--paper-pager-dots-margin'); |
| 205 | + const margin = marginPx ? marginPx.match(/\d+/)[0] : 5; |
| 206 | + this.$.canvas.height = (10 + 2 * margin); |
| 207 | + this.$.canvas.width = items.length * (10 + 2 * margin); |
174 | 208 | }, |
175 | 209 |
|
176 | | - _processRelativeRect: function(element, parent) { |
177 | | - var output = { |
178 | | - height: element.height, |
179 | | - width: element.width, |
180 | | - top: element.top - parent.top, |
181 | | - right: Math.abs(element.right - parent.right), |
182 | | - bottom: Math.abs(element.bottom - parent.bottom), |
183 | | - left: element.left - parent.left |
| 210 | + _selectedChanged: async function(selected, lastSelected) { |
| 211 | + if (!this._draw) return; |
| 212 | + if (this.accessible) { |
| 213 | + this._tabindex = this._tabindex.bind(this); |
| 214 | + setTimeout(this._tabindex); |
| 215 | + } |
| 216 | + if (this.$$('.ready')) this.$$('.ready').classList.remove('ready'); |
| 217 | + this.$.canvas.style.pointerEvents = 'auto'; |
| 218 | + const ctx = this._draw, |
| 219 | + color = this.getComputedStyleValue('--paper-pager-color') || 'white', |
| 220 | + marginPx = this.getComputedStyleValue('--paper-pager-dots-margin'), |
| 221 | + margin = marginPx ? marginPx.match(/\d+/)[0] : 5, |
| 222 | + y = margin + 5, |
| 223 | + width = this.$.canvas.width, |
| 224 | + height = this.$.canvas.height, |
| 225 | + start = (margin * 2 + 10) * (lastSelected + 1) - 10, |
| 226 | + end = (margin * 2 + 10) * (selected + 1) - 10, |
| 227 | + duration = this.transitionDuration, |
| 228 | + cycles = duration / 17, |
| 229 | + frameDistance = (start - end) / Math.round(cycles); |
| 230 | + let i = 0, |
| 231 | + pos = start; |
| 232 | + const draw = () => { |
| 233 | + i++; |
| 234 | + pos -= frameDistance; |
| 235 | + ctx.beginPath(); |
| 236 | + ctx.clearRect(0, 0, width, height); |
| 237 | + ctx.moveTo(start, y); |
| 238 | + ctx.lineTo(pos, y); |
| 239 | + ctx.lineWidth = 10; |
| 240 | + ctx.strokeStyle = color; |
| 241 | + ctx.lineCap = 'round'; |
| 242 | + ctx.stroke(); |
| 243 | + if (i >= cycles) { |
| 244 | + clearInterval(interval); |
| 245 | + ctx.clearRect(0, 0, width, height); |
| 246 | + ctx.moveTo(start, y); |
| 247 | + ctx.lineTo(end, y); |
| 248 | + ctx.lineWidth = 10; |
| 249 | + ctx.strokeStyle = color; |
| 250 | + ctx.lineCap = 'round'; |
| 251 | + ctx.stroke(); |
| 252 | + } |
184 | 253 | }; |
185 | | - |
186 | | - return output; |
| 254 | + const interval = setInterval(draw, 17); |
| 255 | + await this._wait(this.pauseDuration + duration); |
| 256 | + pos = start; |
| 257 | + i = 0; |
| 258 | + const drawReverse = () => { |
| 259 | + i++; |
| 260 | + pos -= frameDistance; |
| 261 | + ctx.beginPath(); |
| 262 | + ctx.clearRect(0, 0, width, height); |
| 263 | + ctx.moveTo(end, y); |
| 264 | + ctx.lineTo(pos, y); |
| 265 | + ctx.lineWidth = 10; |
| 266 | + ctx.strokeStyle = color; |
| 267 | + ctx.lineCap = 'round'; |
| 268 | + ctx.stroke(); |
| 269 | + if (i >= cycles) { |
| 270 | + clearInterval(intervalReverse); |
| 271 | + ctx.clearRect(0, 0, width, height); |
| 272 | + ctx.moveTo(end, y); |
| 273 | + ctx.lineTo(end, y); |
| 274 | + ctx.lineWidth = 10; |
| 275 | + ctx.strokeStyle = color; |
| 276 | + ctx.lineCap = 'round'; |
| 277 | + ctx.stroke(); |
| 278 | + } |
| 279 | + }; |
| 280 | + const intervalReverse = setInterval(drawReverse, 17); |
| 281 | + await this._wait(duration + 17); |
| 282 | + ctx.clearRect(0, 0, width, height); |
| 283 | + this.$.canvas.style.pointerEvents = 'none'; |
| 284 | + this.$$('.iron-selected').classList.add('ready'); |
187 | 285 | }, |
188 | 286 |
|
189 | 287 | _updateStyles: function() { |
190 | 288 | this.updateStyles(); |
| 289 | + }, |
| 290 | + |
| 291 | + _next: function(e) { |
| 292 | + e.detail.keyboardEvent.preventDefault(); |
| 293 | + if (this._focused === this.items.length - 1) { |
| 294 | + this._focused = 0; |
| 295 | + } else { |
| 296 | + this._focused++; |
| 297 | + } |
| 298 | + Polymer.dom(this.root).querySelectorAll('iron-selector > div').forEach(item => { |
| 299 | + if (item.index === this._focused) { |
| 300 | + item.tabIndex = 0; |
| 301 | + item.focus(); |
| 302 | + } else { |
| 303 | + item.tabIndex = -1; |
| 304 | + } |
| 305 | + }); |
| 306 | + }, |
| 307 | + |
| 308 | + _previous: function(e) { |
| 309 | + e.detail.keyboardEvent.preventDefault(); |
| 310 | + if (this._focused === 0) { |
| 311 | + this._focused = this.items.length - 1; |
| 312 | + } else { |
| 313 | + this._focused--; |
| 314 | + } |
| 315 | + Polymer.dom(this.root).querySelectorAll('iron-selector > div').forEach(item => { |
| 316 | + if (item.index === this._focused) { |
| 317 | + item.tabIndex = 0; |
| 318 | + item.focus(); |
| 319 | + } else { |
| 320 | + item.tabIndex = -1; |
| 321 | + } |
| 322 | + }); |
| 323 | + }, |
| 324 | + |
| 325 | + _enterSelected: function(e) { |
| 326 | + e.detail.keyboardEvent.preventDefault(); |
| 327 | + this.selected = this._focused; |
| 328 | + }, |
| 329 | + |
| 330 | + _tabindex: function() { |
| 331 | + this._focused = this.selected; |
| 332 | + Polymer.dom(this.root).querySelectorAll('iron-selector > div').forEach(item => { |
| 333 | + item.tabIndex = item.index === this.selected ? 0 : -1; |
| 334 | + }); |
| 335 | + }, |
| 336 | + |
| 337 | + _setupAccessibility: function(a11y) { |
| 338 | + if (a11y) { |
| 339 | + Polymer.RenderStatus.afterNextRender(this, () => { |
| 340 | + this.$$('iron-selector .iron-selected').tabIndex = 0; |
| 341 | + }); |
| 342 | + this._focused = this.selected; |
| 343 | + this.addOwnKeyBinding('down', '_previous'); |
| 344 | + this.addOwnKeyBinding('up', '_next'); |
| 345 | + this.addOwnKeyBinding('left', '_previous'); |
| 346 | + this.addOwnKeyBinding('right', '_next'); |
| 347 | + this.addOwnKeyBinding('space', '_enterSelected'); |
| 348 | + this.addOwnKeyBinding('enter', '_enterSelected'); |
| 349 | + } else { |
| 350 | + Polymer.dom(this.root).querySelectorAll('iron-selector > div').forEach(item => { |
| 351 | + item.tabIndex = -1; |
| 352 | + }); |
| 353 | + this.removeOwnKeyBindings(); |
| 354 | + } |
| 355 | + }, |
| 356 | + |
| 357 | + _wait: function(ms) { |
| 358 | + return new Promise(r => setTimeout(r, ms)); |
191 | 359 | } |
192 | 360 | }); |
193 | 361 | </script> |
|
0 commit comments