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 | ****************************************************************************************/
|
12 | import Penpal from 'penpal';
|
13 | import Logger from './utils/logger';
|
14 | const CONNECTION_TIMEOUT_DURATION = 10000;
|
15 | const EXTENSION_RESPONSE_TIMEOUT_DURATION = 10000;
|
16 | const RENDER_TIMEOUT_DURATION = 2000;
|
17 | const logger = new Logger('ExtensionBridge:Parent');
|
18 |
|
19 | const noop = () => {};
|
20 |
|
21 | export 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 |
|
78 | export 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 | };
|
181 | export const setDebug = value => {
|
182 | Penpal.debug = value;
|
183 | Logger.enabled = value;
|
184 | }; |
\ | No newline at end of file |