diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..99692cd --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react","es2015"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..554dce6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/Accordion.js +node_modules +.DS_Store +npm-debug.log diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..90c978b --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +example/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ae7b119 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Glenn Flanagan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef284bb --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# React Responsive Accordion Component + +React component to wrap content in Accordion element. + +![Alt text](example/img/example.gif) + +This component is dependent on [React Collapsible](https://github.com/glennflanagan/react-collapsible) + + +## Installation +Installation can be achieved via NPM. +``` +npm install react-responsive-accordion +``` + +Alternatively just download the `Accordion.js` file form the `src` folder and include it in your project in your chosen way. + +## Usage +Collapsible can receive any HTML elements or React component as it's children. Collapsible will wrap the contents, as well as generate a trigger element which will control showing and hiding. + +### ES6 +```javascript + + +``` + +With a little CSS becomes + +![Alt text](example/img/becomes.png) + + +## Properties *(Options)* + +### `triggerText` | *string* | **required** +The text to appear in the trigger link. + +### `triggerTextWhenOpen` | *string* +Optional trigger text to change to when the Collapsible is open. + +### `transitionTime` | *number* | default: 400 +The number of milliseconds for the open/close transition to take. + +### `easing` | *string* | default: 'liner' +The CSS easing method you wish to apply to the open/close transition. This string can be any valid value of CSS `transition-timing-function`. For reference view the [MDN documentation](https://developer.mozilla.org/en/docs/Web/CSS/transition-timing-function). + +### `open` | *bool* | default: false +Set to true if you want the Collapsible to begin in the open state. You can also use this prop to manage the state from a parent component. + +### `classParentString` | *string* | default: Collapsible +Use this to overwrite the parent CSS class for the Collapsible component parts. Read more in the CSS section below. + +## CSS Styles +In theory you don't need any CSS to get this to work, but let's face it, it'd be pretty rubbish without it. + +By default the parent CSS class name is `.Collapsible` but this can be changed by setting the `classParentString` property on the component. + +The CSS class names follow a [type of BEM pattern](http://getbem.com/introduction/) of CSS naming. Below is a list of the CSS classes available on the component. + + +### `.Collapsible` +The parent element for the components. + +### `.Collapsible__trigger` +The trigger link that controls the opening and closing of the component. +The state of the component is also reflected on this element with the modifier classes; +- `is-closed` | Closed state +- `is-open` | Open setState + +### `.Collapsible__contentOuter` +The outer container that hides the content. This is set to `overflow: hidden` within the javascript but everything else about it is for you to change. + +### `.Collapsible__contentInner` +This is a container for the content passed into the compoenent. This keeps everything nice and neat and allows the component to do all it's whizzy calculations. + + +## Example +An example of the component in action is available in the example folder. To see it in action you can run `npm install` and then run `gulp`. This will compile all the JSX into JS and open the example page using BrowserSync. + +## Licence +React Responsive Collapsible Section Component is [MIT licensed](LICENSE.md) diff --git a/browserconfig.xml b/browserconfig.xml new file mode 100644 index 0000000..9078bfe --- /dev/null +++ b/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #00aba9 + + + diff --git a/example/_src/jsx/app.js b/example/_src/jsx/app.js new file mode 100644 index 0000000..c3f8753 --- /dev/null +++ b/example/_src/jsx/app.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './components/App'; + +ReactDOM.render( + , + document.getElementById('App') +); diff --git a/example/_src/jsx/components/App.js b/example/_src/jsx/components/App.js new file mode 100644 index 0000000..e5494a2 --- /dev/null +++ b/example/_src/jsx/components/App.js @@ -0,0 +1,46 @@ +import React from 'react'; +import Accordion from '../../../../src/Accordion'; + + +var App = React.createClass({ + + + render: function() { + return( +
+ + +
+

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+ +
+

Dolor sit amet, consectetur adipxercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+ +
+

Eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + +
+

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+ +
+

Dolor sit amet, consectetur adipxercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+ +
+

Eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+
+
+
+
+ +
+ ); + } + +}); + +export default App; diff --git a/example/_src/sass/base/_clearfix.scss b/example/_src/sass/base/_clearfix.scss new file mode 100644 index 0000000..40ad0ad --- /dev/null +++ b/example/_src/sass/base/_clearfix.scss @@ -0,0 +1,26 @@ +/** + * For modern browsers + * 1. The space content is one way to avoid an Opera bug when the + * contenteditable attribute is included anywhere else in the document. + * Otherwise it causes space to appear at the top and bottom of elements + * that are clearfixed. + * 2. The use of `table` rather than `block` is only necessary if using + * `:before` to contain the top-margins of child elements. + */ +.cf:before, +.cf:after { + content: " "; /* 1 */ + display: table; /* 2 */ +} + +.cf:after { + clear: both; +} + +/** + * For IE 6/7 only + * Include this rule to trigger hasLayout and contain floats. + */ +.cf { + *zoom: 1; +} \ No newline at end of file diff --git a/example/_src/sass/base/_mixins.scss b/example/_src/sass/base/_mixins.scss new file mode 100644 index 0000000..9b804e6 --- /dev/null +++ b/example/_src/sass/base/_mixins.scss @@ -0,0 +1,11 @@ +@function strip-unit($num) { + @return $num / ($num * 0 + 1); +} + + +@function rem($sizeValue: 1.6) { + + $value: strip-unit($sizeValue / $BASE_FONT_SIZE); + + @return $value + rem; +} \ No newline at end of file diff --git a/example/_src/sass/base/_reset.scss b/example/_src/sass/base/_reset.scss new file mode 100644 index 0000000..753d4b4 --- /dev/null +++ b/example/_src/sass/base/_reset.scss @@ -0,0 +1,54 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} \ No newline at end of file diff --git a/example/_src/sass/base/_variables.scss b/example/_src/sass/base/_variables.scss new file mode 100644 index 0000000..486c9b2 --- /dev/null +++ b/example/_src/sass/base/_variables.scss @@ -0,0 +1,12 @@ +$yellow: rgb(248, 159, 0); +$cyan: rgb(0, 172, 157); +$grey: rgb(51, 51, 51); +$black: rgb(38, 38, 38); +$base: rgb(255, 255, 255); +$lightGrey: rgb(235, 235, 235); + +$BASE_FONT_SIZE: 16px; + +$breakpointMega: 1600px; +$breakpointLarge: 990px; +$breakpointMed: 767px; diff --git a/example/_src/sass/components/_Collapsible.scss b/example/_src/sass/components/_Collapsible.scss new file mode 100644 index 0000000..0044444 --- /dev/null +++ b/example/_src/sass/components/_Collapsible.scss @@ -0,0 +1,52 @@ +// The main container element +.Collapsible { + background-color: $base; +} + + +//The content within the collaspable area +.Collapsible__contentInner { + padding: 10px; + border: 1px solid $lightGrey; + border-top: 0; + + p { + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + + &:last-child { + margin-bottom: 0; + } + } +} + +//The link which when clicked opens the collapsable area +.Collapsible__trigger { + display: block; + font-weight: 400; + text-decoration: none; + color: $grey; + position: relative; + border: 1px solid white; + padding: 10px; + background: $cyan; + color: white; + + + &:after { + font-family: 'FontAwesome'; + content: '\f107'; + position: absolute; + right: 10px; + top: 10px; + display: block; + transition: transform 300ms; + } + + &.is-open { + &:after { + transform: rotateZ(180deg); + } + } +} diff --git a/example/_src/sass/main.scss b/example/_src/sass/main.scss new file mode 100644 index 0000000..bee5c8f --- /dev/null +++ b/example/_src/sass/main.scss @@ -0,0 +1,37 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:400,700,700italic,400italic); +@import "base/reset"; +@import "base/clearfix"; +@import "base/variables"; +@import "base/mixins"; + +html { + font-size: $BASE_FONT_SIZE; +} + +body { + background-color: $grey; + font-family: 'Roboto', sans-serif; + padding-top: 50px; +} + +h1 { + color: white; + text-align: center; + font-size: 30px; + margin-bottom: 50px; +} + +h2 { + font-weight: 800; + text-transform: uppercase; + margin-top: 20px; + margin-bottom: 20px; +} + +#App { + max-width: 960px; + margin-left: auto; + margin-right: auto; +} + +@import "components/Collapsible"; diff --git a/example/css/main.css b/example/css/main.css new file mode 100644 index 0000000..e40b268 --- /dev/null +++ b/example/css/main.css @@ -0,0 +1,138 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:400,700,700italic,400italic); +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; } + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; } + +body { + line-height: 1; } + +ol, ul { + list-style: none; } + +blockquote, q { + quotes: none; } + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; } + +table { + border-collapse: collapse; + border-spacing: 0; } + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +/** + * For modern browsers + * 1. The space content is one way to avoid an Opera bug when the + * contenteditable attribute is included anywhere else in the document. + * Otherwise it causes space to appear at the top and bottom of elements + * that are clearfixed. + * 2. The use of `table` rather than `block` is only necessary if using + * `:before` to contain the top-margins of child elements. + */ +.cf:before, +.cf:after { + content: " "; + /* 1 */ + display: table; + /* 2 */ } + +.cf:after { + clear: both; } + +/** + * For IE 6/7 only + * Include this rule to trigger hasLayout and contain floats. + */ +.cf { + *zoom: 1; } + +html { + font-size: 16px; } + +body { + background-color: #333333; + font-family: 'Roboto', sans-serif; + padding-top: 50px; } + +h1 { + color: white; + text-align: center; + font-size: 30px; + margin-bottom: 50px; } + +h2 { + font-weight: 800; + text-transform: uppercase; + margin-top: 20px; + margin-bottom: 20px; } + +#App { + max-width: 960px; + margin-left: auto; + margin-right: auto; } + +.Collapsible { + background-color: white; } + +.Collapsible__contentInner { + padding: 10px; + border: 1px solid #ebebeb; + border-top: 0; } + .Collapsible__contentInner p { + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; } + .Collapsible__contentInner p:last-child { + margin-bottom: 0; } + +.Collapsible__trigger { + display: block; + font-weight: 400; + text-decoration: none; + color: #333333; + position: relative; + border: 1px solid white; + padding: 10px; + background: #00ac9d; + color: white; } + .Collapsible__trigger:after { + font-family: 'FontAwesome'; + content: '\f107'; + position: absolute; + right: 10px; + top: 10px; + display: block; + transition: transform 300ms; } + .Collapsible__trigger.is-open:after { + transform: rotateZ(180deg); } diff --git a/example/img/becomes.png b/example/img/becomes.png new file mode 100644 index 0000000..034f299 Binary files /dev/null and b/example/img/becomes.png differ diff --git a/example/img/example.gif b/example/img/example.gif new file mode 100644 index 0000000..df1aecd Binary files /dev/null and b/example/img/example.gif differ diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..982fdae --- /dev/null +++ b/example/index.html @@ -0,0 +1,19 @@ + + + + + + React Responsive Accordion Component Example + + + + + + + + +

