1 | /*
|
2 | Copyright 2013-2015 ASIAL CORPORATION
|
3 |
|
4 | Licensed under the Apache License, Version 2.0 (the "License");
|
5 | you may not use this file except in compliance with the License.
|
6 | You may obtain a copy of the License at
|
7 |
|
8 | http://www.apache.org/licenses/LICENSE-2.0
|
9 |
|
10 | Unless required by applicable law or agreed to in writing, software
|
11 | distributed under the License is distributed on an "AS IS" BASIS,
|
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | See the License for the specific language governing permissions and
|
14 | limitations under the License.
|
15 |
|
16 | */
|
17 |
|
18 | import onsElements from '../ons/elements.js';
|
19 | import animit from '../ons/animit.js';
|
20 | import util from '../ons/util.js';
|
21 | import styler from '../ons/styler.js';
|
22 | import autoStyle from '../ons/autostyle.js';
|
23 | import ModifierUtil from '../ons/internal/modifier-util.js';
|
24 | import AnimatorFactory from '../ons/internal/animator-factory.js';
|
25 | import { ListItemAnimator, SlideListItemAnimator } from './ons-list-item/animator.js';
|
26 | import BaseElement from './base/base-element.js';
|
27 | import contentReady from '../ons/content-ready.js';
|
28 |
|
29 | const defaultClassName = 'list-item';
|
30 | const scheme = {
|
31 | '.list-item': 'list-item--*',
|
32 | '.list-item__left': 'list-item--*__left',
|
33 | '.list-item__center': 'list-item--*__center',
|
34 | '.list-item__right': 'list-item--*__right',
|
35 | '.list-item__label': 'list-item--*__label',
|
36 | '.list-item__title': 'list-item--*__title',
|
37 | '.list-item__subtitle': 'list-item--*__subtitle',
|
38 | '.list-item__thumbnail': 'list-item--*__thumbnail',
|
39 | '.list-item__icon': 'list-item--*__icon'
|
40 | };
|
41 |
|
42 | const _animatorDict = {
|
43 | 'default': SlideListItemAnimator,
|
44 | 'none': ListItemAnimator
|
45 | };
|
46 |
|
47 | /**
|
48 | * @element ons-list-item
|
49 | * @category list
|
50 | * @modifier tappable
|
51 | * [en]Make the list item change appearance when it's tapped. On iOS it is better to use the "tappable" and "tap-background-color" attribute for better behavior when scrolling.[/en]
|
52 | * [ja]タップやクリックした時に効果が表示されるようになります。[/ja]
|
53 | * @modifier chevron
|
54 | * [en]Display a chevron at the right end of the list item and make it change appearance when tapped.[/en]
|
55 | * [ja][/ja]
|
56 | * @modifier longdivider
|
57 | * [en]Displays a long horizontal divider between items.[/en]
|
58 | * [ja][/ja]
|
59 | * @modifier nodivider
|
60 | * [en]Removes the divider between list items.[/en]
|
61 | * [ja][/ja]
|
62 | * @modifier material
|
63 | * [en]Display a Material Design list item.[/en]
|
64 | * [ja][/ja]
|
65 | * @description
|
66 | * [en]
|
67 | * Component that represents each item in a list. The list item is composed of four parts that are represented with the `left`, `center`, `right` and `expandable-content` classes. These classes can be used to ensure that the content of the list items is properly aligned.
|
68 | *
|
69 | * ```
|
70 | * <ons-list-item>
|
71 | * <div class="left">Left</div>
|
72 | * <div class="center">Center</div>
|
73 | * <div class="right">Right</div>
|
74 | * <div class="expandable-content">Expandable content</div>
|
75 | * </ons-list-item>
|
76 | * ```
|
77 | *
|
78 | * There are also a number of classes (prefixed with `list-item__*`) that help when putting things like icons and thumbnails into the list items.
|
79 | * [/en]
|
80 | * [ja][/ja]
|
81 | * @seealso ons-list
|
82 | * [en]ons-list component[/en]
|
83 | * [ja]ons-listコンポーネント[/ja]
|
84 | * @seealso ons-list-header
|
85 | * [en]ons-list-header component[/en]
|
86 | * [ja]ons-list-headerコンポーネント[/ja]
|
87 | * @codepen yxcCt
|
88 | * @tutorial vanilla/Reference/list
|
89 | * @example
|
90 | * <ons-list-item>
|
91 | * <div class="left">
|
92 | * <ons-icon icon="md-face" class="list-item__icon"></ons-icon>
|
93 | * </div>
|
94 | * <div class="center">
|
95 | * <div class="list-item__title">Title</div>
|
96 | * <div class="list-item__subtitle">Subtitle</div>
|
97 | * </div>
|
98 | * <div class="right">
|
99 | * <ons-switch></ons-switch>
|
100 | * </div>
|
101 | * </ons-list-item>
|
102 | */
|
103 | export default class ListItemElement extends BaseElement {
|
104 |
|
105 | /**
|
106 | * @attribute modifier
|
107 | * @type {String}
|
108 | * @description
|
109 | * [en]The appearance of the list item.[/en]
|
110 | * [ja]各要素の表現を指定します。[/ja]
|
111 | */
|
112 |
|
113 | /**
|
114 | * @attribute lock-on-drag
|
115 | * @type {Boolean}
|
116 | * @description
|
117 | * [en]Prevent vertical scrolling when the user drags horizontally.[/en]
|
118 | * [ja]この属性があると、ユーザーがこの要素を横方向にドラッグしている時に、縦方向のスクロールが起きないようになります。[/ja]
|
119 | */
|
120 |
|
121 | /**
|
122 | * @property lockOnDrag
|
123 | * @type {Boolean}
|
124 | * @description
|
125 | * [en]Prevent vertical scrolling when the user drags horizontally.[/en]
|
126 | * [ja]この属性があると、ユーザーがこの要素を横方向にドラッグしている時に、縦方向のスクロールが起きないようになります。[/ja]
|
127 | */
|
128 |
|
129 | /**
|
130 | * @attribute tappable
|
131 | * @type {Boolean}
|
132 | * @description
|
133 | * [en]Makes the element react to taps. `prevent-tap` attribute can be added to child elements like buttons or inputs to prevent this effect. `ons-*` elements are ignored by default.[/en]
|
134 | * [ja][/ja]
|
135 | */
|
136 |
|
137 | /**
|
138 | * @property tappable
|
139 | * @type {Boolean}
|
140 | * @description
|
141 | * [en]Makes the element react to taps. `prevent-tap` attribute can be added to child elements like buttons or inputs to prevent this effect. `ons-*` elements are ignored by default.[/en]
|
142 | * [ja][/ja]
|
143 | */
|
144 |
|
145 | /**
|
146 | * @attribute tap-background-color
|
147 | * @type {Color}
|
148 | * @description
|
149 | * [en] Changes the background color when tapped. For this to work, the attribute "tappable" needs to be set. The default color is "#d9d9d9". It will display as a ripple effect on Android.[/en]
|
150 | * [ja][/ja]
|
151 | */
|
152 |
|
153 | /**
|
154 | * @property tapBackgroundColor
|
155 | * @type {Color}
|
156 | * @description
|
157 | * [en] Changes the background color when tapped. For this to work, the attribute "tappable" needs to be set. The default color is "#d9d9d9". It will display as a ripple effect on Android.[/en]
|
158 | * [ja][/ja]
|
159 | */
|
160 |
|
161 | /**
|
162 | * @attribute expandable
|
163 | * @type {Boolean}
|
164 | * @description
|
165 | * [en]Makes the element able to be expanded to reveal extra content. For this to work, the expandable content must be defined in `div.expandable-content`.[/en]
|
166 | * [ja][/ja]
|
167 | */
|
168 |
|
169 | /**
|
170 | * @property expandable
|
171 | * @initonly
|
172 | * @type {Boolean}
|
173 | * @description
|
174 | * [en]Makes the element able to be expanded to reveal extra content. For this to work, the expandable content must be defined in `div.expandable-content`.[/en]
|
175 | * [ja][/ja]
|
176 | */
|
177 |
|
178 | /**
|
179 | * @attribute expanded
|
180 | * @type {Boolean}
|
181 | * @description
|
182 | * [en]For expandable list items, specifies whether the expandable content is expanded or not.[/en]
|
183 | * [ja][/ja]
|
184 | */
|
185 |
|
186 | /**
|
187 | * @property expanded
|
188 | * @type {Boolean}
|
189 | * @description
|
190 | * [en]For expandable list items, specifies whether the expandable content is expanded or not.[/en]
|
191 | * [ja][/ja]
|
192 | */
|
193 |
|
194 | /**
|
195 | * @event expand
|
196 | * @description
|
197 | * [en]For expandable list items, fires when the list item is expanded or contracted.[/en]
|
198 | * [ja][/ja]
|
199 | */
|
200 |
|
201 | /**
|
202 | * @attribute animation
|
203 | * @type {String}
|
204 | * @default default
|
205 | * @description
|
206 | * [en]The animation used when showing and hiding the expandable content. Can be either `"default"` or `"none"`.[/en]
|
207 | * [ja][/ja]
|
208 | */
|
209 |
|
210 | /**
|
211 | * @property animation
|
212 | * @type {String}
|
213 | * @default default
|
214 | * @description
|
215 | * [en]The animation used when showing and hiding the expandable content. Can be either `"default"` or `"none"`.[/en]
|
216 | * [ja][/ja]
|
217 | */
|
218 |
|
219 | constructor() {
|
220 | super();
|
221 |
|
222 | this._animatorFactory = this._updateAnimatorFactory();
|
223 |
|
224 | // Elements ignored when tapping
|
225 | const re = /^ons-(?!col$|row$|if$)/i;
|
226 | this._shouldIgnoreTap = e => e.hasAttribute('prevent-tap') || re.test(e.tagName);
|
227 |
|
228 | // show and hide functions for Vue hidable mixin
|
229 | this.show = this.showExpansion;
|
230 | this.hide = this.hideExpansion;
|
231 |
|
232 | contentReady(this, () => {
|
233 | this._compile();
|
234 | });
|
235 | }
|
236 |
|
237 | /**
|
238 | * Compiles the list item.
|
239 | *
|
240 | * Various elements are allowed in the body of a list item:
|
241 | *
|
242 | * - div.left, div.right, and div.center are allowed as direct children
|
243 | * - if div.center is not defined, anything that isn't div.left, div.right or div.expandable-content will be put in a div.center
|
244 | * - if div.center is defined, anything that isn't div.left, div.right or div.expandable-content will be ignored
|
245 | * - if list item has expandable attribute:
|
246 | * - div.expandable-content is allowed as a direct child
|
247 | * - div.top is allowed as direct child
|
248 | * - if div.top is defined, anything that isn't div.expandable-content should be inside div.top - anything else will be ignored
|
249 | * - if div.right is not defined, a div.right will be created with a drop-down chevron
|
250 | *
|
251 | * See the tests for examples.
|
252 | */
|
253 | _compile() {
|
254 | autoStyle.prepare(this);
|
255 | this.classList.add(defaultClassName);
|
256 |
|
257 | let top, expandableContent;
|
258 | let topContent = [];
|
259 | Array.from(this.childNodes).forEach(node => {
|
260 | if (node.nodeType !== Node.ELEMENT_NODE) {
|
261 | topContent.push(node);
|
262 | } else if (node.classList.contains('top')) {
|
263 | top = node;
|
264 | } else if (node.classList.contains('expandable-content')) {
|
265 | expandableContent = node;
|
266 | } else {
|
267 | topContent.push(node);
|
268 | }
|
269 |
|
270 | if (node.nodeName !== 'ONS-RIPPLE') {
|
271 | node.remove();
|
272 | }
|
273 | });
|
274 | topContent = top ? Array.from(top.childNodes) : topContent;
|
275 |
|
276 | let left, right, center;
|
277 | const centerContent = [];
|
278 | topContent.forEach(node => {
|
279 | if (node.nodeType !== Node.ELEMENT_NODE) {
|
280 | centerContent.push(node);
|
281 | } else if (node.classList.contains('left')) {
|
282 | left = node;
|
283 | } else if (node.classList.contains('right')) {
|
284 | right = node;
|
285 | } else if (node.classList.contains('center')) {
|
286 | center = node;
|
287 | } else {
|
288 | centerContent.push(node);
|
289 | }
|
290 | });
|
291 |
|
292 | if (this.hasAttribute('expandable')) {
|
293 | this.classList.add('list-item--expandable');
|
294 |
|
295 | if (!top) {
|
296 | top = document.createElement('div');
|
297 | top.classList.add('top');
|
298 | }
|
299 | top.classList.add('list-item__top');
|
300 | this.appendChild(top);
|
301 | this._top = top;
|
302 |
|
303 | if (expandableContent) {
|
304 | expandableContent.classList.add('list-item__expandable-content');
|
305 | this.appendChild(expandableContent);
|
306 | }
|
307 |
|
308 | if (!right) {
|
309 | right = document.createElement('div');
|
310 | right.classList.add('list-item__right', 'right');
|
311 |
|
312 | // We cannot use a pseudo-element for this chevron, as we cannot animate it using
|
313 | // JS. So, we make a chevron span instead.
|
314 | const chevron = document.createElement('span');
|
315 | chevron.classList.add('list-item__expand-chevron');
|
316 | right.appendChild(chevron);
|
317 | }
|
318 |
|
319 | // The case where the list item should already start expanded.
|
320 | // Adding the class early stops the animation from running at startup.
|
321 | if (this.expanded) {
|
322 | this.classList.add('list-item--expanded');
|
323 | }
|
324 | } else {
|
325 | top = this;
|
326 | }
|
327 |
|
328 | if (!center) {
|
329 | center = document.createElement('div');
|
330 | center.classList.add('center');
|
331 | centerContent.forEach(node => center.appendChild(node));
|
332 | }
|
333 | center.classList.add('list-item__center');
|
334 | top.appendChild(center);
|
335 |
|
336 | if (left) {
|
337 | left.classList.add('list-item__left');
|
338 | top.appendChild(left);
|
339 | }
|
340 | if (right) {
|
341 | right.classList.add('list-item__right');
|
342 | top.appendChild(right);
|
343 | }
|
344 |
|
345 | util.updateRipple(this);
|
346 | ModifierUtil.initModifier(this, scheme);
|
347 | }
|
348 |
|
349 | /**
|
350 | * @method showExpansion
|
351 | * @signature showExpansion()
|
352 | * @description
|
353 | * [en]Show the expandable content if the element is expandable.[/en]
|
354 | * [ja][/ja]
|
355 | */
|
356 | showExpansion() {
|
357 | this.expanded = true;
|
358 | }
|
359 |
|
360 | /**
|
361 | * @method hideExpansion
|
362 | * @signature hideExpansion()
|
363 | * @description
|
364 | * [en]Hide the expandable content if the element expandable.[/en]
|
365 | * [ja][/ja]
|
366 | */
|
367 | hideExpansion() {
|
368 | this.expanded = false;
|
369 | }
|
370 |
|
371 | toggleExpansion() {
|
372 | this.expanded = !this.expanded;
|
373 | }
|
374 |
|
375 | _animateExpansion() {
|
376 | // Stops the animation from running in the case where the list item should start already expanded
|
377 | const expandedAtStartup = this.expanded && this.classList.contains('list-item--expanded');
|
378 |
|
379 | if (!this.hasAttribute('expandable') || this._expanding || expandedAtStartup) {
|
380 | return;
|
381 | }
|
382 |
|
383 | this._expanding = true;
|
384 |
|
385 | const expandedCallback = () => {
|
386 | this._expanding = false;
|
387 |
|
388 | if (this.expanded) {
|
389 | this.classList.add('list-item--expanded');
|
390 | } else {
|
391 | this.classList.remove('list-item--expanded');
|
392 | }
|
393 | };
|
394 |
|
395 | const animator = this._animatorFactory.newAnimator();
|
396 |
|
397 | if (animator._animateExpansion) {
|
398 | animator._animateExpansion(this, this.expanded, expandedCallback);
|
399 | } else {
|
400 | expandedCallback();
|
401 | }
|
402 | }
|
403 |
|
404 | _updateAnimatorFactory() {
|
405 | return new AnimatorFactory({
|
406 | animators: _animatorDict,
|
407 | baseClass: ListItemAnimator,
|
408 | baseClassName: 'ListItemAnimator',
|
409 | defaultAnimation: this.getAttribute('animation') || 'default'
|
410 | });
|
411 | }
|
412 |
|
413 | static get observedAttributes() {
|
414 | return ['modifier', 'class', 'ripple', 'animation', 'expanded'];
|
415 | }
|
416 |
|
417 | get expandableContent() {
|
418 | return this.querySelector('.list-item__expandable-content');
|
419 | }
|
420 |
|
421 | get expandChevron() {
|
422 | return this.querySelector('.list-item__expand-chevron');
|
423 | }
|
424 |
|
425 | attributeChangedCallback(name, last, current) {
|
426 | switch (name) {
|
427 | case 'class':
|
428 | util.restoreClass(this, defaultClassName, scheme);
|
429 | break;
|
430 | case 'modifier':
|
431 | ModifierUtil.onModifierChanged(last, current, this, scheme);
|
432 | break;
|
433 | case 'ripple':
|
434 | util.updateRipple(this);
|
435 | break;
|
436 | case 'animation':
|
437 | this._animatorFactory = this._updateAnimatorFactory();
|
438 | break;
|
439 | case 'expanded':
|
440 | contentReady(this, () => this._animateExpansion());
|
441 | break;
|
442 | }
|
443 | }
|
444 |
|
445 | connectedCallback() {
|
446 | contentReady(this, () => {
|
447 | this._setupListeners(true);
|
448 | this._originalBackgroundColor = this.style.backgroundColor;
|
449 | this.tapped = false;
|
450 | });
|
451 | }
|
452 |
|
453 | disconnectedCallback() {
|
454 | this._setupListeners(false);
|
455 | }
|
456 |
|
457 | _setupListeners(add) {
|
458 | const action = (add ? 'add' : 'remove') + 'EventListener';
|
459 | util[action](this, 'touchstart', this._onTouch, { passive: true });
|
460 | util[action](this, 'touchmove', this._onRelease, { passive: true });
|
461 | this[action]('touchcancel', this._onRelease);
|
462 | this[action]('touchend', this._onRelease);
|
463 | this[action]('touchleave', this._onRelease);
|
464 | this[action]('drag', this._onDrag);
|
465 | this[action]('mousedown', this._onTouch);
|
466 | this[action]('mouseup', this._onRelease);
|
467 | this[action]('mouseout', this._onRelease);
|
468 |
|
469 | if (this._top) {
|
470 | this._top[action]('click', this._onClickTop.bind(this));
|
471 | }
|
472 | }
|
473 |
|
474 | _onClickTop() {
|
475 | if (!this._expanding) {
|
476 | this.toggleExpansion();
|
477 | this.dispatchEvent(new Event('expand'));
|
478 | this.dispatchEvent(new Event('expansion')); // expansion is deprecated but emit to avoid breaking user code
|
479 | }
|
480 | }
|
481 |
|
482 | _onDrag(event) {
|
483 | const gesture = event.gesture;
|
484 | // Prevent vertical scrolling if the users pans left or right.
|
485 | if (this.hasAttribute('lock-on-drag') && ['left', 'right'].indexOf(gesture.direction) > -1) {
|
486 | gesture.preventDefault();
|
487 | }
|
488 | }
|
489 |
|
490 | _onTouch(e) {
|
491 | if (this.tapped ||
|
492 | (this !== e.target && (this._shouldIgnoreTap(e.target) || util.findParent(e.target, this._shouldIgnoreTap, p => p === this)))
|
493 | ) {
|
494 | return; // Ignore tap
|
495 | }
|
496 |
|
497 | this.tapped = true;
|
498 | const touchStyle = { transition: 'background-color 0.0s linear 0.02s, box-shadow 0.0s linear 0.02s' };
|
499 |
|
500 | if (this.hasAttribute('tappable')) {
|
501 | if (this.style.backgroundColor) {
|
502 | this._originalBackgroundColor = this.style.backgroundColor;
|
503 | }
|
504 |
|
505 | touchStyle.backgroundColor = this.getAttribute('tap-background-color') || '#d9d9d9';
|
506 | touchStyle.boxShadow = `0px -1px 0px 0px ${touchStyle.backgroundColor}`;
|
507 | }
|
508 |
|
509 | styler(this, touchStyle);
|
510 | }
|
511 |
|
512 | _onRelease() {
|
513 | this.tapped = false;
|
514 | this.style.backgroundColor = this._originalBackgroundColor || '';
|
515 | styler.clear(this, 'transition boxShadow');
|
516 | }
|
517 | }
|
518 |
|
519 | util.defineBooleanProperties(ListItemElement, ['expanded', 'expandable', 'tappable', 'lock-on-drag']);
|
520 | util.defineStringProperties(ListItemElement, ['animation', 'tap-background-color']);
|
521 |
|
522 | onsElements.ListItem = ListItemElement;
|
523 | customElements.define('ons-list-item', ListItemElement);
|