// Copyright (C) 2017 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. (function() { 'use strict'; Polymer({ is: 'gr-fixed-panel', properties: { floatingDisabled: Boolean, readyForMeasure: { type: Boolean, observer: '_readyForMeasureObserver', }, keepOnScroll: { type: Boolean, value: false, }, _isMeasured: { type: Boolean, value: false, }, /** * Initial offset from the top of the document, in pixels. */ _topInitial: Number, /** * Current offset from the top of the window, in pixels. */ _topLast: Number, _headerHeight: Number, _headerFloating: { type: Boolean, value: false, }, _observer: { type: Object, value: null, }, _webComponentsReady: Boolean, }, attached() { if (this.floatingDisabled) { return; } // Enable content measure unless blocked by param. if (this.readyForMeasure !== false) { this.readyForMeasure = true; } this.listen(window, 'resize', 'update'); this.listen(window, 'scroll', '_updateOnScroll'); this._observer = new MutationObserver(this.update.bind(this)); this._observer.observe(this.$.header, {childList: true, subtree: true}); }, detached() { this.unlisten(window, 'scroll', '_updateOnScroll'); this.unlisten(window, 'resize', 'update'); if (this._observer) { this._observer.disconnect(); } }, _readyForMeasureObserver(readyForMeasure) { if (readyForMeasure) { this.update(); } }, _computeHeaderClass(headerFloating, topLast) { const fixedAtTop = this.keepOnScroll && topLast === 0; return [ headerFloating ? 'floating' : '', fixedAtTop ? 'fixedAtTop' : '', ].join(' '); }, _getScrollY() { return window.scrollY; }, unfloat() { if (this.floatingDisabled) { return; } this.$.header.style.top = ''; this._headerFloating = false; this.updateStyles({'--header-height': ''}); }, update() { this.debounce('update', () => { this._updateDebounced(); }, 100); }, _updateOnScroll() { this.debounce('update', () => { this._updateDebounced(); }); }, _updateDebounced() { if (this.floatingDisabled) { return; } this._isMeasured = false; this._maybeFloatHeader(); this._reposition(); }, _reposition() { if (!this._headerFloating) { return; } const header = this.$.header; const scrollY = this._topInitial - this._getScrollY(); let newTop; if (this.keepOnScroll) { if (scrollY > 0) { // Reposition to imitate natural scrolling. newTop = scrollY; } else { newTop = 0; } } else if (scrollY > -this._headerHeight || this._topLast < -this._headerHeight) { // Allow to scroll away, but ignore when far behind the edge. newTop = scrollY; } else { newTop = -this._headerHeight; } if (this._topLast !== newTop) { if (newTop === undefined) { header.style.top = ''; } else { header.style.top = newTop + 'px'; } this._topLast = newTop; } }, _measure() { if (this._isMeasured) { return; // Already measured. } const rect = this.$.header.getBoundingClientRect(); if (rect.height === 0 && rect.width === 0) { return; // Not ready for measurement yet. } const top = document.body.scrollTop + rect.top; this._topLast = top; this._headerHeight = rect.height; this._topInitial = this.getBoundingClientRect().top + document.body.scrollTop; this._isMeasured = true; }, _isFloatingNeeded() { return this.keepOnScroll || document.body.scrollWidth > document.body.clientWidth; }, _maybeFloatHeader() { if (!this._isFloatingNeeded()) { return; } this._measure(); if (this._isMeasured) { this._floatHeader(); } }, _floatHeader() { this.updateStyles({'--header-height': this._headerHeight + 'px'}); this._headerFloating = true; }, }); })();