UNPKG

16.6 kBJavaScriptView Raw
1/*
2Copyright 2013-2015 ASIAL CORPORATION
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15
16*/
17
18import onsElements from '../ons/elements.js';
19import animit from '../ons/animit.js';
20import util from '../ons/util.js';
21import styler from '../ons/styler.js';
22import autoStyle from '../ons/autostyle.js';
23import ModifierUtil from '../ons/internal/modifier-util.js';
24import AnimatorFactory from '../ons/internal/animator-factory.js';
25import { ListItemAnimator, SlideListItemAnimator } from './ons-list-item/animator.js';
26import BaseElement from './base/base-element.js';
27import contentReady from '../ons/content-ready.js';
28
29const defaultClassName = 'list-item';
30const 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
42const _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 */
103export 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
519util.defineBooleanProperties(ListItemElement, ['expanded', 'expandable', 'tappable', 'lock-on-drag']);
520util.defineStringProperties(ListItemElement, ['animation', 'tap-background-color']);
521
522onsElements.ListItem = ListItemElement;
523customElements.define('ons-list-item', ListItemElement);