Skip to content

Commit

Permalink
Merge pull request #12 from Microsoft/sboghani/button-styles
Browse files Browse the repository at this point in the history
Expose function to style buttons with svg icon
  • Loading branch information
sahirboghani authored May 2, 2019
2 parents 7ac07a6 + 6dff9d7 commit 3008ca0
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 137 deletions.
289 changes: 153 additions & 136 deletions src/launchAsync.ts
Original file line number Diff line number Diff line change
@@ -1,136 +1,153 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Content } from './content';
import { Options } from './options';
import { Error } from './error';

type Message = {
cogSvcsAccessToken: string;
request: Content;
launchToPostMessageSentDurationInMs: number;
};

/**
* Launch the Immersive Reader within an iframe.
* @param token The authentication token returned from the Cognitive Services issueToken API.
* @param content The content that should be shown in the Immersive Reader.
* @param options Options for configuring the look and feel of the Immersive Reader.
* @return A promise that resolves when the Immersive Reader is launched. The promise resolves with the div that contains an iframe which contains the Immersive Reader.
*/
export function launchAsync(token: string, content: Content, options?: Options): Promise<HTMLDivElement> {
return new Promise((resolve, reject: (reason: Error) => void): void => {
if (!token) {
reject({ code: 'BadArgument', message: 'Token must not be null' });
return;
}

if (!content) {
reject({ code: 'BadArgument', message: 'Content must not be null' });
return;
}

if (!content.chunks) {
reject({ code: 'BadArgument', message: 'Chunks must not be null' });
return;
}

if (!content.chunks.length) {
reject({ code: 'BadArgument', message: 'Chunks must not be empty' });
return;
}

const startTime = Date.now();
options = {
uiZIndex: 1000,
timeout: 15000, // Default to 15 seconds
useWebview: false,
...options
};

// Ensure that we were given a number for the UI z-index
if (!options.uiZIndex || typeof options.uiZIndex !== 'number') {
options.uiZIndex = 1000;
}

let timeoutId: number | null = null;
const iframeContainer: HTMLDivElement = document.createElement('div');
const iframe: HTMLIFrameElement = options.useWebview ? <HTMLIFrameElement>document.createElement('webview') : document.createElement('iframe');
const bodyOverflow: string | null = document.body.style.overflow;
const htmlOverflow: string | null = document.documentElement.style.overflow;

const resetTimeout = (): void => {
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
};

const reset = (): void => {
// Remove container along with the iframe inside of it
if (document.body.contains(iframeContainer)) {
document.body.removeChild(iframeContainer);
}

// Clear the timeout timer
resetTimeout();

// Re-enable scrolling
document.body.style.overflow = bodyOverflow;
document.documentElement.style.overflow = htmlOverflow;
};

// Reset variables
reset();

const messageHandler = (e: any): void => {
if (!e || !e.data) { return; }

if (e.data === 'ImmersiveReader-Exit') {
reset();
window.removeEventListener('message', messageHandler);
} else if (e.data === 'ImmersiveReader-ReadyForContent') {
resetTimeout();
const message: Message = {
cogSvcsAccessToken: token,
request: content,
launchToPostMessageSentDurationInMs: Date.now() - startTime
};
iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Content', messageValue: message }), '*');
resolve(iframeContainer);
}
};
window.addEventListener('message', messageHandler);

// Reject the promise if the Immersive Reader page fails to load.
timeoutId = window.setTimeout((): void => {
reject({ code: 'Timeout', message: `Page failed to load after timeout (${options.timeout} ms)` });
}, options.timeout);

// Create and style iframe
iframe.setAttribute('allowfullscreen', '');
iframe.style.cssText = 'position: static; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0';

// Send an initial message to the webview so it has a reference to this parent window
if (options.useWebview) {
iframe.addEventListener('loadstop', () => {
iframe.contentWindow.postMessage(JSON.stringify({ messageType: 'WebviewHost' }), '*');
});
}

let src = 'https://learningtools.onenote.com/learningtoolsapp/cognitive/reader?exitCallback=ImmersiveReader-Exit';
if (options.uiLang) {
src += '&omkt=' + options.uiLang;
}
iframe.src = src;

iframeContainer.style.cssText = `position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden`;

iframeContainer.appendChild(iframe);
document.body.append(iframeContainer);

// Disable body scrolling
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
});
}
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Content } from './content';
import { Options } from './options';
import { Error } from './error';