React Responsive Accordion Component Example

+
+ + + diff --git a/example/js/app.js b/example/js/app.js new file mode 100644 index 0000000..0f6db6a --- /dev/null +++ b/example/js/app.js @@ -0,0 +1,19418 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o camelize('background-color') + * < "backgroundColor" + * + * @param {string} string + * @return {string} + */ +function camelize(string) { + return string.replace(_hyphenPattern, function (_, character) { + return character.toUpperCase(); + }); +} + +module.exports = camelize; +},{}],6:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule camelizeStyleName + * @typechecks + */ + +'use strict'; + +var camelize = require('./camelize'); + +var msPattern = /^-ms-/; + +/** + * Camelcases a hyphenated CSS property name, for example: + * + * > camelizeStyleName('background-color') + * < "backgroundColor" + * > camelizeStyleName('-moz-transition') + * < "MozTransition" + * > camelizeStyleName('-ms-transition') + * < "msTransition" + * + * As Andi Smith suggests + * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix + * is converted to lowercase `ms`. + * + * @param {string} string + * @return {string} + */ +function camelizeStyleName(string) { + return camelize(string.replace(msPattern, 'ms-')); +} + +module.exports = camelizeStyleName; +},{"./camelize":5}],7:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule containsNode + * @typechecks + */ + +'use strict'; + +var isTextNode = require('./isTextNode'); + +/*eslint-disable no-bitwise */ + +/** + * Checks if a given DOM node contains or is another DOM node. + * + * @param {?DOMNode} outerNode Outer DOM node. + * @param {?DOMNode} innerNode Inner DOM node. + * @return {boolean} True if `outerNode` contains or is `innerNode`. + */ +function containsNode(_x, _x2) { + var _again = true; + + _function: while (_again) { + var outerNode = _x, + innerNode = _x2; + _again = false; + + if (!outerNode || !innerNode) { + return false; + } else if (outerNode === innerNode) { + return true; + } else if (isTextNode(outerNode)) { + return false; + } else if (isTextNode(innerNode)) { + _x = outerNode; + _x2 = innerNode.parentNode; + _again = true; + continue _function; + } else if (outerNode.contains) { + return outerNode.contains(innerNode); + } else if (outerNode.compareDocumentPosition) { + return !!(outerNode.compareDocumentPosition(innerNode) & 16); + } else { + return false; + } + } +} + +module.exports = containsNode; +},{"./isTextNode":20}],8:[function(require,module,exports){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule createArrayFromMixed + * @typechecks + */ + +'use strict'; + +var toArray = require('./toArray'); + +/** + * Perform a heuristic test to determine if an object is "array-like". + * + * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?" + * Joshu replied: "Mu." + * + * This function determines if its argument has "array nature": it returns + * true if the argument is an actual array, an `arguments' object, or an + * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()). + * + * It will return false for other array-like objects like Filelist. + * + * @param {*} obj + * @return {boolean} + */ +function hasArrayNature(obj) { + return( + // not null/false + !!obj && ( + // arrays are objects, NodeLists are functions in Safari + typeof obj == 'object' || typeof obj == 'function') && + // quacks like an array + 'length' in obj && + // not window + !('setInterval' in obj) && + // no DOM node should be considered an array-like + // a 'select' element has 'length' and 'item' properties on IE8 + typeof obj.nodeType != 'number' && ( + // a real array + Array.isArray(obj) || + // arguments + 'callee' in obj || + // HTMLCollection/NodeList + 'item' in obj) + ); +} + +/** + * Ensure that the argument is an array by wrapping it in an array if it is not. + * Creates a copy of the argument if it is already an array. + * + * This is mostly useful idiomatically: + * + * var createArrayFromMixed = require('createArrayFromMixed'); + * + * function takesOneOrMoreThings(things) { + * things = createArrayFromMixed(things); + * ... + * } + * + * This allows you to treat `things' as an array, but accept scalars in the API. + * + * If you need to convert an array-like object, like `arguments`, into an array + * use toArray instead. + * + * @param {*} obj + * @return {array} + */ +function createArrayFromMixed(obj) { + if (!hasArrayNature(obj)) { + return [obj]; + } else if (Array.isArray(obj)) { + return obj.slice(); + } else { + return toArray(obj); + } +} + +module.exports = createArrayFromMixed; +},{"./toArray":28}],9:[function(require,module,exports){ +(function (process){ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule createNodesFromMarkup + * @typechecks + */ + +/*eslint-disable fb-www/unsafe-html*/ + +'use strict'; + +var ExecutionEnvironment = require('./ExecutionEnvironment'); + +var createArrayFromMixed = require('./createArrayFromMixed'); +var getMarkupWrap = require('./getMarkupWrap'); +var invariant = require('./invariant'); + +/** + * Dummy container used to render all markup. + */ +var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElement('div') : null; + +/** + * Pattern used by `getNodeName`. + */ +var nodeNamePattern = /^\s*<(\w+)/; + +/** + * Extracts the `nodeName` of the first element in a string of markup. + * + * @param {string} markup String of markup. + * @return {?string} Node name of the supplied markup. + */ +function getNodeName(markup) { + var nodeNameMatch = markup.match(nodeNamePattern); + return nodeNameMatch && nodeNameMatch[1].toLowerCase(); +} + +/** + * Creates an array containing the nodes rendered from the supplied markup. The + * optionally supplied `handleScript` function will be invoked once for each + *