/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
extensions/amp-carousel/0.1/scrollable-carousel.js
440 строк
11 KB
Jake Fried
♻️ amp lint --fix: sort/order (#37353)
12 янв 2022, 22:24
Не верифицирован
12 янв 2022, 22:24
8c34f2d
Код
Авторство
О чём код?
import {ActionTrust_Enum} from '#core/constants/action-constants'; import {Keys_Enum} from '#core/constants/key-codes'; import {isLayoutSizeFixed} from '#core/dom/layout'; import {observeIntersections} from '#core/dom/layout/viewport-observer'; import {numeric} from '#core/dom/transition'; import {Services} from '#service'; import {Animation} from '#utils/animation'; import {listen} from '#utils/event-helper'; import {dev} from '#utils/log'; import {buildDom} from './build-dom'; import {CarouselControls} from './carousel-controls'; /** @const {string} */ const TAG = 'amp-scrollable-carousel'; export class AmpScrollableCarousel extends AMP.BaseElement { /** @param {AmpElement} element */ constructor(element) { super(element); /** @private {number} */ this.pos_ = 0; /** @private {number} */ this.oldPos_ = 0; /** @private {?Array<Element>} */ this.cells_ = null; /** @private {?Element} */ this.container_ = null; /** @private {?number} */ this.scrollTimerId_ = null; /** @private {?UnlistenDef} */ this.unobserveIntersections_ = null; /** @private {CarouselControls} */ this.controls_ = null; } /** @override */ isLayoutSupported(layout) { return isLayoutSizeFixed(layout); } /** @override */ isRelayoutNeeded() { return true; } /** * Attaches event handlers. * @private */ setupBehavior_() { this.cancelTouchEvents_(); this.container_.addEventListener('scroll', this.scrollHandler_.bind(this)); this.container_.addEventListener( 'keydown', this.keydownHandler_.bind(this) ); this.cells_.forEach((cell) => { Services.ownersForDoc(this.element).setOwner(cell, this.element); }); this.registerAction( 'goToSlide', (invocation) => { const {args} = invocation; if (args) { const index = parseInt(args['index'], 10); this.goToSlide(index); } }, ActionTrust_Enum.LOW ); /** If the element is in an email document, allow its `goToSlide` action. */ Services.actionServiceForDoc(this.element).addToAllowlist( 'amp-carousel', 'goToSlide', ['email'] ); } /** @override */ buildCallback() { const {cells, container, nextButton, prevButton} = buildDom(this.element); this.container_ = container; this.cells_ = cells; this.controls_ = new CarouselControls({ element: this.element, prevButton, nextButton, go: this.go.bind(this), }); this.setupBehavior_(); } /** @override */ layoutCallback() { this.unobserveIntersections_ = observeIntersections( this.element, ({isIntersecting}) => this.viewportCallback(isIntersecting) ); this.doLayout_(this.pos_); this.preloadNext_(this.pos_, 1); this.controls_.setControlsState({ prev: this.hasPrev_(), next: this.hasNext_(), }); return Promise.resolve(); } /** @override */ unlayoutCallback() { this.unobserveIntersections_?.(); this.unobserveIntersections_ = null; return true; } /** * Handles when carousel comes into and out of viewport. * @param {boolean} inViewport */ viewportCallback(inViewport) { this.updateInViewport_(this.pos_, this.pos_); if (inViewport) { this.controls_.hintControls(); } } /** * Does all the work needed to proceed to next * desired direction. * @param {number} dir -1 or 1 * @param {boolean} animate */ go(dir, animate) { const newPos = this.nextPos_(this.pos_, dir); const oldPos = this.pos_; if (newPos == oldPos) { return; } if (!animate) { this.commitSwitch_(newPos); this.container_./*OK*/ scrollLeft = newPos; } else { /** @const {TransitionDef<number>} */ const interpolate = numeric(oldPos, newPos); const duration = 200; const curve = 'ease-in-out'; Animation.animate( this.element, (pos) => { this.container_./*OK*/ scrollLeft = interpolate(pos); }, duration, curve ).thenAlways(() => { this.commitSwitch_(newPos); }); } } /** * Scrolls to the slide at the given slide index. * @param {number} index * @return {*} TODO(#23582): Specify return type */ goToSlide(index) { const noOfSlides = this.cells_.length; if (!isFinite(index) || index < 0 || index >= noOfSlides) { this.user().error(TAG, 'Invalid [slide] value: %s', index); return Promise.resolve(); } const oldPos = this.pos_; let newPos = oldPos; const measureNewPosition = () => { newPos = this.getPosForSlideIndex_(index); }; const mutateNewPosition = () => { if (newPos == oldPos) { return; } /** @const {TransitionDef<number>} */ const interpolate = numeric(oldPos, newPos); const duration = 200; const curve = 'ease-in-out'; Animation.animate( this.element, (pos) => { this.container_./*OK*/ scrollLeft = interpolate(pos); }, duration, curve ).thenAlways(() => { this.commitSwitch_(newPos); }); }; this.measureMutateElement(measureNewPosition, mutateNewPosition); } /** * Calculates the target scroll position for the given slide index. * @param {number} index * @return {number} */ getPosForSlideIndex_(index) { const containerWidth = this.element./*OK*/ offsetWidth; const targetPosition = this.cells_[index]./*OK*/ offsetLeft; const targetWidth = this.cells_[index]./*OK*/ offsetWidth; return targetPosition - (containerWidth - targetWidth) / 2; } /** * Handles scroll on the carousel container. * @private */ scrollHandler_() { const currentScrollLeft = this.container_./*OK*/ scrollLeft; this.pos_ = currentScrollLeft; if (this.scrollTimerId_ === null) { this.waitForScroll_(currentScrollLeft); } } /** * Escapes Left and Right arrow key events on the carousel container. * This is to prevent them from doubly interacting with surrounding viewer * contexts such as email clients when interacting with the amp-carousel. * @param {KeyboardEvent} event * @private */ keydownHandler_(event) { const {key} = event; if (key == Keys_Enum.LEFT_ARROW || key == Keys_Enum.RIGHT_ARROW) { event.stopPropagation(); } } /** * @param {number} startingScrollLeft * @private */ waitForScroll_(startingScrollLeft) { this.scrollTimerId_ = /** @type {number} */ ( Services.timerFor(this.win).delay(() => { // TODO(yuxichen): test out the threshold for identifying fast scrolling if (Math.abs(startingScrollLeft - this.pos_) < 30) { dev().fine( TAG, 'slow scrolling: %s - %s', startingScrollLeft, this.pos_ ); this.scrollTimerId_ = null; this.commitSwitch_(this.pos_); } else { dev().fine( TAG, 'fast scrolling: %s - %s', startingScrollLeft, this.pos_ ); this.waitForScroll_(this.pos_); } }, 100) ); } /** * Update the slides need to be loaded given current position. * Preload next slides and update control button state. * @param {number} pos * @private */ commitSwitch_(pos) { this.updateInViewport_(pos, this.oldPos_); this.doLayout_(pos); this.preloadNext_(pos, Math.sign(pos - this.oldPos_)); this.oldPos_ = pos; this.pos_ = pos; this.controls_.setControlsState({ prev: this.hasPrev_(), next: this.hasNext_(), }); } /** * @param {number} pos * @param {number} dir * @return {number} * @private */ nextPos_(pos, dir) { const containerWidth = this.element./*OK*/ offsetWidth; const fullWidth = this.container_./*OK*/ scrollWidth; const newPos = pos + dir * containerWidth; if (newPos < 0) { return 0; } if (fullWidth >= containerWidth && newPos > fullWidth - containerWidth) { return fullWidth - containerWidth; } return newPos; } /** * @param {number} pos * @param {function(Element):void} callback * @private */ withinWindow_(pos, callback) { const containerWidth = this.element./*OK*/ offsetWidth; for (let i = 0; i < this.cells_.length; i++) { const cell = this.cells_[i]; if ( cell./*OK*/ offsetLeft + cell./*OK*/ offsetWidth >= pos && cell./*OK*/ offsetLeft <= pos + containerWidth ) { callback(cell); } } } /** * @param {number} pos * @private */ doLayout_(pos) { this.withinWindow_(pos, (cell) => { Services.ownersForDoc(this.element).scheduleLayout(this.element, cell); }); } /** * @param {number} pos * @param {number} dir * @private */ preloadNext_(pos, dir) { const nextPos = this.nextPos_(pos, dir); if (nextPos != pos) { this.withinWindow_(nextPos, (cell) => { Services.ownersForDoc(this.element).schedulePreload(this.element, cell); }); } } /** * @param {number} newPos * @param {number} oldPos * @private */ updateInViewport_(newPos, oldPos) { const seen = []; this.withinWindow_(newPos, (cell) => { seen.push(cell); }); if (oldPos != newPos) { this.withinWindow_(oldPos, (cell) => { if (!seen.includes(cell)) { const owners = Services.ownersForDoc(this.element); owners.schedulePause(this.element, cell); } }); } } /** * @return {boolean} * @private */ hasPrev_() { return this.pos_ != 0; } /** * @return {boolean} * @private */ hasNext_() { const containerWidth = this.element./*OK*/ offsetWidth; const scrollWidth = this.container_./*OK*/ scrollWidth; const maxPos = Math.max(scrollWidth - containerWidth, 0); return this.pos_ != maxPos; } /** Used by amp-lightbox-gallery */ interactionNext() { this.controls_.handleNext(); } /** Used by amp-lightbox-gallery */ interactionPrev() { this.controls_.handlePrev(); } /** * Used by amp-lightbox-gallery * * Does all the work needed to proceed to next * desired direction. * @param {number} dir -1 or 1 * @param {boolean} animate */ goCallback(dir, animate) { this.go(dir, animate); } /** * Cancels the touchmove events for the element so that viewer does not * consider the swipes in the carousel as swipes for changing AMP documents. * @private */ cancelTouchEvents_() { // TODO(aghassemi, #4754): Ideally we only stop propagation of horizontal // touchmove events. listen(this.element, 'touchmove', (event) => event.stopPropagation(), { passive: true, }); } }