/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/service/mutator-impl.js
423 строки
11 KB
Ryan Cebulko
♻️ Move files into #core, #utils to enable tsc typechecking (#36277)
07 окт 2021, 23:26
Не верифицирован
07 окт 2021, 23:26
b6d1550
Код
Авторство
О чём код?
import {areMarginsChanged} from '#core/dom/layout/rect'; import {closest} from '#core/dom/query'; import {computedStyle} from '#core/dom/style'; import {isExperimentOn} from '#experiments'; import {Services} from '#service'; import {dev} from '#utils/log'; import {MutatorInterface} from './mutator-interface'; import {Resource} from './resource'; import {FocusHistory} from '../focus-history'; import {registerServiceBuilderForDoc} from '../service-helpers'; const FOUR_FRAME_DELAY_ = 70; const FOCUS_HISTORY_TIMEOUT_ = 1000 * 60; // 1min const TAG_ = 'Mutator'; /** * @implements {MutatorInterface} */ export class MutatorImpl { /** * @param {!./ampdoc-impl.AmpDoc} ampdoc */ constructor(ampdoc) { /** @const {!./ampdoc-impl.AmpDoc} */ this.ampdoc = ampdoc; /** @const {!Window} */ this.win = ampdoc.win; /** @private @const {!./resources-interface.ResourcesInterface} */ this.resources_ = Services.resourcesForDoc(ampdoc); /** @private @const {!./viewport/viewport-interface.ViewportInterface} */ this.viewport_ = Services.viewportForDoc(this.ampdoc); /** @private @const {!./vsync-impl.Vsync} */ this.vsync_ = Services./*OK*/ vsyncFor(this.win); /** @private @const {!FocusHistory} */ this.activeHistory_ = new FocusHistory(this.win, FOCUS_HISTORY_TIMEOUT_); this.activeHistory_.onFocus((element) => { this.checkPendingChangeSize_(element); }); } /** @override */ forceChangeSize(element, newHeight, newWidth, opt_callback, opt_newMargins) { this.scheduleChangeSize_( Resource.forElement(element), newHeight, newWidth, opt_newMargins, /* event */ undefined, /* force */ true, opt_callback ); } /** @override */ requestChangeSize(element, newHeight, newWidth, opt_newMargins, opt_event) { return new Promise((resolve, reject) => { this.scheduleChangeSize_( Resource.forElement(element), newHeight, newWidth, opt_newMargins, opt_event, /* force */ false, (success) => { if (success) { resolve(); } else { reject(new Error('changeSize attempt denied')); } } ); }); } /** @override */ expandElement(element) { const resource = Resource.forElement(element); resource.completeExpand(); this.resources_.schedulePass(FOUR_FRAME_DELAY_); } /** @override */ attemptCollapse(element) { return new Promise((resolve, reject) => { this.scheduleChangeSize_( Resource.forElement(element), 0, 0, /* newMargin */ undefined, /* event */ undefined, /* force */ false, (success) => { if (success) { const resource = Resource.forElement(element); resource.completeCollapse(); resolve(); } else { reject(dev().createExpectedError('collapse attempt denied')); } } ); }); } /** @override */ collapseElement(element) { const box = this.viewport_.getLayoutRect(element); if (box.width != 0 && box.height != 0) { if (isExperimentOn(this.win, 'dirty-collapse-element')) { this.dirtyElement(element); } else { this.resources_.setRelayoutTop(box.top); } } const resource = Resource.forElement(element); resource.completeCollapse(); // Unlike completeExpand(), there's no requestMeasure() call here that // requires another pass (with IntersectionObserver). this.resources_.schedulePass(FOUR_FRAME_DELAY_); } /** @override */ measureElement(measurer) { return this.vsync_.measurePromise(measurer); } /** @override */ mutateElement(element, mutator, skipRemeasure) { return this.measureMutateElementResources_( element, null, mutator, skipRemeasure ); } /** @override */ measureMutateElement(element, measurer, mutator) { return this.measureMutateElementResources_(element, measurer, mutator); } /** * Returns the layout margins for the resource. * @param {!Resource} resource * @return {!../layout-rect.LayoutMarginsDef} * @private */ getLayoutMargins_(resource) { const style = computedStyle(this.win, resource.element); return { top: parseInt(style.marginTop, 10) || 0, right: parseInt(style.marginRight, 10) || 0, bottom: parseInt(style.marginBottom, 10) || 0, left: parseInt(style.marginLeft, 10) || 0, }; } /** * Handles element mutation (and measurement) APIs in the Resources system. * * @param {!Element} element * @param {?function()} measurer * @param {function()} mutator * @param {boolean} skipRemeasure * @return {!Promise} */ measureMutateElementResources_( element, measurer, mutator, skipRemeasure = false ) { const calcRelayoutTop = () => { const box = this.viewport_.getLayoutRect(element); if (box.width != 0 && box.height != 0) { return box.top; } return -1; }; let relayoutTop = -1; // TODO(jridgewell): support state return this.vsync_.runPromise({ measure: () => { if (measurer) { measurer(); } if (!skipRemeasure) { relayoutTop = calcRelayoutTop(); } }, mutate: () => { mutator(); // `skipRemeasure` is set by callers when we know that `mutator` // cannot cause a change in size/position e.g. toggleLoading(). if (skipRemeasure) { return; } if (element.classList.contains('i-amphtml-element')) { const r = Resource.forElement(element); r.requestMeasure(); } const ampElements = element.getElementsByClassName('i-amphtml-element'); for (let i = 0; i < ampElements.length; i++) { const r = Resource.forElement(ampElements[i]); r.requestMeasure(); } this.resources_.schedulePass(FOUR_FRAME_DELAY_); if (relayoutTop != -1) { this.resources_.setRelayoutTop(relayoutTop); } // Need to measure again in case the element has become visible or // shifted. this.vsync_.measure(() => { const updatedRelayoutTop = calcRelayoutTop(); if (updatedRelayoutTop != -1 && updatedRelayoutTop != relayoutTop) { this.resources_.setRelayoutTop(updatedRelayoutTop); this.resources_.schedulePass(FOUR_FRAME_DELAY_); } this.resources_.maybeHeightChanged(); }); }, }); } /** * Dirties the cached element measurements after a mutation occurs. * * TODO(jridgewell): This API needs to be audited. Common practice is * to pass the amp-element in as the root even though we are only * mutating children. If the amp-element is passed, we invalidate * everything in the parent layer above it, where only invalidating the * amp-element was necessary (only children were mutated, only * amp-element's scroll box is affected). * * @param {!Element} element */ dirtyElement(element) { let relayoutAll = false; const isAmpElement = element.classList.contains('i-amphtml-element'); if (isAmpElement) { const r = Resource.forElement(element); this.resources_.setRelayoutTop(r.getLayoutBox().top); } else { relayoutAll = true; } this.resources_.schedulePass(FOUR_FRAME_DELAY_, relayoutAll); } /** * Reschedules change size request when an overflown element is activated. * @param {!Element} element * @private */ checkPendingChangeSize_(element) { const resourceElement = closest( element, (el) => !!Resource.forElementOptional(el) ); if (!resourceElement) { return; } const resource = Resource.forElement(resourceElement); const pendingChangeSize = resource.getPendingChangeSize(); if (pendingChangeSize !== undefined) { this.scheduleChangeSize_( resource, pendingChangeSize.height, pendingChangeSize.width, pendingChangeSize.margins, /* event */ undefined, /* force */ true ); } } /** * Schedules change of the element's height. * @param {!Resource} resource * @param {number|undefined} newHeight * @param {number|undefined} newWidth * @param {!../layout-rect.LayoutMarginsChangeDef|undefined} newMargins * @param {?Event|undefined} event * @param {boolean} force * @param {function(boolean)=} opt_callback A callback function * @private */ scheduleChangeSize_( resource, newHeight, newWidth, newMargins, event, force, opt_callback ) { if (resource.hasBeenMeasured() && !newMargins) { this.completeScheduleChangeSize_( resource, newHeight, newWidth, undefined, event, force, opt_callback ); } else { // This is a rare case since most of times the element itself schedules // resize requests. However, this case is possible when another element // requests resize of a controlled element. This also happens when a // margin size change is requested, since existing margins have to be // measured in this instance. this.vsync_.measure(() => { if (!resource.hasBeenMeasured()) { resource.measure(); } const marginChange = newMargins ? { newMargins, currentMargins: this.getLayoutMargins_(resource), } : undefined; this.completeScheduleChangeSize_( resource, newHeight, newWidth, marginChange, event, force, opt_callback ); }); } } /** * @param {!Resource} resource * @param {number|undefined} newHeight * @param {number|undefined} newWidth * @param {!./resources-interface.MarginChangeDef|undefined} marginChange * @param {?Event|undefined} event * @param {boolean} force * @param {function(boolean)=} opt_callback A callback function * @private */ completeScheduleChangeSize_( resource, newHeight, newWidth, marginChange, event, force, opt_callback ) { resource.resetPendingChangeSize(); const layoutSize = resource.getLayoutSize(); if ( (newHeight === undefined || newHeight == layoutSize.height) && (newWidth === undefined || newWidth == layoutSize.width) && (marginChange === undefined || !areMarginsChanged( marginChange.currentMargins, marginChange.newMargins )) ) { if ( newHeight === undefined && newWidth === undefined && marginChange === undefined ) { dev().error( TAG_, 'attempting to change size with undefined dimensions', resource.debugid ); } // Nothing to do. if (opt_callback) { opt_callback(/* success */ true); } return; } this.resources_.updateOrEnqueueMutateTask( resource, /** {!ChangeSizeRequestDef} */ { resource, newHeight, newWidth, marginChange, event, force, callback: opt_callback, } ); // With IntersectionObserver, we still want to schedule a pass to execute // the requested measures of the newly resized element(s). this.resources_.schedulePassVsync(); } } /** * @param {!./ampdoc-impl.AmpDoc} ampdoc */ export function installMutatorServiceForDoc(ampdoc) { registerServiceBuilderForDoc(ampdoc, 'mutator', MutatorImpl); }