diff --git a/.gitignore b/.gitignore index 00cbbdf..c1a4f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ typings/ # dotenv environment variables file .env +example/bundle.js +example/bundle.js.map \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b68090e --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +example/ +docs/ \ No newline at end of file diff --git a/README.md b/README.md index 4f79efe..44bb56c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,111 @@ # react-giphy-selector -A search modal for picking the perfect giphy. +A search and select React.JS component for picking the perfect giphy. + +![Example selector](./docs/example_1.gif) + +> This component is highly-customizable and only provides basic styling out-of-box. The example above includes simple customization to a few elements. You can view this example in `/example/src`. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) + +## Installation + +You just install `react-giphy-selector` the good ole' fashion way through NPM: + +``` +npm install --save react-giphy-selector +``` + +## Usage + +This package exports the `Selector` React component and then two helper `enums`: + +```js +import {Selector, ResultSort, Rating} from "react-giphy-selector"; + +``` + +- [Selector](#selector) +- [Rating](#rating) +- [ResultSort](#resultSort) + +### Selector + +The selector component contains all of the search, display, and selection logic. The only required properties are `apiKey` and `onGifSelected`. + +```jsx + +``` + +That said, there are a bunch of props that allow you to make this component your own. Note: the `?` included at the end of a property name denotes it as optional. + +- `apiKey: string`: [Your Giphy Project API Key](https://developers.giphy.com/). +- `onGifSelected?: (gifObject: IGifObject) => void`: The function to fire when a gif search result has been selected. The `IGifObject` represents the full [GIF Object](https://developers.giphy.com/docs/#gif-object) returned via the Giphy API. +- `rating?: Rating`: The maximum rating you want to allow in your search results. Use the exported [Rating](#rating) enum for help. Default: `Rating.G`. +- `sort?: ResultSort`: The sort order of the search results. Use the helper enum [ResultSort](#resultsort). Default: `ResultSort.Relevant`. +- `limit?: number`: The number of results to return. Default: `20`. +- `suggestions?: string[]`: An array containing one-click searches to make it easy for your user. Will not show suggestions section if none are passed. Default: `[]`. +- `queryInputPlaceholder?: string`: The placeholder text for the search bar text input. Default `'Enter search text'`. +- `resultColumns?: number`: The number of columns to divide the search results into. Default: `3`. +- `showGiphyMark?: boolean`: Indicates whether to show the "powered by Giphy" mark in the selector. This is required when [using a Giphy Production API Key](https://developers.giphy.com/docs/#production-key). Default: `true`. + +#### Styling your Selector + +There are a bunch of `props` to help you customize the style of the the selector. Both the `className` and the `style` methods are available. `react-giphy-selector` is very intentionally unopinionated about how exactly each section of the selector should look. Instead, the package offers a lot of customization and flexibility through the props below. + +The images below will help you understand the nomenclature of the components: + +![Diagram of component nomenclature for query form, suggestions, and footer](./docs/components_1.png) +![Diagram of component nomenclature for search results](./docs/components_2.png) + +Here are all the props available for styling the component: + +- `queryFormClassName?: string`: Additional `className` for the query form section of the component. You can find the default style applied in `src/components/QueryForm.css`. +- `queryFormInputClassName?: string`: Additional `className` for the text input in the query form. You can find the default style applied in `src/components/QueryForm.css`. +- `queryFormSubmitClassName?: string`: Additional `className` for the submit button in the query form. You can find the default style applied in `src/components/QueryForm.css`. +- `queryFormStyle?: object`: A style object to add to the query form style. You can find the default style applied in `src/components/QueryForm.css`. +- `queryFormInputStyle?: object`: A style object to add to the text input in the query form. You can find the default style applied in `src/components/QueryForm.css`. +- `queryFormSubmitStyle?: object`: A style object to add to the submit button in the query form. You can find the default style applied in `src/components/QueryForm.css`. +- `queryFormSubmitContent?: string or Component`: You can pass in a `string` or your own component to render inside the submit button in the query form. This allows you to pass in things like custom icons. Default: `'Search'`. +- `searchResultsClassName?: string`: Additional `className` for the search results component. You can find the default style in `src/components/SearchResults.css`. +- `searchResultsStyle?: object`: A style object to the add to the search results container. You can find the default style in `src/components/SearchResults.css`. +- `searchResultClassName?: string`: Additional `className` to add to a search result. Search results are `a` elements. You can find the default style in `src/components/SearchResult.css`. +- `searchResultStyle?: object`: A style object to add to a search result. Search results are `a` elements. You can find the default style in `src/components/SearchResult.css`. +- `suggestionsClassName?: string`: Additional `className` to add to the suggestions container. You can find the default style in `src/components/Suggestions.css`. +- `suggestionsStyle?: object`: A style object to add to the suggestions container. You can find the default style in `src/components/Suggestions.css`. +- `suggestionClassName?: string`: Additional `className` to add to a suggestion. This is an `a` element. You can find the default style in `src/components/Suggestion.css`. +- `suggestionStyle?: object`: A style object to add to a suggestion. This is an `a` element. You can find the default style in `src/components/Suggestion.css`. +- `loaderClassName?: string`: Additional `className` to add to the loader container. You can find the default style in `src/components/Selector.css`. +- `loaderStyle?: object`: A style object to add to the loader container. You can find the default style in `src/components/Selector.css`. +- `loaderContent?: string or Component`: You can pass in a `string` or customer component to display when results are loading. Default `'Loading'...`. +- `searchErrorClassName?: string`: Additional `className` to add to the error message shown on broken searches. You can find the default style in `src/components/Selector.css`. +- `searchErrorStyle?: object`: A style object to add to the error message shown on broken searches. You can find the default style in `src/components/Selector.css`. +- `footerClassName?: string`: Additional `className` to add to footer of selector. You can find the default style in `src/components/Selector.css`. +- `footerStyle?: object`: A style object to add to footer of selector. You can find the default style in `src/components/Selector.css`. + +If you have a cool style you'd like to share, please [make an issue](https://github.com/tshaddix/react-giphy-selector/issues). + +### Rating + +The `Rating` enum contains all the possible ratings you can limit searches to: + +```js +Rating.Y +Rating.G +Rating.PG +Rating.PG13 +Rating.R +``` + +### ResultSort + +The `ResultSort` enum contains the different sort methods supported by the Giphy API. + +```js +ResultSort.Recent // ordered by most recent +ResultSort.Relevant // ordered by relevance +``` \ No newline at end of file diff --git a/docs/components_1.png b/docs/components_1.png new file mode 100644 index 0000000..1f4aa48 Binary files /dev/null and b/docs/components_1.png differ diff --git a/docs/components_2.png b/docs/components_2.png new file mode 100644 index 0000000..41cc4f3 Binary files /dev/null and b/docs/components_2.png differ diff --git a/docs/example_1.gif b/docs/example_1.gif new file mode 100644 index 0000000..c0592f3 Binary files /dev/null and b/docs/example_1.gif differ diff --git a/example/example.html b/example/example.html new file mode 100644 index 0000000..4e7953b --- /dev/null +++ b/example/example.html @@ -0,0 +1,13 @@ + + + + + React Giphy Selector - Example + + +
+ + + + + \ No newline at end of file diff --git a/example/src/example.css b/example/src/example.css new file mode 100644 index 0000000..6420ff1 --- /dev/null +++ b/example/src/example.css @@ -0,0 +1,39 @@ +.customQueryFormSubmit { + background: #0097cf; + color: #fff; + border: 0; + padding: 10px 20px; + font-size: 16px; + font-weight: bold; +} + +.customQueryFormInput { + padding-left: 10px; + padding-right: 10px; + border: 1px solid #e0e0e0; + border-right: 0; + box-shadow: none; +} + +.customSearchResults { + max-height: 400px; + margin-top: 10px; + margin-bottom: 10px; +} + +/******************************** +CSS classes for example fake modal +********************************/ + +body { + background: #333; + font-family: sans-serif; +} + +.modal { + max-width: 600px; + margin: 60px auto; + background: #fff; + padding: 20px; + border-bottom: 5px solid #111; +} \ No newline at end of file diff --git a/example/src/example.tsx b/example/src/example.tsx new file mode 100644 index 0000000..8d9541b --- /dev/null +++ b/example/src/example.tsx @@ -0,0 +1,98 @@ +import { Selector, Rating } from "../../lib"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +const customStyle = require("./example.css"); + +// feel free to change these :) +const suggestions = [ + "watching", + "quiz", + "stop it", + "nice one", + "learning", + "reading", + "working" +]; + +interface IExampleProps { + suggestions: string[]; +} + +interface IExampleState { + apiKey: string; + isKeySubmitted: boolean; +} + +class ExampleApp extends React.Component { + state: IExampleState; + + constructor(props: IExampleProps) { + super(props); + + this.state = { + apiKey: "", + isKeySubmitted: false + }; + + this.onKeyChange = this.onKeyChange.bind(this); + this.onKeySubmit = this.onKeySubmit.bind(this); + this.onGifSelected = this.onGifSelected.bind(this); + } + + public onKeyChange(event: any): void { + this.setState({ apiKey: event.target.value }); + } + + public onKeySubmit(event: any): void { + event.preventDefault(); + + this.setState({ + isKeySubmitted: true + }); + } + + public onGifSelected(gifObject: any): void { + alert(`You selected a gif! id: ${gifObject.id}`); + } + + public render(): JSX.Element { + const { apiKey, isKeySubmitted } = this.state; + const { suggestions } = this.props; + + if (!isKeySubmitted) { + return ( +
+ + +
+ ); + } + + return ( +
+ +
+ ); + } +} + +ReactDOM.render( + , + document.getElementById("example") +); diff --git a/example/webpack.config.js b/example/webpack.config.js new file mode 100644 index 0000000..d63b8c4 --- /dev/null +++ b/example/webpack.config.js @@ -0,0 +1,30 @@ +module.exports = { + entry: "./example/src/example.tsx", + output: { + filename: "bundle.js", + path: __dirname + }, + + // Enable sourcemaps for debugging webpack's output. + devtool: "source-map", + + resolve: { + // Add '.ts' and '.tsx' as resolvable extensions. + extensions: [".ts", ".tsx", ".js", ".json"] + }, + + module: { + rules: [ + // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. + { test: /\.tsx?$/, loader: "awesome-typescript-loader" }, + + // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. + { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }, + + { + test: /\.css$/, + loader: ["style-loader", "css-loader?sourceMap&modules"] + } + ] + } +}; \ No newline at end of file diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..fbc5b27 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,2 @@ +export {Rating, ResultSort, IGifImage, IGifObject} from './src/types'; +export {Selector, ISelectorProps} from './src/components/Selector'; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..0cd9be8 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,23013 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["ReactGiphySelector"] = factory(); + else + root["ReactGiphySelector"] = factory(); +})(typeof self !== 'undefined' ? self : this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 14); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; +process.prependListener = noop; +process.prependOnceListener = noop; + +process.listeners = function (name) { return [] } + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/* WEBPACK VAR INJECTION */(function(process) { + +if (process.env.NODE_ENV === 'production') { + module.exports = __webpack_require__(16); +} else { + module.exports = __webpack_require__(17); +} + +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(0))) + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +/* global define */ + +(function () { + 'use strict'; + + var hasOwn = {}.hasOwnProperty; + + function classNames () { + var classes = []; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + if (!arg) continue; + + var argType = typeof arg; + + if (argType === 'string' || argType === 'number') { + classes.push(arg); + } else if (Array.isArray(arg)) { + classes.push(classNames.apply(null, arg)); + } else if (argType === 'object') { + for (var key in arg) { + if (hasOwn.call(arg, key) && arg[key]) { + classes.push(key); + } + } + } + } + + return classes.join(' '); + } + + if (typeof module !== 'undefined' && module.exports) { + module.exports = classNames; + } else if (true) { + // register as 'classnames', consistent with npm package name + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = (function () { + return classNames; + }).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), + __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else { + window.classNames = classNames; + } +}()); + + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ +// css base code, injected by the css-loader +module.exports = function(useSourceMap) { + var list = []; + + // return the list of modules as css string + list.toString = function toString() { + return this.map(function (item) { + var content = cssWithMappingToString(item, useSourceMap); + if(item[2]) { + return "@media " + item[2] + "{" + content + "}"; + } else { + return content; + } + }).join(""); + }; + + // import a list of modules into the list + list.i = function(modules, mediaQuery) { + if(typeof modules === "string") + modules = [[null, modules, ""]]; + var alreadyImportedModules = {}; + for(var i = 0; i < this.length; i++) { + var id = this[i][0]; + if(typeof id === "number") + alreadyImportedModules[id] = true; + } + for(i = 0; i < modules.length; i++) { + var item = modules[i]; + // skip already imported module + // this implementation is not 100% perfect for weird media query combinations + // when a module is imported multiple times with different media queries. + // I hope this will never occur (Hey this way we have smaller bundles) + if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { + if(mediaQuery && !item[2]) { + item[2] = mediaQuery; + } else if(mediaQuery) { + item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; + } + list.push(item); + } + } + }; + return list; +}; + +function cssWithMappingToString(item, useSourceMap) { + var content = item[1] || ''; + var cssMapping = item[3]; + if (!cssMapping) { + return content; + } + + if (useSourceMap && typeof btoa === 'function') { + var sourceMapping = toComment(cssMapping); + var sourceURLs = cssMapping.sources.map(function (source) { + return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */' + }); + + return [content].concat(sourceURLs).concat([sourceMapping]).join('\n'); + } + + return [content].join('\n'); +} + +// Adapted from convert-source-map (MIT) +function toComment(sourceMap) { + // eslint-disable-next-line no-undef + var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); + var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64; + + return '/*# ' + data + ' */'; +} + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +var stylesInDom = {}; + +var memoize = function (fn) { + var memo; + + return function () { + if (typeof memo === "undefined") memo = fn.apply(this, arguments); + return memo; + }; +}; + +var isOldIE = memoize(function () { + // Test for IE <= 9 as proposed by Browserhacks + // @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805 + // Tests for existence of standard globals is to allow style-loader + // to operate correctly into non-standard environments + // @see https://github.com/webpack-contrib/style-loader/issues/177 + return window && document && document.all && !window.atob; +}); + +var getElement = (function (fn) { + var memo = {}; + + return function(selector) { + if (typeof memo[selector] === "undefined") { + var styleTarget = fn.call(this, selector); + // Special case to return head of iframe instead of iframe itself + if (styleTarget instanceof window.HTMLIFrameElement) { + try { + // This will throw an exception if access to iframe is blocked + // due to cross-origin restrictions + styleTarget = styleTarget.contentDocument.head; + } catch(e) { + styleTarget = null; + } + } + memo[selector] = styleTarget; + } + return memo[selector] + }; +})(function (target) { + return document.querySelector(target) +}); + +var singleton = null; +var singletonCounter = 0; +var stylesInsertedAtTop = []; + +var fixUrls = __webpack_require__(39); + +module.exports = function(list, options) { + if (typeof DEBUG !== "undefined" && DEBUG) { + if (typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment"); + } + + options = options || {}; + + options.attrs = typeof options.attrs === "object" ? options.attrs : {}; + + // Force single-tag solution on IE6-9, which has a hard limit on the # of