Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic support for lightboxes in inabox #9646

Merged
merged 8 commits into from Jun 12, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
127 changes: 127 additions & 0 deletions ads/inabox/frame-overlay-helper.js
@@ -0,0 +1,127 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* 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.
*/
import {fakeVsync, timer} from './util';
import {
centerFrameUnderVsyncMutate,
expandFrameUnderVsyncMutate,
collapseFrameUnderVsyncMutate,
} from '../../src/full-overlay-frame-helper';


const CENTER_TRANSITION_TIME_MS = 500;
const CENTER_TRANSITION_END_WAIT_TIME_MS = 200;


/**
* Places the child frame in full overlay mode.
* @param {!Window} win Host window.
* @param {!HTMLIFrameElement} iframe
* @param {!Function} onFinish
* @private
*/
const expandFrameImpl = function(win, iframe, onFinish) {
fakeVsync(win, {
measure(state) {
state.viewportSize = {
width: win./*OK*/innerWidth,
height: win./*OK*/innerHeight,
};
state.rect = iframe./*OK*/getBoundingClientRect();
},
mutate(state) {
centerFrameUnderVsyncMutate(iframe, state.rect, state.viewportSize,
CENTER_TRANSITION_TIME_MS);

timer(() => {
fakeVsync(win, {
mutate() {
expandFrameUnderVsyncMutate(iframe, state.viewportSize);
onFinish();
},
});
}, CENTER_TRANSITION_TIME_MS + CENTER_TRANSITION_END_WAIT_TIME_MS);
},
}, {});
};


/**
* Resets the frame from full overlay mode.
* @param {!Window} win Host window.
* @param {!HTMLIFrameElement} iframe
* @param {!Function} onFinish
* @private
*/
const collapseFrameImpl = function(win, iframe, onFinish) {
fakeVsync(win, {
mutate() {
collapseFrameUnderVsyncMutate(iframe);
onFinish();
},
});
};


/**
* Places the child frame in full overlay mode.
* @param {!Window} win Host window.
* @param {!HTMLIFrameElement} iframe
* @param {!Function} onFinish
*/
export let expandFrame = expandFrameImpl;


/**
* @param {!Function} implFn
* @visibleForTesting
*/
export function stubExpandFrameForTesting(implFn) {
expandFrame = implFn;
}


/**
* @visibleForTesting
*/
export function resetExpandFrameForTesting() {
expandFrame = expandFrameImpl;
}


/**
* Places the child frame in full overlay mode.
* @param {!Window} win Host window.
* @param {!HTMLIFrameElement} iframe
* @param {!Function} onFinish
*/
export let collapseFrame = collapseFrameImpl;


/**
* @param {!Function} implFn
* @visibleForTesting
*/
export function stubCollapseFrameForTesting(implFn) {
collapseFrame = implFn;
}


/**
* @visibleForTesting
*/
export function resetCollapseFrameForTesting() {
collapseFrame = collapseFrameImpl;
}
116 changes: 102 additions & 14 deletions ads/inabox/inabox-messaging-host.js
Expand Up @@ -21,10 +21,33 @@ import {
MessageType,
} from '../../src/3p-frame-messaging';
import {dev} from '../../src/log';
import {expandFrame, collapseFrame} from './frame-overlay-helper';

/** @const */
const TAG = 'InaboxMessagingHost';


class NamedObservable {
constructor() {
this.map_ = {};
}

listen(key, callback) {
if (key in this.map_) {
dev().fine(TAG, `Overriding message callback [${key}]`);
}
this.map_[key] = callback;
}

fire(key, thisArg, args) {
if (key in this.map_) {
return this.map_[key].apply(thisArg, args);
}
return false;
}
}


export class InaboxMessagingHost {

constructor(win, iframes) {
Expand All @@ -33,6 +56,16 @@ export class InaboxMessagingHost {
this.iframeMap_ = Object.create(null);
this.registeredIframeSentinels_ = Object.create(null);
this.positionObserver_ = new PositionObserver(win);
this.msgObservable_ = new NamedObservable();

this.msgObservable_.listen(
MessageType.SEND_POSITIONS, this.handleSendPositions_);

this.msgObservable_.listen(
MessageType.FULL_OVERLAY_FRAME, this.handleEnterFullOverlay_);

this.msgObservable_.listen(
MessageType.RESET_FULL_OVERLAY_FRAME, this.handleResetFullOverlay_);
}

/**
Expand Down Expand Up @@ -60,23 +93,78 @@ export class InaboxMessagingHost {
return false;
}

if (request.type == MessageType.SEND_POSITIONS) {
// To prevent double tracking for the same requester.
if (this.registeredIframeSentinels_[request.sentinel]) {
return false;
}
this.registeredIframeSentinels_[request.sentinel] = true;
this.positionObserver_.observe(iframe, data => {
dev().fine(TAG, `Sent position data to [${request.sentinel}]`, data);
message.source./*OK*/postMessage(
serializeMessage(MessageType.POSITION, request.sentinel, data),
message.origin);
});
return true;
} else {
if (!this.msgObservable_.fire(request.type, this,
[iframe, request, message.source, message.origin])) {
dev().warn(TAG, 'Unprocessed AMP message:', message);
return false;
}

return true;
}

/**
* @param {!HTMLIFrameElement} iframe
* @param {!Object} request
* @param {!Window} source
* @param {string} origin
* @return {boolean}
*/
handleSendPositions_(iframe, request, source, origin) {
// To prevent double tracking for the same requester.
if (this.registeredIframeSentinels_[request.sentinel]) {
return false;
}
this.registeredIframeSentinels_[request.sentinel] = true;
this.positionObserver_.observe(iframe, data => {
dev().fine(TAG, `Sent position data to [${request.sentinel}]`, data);
source./*OK*/postMessage(
serializeMessage(MessageType.POSITION, request.sentinel, data),
origin);
});
return true;
}

