UNPKG

8.71 kBJavaScriptView Raw
1/***************************************************************************************
2 * (c) 2017 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 ****************************************************************************************/
12import Penpal from 'penpal';
13import Logger from './utils/logger';
14const CONNECTION_TIMEOUT_DURATION = 10000;
15const EXTENSION_RESPONSE_TIMEOUT_DURATION = 10000;
16const RENDER_TIMEOUT_DURATION = 2000;
17const logger = new Logger('ExtensionBridge:Parent');
18
19const noop = () => {};
20
21export const ERROR_CODES = {
22 CONNECTION_TIMEOUT: 'connectionTimeout',
23 RENDER_TIMEOUT: 'renderTimeout',
24 EXTENSION_RESPONSE_TIMEOUT: 'extensionResponseTimeout',
25 DESTROYED: 'destroyed'
26};
27/**
28 * An object providing bridge-related API.
29 * @typedef {Object} Bridge
30 * @property {Promise} The promise will be resolved once (1) communication with the iframe has
31 * been established, (2) the iframe has been resized to its content, and (3) the iframe has
32 * acknowledged receiving the initial init() call. The promise will be resolved
33 * with an {IframeAPI} object that will act as the API to use to communicate with the iframe.
34 * @property {HTMLIframeElement} iframe The created iframe. You may use this to add classes to the
35 * iframe, etc.
36 * @property {Function} destroy Removes the iframe from its container and cleans up any supporting
37 * utilities.
38 */
39
40/**
41 * An API the consumer will use to call methods on the iframe.
42 * @typedef {Object} IframeAPI
43 * @property {Function} validate Validates the extension view.
44 * @property {Function} getSettings Retrieves settings from the extension view.
45 */
46
47/**
48 * Loads an extension iframe and connects all the necessary APIs.
49 * @param {Object} options
50 * @param {string} options.url The URL of the extension view to load.
51 * @param {Object} [options.extensionInitOptions={}] The options to be passed to the initial init()
52 * call on the extension view.
53 * @param {HTMLElement} [options.container=document.body] The container DOM element to which the
54 * extension iframe should be added.
55 * @param {number} [options.connectionTimeoutDuration=10000] The amount of time, in milliseconds,
56 * that must pass while attempting to establish communication with the iframe before rejecting
57 * the returned promise with a CONNECTION_TIMEOUT error code.
58 * @param {number} [options.renderTimeoutDuration=2000] The amount of time, in milliseconds,
59 * that must pass while attempting to render the iframe before rejecting the returned promise
60 * with a RENDER_TIMEOUT error code. This duration begins after communication with the iframe
61 * has been established.
62 * @param {number} [options.extensionResponseTimeoutDuration=10000] The amount of time, in
63 * milliseconds, that must pass while attempting to receive response from extension validate
64 * or getSettings methods before rejecting the returned promise with a EXTENSION_RESPONSE_TIMEOUT
65 * error code.
66 * @param {Function} [options.openCodeEditor] The function to call when the extension view requests
67 * that the code editor should open. The function may be passed existing code and should return
68 * a promise to be resolved with updated code.
69 * @param {Function} [options.openRegexTester] The function to call when the extension view requests
70 * that the regex tester should open. The function may be passed an existing regular expression
71 * string and should return a promise to be resolved with an updated regular expression string.
72 * @param {Function} [options.openDataElementSelector] The function to call when the extension view
73 * requests that the data element selector should open. The function should return a promise that
74 * is resolved with the selected data element name.
75 * @returns {Bridge}
76 */
77
78export const loadIframe = options => {
79 const url = options.url,
80 _options$extensionIni = options.extensionInitOptions,
81 extensionInitOptions = _options$extensionIni === void 0 ? {} : _options$extensionIni,
82 _options$container = options.container,
83 container = _options$container === void 0 ? document.body : _options$container,
84 _options$connectionTi = options.connectionTimeoutDuration,
85 connectionTimeoutDuration = _options$connectionTi === void 0 ? CONNECTION_TIMEOUT_DURATION : _options$connectionTi,
86 _options$renderTimeou = options.renderTimeoutDuration,
87 renderTimeoutDuration = _options$renderTimeou === void 0 ? RENDER_TIMEOUT_DURATION : _options$renderTimeou,
88 _options$extensionRes = options.extensionResponseTimeoutDuration,
89 extensionResponseTimeoutDuration = _options$extensionRes === void 0 ? EXTENSION_RESPONSE_TIMEOUT_DURATION : _options$extensionRes,
90 _options$openCodeEdit = options.openCodeEditor,
91 openCodeEditor = _options$openCodeEdit === void 0 ? noop : _options$openCodeEdit,
92 _options$openRegexTes = options.openRegexTester,
93 openRegexTester = _options$openRegexTes === void 0 ? noop : _options$openRegexTes,
94 _options$openDataElem = options.openDataElementSelector,
95 openDataElementSelector = _options$openDataElem === void 0 ? noop : _options$openDataElem,
96 _options$markAsDirty = options.markAsDirty,
97 markAsDirty = _options$markAsDirty === void 0 ? noop : _options$markAsDirty;
98 let destroy;
99 let iframe;
100
101 const createOpenSharedViewProxy = openSharedViewFn => {
102 return function () {
103 return Promise.resolve(openSharedViewFn(...arguments));
104 };
105 };
106
107 const loadPromise = new Promise((resolve, reject) => {
108 let renderTimeoutId;
109 const penpalConnection = Penpal.connectToChild({
110 url,
111 appendTo: container,
112 timeout: connectionTimeoutDuration,
113 methods: {
114 openCodeEditor: createOpenSharedViewProxy(openCodeEditor),
115 openRegexTester: createOpenSharedViewProxy(openRegexTester),
116 openDataElementSelector: createOpenSharedViewProxy(openDataElementSelector),
117
118 extensionRegistered() {
119 logger.log('Extension registered.');
120 penpalConnection.promise.then(child => {
121 child.init(extensionInitOptions).then(() => {
122 clearTimeout(renderTimeoutId);
123 logger.log('Extension initialized.');
124 resolve({
125 // We hand init back even though we just called init(). This is really for
126 // the sandbox tool's benefit so a developer testing their extension view can
127 // initialize multiple times with different info.
128 init: child.init,
129 validate: function validate() {
130 return Promise.race([new Promise((_, reject) => {
131 setTimeout(() => {
132 reject(ERROR_CODES.EXTENSION_RESPONSE_TIMEOUT);
133 }, extensionResponseTimeoutDuration);
134 }), child.validate(...arguments)]);
135 },
136 getSettings: function getSettings() {
137 return Promise.race([new Promise((_, reject) => {
138 setTimeout(() => {
139 reject(ERROR_CODES.EXTENSION_RESPONSE_TIMEOUT);
140 }, extensionResponseTimeoutDuration);
141 }), child.getSettings(...arguments)]);
142 }
143 });
144 }).catch(error => {
145 clearTimeout(renderTimeoutId);
146 reject(error);
147 });
148 });
149 },
150
151 markAsDirty
152 }
153 });
154 penpalConnection.promise.then(() => {
155 renderTimeoutId = setTimeout(() => {
156 reject(ERROR_CODES.RENDER_TIMEOUT);
157 destroy();
158 }, renderTimeoutDuration);
159 }, error => {
160 if (error.code === Penpal.ERR_CONNECTION_TIMEOUT) {
161 reject(ERROR_CODES.CONNECTION_TIMEOUT);
162 } else {
163 reject(error);
164 }
165 });
166
167 destroy = () => {
168 reject(ERROR_CODES.DESTROYED);
169 penpalConnection.destroy();
170 };
171
172 iframe = penpalConnection.iframe;
173 });
174 iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups');
175 return {
176 promise: loadPromise,
177 iframe,
178 destroy
179 };
180};
181export const setDebug = value => {
182 Penpal.debug = value;
183 Logger.enabled = value;
184};
\No newline at end of file