Skip to content

Commit

Permalink
✨Handling sticky, fixed and hidden elements in amp-next-page v2 (#2…
Browse files Browse the repository at this point in the history
…6106)

* Prototyping sticky element handling in amp-next-page

* Deprecate amp-next-page-keep in favor of amp-next-page-hidden

* Visibility bug fix

* Remove animation added for testing

* Exported host-page-specific parameters into HostPage

* Undo validator changes for now
  • Loading branch information
wassgha authored Jan 13, 2020
1 parent 8d703e8 commit 374a04f
Show file tree
Hide file tree
Showing 8 changed files with 1,342 additions and 46 deletions.
12 changes: 11 additions & 1 deletion extensions/amp-next-page/0.2/amp-next-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,15 @@
*/

.i-amphtml-next-page-document {
overflow-y: hidden;
overflow: hidden;
}

.i-amphtml-next-page-document:not(.amp-next-page-document-visible)
[i-amphtml-fixedid] {
display: none;
}

.i-amphtml-next-page-document.amp-next-page-document-visible {
opacity: 1;
overflow: visible;
}
52 changes: 41 additions & 11 deletions extensions/amp-next-page/0.2/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,14 @@ export const PageState = {
INSERTED: 4,
};

const VISIBLE_DOC_CLASS = 'amp-next-page-document-visible';