type Message = {
cogSvcsAccessToken: string;
request: Content;
launchToPostMessageSentDurationInMs: number;
};

/**
* Launch the Immersive Reader within an iframe.
* @param token The authentication token returned from the Cognitive Services issueToken API.
* @param content The content that should be shown in the Immersive Reader.
* @param options Options for configuring the look and feel of the Immersive Reader.
* @return A promise that resolves when the Immersive Reader is launched. The promise resolves with the div that contains an iframe which contains the Immersive Reader.
*/
export function launchAsync(token: string, content: Content, options?: Options): Promise<HTMLDivElement> {
return new Promise((resolve, reject: (reason: Error) => void): void => {
if (!token) {
reject({ code: 'BadArgument', message: 'Token must not be null' });
return;
}

if (!content) {
reject({ code: 'BadArgument', message: 'Content must not be null' });
return;
}

if (!content.chunks) {
reject({ code: 'BadArgument', message: 'Chunks must not be null' });
return;
}

if (!content.chunks.length) {
reject({ code: 'BadArgument', message: 'Chunks must not be empty' });
return;
}

const startTime = Date.now();
options = {
uiZIndex: 1000,
timeout: 15000, // Default to 15 seconds
useWebview: false,
...options
};

// Ensure that we were given a number for the UI z-index
if (!options.uiZIndex || typeof options.uiZIndex !== 'number') {
options.uiZIndex = 1000;
}

let timeoutId: number | null = null;
const iframeContainer: HTMLDivElement = document.createElement('div');
const iframe: HTMLIFrameElement = options.useWebview ? <HTMLIFrameElement>document.createElement('webview') : document.createElement('iframe');
const bodyOverflow: string | null = document.body.style.overflow;
const htmlOverflow: string | null = document.documentElement.style.overflow;

const resetTimeout = (): void => {
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
};

const reset = (): void => {
// Remove container along with the iframe inside of it
if (document.body.contains(iframeContainer)) {
document.body.removeChild(iframeContainer);
}

// Clear the timeout timer
resetTimeout();

// Re-enable scrolling
document.body.style.overflow = bodyOverflow;
document.documentElement.style.overflow = htmlOverflow;
};

// Reset variables
reset();

const messageHandler = (e: any): void => {
if (!e || !e.data) { return; }

if (e.data === 'ImmersiveReader-Exit') {
reset();
window.removeEventListener('message', messageHandler);
} else if (e.data === 'ImmersiveReader-ReadyForContent') {
resetTimeout();
const message: Message = {
cogSvcsAccessToken: token,
request: content,
launchToPostMessageSentDurationInMs: Date.now() - startTime
};
iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Content', messageValue: message }), '*');
resolve(iframeContainer);
}
};
window.addEventListener('message', messageHandler);

// Reject the promise if the Immersive Reader page fails to load.
timeoutId = window.setTimeout((): void => {
reject({ code: 'Timeout', message: `Page failed to load after timeout (${options.timeout} ms)` });
}, options.timeout);

// Create and style iframe
iframe.setAttribute('allowfullscreen', '');
iframe.style.cssText = 'position: static; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0';

// Send an initial message to the webview so it has a reference to this parent window
if (options.useWebview) {
iframe.addEventListener('loadstop', () => {
iframe.contentWindow.postMessage(JSON.stringify({ messageType: 'WebviewHost' }), '*');
});
}

let src = 'https://learningtools.onenote.com/learningtoolsapp/cognitive/reader?exitCallback=ImmersiveReader-Exit';
if (options.uiLang) {
src += '&omkt=' + options.uiLang;
}
iframe.src = src;

iframeContainer.style.cssText = `position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden`;

iframeContainer.appendChild(iframe);
document.body.append(iframeContainer);

// Disable body scrolling
document.body.style.overflow = 'hidden';
document.documentElement.style.overflow = 'hidden';
});
}

