diff options
Diffstat (limited to 'packages/gitbook-core/src/components')
-rw-r--r-- | packages/gitbook-core/src/components/Backdrop.js | 56 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Button.js | 22 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/ButtonGroup.js | 23 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/ContextProvider.js | 34 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Dropdown.js | 126 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/HTMLContent.js | 77 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/HotKeys.js | 59 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/I18nProvider.js | 28 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Icon.js | 28 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Import.js | 48 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/InjectedComponent.js | 117 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Link.js | 37 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/PJAXWrapper.js | 102 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Panel.js | 22 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Tooltipped.js | 44 |
15 files changed, 823 insertions, 0 deletions
diff --git a/packages/gitbook-core/src/components/Backdrop.js b/packages/gitbook-core/src/components/Backdrop.js new file mode 100644 index 0000000..7b34b0d --- /dev/null +++ b/packages/gitbook-core/src/components/Backdrop.js @@ -0,0 +1,56 @@ +const React = require('react'); +const HotKeys = require('./HotKeys'); + +/** + * Backdrop for modals, dropdown, etc. that covers the whole screen + * and handles click and pressing escape. + * + * <Backdrop onClose={onCloseModal} /> + */ +const Backdrop = React.createClass({ + propTypes: { + // Callback when backdrop is closed + onClose: React.PropTypes.func.isRequired, + // Z-index for the backdrop + zIndex: React.PropTypes.number, + children: React.PropTypes.node + }, + + getDefaultProps() { + return { + zIndex: 200 + }; + }, + + onClick(event) { + const { onClose } = this.props; + + event.preventDefault(); + event.stopPropagation(); + + onClose(); + }, + + render() { + const { zIndex, children, onClose } = this.props; + const style = { + zIndex, + position: 'fixed', + top: 0, + right: 0, + width: '100%', + height: '100%' + }; + const keyMap = { + 'escape': onClose + }; + + return ( + <HotKeys keyMap={keyMap}> + <div style={style} onClick={this.onClick}>{children}</div> + </HotKeys> + ); + } +}); + +module.exports = Backdrop; diff --git a/packages/gitbook-core/src/components/Button.js b/packages/gitbook-core/src/components/Button.js new file mode 100644 index 0000000..4d929b8 --- /dev/null +++ b/packages/gitbook-core/src/components/Button.js @@ -0,0 +1,22 @@ +const React = require('react'); +const classNames = require('classnames'); + +const Button = React.createClass({ + propTypes: { + active: React.PropTypes.bool, + className: React.PropTypes.string, + children: React.PropTypes.node, + onClick: React.PropTypes.func + }, + + render() { + const { children, active, onClick } = this.props; + const className = classNames('GitBook-Button', this.props.className, { + active + }); + + return <button className={className} onClick={onClick}>{children}</button>; + } +}); + +module.exports = Button; diff --git a/packages/gitbook-core/src/components/ButtonGroup.js b/packages/gitbook-core/src/components/ButtonGroup.js new file mode 100644 index 0000000..4c20b68 --- /dev/null +++ b/packages/gitbook-core/src/components/ButtonGroup.js @@ -0,0 +1,23 @@ +const React = require('react'); +const classNames = require('classnames'); + +const ButtonGroup = React.createClass({ + propTypes: { + className: React.PropTypes.string, + children: React.PropTypes.node, + onClick: React.PropTypes.func + }, + + render() { + let { className, children } = this.props; + + className = classNames( + 'GitBook-ButtonGroup', + className + ); + + return <div className={className}>{children}</div>; + } +}); + +module.exports = ButtonGroup; diff --git a/packages/gitbook-core/src/components/ContextProvider.js b/packages/gitbook-core/src/components/ContextProvider.js new file mode 100644 index 0000000..96a44e3 --- /dev/null +++ b/packages/gitbook-core/src/components/ContextProvider.js @@ -0,0 +1,34 @@ +const React = require('react'); +const { Provider } = require('react-redux'); + +const ContextShape = require('../propTypes/Context'); + +/** + * React component to provide a GitBook context to children components. + */ + +const ContextProvider = React.createClass({ + propTypes: { + context: ContextShape.isRequired, + children: React.PropTypes.node + }, + + childContextTypes: { + gitbook: ContextShape + }, + + getChildContext() { + const { context } = this.props; + + return { + gitbook: context + }; + }, + + render() { + const { context, children } = this.props; + return <Provider store={context.store}>{children}</Provider>; + } +}); + +module.exports = ContextProvider; diff --git a/packages/gitbook-core/src/components/Dropdown.js b/packages/gitbook-core/src/components/Dropdown.js new file mode 100644 index 0000000..83a377f --- /dev/null +++ b/packages/gitbook-core/src/components/Dropdown.js @@ -0,0 +1,126 @@ +const React = require('react'); +const classNames = require('classnames'); + +/** + * Dropdown to display a menu + * + * <Dropdown.Container> + * + * <Button /> + * + * <Dropdown.Menu> + * <Dropdown.Item href={...}> ... </Dropdown.Item> + * <Dropdown.Item onClick={...}> ... </Dropdown.Item> + * </Dropdown.Menu> + * </Dropdown.Container> + */ + +const DropdownContainer = React.createClass({ + propTypes: { + className: React.PropTypes.string, + children: React.PropTypes.node + }, + + render() { + let { className, children } = this.props; + + className = classNames( + 'GitBook-Dropdown', + className + ); + + return ( + <div className={className}> + {children} + </div> + ); + } +}); + +/** + * A dropdown item which can contains informations. + */ +const DropdownItem = React.createClass({ + propTypes: { + children: React.PropTypes.node + }, + + render() { + const { children } = this.props; + + return ( + <div className="GitBook-DropdownItem"> + {children} + </div> + ); + } +}); + + +/** + * A dropdown item, which is always a link. + */ +const DropdownItemLink = React.createClass({ + propTypes: { + children: React.PropTypes.node, + onClick: React.PropTypes.func, + href: React.PropTypes.string + }, + + onClick(event) { + const { onClick, href } = this.props; + + if (href) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (onClick) { + onClick(); + } + }, + + render() { + const { children, href, ...otherProps } = this.props; + + return ( + <a {...otherProps} className="GitBook-DropdownItemLink" href={href || '#'} onClick={this.onClick} > + {children} + </a> + ); + } +}); + + +/** + * A DropdownMenu to display DropdownItems. Must be inside a + * DropdownContainer. + */ +const DropdownMenu = React.createClass({ + propTypes: { + className: React.PropTypes.string, + children: React.PropTypes.node + }, + + render() { + let { className, children } = this.props; + className = classNames('GitBook-DropdownMenu', className); + + return ( + <div className={className}> + {children} + </div> + ); + } +}); + +const Dropdown = { + Item: DropdownItem, + ItemLink: DropdownItemLink, + Menu: DropdownMenu, + Container: DropdownContainer +}; + +module.exports = Dropdown; diff --git a/packages/gitbook-core/src/components/HTMLContent.js b/packages/gitbook-core/src/components/HTMLContent.js new file mode 100644 index 0000000..9d15398 --- /dev/null +++ b/packages/gitbook-core/src/components/HTMLContent.js @@ -0,0 +1,77 @@ +const React = require('react'); +const ReactSafeHtml = require('react-safe-html'); +const { DOMProperty } = require('react-dom/lib/ReactInjection'); +const htmlTags = require('html-tags'); +const entities = require('entities'); + +const { InjectedComponent } = require('./InjectedComponent'); + +DOMProperty.injectDOMPropertyConfig({ + Properties: { + align: DOMProperty.MUST_USE_ATTRIBUTE + }, + isCustomAttribute: (attributeName) => { + return attributeName === 'align'; + } +}); + +/* + HTMLContent is a container for the page HTML that parse the content and + render the right block. + All html elements can be extended using the injected component. + */ + +function inject(injectedProps, Component) { + return (props) => { + const cleanProps = { + ...props, + className: props['class'] + }; + delete cleanProps['class']; + + return ( + <InjectedComponent {...injectedProps(cleanProps)}> + <Component {...cleanProps} /> + </InjectedComponent> + ); + }; +} + +const COMPONENTS = { + // Templating blocks are exported as <xblock name="youtube" props="{}" /> + 'xblock': inject( + ({name, props}) => { + props = entities.decodeHTML(props); + return { + matching: { role: `block:${name}` }, + props: JSON.parse(props) + }; + }, + props => <div {...props} /> + ) +}; + +htmlTags.forEach(tag => { + COMPONENTS[tag] = inject( + props => { + return { + matching: { role: `html:${tag}` }, + props + }; + }, + props => React.createElement(tag, props) + ); +}); + +const HTMLContent = React.createClass({ + propTypes: { + html: React.PropTypes.string.isRequired + }, + + render() { + const { html } = this.props; + return <ReactSafeHtml html={html} components={COMPONENTS} />; + } +}); + +module.exports = HTMLContent; diff --git a/packages/gitbook-core/src/components/HotKeys.js b/packages/gitbook-core/src/components/HotKeys.js new file mode 100644 index 0000000..e2a8154 --- /dev/null +++ b/packages/gitbook-core/src/components/HotKeys.js @@ -0,0 +1,59 @@ +const React = require('react'); +const Mousetrap = require('mousetrap'); +const { Map } = require('immutable'); + +/** + * Defines hotkeys globally when this component is mounted. + * + * keyMap = { + * 'escape': (e) => quit() + * 'mod+s': (e) => save() + * } + * + * <HotKeys keyMap={keyMap}> + * < ... /> + * </HotKeys> + */ + +const HotKeys = React.createClass({ + propTypes: { + children: React.PropTypes.node.isRequired, + keyMap: React.PropTypes.objectOf(React.PropTypes.func) + }, + + getDefaultProps() { + return { keyMap: {} }; + }, + + updateBindings(keyMap) { + Map(keyMap).forEach((handler, key) => { + Mousetrap.bind(key, handler); + }); + }, + + clearBindings(keyMap) { + Map(keyMap).forEach((handler, key) => { + Mousetrap.unbind(key, handler); + }); + }, + + componentDidMount() { + this.updateBindings(this.props.keyMap); + }, + + componentDidUpdate(prevProps) { + this.clearBindings(prevProps.keyMap); + this.updateBindings(this.props.keyMap); + }, + + componentWillUnmount() { + this.clearBindings(this.props.keyMap); + }, + + render() { + // Simply render the only child + return React.Children.only(this.props.children); + } +}); + +module.exports = HotKeys; diff --git a/packages/gitbook-core/src/components/I18nProvider.js b/packages/gitbook-core/src/components/I18nProvider.js new file mode 100644 index 0000000..b6b2d0f --- /dev/null +++ b/packages/gitbook-core/src/components/I18nProvider.js @@ -0,0 +1,28 @@ +const { Map } = require('immutable'); +const React = require('react'); +const intl = require('react-intl'); +const ReactRedux = require('react-redux'); + +const I18nProvider = React.createClass({ + propTypes: { + children: React.PropTypes.node, + messages: React.PropTypes.object + }, + + render() { + let { messages } = this.props; + messages = messages.get('en', Map()).toJS(); + + return ( + <intl.IntlProvider locale={'en'} messages={messages}> + {this.props.children} + </intl.IntlProvider> + ); + } +}); + +const mapStateToProps = state => { + return { messages: state.i18n.messages }; +}; + +module.exports = ReactRedux.connect(mapStateToProps)(I18nProvider); diff --git a/packages/gitbook-core/src/components/Icon.js b/packages/gitbook-core/src/components/Icon.js new file mode 100644 index 0000000..5f2c751 --- /dev/null +++ b/packages/gitbook-core/src/components/Icon.js @@ -0,0 +1,28 @@ +const React = require('react'); + +const Icon = React.createClass({ + propTypes: { + id: React.PropTypes.string, + type: React.PropTypes.string, + className: React.PropTypes.string + }, + + getDefaultProps() { + return { + type: 'fa' + }; + }, + + render() { + const { id, type } = this.props; + let { className } = this.props; + + if (id) { + className = 'GitBook-Icon ' + type + ' ' + type + '-' + id; + } + + return <i className={className}/>; + } +}); + +module.exports = Icon; diff --git a/packages/gitbook-core/src/components/Import.js b/packages/gitbook-core/src/components/Import.js new file mode 100644 index 0000000..68318b9 --- /dev/null +++ b/packages/gitbook-core/src/components/Import.js @@ -0,0 +1,48 @@ +const React = require('react'); +const Head = require('react-helmet'); +const ReactRedux = require('react-redux'); + +/** + * Resolve a file url to a relative url in current state + * @param {String} href + * @param {State} state + * @return {String} + */ +function resolveForCurrentFile(href, state) { + const { file } = state; + return file.relative(href); +} + +const ImportLink = ReactRedux.connect((state, {rel, href}) => { + href = resolveForCurrentFile(href, state); + + return { + link: [ + { + rel, + href + } + ] + }; +})(Head); + +const ImportScript = ReactRedux.connect((state, {type, src}) => { + src = resolveForCurrentFile(src, state); + + return { + script: [ + { + type, + src + } + ] + }; +})(Head); + +const ImportCSS = props => <ImportLink rel="stylesheet" {...props} />; + +module.exports = { + ImportLink, + ImportScript, + ImportCSS +}; diff --git a/packages/gitbook-core/src/components/InjectedComponent.js b/packages/gitbook-core/src/components/InjectedComponent.js new file mode 100644 index 0000000..097edaf --- /dev/null +++ b/packages/gitbook-core/src/components/InjectedComponent.js @@ -0,0 +1,117 @@ +const React = require('react'); +const ReactRedux = require('react-redux'); +const { List } = require('immutable'); + +const { findMatchingComponents } = require('../actions/components'); + +/* + Public: InjectedComponent makes it easy to include a set of dynamically registered + components inside of your React render method. Rather than explicitly render + an array of buttons, for example, you can use InjectedComponentSet: + + ```js + <InjectedComponentSet className="message-actions" + matching={{role: 'ThreadActionButton'}} + props={{ a: 1 }}> + ``` + + InjectedComponentSet will look up components registered for the location you provide, + render them inside a {Flexbox} and pass them `exposedProps`. By default, all injected + children are rendered inside {UnsafeComponent} wrappers to prevent third-party code + from throwing exceptions that break React renders. + + InjectedComponentSet monitors the ComponentStore for changes. If a new component + is registered into the location you provide, InjectedComponentSet will re-render. + If no matching components is found, the InjectedComponent renders an empty span. + */ + +const Injection = React.createClass({ + propTypes: { + component: React.PropTypes.func, + props: React.PropTypes.object, + children: React.PropTypes.node + }, + + render() { + const Comp = this.props.component; + const { props, children } = this.props; + + // TODO: try to render with an error handling for unsafe component + return <Comp {...props}>{children}</Comp>; + } +}); + +const InjectedComponentSet = React.createClass({ + propTypes: { + components: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.func), + React.PropTypes.instanceOf(List) + ]).isRequired, + props: React.PropTypes.object, + children: React.PropTypes.node + }, + + render() { + const { components, props, children, ...divProps } = this.props; + + const inner = components.map((Comp, i) => <Injection key={i} component={Comp} props={props} />); + + return ( + <div {...divProps}> + {children} + {inner} + </div> + ); + } +}); + +/** + * Render only the first component matching + */ +const InjectedComponent = React.createClass({ + propTypes: { + components: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.func), + React.PropTypes.instanceOf(List) + ]).isRequired, + props: React.PropTypes.object, + children: React.PropTypes.node + }, + + render() { + let { components, props, children } = this.props; + + if (!children) { + children = null; + } else { + children = React.Children.only(children); + } + + return components.reduce((inner, Comp) => { + return ( + <Injection component={Comp} props={props}> + {inner} + </Injection> + ); + }, children); + } +}); + +/** + * Map Redux state to InjectedComponentSet's props + */ +function mapStateToProps(state, props) { + const { components } = state; + const { matching } = props; + + return { + components: findMatchingComponents(components, matching) + }; +} + +const connect = ReactRedux.connect(mapStateToProps); + +module.exports = { + InjectedComponent: connect(InjectedComponent), + InjectedComponentSet: connect(InjectedComponentSet) +}; diff --git a/packages/gitbook-core/src/components/Link.js b/packages/gitbook-core/src/components/Link.js new file mode 100644 index 0000000..ab364bb --- /dev/null +++ b/packages/gitbook-core/src/components/Link.js @@ -0,0 +1,37 @@ +const React = require('react'); +const ReactRedux = require('react-redux'); + +const File = require('../models/File'); +const SummaryArticle = require('../models/SummaryArticle'); +const SummaryArticleShape = require('../propTypes/SummaryArticle'); +const FileShape = require('../propTypes/File'); + +const Link = React.createClass({ + propTypes: { + currentFile: FileShape, + children: React.PropTypes.node, + + // Destination of the link + to: React.PropTypes.oneOfType([ + React.PropTypes.string, + SummaryArticleShape, + FileShape + ]) + }, + + render() { + const { currentFile, to, children, ...props } = this.props; + let href = to; + + if (SummaryArticle.is(to) || File.is(to)) { + href = to.url; + } + + href = currentFile.relative(href); + return <a href={href} {...props}>{children}</a>; + } +}); + +module.exports = ReactRedux.connect(state => { + return { currentFile: state.file }; +})(Link); diff --git a/packages/gitbook-core/src/components/PJAXWrapper.js b/packages/gitbook-core/src/components/PJAXWrapper.js new file mode 100644 index 0000000..6ed0697 --- /dev/null +++ b/packages/gitbook-core/src/components/PJAXWrapper.js @@ -0,0 +1,102 @@ +const React = require('react'); +const ReactRedux = require('react-redux'); +const History = require('../actions/history'); + +/** + * Check if an element is inside a link + * @param {DOMElement} el + * @param {String} name + * @return {DOMElement|undefined + */ +function findParentByTagName(el, name) { + while (el && el !== document) { + if (el.tagName && el.tagName.toUpperCase() === name) { + return el; + } + el = el.parentNode; + } + + return false; +} + +/** + * Internal: Return the `href` component of given URL object with the hash + * portion removed. + * + * @param {Location|HTMLAnchorElement} location + * @return {String} + */ +function stripHash(location) { + return location.href.replace(/#.*/, ''); +} + +/** + * Test if a click event should be handled, + * return the new url if it's a normal lcick + */ +function getHrefForEvent(event) { + const link = findParentByTagName(event.target, 'A'); + + if (!link) + return; + + // Middle click, cmd click, and ctrl click should open + // links in a new tab as normal. + if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) + return; + + // Ignore cross origin links + if (location.protocol !== link.protocol || location.hostname !== link.hostname) + return; + + // Ignore case when a hash is being tacked on the current URL + if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location)) + return; + + // Ignore event with default prevented + if (event.defaultPrevented) + return; + + // Explicitly ignored + if (link.getAttribute('data-nopjax')) + return; + + return link.pathname; +} + +/* + Wrapper to bind all navigation events to fetch pages. + */ + +const PJAXWrapper = React.createClass({ + propTypes: { + children: React.PropTypes.node, + dispatch: React.PropTypes.func + }, + + onClick(event) { + const { dispatch } = this.props; + const href = getHrefForEvent(event); + + if (!href) { + return; + } + + event.preventDefault(); + dispatch(History.push(href)); + }, + + componentDidMount() { + document.addEventListener('click', this.onClick, false); + }, + + componentWillUnmount() { + document.removeEventListener('click', this.onClick, false); + }, + + render() { + return React.Children.only(this.props.children); + } +}); + +module.exports = ReactRedux.connect()(PJAXWrapper); diff --git a/packages/gitbook-core/src/components/Panel.js b/packages/gitbook-core/src/components/Panel.js new file mode 100644 index 0000000..694cc29 --- /dev/null +++ b/packages/gitbook-core/src/components/Panel.js @@ -0,0 +1,22 @@ +const React = require('react'); +const classNames = require('classnames'); + +const Panel = React.createClass({ + propTypes: { + className: React.PropTypes.string, + children: React.PropTypes.node + }, + + render() { + let { className, children } = this.props; + className = classNames('GitBook-Panel', className); + + return ( + <div className={className}> + {children} + </div> + ); + } +}); + +module.exports = Panel; diff --git a/packages/gitbook-core/src/components/Tooltipped.js b/packages/gitbook-core/src/components/Tooltipped.js new file mode 100644 index 0000000..4d297fd --- /dev/null +++ b/packages/gitbook-core/src/components/Tooltipped.js @@ -0,0 +1,44 @@ +const React = require('react'); +const classNames = require('classnames'); + +const POSITIONS = { + BOTTOM_RIGHT: 'e', + BOTTOM_LEFT: 'w', + TOP_LEFT: 'nw', + TOP_RIGHT: 'ne', + BOTTOM: '', + TOP: 'n' +}; + +const Tooltipped = React.createClass({ + propTypes: { + title: React.PropTypes.string.isRequired, + position: React.PropTypes.string, + open: React.PropTypes.bool, + children: React.PropTypes.node + }, + + statics: { + POSITIONS + }, + + render() { + const { title, position, open, children } = this.props; + + const className = classNames( + 'GitBook-Tooltipped', + position ? 'Tooltipped-' + position : '', + { + 'Tooltipped-o': open + } + ); + + return ( + <div className={className} aria-label={title}> + {children} + </div> + ); + } +}); + +module.exports = Tooltipped; |