export class Page {
/**
* @param {!./service.NextPageService} manager
* @param {{ url: string, title: string, image: string }} meta
* @param {!PageState=} initState
* @param {!VisibilityState=} initVisibility
*/
constructor(
manager,
meta,
initState = PageState.QUEUED,
initVisibility = VisibilityState.PRERENDER
) {
constructor(manager, meta) {
/** @private @const {!./service.NextPageService} */
this.manager_ = manager;
/** @private @const {string} */
Expand All @@ -53,9 +48,9 @@ export class Page {
/** @private {?../../../src/runtime.ShadowDoc} */
this.shadowDoc_ = null;
/** @private {!PageState} */
this.state_ = initState;
this.state_ = PageState.QUEUED;
/** @private {!VisibilityState} */
this.visibilityState_ = initVisibility;
this.visibilityState_ = VisibilityState.PRERENDER;
/** @private {!ViewportRelativePos} */
this.relativePos_ = ViewportRelativePos.OUTSIDE_VIEWPORT;
}
Expand Down Expand Up @@ -87,6 +82,14 @@ export class Page {
return this.relativePos_;
}

/** @return {!Document|undefined} */
get document() {
if (!this.shadowDoc_) {
return;
}
return /** @type {!Document} */ (this.shadowDoc_.ampdoc.getRootNode());
}

/** @param {!ViewportRelativePos} position */
set relativePos(position) {
this.relativePos_ = position;
Expand Down Expand Up @@ -118,10 +121,13 @@ export class Page {
this.visibilityState_ = visibilityState;
if (this.shadowDoc_) {
this.shadowDoc_.setVisibilityState(visibilityState);
this.shadowDoc_.ampdoc
.getBody()
.classList.toggle(VISIBLE_DOC_CLASS, this.isVisible());
}

// Switch the title and url of the page to reflect this page
if (visibilityState === VisibilityState.VISIBLE) {
if (this.isVisible()) {
this.manager_.setTitlePage(this);
}
}
Expand Down Expand Up @@ -179,3 +185,27 @@ export class Page {
});
}
}

export class HostPage extends Page {
/**
* @param {!./service.NextPageService} manager
* @param {{ url: string, title: string, image: string }} meta
* @param {!PageState} initState
* @param {!VisibilityState} initVisibility
* @param {!Document} initDoc
*/
constructor(manager, meta, initState, initVisibility, initDoc) {
super(manager, meta);
/** @override */
this.state_ = initState;
/** @override */
this.visibilityState_ = initVisibility;
/** @private {!Document} */
this.document_ = initDoc;
}

/** @override */
get document() {
return this.document_;
}
}
102 changes: 84 additions & 18 deletions extensions/amp-next-page/0.2/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
*/

import {CSS} from '../../../build/amp-next-page-0.2.css';
import {HostPage, Page, PageState} from './page';
import {MultidocManager} from '../../../src/multidoc-manager';
import {Page, PageState} from './page';
import {
PositionObserver, // eslint-disable-line no-unused-vars
} from '../../../src/service/position-observer/position-observer-impl';
import {Services} from '../../../src/services';
import {VisibilityState} from '../../../src/visibility-state';
import {
Expand All @@ -35,8 +32,10 @@ import {
parseOgImage,
parseSchemaImage,
} from '../../../src/mediasession-helper';
import {sanitizeDoc, validatePage, validateUrl} from './utils';
import {toArray} from '../../../src/types';
import {toggle} from '../../../src/style';
import {tryParseJson} from '../../../src/json';
import {validatePage, validateUrl} from './utils';
import VisibilityObserver, {ViewportRelativePos} from './visibility-observer';

const TAG = 'amp-next-page';
Expand Down Expand Up @@ -93,7 +92,10 @@ export class NextPageService {
this.lastScrollTop_ = 0;

/** @private {?Page} */
this.initialPage_ = null;
this.hostPage_ = null;

/** @private {!Object<string, !Element>} */
this.replaceableElements_ = {};
}

/**
Expand All @@ -110,7 +112,8 @@ export class NextPageService {
*/
build(element) {
// Create a reference to the host page
this.initialPage_ = this.createInitialPage();
this.hostPage_ = this.createHostPage();
this.toggleHiddenAndReplaceableElements(this.win_.document);

this.history_ = Services.historyForDoc(this.ampdoc_);
this.initializeHistory();
Expand All @@ -132,8 +135,8 @@ export class NextPageService {
this.element_.appendChild(this.moreBox_);

if (!this.pages_) {
this.pages_ = [this.initialPage_];
this.setLastFetchedPage(this.initialPage_);
this.pages_ = [this.hostPage_];
this.setLastFetchedPage(this.hostPage_);
}

this.getPagesPromise_().then(pages => {
Expand Down Expand Up @@ -209,8 +212,17 @@ export class NextPageService {

// If no page is visible then the host page should be
if (!this.pages_.some(page => page.isVisible())) {
this.initialPage_.setVisibility(VisibilityState.VISIBLE);
this.hostPage_.setVisibility(VisibilityState.VISIBLE);
}

// Hide elements if necessary
this.pages_
.filter(page => page.isVisible())
.forEach(page =>
this.toggleHiddenAndReplaceableElements(
/** @type {!Document} */ (dev().assertElement(page.document))
)
);
}

/**
Expand All @@ -231,7 +243,7 @@ export class NextPageService {
const shouldHide =
page.relativePos === ViewportRelativePos.LEAVING_VIEWPORT ||
page.relativePos === ViewportRelativePos.OUTSIDE_VIEWPORT ||
page === this.initialPage_;
page === this.hostPage_;
return shouldHide && page.isVisible();
})
.forEach(page => page.setVisibility(VisibilityState.HIDDEN));
Expand All @@ -249,7 +261,7 @@ export class NextPageService {
* the provided page
* @param {?Page=} page
*/
setTitlePage(page = this.initialPage_) {
setTitlePage(page = this.hostPage_) {
if (!page) {
dev().warn(TAG, 'setTitlePage called before next-page-service is built');
return;
Expand All @@ -264,29 +276,30 @@ export class NextPageService {
* replace when they become visible
*/
initializeHistory() {
const {title, url} = this.initialPage_;
const {title, url} = this.hostPage_;
this.history_.push(undefined /** opt_onPop */, {title, url});
}

/**
*
* @return {!Page}
*/
createInitialPage() {
createHostPage() {
const doc = this.win_.document;
const {title, location} = doc;
const {href: url} = location;
const image =
parseSchemaImage(doc) || parseOgImage(doc) || parseFavicon(doc) || '';
return new Page(
return new HostPage(
this,
{
url,
title,
image,
},
PageState.INSERTED /** initState */,
VisibilityState.VISIBLE /** initVisibility */
VisibilityState.VISIBLE /** initVisibility */,
doc /** initDoc */
);
}

Expand All @@ -305,8 +318,8 @@ export class NextPageService {

const shadowRoot = this.win_.document.createElement('div');

// Handles extension deny-lists and sticky items
sanitizeDoc(doc);
// Handles extension deny-lists
this.sanitizeDoc(doc);

// Insert the separator
this.element_.insertBefore(this.separator_.cloneNode(true), this.moreBox_);
Expand Down Expand Up @@ -337,6 +350,59 @@ export class NextPageService {
}
}

/**
* Removes redundancies and unauthorized extensions and elements
* @param {!Document} doc Document to attach.
*/
sanitizeDoc(doc) {
// TODO(wassgha): Parse for more pages to queue

// TODO(wassgha): Allow amp-analytics after bug bash
toArray(doc.querySelectorAll('amp-analytics')).forEach(removeElement);
// Make sure all hidden elements are initially invisible
this.toggleHiddenAndReplaceableElements(doc, false /** isVisible */);
}

/**
* Hides or shows elements based on the `amp-next-page-hide` and
* `amp-next-page-replace` attributes
* @param {!Document} doc Document to attach.
* @param {boolean=} isVisible Whether this page is visible or not
*/
toggleHiddenAndReplaceableElements(doc, isVisible = true) {
// Hide elements that have [amp-next-page-hide] on child documents
if (doc !== this.hostPage_.document) {
toArray(doc.querySelectorAll('[amp-next-page-hide]')).forEach(element =>
toggle(element, false /** opt_display */)
);
}

// Element replacing is only concerned with the visible page
if (!isVisible) {
return;
}

// Replace elements that have [amp-next-page-replace]
toArray(doc.querySelectorAll('[amp-next-page-replace]')).forEach(
element => {
let uniqueId = element.getAttribute('amp-next-page-replace');
if (!uniqueId) {
uniqueId = String(Date.now() + Math.floor(Math.random() * 100));
element.setAttribute('amp-next-page-replace', uniqueId);
}

if (
this.replaceableElements_[uniqueId] &&
this.replaceableElements_[uniqueId] !== element
) {
toggle(this.replaceableElements_[uniqueId], false /** opt_display */);
}
this.replaceableElements_[uniqueId] = element;
toggle(element, true /** opt_display */);
}
);
}

/**
* @return {number} viewports left to reach the end of the document
* @private
Expand Down
16 changes: 0 additions & 16 deletions extensions/amp-next-page/0.2/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import {
parseUrlDeprecated,
resolveRelativeUrl,
} from '../../../src/url';
import {removeElement} from '../../../src/dom';
import {toArray} from '../../../src/types';
import {user, userAssert} from '../../../src/log';

/**
Expand Down Expand Up @@ -70,17 +68,3 @@ export function validatePage(page, hostUrl) {
(url.hash || '');
}
}

/**
* Removes redundancies and unauthorized extensions and elements
* @param {!Document} doc Document to attach.
*/
export function sanitizeDoc(doc) {
// TODO(wassgha): Implement handling of sticky elements
// TODO(wassgha): Implement persistence of repeating elements (e.g amp-sidebar)

// TODO(wassgha): Parse for more pages to queue

// TODO(wassgha): Allow amp-analytics after bug bash
toArray(doc.querySelectorAll('amp-analytics')).forEach(removeElement);
}
Loading

0 comments on commit 374a04f

Please sign in to comment.