/**
* @param {!HTMLIFrameElement} iframe
* @param {!Object} request
* @param {!Window} source
* @param {string} origin
* @return {boolean}
*/
// TODO(alanorozco):
// 1. Reject request if frame is out of focus
// 2. Disable zoom and scroll on parent doc
handleEnterFullOverlay_(iframe, request, source, origin) {
expandFrame(this.win_, iframe, () => {
source./*OK*/postMessage(
serializeMessage(
MessageType.FULL_OVERLAY_FRAME_RESPONSE,
request.sentinel,
{content: {accept: true}}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming consistency with resize API ->
success: true.

also, why nest the data in content?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I nested the content since serializeMessage adds top-level members and that's a little weird :/

Renaming to top-level success is still alright I guess. It's a simple API. Changed.

origin);
});

return true;
}

/**
* @param {!HTMLIFrameElement} iframe
* @param {!Object} request
* @param {!Window} source
* @param {string} origin
* @return {boolean}
*/
handleResetFullOverlay_(iframe, request, source, origin) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleExitFullOverlay

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to handleCancelFullOverlay_ for consistency with message type.

collapseFrame(this.win_, iframe, () => {
source./*OK*/postMessage(
serializeMessage(
MessageType.RESET_FULL_OVERLAY_FRAME_RESPONSE,
request.sentinel,
{content: {accept: true}}),
origin);
});

return true;
}

/**
Expand Down
56 changes: 56 additions & 0 deletions ads/inabox/util.js
@@ -0,0 +1,56 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* 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.
*/


/**
* Executes a "fake vsync" read/write.
* This function exists mainly since the vsync service is not available for the
* inabox host script.
* It also helps with maintainability. Since the passed tasks have to define
* measure and mutate callbacks, it makes it harder for the calling code to be
* changed in a way that screws up the read-write order.
* Please note that this is NOT real vsync. Concurrent reads and writes ARE NOT
* BATCHED. This means that using this can still cause layout thrashing if it's
* being called more than once within the same frame. Use with caution.
* @param {!Window} win
* @param {{measure: (Function|undefined), mutate: !Function}} task
* @param {!Object=} opt_state
* @visibleForTesting
*/
// TODO(alanorozco): Figure out a longer-term solution
export function fakeVsync(win, task, opt_state) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call it something else. fake is not a good word in production code
something like runOnAnimationFrame ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to restrictedVsync.

win.requestAnimationFrame(() => {
if (task.measure) {
task.measure(opt_state);
}
task.mutate(opt_state);
});
}


/**
* Executes a function after a certain time.
* The timer service is not available for the inabox host script, hence this
* function.
* Not using setTimeout directly allows us to execute the callback directly on
* tests.
* @param {!Function} callback
* @param {number} timeMs
* @visibleForTesting
*/
export function timer(callback, timeMs) {
setTimeout(callback, timeMs);
}
15 changes: 13 additions & 2 deletions extensions/amp-lightbox/0.1/amp-lightbox.js
Expand Up @@ -217,8 +217,13 @@ class AmpLightbox extends AMP.BaseElement {
this.boundCloseOnEscape_ = this.closeOnEscape_.bind(this);
this.win.document.documentElement.addEventListener(
'keydown', this.boundCloseOnEscape_);
this.getViewport().enterLightboxMode()
.then(() => this.finalizeOpen_());
}

finalizeOpen_() {
// TODO(alanorozco): backport iframe overlay logic into viewport service
this.maybeEnterFrameFullOverlayMode_();
this.getViewport().enterLightboxMode();

if (this.isScrollable_) {
st.setStyle(this.element, 'webkitOverflowScrolling', 'touch');
Expand Down Expand Up @@ -316,8 +321,14 @@ class AmpLightbox extends AMP.BaseElement {
if (this.isScrollable_) {
st.setStyle(this.element, 'webkitOverflowScrolling', '');
}
this.getViewport().leaveLightboxMode();
this.getViewport().leaveLightboxMode()
.then(() => this.finalizeClose_());
}

finalizeClose_() {
// TODO(alanorozco): backport iframe overlay logic into viewport service
this.maybeLeaveFrameFullOverlayMode_();

this./*OK*/collapse();
if (this.historyId_ != -1) {
this.getHistory_().pop(this.historyId_);
Expand Down
7 changes: 6 additions & 1 deletion src/3p-frame-messaging.js
Expand Up @@ -36,12 +36,17 @@ export const MessageType = {
EMBED_SIZE_DENIED: 'embed-size-denied',
NO_CONTENT: 'no-content',

// For the frame to be placed in full overlay mode for lightboxes
FULL_OVERLAY_FRAME: 'full-overlay-frame',
FULL_OVERLAY_FRAME_RESPONSE: 'full-overlay-frame-response',
RESET_FULL_OVERLAY_FRAME: 'reset-full-overlay-frame',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming suggestion: RESET -> CANCEL, FULL-> FULLSCREEN, and remove FRAME (given the context)

FULLSCREEN_OVERLAY:
CANCEL_FULLSCREEN_OVERLAY

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like "fullscreen" specifically because it's not really fullscreen. It fills the entire viewport, but the browser chrome may still be visible. Changed reset to cancel.

RESET_FULL_OVERLAY_FRAME_RESPONSE: 'reset-full-overlay-frame-response',

// For amp-inabox
SEND_POSITIONS: 'send-positions',
POSITION: 'position',
};


/**
* Listens for the specified event on the element.
* @param {!EventTarget} element
Expand Down