/**
* Append an <img/> of assets/icon.svg to every element with the specified className. Intended to be used with empty divs to avoid default styles
*/
export function renderLaunchButtons(className: string): void {
const launchDivs: HTMLCollectionOf<Element> = document.getElementsByClassName(className);
const iconImagePath: string = 'https://contentstorage.onenote.office.net/onenoteltir/permanent-static-resources/immersive-reader-icon.svg';

for (const launchDiv of launchDivs) {
const castedLaunchDiv = launchDiv as HTMLDivElement;
const iconImage: HTMLImageElement = document.createElement('img');
iconImage.src = iconImagePath;

castedLaunchDiv.style.height = castedLaunchDiv.style.width = '20px';
castedLaunchDiv.appendChild(iconImage);
}
}
69 changes: 68 additions & 1 deletion tests/launchAsync.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { launchAsync } from '../src/launchAsync';
import { launchAsync, renderLaunchButtons } from '../src/launchAsync';
import { Content } from '../src/content';
import { Options } from '../src/options';

Expand Down Expand Up @@ -106,4 +106,71 @@ describe('launchAsync tests', () => {
expect(reason.code).toBe('Timeout');
}
});
});

describe('renderLaunchButtons', () => {
it('styles a single div as expected', () => {
const numImgsBeforeStyling = document.getElementsByTagName('img').length;
const newDiv: HTMLDivElement = document.createElement('div');
newDiv.className = 'IRLaunchDiv';
document.body.appendChild(newDiv);

expect(numImgsBeforeStyling).toBe(0);

renderLaunchButtons('IRLaunchDiv');
const numImgsAfterStyling = document.getElementsByTagName('img').length;

expect(numImgsAfterStyling).toBe(1);

// Cleanup the DOM
newDiv.remove();
});

it('styles multiple divs as expected', () => {
const numImgsBeforeStyling = document.getElementsByTagName('img').length;
const newDiv1: HTMLDivElement = document.createElement('div');
const newDiv2: HTMLDivElement = document.createElement('div');
const newDiv3: HTMLDivElement = document.createElement('div');
newDiv1.className = newDiv2.className = newDiv3.className = 'IRLaunchDiv';

document.body.appendChild(newDiv1);
document.body.appendChild(newDiv2);
document.body.appendChild(newDiv3);

expect(numImgsBeforeStyling).toBe(0);

renderLaunchButtons('IRLaunchDiv');

const numImgsAfterStyling = document.getElementsByTagName('img').length;

expect(numImgsAfterStyling).toBe(3);

// Cleanup the DOM
newDiv1.remove();
newDiv2.remove();
newDiv3.remove();
});

it('does nothing for incorrect classnames', () => {
const numImgsBeforeStyling = document.getElementsByTagName('img').length;
const newDiv1: HTMLDivElement = document.createElement('div');
const newDiv2: HTMLDivElement = document.createElement('div');
newDiv1.className = 'IRLaunchDivv';
newDiv2.className = 'IRLaunchhDiv';

document.body.appendChild(newDiv1);
document.body.appendChild(newDiv2);

expect(numImgsBeforeStyling).toBe(0);

renderLaunchButtons('IRLaunchDiv');

const numImgsAfterStyling = document.getElementsByTagName('img').length;

expect(numImgsAfterStyling).toBe(0);

// Cleanup the DOM
newDiv1.remove();
newDiv2.remove();
});
});

0 comments on commit 3008ca0

Please sign in to comment.