UNPKG

13.4 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 util from '../ons/util.js';
20import internal from '../ons/internal/index.js';
21import autoStyle from '../ons/autostyle.js';
22import ModifierUtil from '../ons/internal/modifier-util.js';
23import BaseElement from './base/base-element.js';
24import deviceBackButtonDispatcher from '../ons/internal/device-back-button-dispatcher.js';
25import contentReady from '../ons/content-ready.js';
26
27import './ons-toolbar.js'; // ensures that 'ons-toolbar' element is registered
28
29const defaultClassName = 'page';
30const scheme = {
31 '': 'page--*',
32 '.page__content': 'page--*__content',
33 '.page__background': 'page--*__background'
34};
35
36/**
37 * @element ons-page
38 * @category page
39 * @modifier material
40 * [en]Material Design style[/en]
41 * [ja][/ja]
42 * @description
43 * [en]
44 * This component defines the root of each page. If the content is large it will become scrollable.
45 *
46 * A navigation bar can be added to the top of the page using the `<ons-toolbar>` element.
47 * [/en]
48 * [ja]ページ定義のためのコンポーネントです。このコンポーネントの内容はスクロールが許可されます。[/ja]
49 * @tutorial vanilla/Reference/page
50 * @guide lifecycle.html#events
51 * [en]Overview of page events[/en]
52 * [ja]Overview of page events[/ja]
53 * @guide fundamentals.html#managing-pages
54 * [en]Managing multiple pages[/en]
55 * [ja]複数のページを管理する[/ja]
56 * @guide theming.html#modifiers [en]More details about the `modifier` attribute[/en][ja]modifier属性の使い方[/ja]
57 * @seealso ons-toolbar
58 * [en]Use the `<ons-toolbar>` element to add a navigation bar to the top of the page.[/en]
59 * [ja][/ja]
60 * @example
61 * <ons-page>
62 * <ons-toolbar>
63 * <div class="left">
64 * <ons-back-button>Back</ons-back-button>
65 * </div>
66 * <div class="center">Title</div>
67 * <div class="right">
68 * <ons-toolbar-button>
69 * <ons-icon icon="md-menu"></ons-icon>
70 * </ons-toolbar-button>
71 * </div>
72 * </ons-toolbar>
73 *
74 * <p>Page content</p>
75 * </ons-page>
76 *
77 * @example
78 * <script>
79 * myApp.handler = function(done) {
80 * loadMore().then(done);
81 * }
82 * </script>
83 *
84 * <ons-page on-infinite-scroll="myApp.handler">
85 * <ons-toolbar>
86 * <div class="center">List</div>
87 * </ons-toolbar>
88 *
89 * <ons-list>
90 * <ons-list-item>#1</ons-list-item>
91 * <ons-list-item>#2</ons-list-item>
92 * <ons-list-item>#3</ons-list-item>
93 * ...
94 * </ons-list>
95 * </ons-page>
96 */
97export default class PageElement extends BaseElement {
98
99 /**
100 * @event init
101 * @description
102 * [en]Fired right after the page is attached.[/en]
103 * [ja]ページがアタッチされた後に発火します。[/ja]
104 * @param {Object} event [en]Event object.[/en]
105 */
106
107 /**
108 * @event show
109 * @description
110 * [en]Fired right after the page is shown.[/en]
111 * [ja]ページが表示された後に発火します。[/ja]
112 * @param {Object} event [en]Event object.[/en]
113 */
114
115 /**
116 * @event hide
117 * @description
118 * [en]Fired right after the page is hidden.[/en]
119 * [ja]ページが隠れた後に発火します。[/ja]
120 * @param {Object} event [en]Event object.[/en]
121 */
122
123 /**
124 * @event destroy
125 * @description
126 * [en]Fired right before the page is destroyed.[/en]
127 * [ja]ページが破棄される前に発火します。[/ja]
128 * @param {Object} event [en]Event object.[/en]
129 */
130
131 /**
132 * @attribute modifier
133 * @type {String}
134 * @description
135 * [en]Specify modifier name to specify custom styles.[/en]
136 * [ja]スタイル定義をカスタマイズするための名前を指定します。[/ja]
137 */
138
139 /**
140 * @attribute on-infinite-scroll
141 * @type {String}
142 * @description
143 * [en]Path of the function to be executed on infinite scrolling. Example: `app.loadData`. The function receives a done callback that must be called when it's finished.[/en]
144 * [ja][/ja]
145 */
146
147 constructor() {
148 super();
149
150 this._deriveHooks();
151
152 this._defaultClassName = defaultClassName;
153 this.classList.add(defaultClassName);
154
155 this._initialized = false;
156
157 contentReady(this, () => {
158 this._compile();
159
160 this._isShown = false;
161 this._contentElement = this._getContentElement();
162 this._backgroundElement = this._getBackgroundElement();
163 });
164 }
165
166 _compile() {
167 autoStyle.prepare(this);
168
169 const toolbar = util.findChild(this, 'ons-toolbar');
170
171 const background = util.findChild(this, '.page__background') || util.findChild(this, '.background') || document.createElement('div');
172 background.classList.add('page__background');
173 this.insertBefore(background, !toolbar && this.firstChild || toolbar && toolbar.nextSibling);
174
175 const content = util.findChild(this, '.page__content') || util.findChild(this, '.content') || document.createElement('div');
176 content.classList.add('page__content');
177 if (!content.parentElement) {
178 util.arrayFrom(this.childNodes).forEach(node => {
179 if (node.nodeType !== 1 || this._elementShouldBeMoved(node)) {
180 content.appendChild(node); // Can trigger detached connectedCallbacks
181 }
182 });
183 }
184
185 this._tryToFillStatusBar(content); // Must run before child pages try to fill status bar.
186 this.insertBefore(content, background.nextSibling); // Can trigger attached connectedCallbacks
187
188 if ((!toolbar || !util.hasModifier(toolbar, 'transparent'))
189 && content.children.length === 1
190 && util.isPageControl(content.children[0])
191 ) {
192 this._defaultClassName += ' page--wrapper';
193 this.attributeChangedCallback('class');
194 }
195
196 const bottomToolbar = util.findChild(this, 'ons-bottom-toolbar');
197 if (bottomToolbar) {
198 this._defaultClassName += ' page-with-bottom-toolbar';
199 this.attributeChangedCallback('class');
200 }
201
202 ModifierUtil.initModifier(this, scheme);
203 }
204
205 _elementShouldBeMoved(el) {
206 if (el.classList.contains('page__background')) {
207 return false;
208 }
209 const tagName = el.tagName.toLowerCase();
210 if (tagName === 'ons-fab') {
211 return !el.hasAttribute('position');
212 }
213 const fixedElements = ['script', 'ons-toolbar', 'ons-bottom-toolbar', 'ons-modal', 'ons-speed-dial', 'ons-dialog', 'ons-alert-dialog', 'ons-popover', 'ons-action-sheet'];
214 return el.hasAttribute('inline') || fixedElements.indexOf(tagName) === -1;
215 }
216
217 _tryToFillStatusBar(content = this._contentElement) {
218 internal.autoStatusBarFill(() => {
219 util.toggleAttribute(this, 'status-bar-fill',
220 !util.findParent(this, e => e.hasAttribute('status-bar-fill')) // Not already filled
221 && (this._canAnimateToolbar(content) || !util.findChild(content, util.isPageControl)) // Has toolbar or cannot delegate
222 );
223 });
224 }
225
226 _canAnimateToolbar(content = this._contentElement) {
227 if (util.findChild(this, 'ons-toolbar')) {
228 return true;
229 }
230
231 return !!util.findChild(content, el => {
232 return util.match(el, 'ons-toolbar') && !el.hasAttribute('inline');
233 });
234 }
235
236 connectedCallback() {
237 if (!util.isAttached(this)) { // Avoid detached calls
238 return;
239 }
240
241 contentReady(this, () => {
242 this._tryToFillStatusBar(); // Ensure status bar when the element was compiled before connected
243
244 if (this.hasAttribute('on-infinite-scroll')) {
245 this.attributeChangedCallback('on-infinite-scroll', null, this.getAttribute('on-infinite-scroll'));
246 }
247
248 if (!this._initialized) {
249 this._initialized = true;
250
251 setImmediate(() => {
252 this.onInit && this.onInit();
253 util.triggerElementEvent(this, 'init');
254 });
255
256 if (!util.hasAnyComponentAsParent(this)) {
257 setImmediate(() => this._show());
258 }
259 }
260 });
261 }
262
263 updateBackButton(show) {
264 if (this.backButton) {
265 show ? this.backButton.show() : this.backButton.hide();
266 }
267 }
268
269 set name(str) {
270 this.setAttribute('name', str);
271 }
272
273 get name() {
274 return this.getAttribute('name');
275 }
276
277 get backButton() {
278 return this.querySelector('ons-back-button');
279 }
280
281 /**
282 * @property onInfiniteScroll
283 * @description
284 * [en]Function to be executed when scrolling to the bottom of the page. The function receives a done callback as an argument that must be called when it's finished.[/en]
285 * [ja][/ja]
286 */
287 set onInfiniteScroll(value) {
288 if (value && !(value instanceof Function)) {
289 util.throw('"onInfiniteScroll" must be function or null');
290 }
291
292 contentReady(this, () => {
293 if (!value) {
294 this._contentElement.removeEventListener('scroll', this._boundOnScroll);
295 } else if (!this._onInfiniteScroll) {
296 this._infiniteScrollLimit = 0.9;
297 this._boundOnScroll = this._onScroll.bind(this);
298 setImmediate(() => this._contentElement.addEventListener('scroll', this._boundOnScroll));
299 }
300 this._onInfiniteScroll = value;
301 });
302 }
303
304 get onInfiniteScroll() {
305 return this._onInfiniteScroll;
306 }
307
308 _onScroll() {
309 const c = this._contentElement,
310 overLimit = (c.scrollTop + c.clientHeight) / c.scrollHeight >= this._infiniteScrollLimit;
311
312 if (this._onInfiniteScroll && !this._loadingContent && overLimit) {
313 this._loadingContent = true;
314 this._onInfiniteScroll(() => this._loadingContent = false);
315 }
316 }
317
318 /**
319 * @property onDeviceBackButton
320 * @type {Object}
321 * @description
322 * [en]Back-button handler.[/en]
323 * [ja]バックボタンハンドラ。[/ja]
324 */
325 get onDeviceBackButton() {
326 return this._backButtonHandler;
327 }
328
329 set onDeviceBackButton(callback) {
330 if (this._backButtonHandler) {
331 this._backButtonHandler.destroy();
332 }
333
334 this._backButtonHandler = deviceBackButtonDispatcher.createHandler(this, callback);
335 }
336
337 get scrollTop() {
338 return this._contentElement.scrollTop;
339 }
340
341 set scrollTop(newValue) {
342 this._contentElement.scrollTop = newValue;
343 }
344
345 _getContentElement() {
346 const result = util.findChild(this, '.page__content');
347 if (result) {
348 return result;
349 }
350 util.throw('Fail to get ".page__content" element');
351 }
352
353 _getBackgroundElement() {
354 const result = util.findChild(this, '.page__background');
355 if (result) {
356 return result;
357 }
358 util.throw('Fail to get ".page__background" element');
359 }
360
361 _getBottomToolbarElement() {
362 return util.findChild(this, 'ons-bottom-toolbar') || internal.nullElement;
363 }
364
365 _getToolbarElement() {
366 return util.findChild(this, 'ons-toolbar') || document.createElement('ons-toolbar');
367 }
368
369 static get observedAttributes() {
370 return ['modifier', 'on-infinite-scroll', 'class'];
371 }
372
373 attributeChangedCallback(name, last, current) {
374 switch (name) {
375 case 'class':
376 util.restoreClass(this, this._defaultClassName, scheme);
377 break;
378 case 'modifier':
379 ModifierUtil.onModifierChanged(last, current, this, scheme);
380 break;
381 case 'on-infinite-scroll':
382 if (current === null) {
383 this.onInfiniteScroll = null;
384 } else {
385 this.onInfiniteScroll = (done) => {
386 const f = util.findFromPath(current);
387 this.onInfiniteScroll = f;
388 f(done);
389 };
390 }
391 break;
392 }
393 }
394
395 _show() {
396 if (!this._isShown && util.isAttached(this)) {
397 this._isShown = true;
398 this.setAttribute('shown', '');
399 this.onShow && this.onShow();
400 util.triggerElementEvent(this, 'show');
401 util.propagateAction(this, '_show');
402 }
403 }
404
405 _hide() {
406 if (this._isShown) {
407 this._isShown = false;
408 this.removeAttribute('shown');
409 this.onHide && this.onHide();
410 util.triggerElementEvent(this, 'hide');
411 util.propagateAction(this, '_hide');
412 }
413 }
414
415 _destroy() {
416 this._hide();
417
418 this.onDestroy && this.onDestroy();
419 util.triggerElementEvent(this, 'destroy');
420
421 if (this.onDeviceBackButton) {
422 this.onDeviceBackButton.destroy();
423 }
424
425 util.propagateAction(this, '_destroy');
426
427 this.remove();
428 }
429
430 _deriveHooks() {
431 this.constructor.events.forEach(event => {
432 const key = 'on' + event.charAt(0).toUpperCase() + event.slice(1);
433 Object.defineProperty(this, key, {
434 configurable: true,
435 enumerable: true,
436 get: () => this[`_${key}`],
437 set: value => {
438 if (!(value instanceof Function)) {
439 util.throw(`"${key}" hook must be a function`);
440 }
441 this[`_${key}`] = value.bind(this);
442 }
443 });
444 });
445 }
446
447 static get events() {
448 return ['init', 'show', 'hide', 'destroy'];
449 }
450
451 /**
452 * @property data
453 * @type {*}
454 * @description
455 * [en]User's custom data passed to `pushPage()`-like methods.[/en]
456 * [ja][/ja]
457 */
458}
459
460onsElements.Page = PageElement;
461customElements.define('ons-page', PageElement);