├── .gitignore
├── LICENSE
├── README.md
├── dist
├── extern.dev.js
└── extern.min.js
├── extern.js
├── index.js
├── instructions
└── fragment.json
├── package.json
├── react
├── error.js
└── loading.js
└── test
├── extern.browser.js
├── extern.test.js
├── fixtures
├── client.js
├── empty.js
├── format.json
├── missing.json
└── what.css
├── index.js
├── static.js
└── ui.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Go Daddy Operating Company, LLC
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a
4 | copy of this software and associated documentation files (the "Software"),
5 | to deal in the Software without restriction, including without limitation
6 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | and/or sell copies of the Software, and to permit persons to whom the
8 | Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > **This library is deprecated, retired and no longer undevelopment.**
3 |
4 | # external
5 |
6 | External is a dual purpose library. It ships with a client-side framework renders
7 | third party or external pages in the most optimal way as possible. This is done
8 | using various of techniques:
9 |
10 | - The payload is downloaded using a fully async streaming XHR request. This way
11 | we can continuously update and render our placeholder while data flows and
12 | therefor reducing the time to render.
13 | - All assets of the page are loaded async, this includes the CSS.
14 | - The received client code is wrapped before execution so client code can re-use
15 | our dependencies while keeping a sandboxed approach.
16 | - While the client was specifically written for the [BigPipe] framework it
17 | should work against any back-end as long as it returns the same data
18 | structure.
19 | - Templates are rendered using React so it's easy to compose and update.
20 |
21 | But we also ship with a server-side framework implementation for [BigPipe] which
22 | makes it possible to serve the client and automatically format all the output in
23 | the expected HTML structure.
24 |
25 | ## Table of Contents
26 |
27 | - [Installation](#installation)
28 | - [Building](#building)
29 | - [Serving](#serving)
30 | - [Listening](#listening)
31 | - [Extern](#extern)
32 | - [BigPipe](#bigpipe)
33 | - [Wire Format](#wire-format)
34 | - [License](#license)
35 |
36 | ## Installation
37 |
38 | The client-side component is composed from various of tiny modules and can be
39 | build using [Browserify]. It can be build-in to other browserify components by
40 | simply requiring the `external` module in your client-code.
41 |
42 | The server side part of this framework can be installed through npm:
43 |
44 | ```
45 | npm install external
46 | ```
47 |
48 | In addition to providing a browserify-able client-side script there is also a
49 | compiled version of this code which lives in the `dist` folder called
50 | `extern.js`. This pre-compiled library exposes it self using the `Extern` global
51 | and therefor does not introduced `require` statement as globals. In all the code
52 | examples in documentation we assume that you have an `Extern` global. If you use
53 | the `dist` build you can skip the following example:
54 |
55 | ```js
56 | var Extern = require('external');
57 | ```
58 |
59 | ### Building
60 |
61 | If you want to generate new stand alone bundles of the Extern library you can
62 | run our `prepublish` and `dev` scripts using the `npm run` command. These
63 | commands do assume that you've installed the `devDependencies` of this project.
64 | To generate a new production build, `dist/extern.min.js` run:
65 |
66 | ```
67 | npm run prepublish
68 | ```
69 |
70 | As this is a `prepublish` script, it means that every release to npm will have
71 | the `dist/extern.js` included. So if browserify isn't your think, you can just
72 | include the `extern/dist/extern.min.js` instead.
73 |
74 | To generate an un-minified build for development purposes you can run:
75 |
76 | ```
77 | npm run dev
78 | ```
79 |
80 | This will generate a new `dist/extern.dev.js` file.
81 |
82 | ### Serving
83 |
84 | Now that you know how to install it and what type of bundles there are you can
85 | decide how to serve the library. When this module is used as plugin in [BigPipe]
86 | it will automatically serve the browserify and plugin combined bundle from:
87 |
88 | ```
89 | http(s)://domain.com/extern.js
90 | ```
91 |
92 | We also mount our `dist` folder on the server so the static assets in this
93 | folder can also be served:
94 |
95 | ```
96 | http(s)://domain.com/extern.min.js
97 | ```
98 |
99 | Now that you've picked your build, and know how the files are served you can
100 | simply put the script tag in your page and your ready to display external
101 | pages/apps.
102 |
103 | ```html
104 |
105 |
108 | ```
109 |
110 | ### Listening
111 |
112 | The easiest way to have `Extern` load your remote pages is by using the
113 | `Extern.listen` method in combination with the `rel="extern"` attributes on
114 | `` elements:
115 |
116 | ```html
117 | Remote
118 | ```
119 |
120 | The `Extern.listen` method will gather all `` elements and search for a `rel`
121 | that is set to `extern` and uses the set `href` of the element as URL that needs
122 | to be remotely loaded.
123 |
124 | ```js
125 | Extern.listen(document.body, {});
126 | ```
127 |
128 | ## Extern
129 |
130 | The following options are supported:
131 |
132 | - **`timeout`** Timeout for dependency loading. If assets take longer we should
133 | render and error template instead. The timeout is in milliseconds.
134 | - **`document`** Reference to the `document` global can be useful if assets need
135 | to be loaded in iframes instead of the global document.
136 | - **`className`** If a link has this className we will automatically load it in
137 | the placeholder. This className will also automatically be add and removed
138 | once the link is clicked. Defaults to `extern-loads`.
139 |
140 | ```js
141 | var extern = new Extern('http://my.example.com/page', document.body, {
142 | timeout: 10000
143 | });
144 | ```
145 |
146 | ### Events
147 |
148 | The returned `extern` instance is actually an `EventEmitter3` instance so you
149 | can listen to the various of events that we're emitting:
150 |
151 | - **error** Emitted when something went so horribly wrong that we decided to
152 | show the error template. This event receives the actual `error` as argument.
153 | - **done** The streaming XHR is finished with loading.
154 | - **name:render** Called when a fragment is about to render in to the
155 | placeholder. The `name` part in the event should be name of the fragment you
156 | want to listen for.
157 | - **name:loaded** All the assets are loaded for the given placeholder name.
158 |
159 | ## Wrapping
160 |
161 | The client code for each fragments are loaded through an XHR connection. This
162 | way we can safely executed third party code by wrapping the execution in a
163 | `try/catch` statement. But not only does this allow us to wrap code, it also
164 | allows us to introduce variables in the function. The following variables are
165 | introduced as "globals":
166 |
167 | - `React`, This is the `react/addons` reference.
168 | - `require`, Reference to our `require` statement so you can re-use all the
169 | bundled things.
170 |
171 | ## API
172 |
173 | The following properties and methods are exposed on the Extern instance.
174 |
175 | #### Extern.listen
176 |
177 | **Exposed on the constructor**
178 |
179 | Scan the current document for all `` elements and attach click
180 | listeners to it so we can automatically update the supplied placeholder with the
181 | contents of the set URL. This method accepts one argument and that is the
182 | `placeholder` DOM element where all pages should loaded in
183 |
184 | ```js
185 | Extern.listen(document.body);
186 | ```
187 |
188 | #### Extern.merge
189 |
190 | **Exposed on the constructor**
191 |
192 | Merge the object of the second argument in to the first argument. It returns the
193 | fully merged first argument.
194 |
195 | ```js
196 | var x = Extern.merge({ foo: 'foo' }, { bar: 'bar' });
197 | ```
198 |
199 | #### Extern.requests
200 |
201 | **Exposed on the constructor**
202 |
203 | A reference to the `requests` module that we're using for our XHR requests.
204 |
205 | ```js
206 | var requests = Extern.requests.
207 | ```
208 |
209 | See [unshiftio/requests](https://github.com/unshiftio/requests) for more
210 | information.
211 |
212 | ## BigPipe
213 |
214 | This library ships with a custom [Fittings] framework implementation for
215 | [BigPipe] which allows us to control how everything is processed inside of
216 | [BigPipe]. Adding it to your BigPipe instance is just as simple as passing a
217 | custom `framework` option while creating a new instance:
218 |
219 | ```js
220 | 'use strict';
221 |
222 | var BigPipe = require('bigpipe')
223 | , Extern = require('external');
224 |
225 | var app = BigPipe.createServer({
226 | framework: Extern,
227 | port: 8080
228 | });
229 | ```
230 |
231 | But the framework can also be set _after_ the construction using the `framework`
232 | method:
233 |
234 | ```js
235 | app.framework(Extern);
236 | ```
237 |
238 | **Please do note that the current Fittings implentation is in the BigPipe master
239 | branch but will out in the release that follows 0.9**
240 |
241 | Once the fittings are installed on the application, it will start spitting out
242 | responses based on the specified [Wire Format](#wire-format) below. The
243 | processing instructions can be found in the [instructions](/instructions) folder
244 | in the root of this repository. But before fiddling with these files I would
245 | suggest giving the [README.md][Fittings] of Fittings a read so you know how the
246 | data formatting works.
247 |
248 | ## Wire Format
249 |
250 | In order to have the broadest support within this framework we came up with a
251 | dedicated wire-format in order to have the server-side and client-side
252 | components interact with each other. While this wire-format is mostly catered to
253 | the needs of an application that is build using the [BigPipe] framework it
254 | should be relatively easy to produce exactly the same output in different
255 | frameworks and programming languages. This wire format is also required in order
256 | to make streaming data as simple as possible as we can trigger buffer flushes
257 | based on this.
258 |
259 | The format that we're using is `\u1337` separated `JSON`. Every time we
260 | encounter the `\u1337` character on the client-side we assume it's the end of
261 | chunk that requires processing. The JSON payload that is send should contain the
262 | following properties:
263 |
264 | - **`_id`** A unique id for the payload that is flushed.
265 | - **`name`** Name of the payload that is flushed. This is used to track
266 | potential child->parent references throughout the flushed payload.
267 | - **`details`** An object that contains:
268 | - **`js`** Array with path names for the JavaScript files that need to be
269 | loaded on the page. We will automatically prepend the server address to
270 | these assets.
271 | - **`css`** Array with path names for the CSS files that need to be
272 | loaded on the page. We will automatically prepend the server address to
273 | these assets.
274 | - **`state`** Additional state that will be spread on the component when we
275 | render it.
276 | - **`template`** An initial HTML template that should be rendered in the given
277 | placeholder.
278 |
279 | ### CSS Assets
280 |
281 | In order to be able to load CSS assets fully async in every browser we need to
282 | know when the styles are applied. This is done by Extern client by adding a DOM
283 | element to the page that has an `id` attribute which contains `_` and the filename of
284 | the asset that is being downloaded (`#_yourfilename`). We therefor **require**
285 | that the CSS file contains CSS selector and sets the `height` property to
286 | `42px`. This allows us to poll the element for height changes to know when the
287 | CSS is fully loaded. So if we have a file called `1aFafa801jz09.css` it should
288 | have the following selector in the source:
289 |
290 | ```css
291 | #_1aFafa801jz09 { height: 42px }
292 | ```
293 |
294 | ## License
295 |
296 | This project has been released under the MIT license, see [LICENSE].
297 |
298 | [Fittings]: https://github.com/bigpipe/fittings
299 | [Browserify]: http://github.com/substack/node-browserify
300 | [BigPipe]: https://github.com/bigpipe/bigpipe
301 |
--------------------------------------------------------------------------------
/extern.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var debug = require('diagnostics')('extern')
4 | , ReactIntl = require('react-intl')
5 | , Assets = require('async-asset')
6 | , React = require('react/addons')
7 | , Requests = require('requests')
8 | , Recovery = require('recovery')
9 | , destroy = require('demolish')
10 | , each = require('async-each')
11 | , URL = require('url-parse');
12 |
13 | /**
14 | * Extern.
15 | *
16 | * Options:
17 | *
18 | * - `cdn` Base URL for the CDN, if non is provide it will use the URL requested
19 | * as cdn URL.
20 | * - `timeout` Maximum time that we're allowed to load a single asset.
21 | * - `manual` Don't `open` by default but do it manual.
22 | * - `document` Optional reference to the `document` global it should use.
23 | *
24 | * @param {String} url Address of the server we're connecting against.
25 | * @param {Element} container Container in which the React should be loaded.
26 | * @param {Object} options Optional configuration.
27 | * @api public
28 | */
29 | function Extern(url, container, options) {
30 | if (!this) return new Extern(url, container, options);
31 |
32 | options = this.options = Extern.merge(
33 | Extern.merge({}, Extern.defaults),
34 | options || {}
35 | );
36 |
37 | //
38 | // Setup our Exponential back off things.
39 | //
40 | Recovery.call(this, options.backoff || {});
41 |
42 | this.buffer = '';
43 | this.components = {};
44 | this.url = new URL(url);
45 | this.container = container;
46 | this.cdn = new URL(options.cdn || url);
47 | this.assets = new Assets(container.parentNode, {
48 | document: options.document || global.document || {},
49 | timeout: options.timeout,
50 | prefix: '_'
51 | });
52 |
53 | //
54 | // Remove parts of the URL that should not be send to CDN's
55 | //
56 | this.cdn.set('query', '');
57 | this.cdn.set('hash', '');
58 |
59 | this.render(this.react.loading, { message: options.loading });
60 | this.on('reconnect', this.open, this);
61 |
62 | if (!options.manual) this.reconnect();
63 | }
64 |
65 | //
66 | // Extern is an EventEmitter so we can listen upon all the things.
67 | //
68 | Extern.prototype = new Recovery();
69 | Extern.prototype.constructor = Extern;
70 | Extern.prototype.emits = require('emits');
71 |
72 | /**
73 | * The default options.
74 | *
75 | * @type {Object}
76 | * @api public
77 | */
78 | Extern.defaults = {
79 | timeout: 30000, // Timeout for assets downloading.
80 | manual: false // Manually start the request.
81 | };
82 |
83 | /**
84 | * Open the streaming connection and download all the data's.
85 | *
86 | * @api public
87 | */
88 | Extern.prototype.open = function open() {
89 | var extern = this;
90 |
91 | extern.stream = new Requests(extern.url.href, {
92 | streaming: true,
93 | method: 'GET',
94 | mode: 'cors'
95 | });
96 |
97 | extern.stream
98 | .on('data', extern.parse.bind(extern))
99 | .on('error', extern.emits('error'))
100 | .on('end', function done(err) {
101 | if (err) extern.render(extern.react.error);
102 |
103 | extern.reconnected(err);
104 | extern.emit('done');
105 | });
106 |
107 | debug('opening connection to %s', extern.url.href);
108 | return extern;
109 | };
110 |
111 | /**
112 | * Render a given React component in our supplied container.
113 | *
114 | * @param {React} component The Component that needs to be rendered.
115 | * @param {Object} spread What ever needs to be spread upon the component.
116 | * @api public
117 | */
118 | Extern.prototype.render = function render(component, spread) {
119 | try {
120 | return React.render(
121 | React.createElement(component, React.__spread(this.options.props || {}, spread || {})),
122 | this.container
123 | );
124 | } catch (e) {
125 | this.emit('error', e);
126 | debug('failed to render React component in the container due to', e);
127 | return this.render(this.react.error);
128 | }
129 | };
130 |
131 | /**
132 | * Parse incoming data.
133 | *
134 | * @param {String} data Received data stream from the XHR request.
135 | * @returns {Boolean} Extracted a fragment from the buffer.
136 | * @api private
137 | */
138 | Extern.prototype.boundary = '\\u1337';
139 | Extern.prototype.parse = function parse(data) {
140 | if (data) this.buffer += data;
141 |
142 | var i;
143 |
144 | if (!~(i = this.buffer.indexOf(this.boundary))) {
145 | debug('received %d of data, but did not contain our boundary yet.', data.length);
146 | return false;
147 | }
148 |
149 | //
150 | // Poor man's parser implementation. It's highly unlikely that we're receiving
151 | // multiple blobs of data in one go.
152 | //
153 | this.read(this.buffer.substr(0, i));
154 |
155 | this.buffer = this.buffer.substr(i + this.boundary.length).trim();
156 | this.parse(''); // Another parse call to see if we received multiple chunks.
157 |
158 | return true;
159 | };
160 |
161 | /**
162 | * Transform the parsed data in to an actual template.
163 | *
164 | * @param {String} fragment The received fragment from the server.
165 | * @api public
166 | */
167 | Extern.prototype.read = function read(fragment) {
168 | try { fragment = JSON.parse(fragment); }
169 | catch (e) {
170 | debug('failed to parse buffer fragment to a valid JSON structure', e);
171 | return this.emit('error', new Error('Failed to parse received JSON'));
172 | }
173 |
174 | var assets = []
175 | , extern = this
176 | , name = fragment.name
177 | , cdn = new URL(this.cdn.toString());
178 |
179 | //
180 | // Make sure that we've received our basic structure.
181 | //
182 | fragment.details = fragment.details || {};
183 |
184 | extern
185 | .once(fragment.name +':loaded', function loaded(err) {
186 | // @TODO handle error
187 | if (err) debug('failed to load %s due to', fragment.name, err);
188 |
189 | if (extern.listeners(fragment.details.parent +':render').length) {
190 | debug('rendering %s as all is loaded', fragment.name);
191 | extern.emit(fragment.name +':render');
192 | }
193 | })
194 | .on(fragment.details.parent +':render', function render() {
195 | debug('parent %s has rendered so re-rendering child %s', fragment.details.parent, fragment.name);
196 | extern.emit(name +':render');
197 | })
198 | .on(fragment.name +':render', function render() {
199 | var component = (fragment.details.js || []).filter(function find(id) {
200 | return id in extern.components;
201 | });
202 |
203 | //
204 | // No JavaScript file was found, so we cannot render the view as we require
205 | // a React component for this.
206 | //
207 | if (!component.length) {
208 | return debug('no React component to render for %s', fragment.name);
209 | }
210 |
211 | extern.render(extern.components[component[0]](), fragment.state);
212 | extern.emit(fragment.name +':rendered', fragment.state);
213 | });
214 |
215 | if (fragment.details.css) Array.prototype.push.apply(assets, fragment.details.css);
216 | if (fragment.details.js) Array.prototype.push.apply(assets, fragment.details.js);
217 |
218 | /**
219 | * Create object that has CDN information.
220 | *
221 | * @param {String} pathname Asset file path.
222 | * @return {Object} details
223 | * @api private
224 | */
225 | function map(pathname) {
226 | cdn.set('pathname', pathname);
227 |
228 | return {
229 | pathname: pathname,
230 | href: cdn.href
231 | };
232 | }
233 |
234 | /**
235 | * Download the asset from the server.
236 | *
237 | * @param {Object} url Formatted URL.
238 | * @param {Function} fn Completion callback.
239 | * @api private
240 | */
241 | function download(url, fn) {
242 | debug('downloading asset %s for %s', url.href, fragment.name);
243 | if (/\.js$/.test(url.pathname)) return extern.download(url, fn);
244 |
245 | extern.assets.add(url.href, fn);
246 | }
247 |
248 | //
249 | // Download any global dependencies before local assets.
250 | //
251 | return each((fragment.details.dependencies || []).map(map), download, function prepared(err) {
252 | if (err) return extern.emit('error', err);
253 |
254 | each(assets.map(map), download, extern.emits(fragment.name +':loaded'));
255 | });
256 | };
257 |
258 | /**
259 | * Download files from the specified remote server so they can be sandboxed
260 | * before evaluation.
261 | *
262 | * @param {Array} urls A list of URL's that should be downloaded from the server.
263 | * @param {Function} fn Completion callback that follows error first callback pattern.
264 | * @returns {Extern}
265 | * @api public
266 | */
267 | Extern.prototype.download = function download(urls, fn) {
268 | var extern = this;
269 |
270 | urls = Array.isArray(urls) ? urls : [urls];
271 | each(urls, function iteration(url, next) {
272 | var buffer = [];
273 |
274 | (new Requests(url.href, {
275 | timeout: extern.options.timeout,
276 | streaming: false,
277 | method: 'GET'
278 | }))
279 | .on('data', function concat(data) {
280 | buffer.push(data);
281 | })
282 | .on('end', function end(err) {
283 | if (err) return next(err);
284 |
285 | extern.sandbox(url.pathname, buffer.join(''));
286 |
287 | //
288 | // Clean-up all references and data that was gathered.
289 | //
290 | this.removeAllListeners();
291 | buffer.length = 0;
292 |
293 | next();
294 | });
295 | }, fn);
296 |
297 | return this;
298 | };
299 |
300 | /**
301 | * Generate a sandboxed environment.
302 | *
303 | * @param {String} pathaname The path to the source file on the server.
304 | * @param {String} buffer The received source from the server.
305 | * @returns {Extern}
306 | * @api public
307 | */
308 | Extern.prototype.sandbox = function sandbox(pathname, buffer) {
309 | if (pathname in this.components) return this;
310 |
311 | var extern = this
312 | , value
313 | , fn;
314 |
315 | //
316 | // We don't really need to do anything in the `catch` statement as the actual
317 | // error will be captured in the stored `conditional` function as it would
318 | // throw as the `fn` is not a function causing the error template to be used.
319 | //
320 | try { fn = new Function('React', 'require', buffer); }
321 | catch (e) {
322 | debug('failed to compile sandbox and create %s due to ', pathname, e);
323 | extern.emit('error', e);
324 | }
325 |
326 | /**
327 | * The component that needs to be rendered by the server.
328 | *
329 | * @returns {React.createClass}
330 | * @api private
331 | */
332 | this.components[pathname] = function conditional() {
333 | if (value) return value;
334 |
335 | //
336 | // Capture the execution of the component. We assume that the given JS
337 | // client code returns the React component that eventually needs to be
338 | // rendered.
339 | //
340 | // If the execution failed, we will show our build-in error component
341 | // instead so something visual is still rendered.
342 | //
343 | try { value = fn(React, require) || extern.react.error; }
344 | catch (e) {
345 | debug('failed to execute component sandbox for %s due to ', pathname, e);
346 | extern.emit('error', e);
347 | value = extern.react.error;
348 | }
349 |
350 | return value;
351 | };
352 |
353 | return this;
354 | };
355 |
356 | /**
357 | * Custom React components which are rendered while we're loading assets.
358 | *
359 | * @type {Object}
360 | * @private
361 | */
362 | Extern.prototype.react = {
363 | loading: require('./react/loading'),
364 | error: require('./react/error')
365 | };
366 |
367 | /**
368 | * Completely destroy and null the said object.
369 | *
370 | * @returns {Boolean} First destruction
371 | * @api public
372 | */
373 | Extern.prototype.destroy = destroy('buffer, components, url, container, assets, stream, options', {
374 | before: function () {
375 | debug('destroying extern instance %s', this.url.href);
376 |
377 | if (this.stream) {
378 | this.stream.destroy();
379 | }
380 | }
381 | });
382 |
383 | /**
384 | * Merge b with object a.
385 | *
386 | * @param {Object} a Target object that should receive props from b.
387 | * @param {Object} b Object that needs to be merged in to a.
388 | * @api public
389 | */
390 | Extern.merge = function merge(a, b) {
391 | return Object.keys(b).reduce(function reduce(state, key) {
392 | state[key] = b[key];
393 | return state;
394 | }, a);
395 | };
396 |
397 | /**
398 | * A handy listener for automatically attaching to various of elements that
399 | * will render the said URL's in the given placeholder.
400 | *
401 | * @param {Element} placeholder The placeholder for the remote pages.
402 | * @param {Object} configuration Configuration of the Extern server.
403 | * @param {Object} events Additional event listeners.
404 | * @returns {Extern}
405 | * @api public
406 | */
407 | Extern.listen = function listen(placeholder, configuration, events) {
408 | events = events || {};
409 | configuration = configuration || {};
410 | configuration.className = configuration.className || 'extern-loads';
411 |
412 | var Instance = this
413 | , links = []
414 | , extern;
415 |
416 | Array.prototype.slice.call(
417 | document.getElementsByTagName('a')
418 | ).forEach(function each(a) {
419 | if (a.rel !== 'extern' || !a.href) return;
420 |
421 | /**
422 | * Render the given URL in the placeholder.
423 | *
424 | * @param {Event} e DOM Event from when we clicked on things.
425 | * @api private
426 | */
427 | function render(e) {
428 | if (e) e.preventDefault();
429 |
430 | //
431 | // Destroy the previous instance so we can clean up memory.
432 | //
433 | if (extern) extern.destroy();
434 | extern = new Instance(a.href, placeholder, configuration);
435 |
436 | for (var name in events) {
437 | extern.on(name, events[name]);
438 | }
439 |
440 | //
441 | // Add/Remove the custom className.
442 | //
443 | links.forEach(function clear(link) {
444 | if (!~link.className.indexOf(configuration.className)) return;
445 | link.className = link.className.replace(new RegExp('(?:^|\\s)'+ configuration.className +'(?!\\S)'), '');
446 | });
447 |
448 | a.className += ' '+ configuration.className;
449 | }
450 |
451 | a.addEventListener('click', render, false);
452 | links.push(a);
453 |
454 | //
455 | // If the given link has our special `active` className we should activate
456 | // the `extern` instance directly so it loads without having to click on the
457 | // URL.
458 | //
459 | if (~a.className.indexOf(configuration.className)) render();
460 | });
461 |
462 | return this;
463 | };
464 |
465 | /**
466 | * Expose the Requests instance so outsiders can also use it.
467 | *
468 | * @type {Function}
469 | * @api public
470 | */
471 | Extern.requests = Requests;
472 |
473 | //
474 | // Expose the Framework
475 | //
476 | module.exports = Extern;
477 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Fittings = require('fittings')
4 | , join = require('path').join
5 | , fs = require('fs');
6 |
7 | /**
8 | * Read files out of our instructions directory.
9 | *
10 | * @param {String} file Filename that we should read.
11 | * @returns {String}
12 | * @api private
13 | */
14 | function read(file) {
15 | return fs.readFileSync(join(__dirname, 'instructions', file), 'utf-8');
16 | }
17 |
18 | /**
19 | * Create a new custom fittings instance so we can fully customize how
20 | * everything should be loaded for external files.
21 | *
22 | * @constructor
23 | * @api public
24 | */
25 | Fittings.extend({
26 | name: 'extern',
27 | fragment: read('fragment.json'),
28 | bootstrap: read('fragment.json'),
29 | library: require.resolve('./extern.js'),
30 | middleware: {
31 | standalone: require('serve-static')(join(__dirname, 'dist'))
32 | }
33 | }).on(module);
34 |
--------------------------------------------------------------------------------
/instructions/fragment.json:
--------------------------------------------------------------------------------
1 | {
2 | "_id": "{fittings:id}",
3 | "name": "{fittings:name}",
4 | "details": {fittings~data},
5 | "state": {fittings@state},
6 | "template": {fittings~template}
7 | }
8 | \u1337
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "external",
3 | "version": "1.0.0",
4 | "description": "Fittings for BigPipe which allows you to use the BigPipe as a third party/external content provider.",
5 | "main": "index.js",
6 | "browser": "extern.js",
7 | "scripts": {
8 | "100%": "istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100",
9 | "test": "node test/index.js",
10 | "node": "mocha test/extern.test.js",
11 | "watch": "mocha --watch test/extern.test.js",
12 | "coverage": "istanbul cover ./node_modules/.bin/_mocha -- test/extern.test.js",
13 | "compile": "mkdir -p dist && NODE_ENV=production browserify -s Extern -g envify -g uglifyify index.js -o dist/extern.min.js",
14 | "prepublish": "npm run compile",
15 | "dev": "mkdir -p dist && NODE_ENV=development browserify -s Extern -g envify index.js -o dist/extern.dev.js",
16 | "static": "node test/static.js"
17 | },
18 | "keywords": [
19 | "external",
20 | "fittings",
21 | "remote",
22 | "loading",
23 | "third",
24 | "party",
25 | "third-party",
26 | "bigpipe"
27 | ],
28 | "author": "Arnout Kazemier",
29 | "license": "MIT",
30 | "dependencies": {
31 | "async-asset": "0.0.x",
32 | "async-each": "0.1.x",
33 | "demolish": "1.0.x",
34 | "diagnostics": "1.0.x",
35 | "emits": "3.0.x",
36 | "fittings": "1.2.x",
37 | "intl": "1.0.x",
38 | "react": "0.13.x",
39 | "react-intl": "1.2.x",
40 | "recovery": "0.2.x",
41 | "requests": "0.1.x",
42 | "serve-static": "1.10.x",
43 | "url-parse": "1.4.x"
44 | },
45 | "devDependencies": {
46 | "access-control": "0.0.x",
47 | "argh": "0.1.x",
48 | "assume": "1.2.x",
49 | "browserify": "10.2.x",
50 | "envify": "3.4.x",
51 | "istanbul": "0.3.x",
52 | "mocha": "2.2.x",
53 | "mochify": "2.10.x",
54 | "mochify-istanbul": "2.3.x",
55 | "node-jsx": "0.13.x",
56 | "pre-commit": "1.0.x",
57 | "uglifyify": "3.0.x"
58 | },
59 | "testling": {
60 | "files": "test/*.browser.js",
61 | "harness": "mocha-bdd",
62 | "browsers": [
63 | "ie/6..latest",
64 | "chrome/22..latest",
65 | "firefox/16..latest",
66 | "safari/latest",
67 | "opera/11.0..latest",
68 | "iphone/latest",
69 | "ipad/latest",
70 | "android-browser/latest"
71 | ]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/react/error.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react/addons')
4 | , ReactIntl = require('react-intl');
5 |
6 | /**
7 | * A simple back-up view for when loading or rendering an actual component
8 | * failed. This way we give users some addition instructions instead of leaving
9 | * them to suffer with a blank page of death.
10 | *
11 | * @returns {React}
12 | * @api public
13 | */
14 | module.exports = React.createClass({
15 | mixins: [ReactIntl.IntlMixin],
16 | render: function render() {
17 | return React.createElement('div', { className: 'error' },
18 | React.createElement('h2', null, 'Yikes!'),
19 | React.createElement('p', null, [
20 | 'Something\'s gone wrong, and we\'re working feverishly to fix the issue.',
21 | 'Please wait a bit and try again.'
22 | ].join(' '))
23 | );
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/react/loading.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react/addons')
4 | , ReactIntl = require('react-intl');
5 |
6 | /**
7 | * A simple back-up view for when loading or rendering an actual component
8 | * failed. This way we give users some addition instructions instead of leaving
9 | * them to suffer with a blank page of death.
10 | *
11 | * @constructor
12 | * @type {React.Component}
13 | * @api public
14 | */
15 | module.exports = React.createClass({
16 | mixins: [ReactIntl.IntlMixin],
17 | getDefaultProps: function getDefaultProps() {
18 | return {
19 | message: 'Loading, please wait.',
20 | container: {
21 | backgroundColor: '#FFF',
22 | marginBottom: '12px',
23 | padding: '25px 5%',
24 | textAlign: 'center'
25 | },
26 | spinner: {
27 | display: 'inline-block',
28 | verticalAlign: 'middle',
29 | marginRight: '5px',
30 | width: '16px',
31 | height: '16px'
32 | }
33 | };
34 | },
35 | render: function render() {
36 | return React.createElement('div', { style: this.props.container },
37 | React.createElement('i', { style: this.props.spinner }),
38 | this.props.message
39 | );
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/test/extern.browser.js:
--------------------------------------------------------------------------------
1 | describe('extern', function () {
2 | 'use strict';
3 |
4 | if (!global.Intl) global.Intl = require('intl');
5 |
6 | var Requests = require('requests')
7 | , Extern = require('../extern')
8 | , assume = require('assume')
9 | , React = require('react')
10 | , ui = require('./ui')
11 | , placeholder
12 | , extern;
13 |
14 | //
15 | // Add all the things to a custom DOM element so we do not accidentally override
16 | // assets or other things that are loaded in the DOM.
17 | //
18 | placeholder = document.createElement('div');
19 | placeholder.id = 'placeholder';
20 | document.body.appendChild(placeholder);
21 |
22 | beforeEach(function () {
23 | placeholder.innerHTML = '';
24 |
25 | extern = new Extern('http://localhost/wut', placeholder, {
26 | props: { custom: 'props', are: 'available' },
27 | timeout: 2000,
28 | manual: true
29 | });
30 | });
31 |
32 | afterEach(function () {
33 | extern.destroy();
34 | });
35 |
36 | it('is exported as a function', function () {
37 | assume(Extern).is.a('function');
38 | });
39 |
40 | it('renders an initial loading screen', function () {
41 | assume(placeholder.innerHTML).includes('Loading');
42 |
43 | extern = new Extern('http://localhost/wut', placeholder, {
44 | loading: 'Custom totally unrelated message',
45 | manual: true
46 | });
47 |
48 | assume(placeholder.innerHTML).includes('totally unrelated');
49 | });
50 |
51 | it('exposes the `.requests` library', function () {
52 | assume(Extern.requests).equals(Requests);
53 | });
54 |
55 | it('removes querystring/hash for CDN urls', function () {
56 | extern.destroy();
57 | extern = new Extern('http://localhost/wut', placeholder, {
58 | cdn: 'https://thisisadifferenturl.com/?querystring=removed#hashtagyolo',
59 | timeout: 2000,
60 | manual: true
61 | });
62 |
63 | assume(extern.cdn.href).equals('https://thisisadifferenturl.com/');
64 |
65 | extern.destroy();
66 | extern = new Extern('http://localhost/wut?q=s#swag', placeholder, {
67 | timeout: 2000,
68 | manual: true
69 | });
70 |
71 | assume(extern.cdn.href).equals('http://localhost/wut');
72 | });
73 |
74 | describe('#open', function () {
75 | it('renders an error template when things fail', function (next) {
76 | extern.open();
77 |
78 | extern.once('done', function () {
79 | assume(placeholder.innerHTML).includes('error');
80 | next();
81 | });
82 | });
83 |
84 | it('requests the given source', function (next) {
85 | extern.destroy();
86 |
87 | extern = new Extern('http://localhost:8080/fixtures/format.json', placeholder, {
88 | timeout: 1000
89 | });
90 |
91 | extern.once('error', next);
92 | extern.once('fixture:rendered', function () {
93 | assume(placeholder.innerHTML).includes('client.js');
94 |
95 | next();
96 | });
97 | });
98 | });
99 |
100 | describe('#render', function () {
101 | it('emits `error` when it fails to render the component', function (next) {
102 | extern.once('error', function (err) {
103 | assume(err).is.instanceOf(Error);
104 | next();
105 | });
106 |
107 | extern.render({ not: 'a real component' });
108 | });
109 |
110 | it('renders the error template', function () {
111 | placeholder.innerHTML = '';
112 | extern.render({ non: 'extistent' });
113 |
114 | assume(placeholder.innerHTML).matches(/error/i);
115 | });
116 |
117 | it('applies the given object as spread data', function () {
118 | var Fixture = React.createClass({
119 | render: function () {
120 | return React.createElement('div', null, this.props.foo);
121 | }
122 | });
123 |
124 | extern.render(Fixture, { foo: 'bar-lal' });
125 | assume(placeholder.innerHTML).matches(/bar-lal/i);
126 | });
127 |
128 | it('merges the supplied props options with the supplied spread', function () {
129 | var Fixture = React.createClass({
130 | render: function () {
131 | assume(this.props.are).equals('merged');
132 | assume(this.props.foo).equals('bar-lal');
133 | assume(this.props.custom).equals('props');
134 |
135 | return React.createElement('div', null, this.props.foo);
136 | }
137 | });
138 |
139 | extern.render(Fixture, { foo: 'bar-lal', are: 'merged' });
140 | assume(placeholder.innerHTML).matches(/bar-lal/i);
141 | });
142 | });
143 |
144 | describe('#parse', function () {
145 | it('adds the supplied data to the buffer if no boundary is found', function () {
146 | assume(extern.buffer).equals('');
147 |
148 | assume(extern.parse('foo')).is.false();
149 | assume(extern.buffer).equals('foo');
150 | });
151 |
152 | it('it continues attempting to read until buffer is full', function () {
153 | extern.parse('foo');
154 | extern.parse('bar');
155 |
156 | assume(extern.buffer).equals('foobar');
157 | assume(extern.parse(extern.boundary)).is.true();
158 |
159 | assume(extern.buffer).equals('');
160 | });
161 |
162 | it('trims away and extra whitespace', function () {
163 | assume(extern.parse('foo, bar'+ extern.boundary + '\n\r\n')).is.true();
164 | assume(extern.buffer).equals('');
165 | });
166 |
167 | it('can parse multiple chunks of data', function () {
168 | extern.parse('foo bar'+ extern.boundary +'moo boo');
169 | assume(extern.buffer).equals('moo boo');
170 |
171 | extern.parse(extern.boundary +'moo'+ extern.boundary);
172 | assume(extern.buffer).equals('');
173 | });
174 |
175 | it('passes the chunk in to the #read method');
176 | });
177 |
178 | describe('#read', function () {
179 | it('emits an error when it fails to parse JSON', function (next) {
180 | extern.once('error', function (err) {
181 | assume(err.message).contains('JSON');
182 | next();
183 | });
184 |
185 | extern.read('{foo');
186 | });
187 |
188 | it('emits `:loaded` if there are no dependencies to load', function (next) {
189 | extern.once('foo:loaded', function () {
190 | next();
191 | });
192 |
193 | extern.read(JSON.stringify({
194 | name: 'foo'
195 | }));
196 | });
197 |
198 | it('displays an error view when there is no component to render', function (next) {
199 | extern.destroy();
200 |
201 | extern = new Extern('http://localhost:8080/fixtures/missing.json', placeholder, {
202 | timeout: 1000
203 | });
204 |
205 | extern.once('error', next);
206 | extern.once('fixture:rendered', function () {
207 | assume(placeholder.innerHTML).includes('error');
208 |
209 | next();
210 | });
211 | });
212 | });
213 |
214 | describe('.listen', function () {
215 | it('has a .listen method', function () {
216 | assume(Extern.listen).is.a('function');
217 | });
218 |
219 | it('returns the Extern', function () {
220 | assume(Extern.listen(placeholder)).equals(Extern);
221 | });
222 |
223 | it('attaches it self to links & downloads the said URL', function (next) {
224 | var a = document.createElement('a');
225 |
226 | a.href = 'http://localhost:8080/fixtures/format.json';
227 | a.rel = 'extern';
228 |
229 | document.body.appendChild(a);
230 | Extern.listen(placeholder, {}, {
231 | 'error': next,
232 | 'fixture:rendered': function () {
233 | assume(placeholder.innerHTML).contains('fixture client.js');
234 |
235 | next();
236 | }
237 | });
238 |
239 | ui.mouse(a, 'click');
240 | });
241 |
242 | it('initializes all the things automatically using classNames', function (next) {
243 | var a = document.createElement('a');
244 |
245 | a.href = 'http://localhost:8080/fixtures/format.json';
246 | a.className = 'foo bar banana';
247 | a.rel = 'extern';
248 |
249 | document.body.appendChild(a);
250 | Extern.listen(placeholder, { className: 'bar' }, {
251 | 'error': next,
252 | 'fixture:rendered': function () {
253 | assume(placeholder.innerHTML).contains('fixture client.js');
254 |
255 | next();
256 | }
257 | });
258 | });
259 | });
260 |
261 | describe('.merge', function () {
262 | it('merges the object in the first arg', function () {
263 | var obj = {};
264 |
265 | Extern.merge(obj, { foo: 'bar' });
266 | assume(obj.foo).equals('bar');
267 | });
268 |
269 | it('returns the supplied object', function () {
270 | var obj = {};
271 |
272 | assume(Extern.merge(obj, { foo: 'bar' })).deep.equals(obj);
273 | assume(obj.foo).equals('bar');
274 | });
275 | });
276 | });
277 |
--------------------------------------------------------------------------------
/test/extern.test.js:
--------------------------------------------------------------------------------
1 | describe('extern', function () {
2 | 'use strict';
3 |
4 | var assume = require('assume')
5 | , Fittings = require('../');
6 |
7 | describe('bigpipe integration', function () {
8 | it('has a custom name', function () {
9 | assume(Fittings.prototype.name).equals('extern');
10 | });
11 |
12 | it('exported as function', function () {
13 | assume(Fittings).is.a('function');
14 | });
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/fixtures/client.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | return React.createClass({
4 | render: function render() {
5 | return React.createElement('strong', null, 'fixture client.js');
6 | }
7 | });
8 |
--------------------------------------------------------------------------------
/test/fixtures/empty.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godaddy/external/d64ee13f9d4d39b2eb51ff3866e4e41f6d941e17/test/fixtures/empty.js
--------------------------------------------------------------------------------
/test/fixtures/format.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fixture",
3 | "details": {
4 | "js": ["/fixtures/client.js"],
5 | "css": ["/fixtures/what.css"]
6 | },
7 | "template": ""
8 | }
9 | \u1337
10 |
--------------------------------------------------------------------------------
/test/fixtures/missing.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fixture",
3 | "details": {
4 | "js": ["/fixtures/empty.js"],
5 | "css": ["/fixtures/what.css"]
6 | },
7 | "template": ""
8 | }
9 | \u1337
10 |
--------------------------------------------------------------------------------
/test/fixtures/what.css:
--------------------------------------------------------------------------------
1 | #placeholder {
2 | height: 10px
3 | }
4 |
5 | #_what { height: 42px }
6 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path')
4 | , Mocha = require('mocha')
5 | , argv = require('argh').argv
6 | , mochify = require('mochify');
7 |
8 | argv.reporter = argv.reporter || 'spec';
9 | argv.ui = argv.ui || 'bdd';
10 |
11 | /**
12 | * Poor mans kill switch. Kills all active hooks.
13 | *
14 | * @api private
15 | */
16 | function kill() {
17 | require('async-each')(kill.hooks, function each(fn, next) {
18 | fn(next);
19 | }, function done(err) {
20 | if (err) return process.exit(1);
21 |
22 | process.exit(0);
23 | });
24 | }
25 |
26 | /**
27 | * All the hooks that need destruction.
28 | *
29 | * @type {Array}
30 | * @private
31 | */
32 | kill.hooks = [];
33 |
34 | //
35 | // This is the magical test runner that setup's all the things and runs various
36 | // of test suites until something starts failing.
37 | //
38 | (function runner(steps) {
39 | if (!steps.length) return kill(), runner;
40 |
41 | var step = steps.shift();
42 |
43 | step(function unregister(fn) {
44 | kill.hooks.push(fn);
45 | }, function register(err) {
46 | if (err) throw err;
47 |
48 | runner(steps);
49 | });
50 |
51 | return runner;
52 | })([
53 | //
54 | // Run the normal node tests.
55 | //
56 | function creamy(kill, next) {
57 | var mocha = new Mocha();
58 |
59 | mocha.reporter(argv.reporter);
60 | mocha.ui(argv.ui);
61 |
62 | //
63 | // The next bulk of logic is required to correctly glob and lookup all the
64 | // files required for testing.
65 | //
66 | mocha.files = [
67 | './test/*.test.js'
68 | ].map(function lookup(glob) {
69 | return Mocha.utils.lookupFiles(glob, ['js']);
70 | }).reduce(function flatten(arr, what) {
71 | Array.prototype.push.apply(arr, what);
72 | return arr;
73 | }, []).map(function resolve(file) {
74 | return path.resolve(file);
75 | });
76 |
77 | //
78 | // Run the mocha test suite inside this node process with a custom callback
79 | // so we don't accidentally exit the process and forget to run the test of the
80 | // tests.
81 | //
82 | mocha.run(function ran(err) {
83 | if (err) err = new Error('Something failed in the mocha test suite');
84 | next(err);
85 | });
86 | },
87 |
88 | //
89 | // Start-up a small static file server so we can download files and fixtures
90 | // inside our PhantomJS test.
91 | //
92 | require('./static'),
93 |
94 | //
95 | // Run the PhantomJS tests now that we have a small static server setup.
96 | //
97 | function phantomjs(kill, next) {
98 | mochify('./test/*.browser.js', {
99 | reporter: argv.reporter,
100 | cover: argv.cover,
101 | ui: argv.ui
102 | })
103 | .bundle(next);
104 | }
105 | ]);
106 |
--------------------------------------------------------------------------------
/test/static.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs')
4 | , url = require('url')
5 | , path = require('path')
6 | , http = require('http')
7 | , access = require('access-control')();
8 |
9 | /**
10 | * Simple static server to serve test files.
11 | *
12 | * @param {Function} kill Kill the server.
13 | * @param {Function} next Continue
14 | * @api private
15 | */
16 | module.exports = function staticserver(kill, next) {
17 | var server = http.createServer(function serve(req, res) {
18 | access(req, res, function () {
19 | var file = path.join(__dirname, url.parse(req.url).pathname);
20 |
21 | if (!fs.existsSync(file)) {
22 | res.statusCode = 404;
23 |
24 | return res.end('nope');
25 | }
26 |
27 | res.statusCode = 200;
28 | fs.createReadStream(file).pipe(res);
29 | });
30 | });
31 |
32 | kill(function close(next) {
33 | server.close(next);
34 | });
35 |
36 | server.listen(8080, next);
37 | };
38 |
39 | if (require.main === module) module.exports(function () {
40 | //
41 | // Ignore me, I'm the kill function of the test runner.
42 | //
43 | }, function () {
44 | console.log('Running static server on localhost:8080');
45 | });
46 |
--------------------------------------------------------------------------------
/test/ui.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Emit mouse events on DOM elements without UI interaction.
5 | *
6 | * @param {DOM} node DOM node
7 | * @param {String} type Mouse event type we should trigger.
8 | * @api public
9 | */
10 | exports.mouse = function mouse(node, type) {
11 | var evt;
12 |
13 | if (node.dispatchEvent) {
14 | if ('function' === typeof MouseEvent) {
15 | evt = new MouseEvent(type, {
16 | bubbles: true,
17 | cancelable: true
18 | });
19 | } else {
20 | // PhantomJS (wat!)
21 | evt = document.createEvent('MouseEvent');
22 | evt.initEvent(type, true, true);
23 | }
24 |
25 | return node.dispatchEvent(evt);
26 | } else {
27 | // IE 8
28 | evt = document.createEventObject('MouseEvent');
29 | return node.fireEvent('on'+ type, evt);
30 | }
31 | };
32 |
33 | /**
34 | * Emit keyboard events on DOM elements without UI interaction.
35 | *
36 | * @param {DOM} node DOM node
37 | * @param {String} type Mouse event type we should trigger.
38 | * @api public
39 | */
40 | exports.keyboard = function keyboard(node, type, code) {
41 | var evt;
42 |
43 | if (node.dispatchEvent) {
44 | if ('function' === typeof KeyboardEvent) {
45 | // Chrome, Safari, Firefox
46 | evt = new KeyboardEvent(type, {
47 | bubbles: true,
48 | cancelable: true
49 | });
50 | } else {
51 | // PhantomJS (wat!)
52 | evt = document.createEvent('KeyboardEvent');
53 | evt.initEvent(type, true, true);
54 | }
55 |
56 | evt.keyCode = code;
57 | return node.dispatchEvent(evt);
58 | } else {
59 | // IE 8
60 | evt = document.createEventObject('KeyboardEvent');
61 | evt.keyCode = code;
62 | return node.fireEvent('on'+ type, evt);
63 | }
64 | };
65 |
--------------------------------------------------------------------------------