diff options
author | Nicolas Gaborit <soreine.plume@gmail.com> | 2016-10-13 12:56:46 +0200 |
---|---|---|
committer | Samy Pessé <samypesse@gmail.com> | 2016-10-13 12:56:46 +0200 |
commit | 3aea3e5d88384822440517c9a2b722c405547155 (patch) | |
tree | 84ef0448c7e2e7ed95941f2795e86ca65f7cd809 | |
parent | 95b3b4ebb7277f7a96ee79e5d75baafb3b5aab1e (diff) | |
download | gitbook-3aea3e5d88384822440517c9a2b722c405547155.zip gitbook-3aea3e5d88384822440517c9a2b722c405547155.tar.gz gitbook-3aea3e5d88384822440517c9a2b722c405547155.tar.bz2 |
Adapt plugin sharing (#1553)
* Reuse old package config
* Add plugin config shape
* Add ButtonGroup to core components
* List all sharing sites
* Displaying buttons from config
* First iteration of Dropdown component (need CSS)
* Using Dropdown for sharing button
* Create HotKeys component
* Move Backdrop to its own file
* Trying a cleaner API for Dropdown
* Add README.md
* livereload: Add missing gitbook-plugin dependency
* sharing: Now use Immutable state
* sharing: Adapt quickly to new Dropdown
* sharing: Fix sharing from dropdown
-rw-r--r-- | packages/gitbook-core/package.json | 1 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Backdrop.js | 38 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/ButtonGroup.js | 23 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/Dropdown.js | 141 | ||||
-rw-r--r-- | packages/gitbook-core/src/components/HotKeys.js | 56 | ||||
-rw-r--r-- | packages/gitbook-core/src/index.js | 6 | ||||
-rw-r--r-- | packages/gitbook-plugin-livereload/package.json | 3 | ||||
-rw-r--r-- | packages/gitbook-plugin-sharing/README.md | 38 | ||||
-rw-r--r-- | packages/gitbook-plugin-sharing/package.json | 53 | ||||
-rw-r--r-- | packages/gitbook-plugin-sharing/src/SITES.js | 72 | ||||
-rw-r--r-- | packages/gitbook-plugin-sharing/src/index.js | 132 | ||||
-rw-r--r-- | packages/gitbook-plugin-sharing/src/optionsShape.js | 20 |
12 files changed, 569 insertions, 14 deletions
diff --git a/packages/gitbook-core/package.json b/packages/gitbook-core/package.json index b5b2d3f..7aea7fb 100644 --- a/packages/gitbook-core/package.json +++ b/packages/gitbook-core/package.json @@ -10,6 +10,7 @@ "history": "^4.3.0", "html-tags": "^1.1.1", "immutable": "^3.8.1", + "mousetrap": "1.6.0", "react": "^15.3.1", "react-dom": "^15.3.1", "react-helmet": "^3.1.0", diff --git a/packages/gitbook-core/src/components/Backdrop.js b/packages/gitbook-core/src/components/Backdrop.js new file mode 100644 index 0000000..18f3c4d --- /dev/null +++ b/packages/gitbook-core/src/components/Backdrop.js @@ -0,0 +1,38 @@ +const React = require('react'); + +/** + * Backdrop for modals, dropdown, etc. that covers the whole screen + * and handles click. + * + * <Backdrop onClick={onCloseModal} /> + */ +const Backdrop = React.createClass({ + propTypes: { + // Callback when backdrop is clicked + onClick: React.PropTypes.func.isRequired, + // Z-index for the backdrop + zIndex: React.PropTypes.number + }, + + getDefaultProps() { + return { + zIndex: 200 + }; + }, + + render() { + const { zIndex, onClick } = this.props; + const style = { + zIndex, + position: 'fixed', + top: 0, + right: 0, + width: '100%', + height: '100%' + }; + + return <div style={style} onClick={onClick}></div>; + } +}); + +module.exports = Backdrop; 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/Dropdown.js b/packages/gitbook-core/src/components/Dropdown.js new file mode 100644 index 0000000..f43a6fc --- /dev/null +++ b/packages/gitbook-core/src/components/Dropdown.js @@ -0,0 +1,141 @@ +const React = require('react'); +const classNames = require('classnames'); + +const Backdrop = require('./Backdrop'); + +/** + * Dropdown to display a menu + * + * <Dropdown.Container> + * + * <Button /> + * + * <Dropdown.Menu open={this.state.open}> + * <Dropdown.Item href={...}> ... </Dropdown.Item> + * <Dropdown.Item onClick={...}> ... </Dropdown.Item> + * + * <Dropdown.Item> A submenu + * <Dropdown.Menu> + * <Dropdown.Item href={...}> Subitem </Dropdown.Item> + * </Dropdown.Menu> + * </Dropdown.Item> + * + * </Dropdown.Menu> + * </Dropdown.Container> + */ + +const DropdownContainer = React.createClass({ + propTypes: { + className: React.PropTypes.string, + span: React.PropTypes.bool, + children: React.PropTypes.node + }, + + render() { + let { className, span, children } = this.props; + + className = classNames( + 'GitBook-Dropdown', + className + ); + + return span ? + <span className={className}>{children}</span> + : <div className={className}>{children}</div>; + } +}); + +/** + * A dropdown item, which is always a link, and can contain a nested + * DropdownMenu. + */ +const DropdownItem = React.createClass({ + propTypes: { + children: React.PropTypes.node, + onClick: React.PropTypes.func, + href: React.PropTypes.string + }, + + onClick(e) { + if (!this.props.href) { + e.preventDefault(); + e.stopPropagation(); + + if (this.props.onClick) this.props.onClick(); + } + }, + + render() { + const { + children, href, + onClick, // eslint-disable-line no-unused-vars + ...otherProps + } = this.props; + + let inner = [], submenu = []; + submenu = filterChildren(children, isDropdownMenu); + inner = filterChildren(children, (child) => !isDropdownMenu(child)); + + return ( + <li className="GitBook-DropdownItem"> + <a href={href || '#'} + onClick={this.onClick} + {...otherProps} > + {inner} + </a> + {submenu} + </li> + ); + } +}); + +/** + * @param {Node} children + * @param {Function} predicate + * @return {Node} children that pass the predicate + */ +function filterChildren(children, predicate) { + return React.Children.map( + children, + (child) => predicate(child) ? child : null + ); +} + +/** + * A DropdownMenu to display DropdownItems. Must be inside a + * DropdownContainer. + */ +const DropdownMenu = React.createClass({ + propTypes: { + className: React.PropTypes.string, + children: React.PropTypes.node, + open: React.PropTypes.bool + }, + + render() { + const { open } = this.props; + const className = classNames( + 'GitBook-DropdownMenu', + { 'GitBook-DropdownMenu-open': open } + ); + + return ( + <ul className={className}> + {this.props.children} + </ul> + ); + } +}); + +function isDropdownMenu(child) { + return (child && child.type && child.type.displayName === 'DropdownMenu'); +} + +const Dropdown = { + Item: DropdownItem, + Menu: DropdownMenu, + Container: DropdownContainer, + Backdrop +}; + +module.exports = Dropdown; diff --git a/packages/gitbook-core/src/components/HotKeys.js b/packages/gitbook-core/src/components/HotKeys.js new file mode 100644 index 0000000..c2c67c8 --- /dev/null +++ b/packages/gitbook-core/src/components/HotKeys.js @@ -0,0 +1,56 @@ +const React = require('react'); +const Mousetrap = require('mousetrap'); +const { string, node, func, shape, arrayOf } = React.PropTypes; + +const bindingShape = shape({ + // A key "escape", a combination of key "mod+s", or a key sequence "ctrl+x ctrl+s" + key: string.isRequired, + // function (event) {} + handler: func.isRequired +}); + +/** + * Defines hotkeys globally when this component is mounted. + * + * keymap = [{ + * key: 'escape', + * handler: (e) => quit() + * }, { + * key: 'mod+s', + * handler: (e) => save() + * }] + * + * <HotKeys keymap={keymap}> + * < ... /> + * </HotKeys> + */ + +const HotKeys = React.createClass({ + propTypes: { + children: node.isRequired, + keymap: arrayOf(bindingShape) + }, + + getDefaultProps() { + return { keymap: [] }; + }, + + componentDidMount() { + this.props.keymap.forEach((binding) => { + Mousetrap.bind(binding.key, binding.handler); + }); + }, + + componentWillUnmount() { + this.props.keymap.forEach((binding) => { + Mousetrap.unbind(binding.key, binding.handler); + }); + }, + + render() { + // Simply render the only child + return React.Children.only(this.props.children); + } +}); + +module.exports = HotKeys; diff --git a/packages/gitbook-core/src/index.js b/packages/gitbook-core/src/index.js index d51d073..f820599 100644 --- a/packages/gitbook-core/src/index.js +++ b/packages/gitbook-core/src/index.js @@ -12,7 +12,10 @@ const { ImportLink, ImportScript, ImportCSS } = require('./components/Import'); const HTMLContent = require('./components/HTMLContent'); const Link = require('./components/Link'); const Icon = require('./components/Icon'); +const HotKeys = require('./components/HotKeys'); const Button = require('./components/Button'); +const ButtonGroup = require('./components/ButtonGroup'); +const Dropdown = require('./components/Dropdown'); const I18nProvider = require('./components/I18nProvider'); const ACTIONS = require('./actions/TYPES'); @@ -49,7 +52,10 @@ module.exports = { FlexBox: Box, Link, Icon, + HotKeys, Button, + ButtonGroup, + Dropdown, // Utilities Shapes, // Librairies diff --git a/packages/gitbook-plugin-livereload/package.json b/packages/gitbook-plugin-livereload/package.json index 58ab612..d5aa912 100644 --- a/packages/gitbook-plugin-livereload/package.json +++ b/packages/gitbook-plugin-livereload/package.json @@ -10,6 +10,9 @@ "dependencies": { "gitbook-core": "4.0.0" }, + "devDependencies": { + "gitbook-plugin": "4.0.0" + }, "scripts": { "build-js": "gitbook-plugin build ./src/index.js ./_assets/plugin.js", "prepublish": "npm run build-js" diff --git a/packages/gitbook-plugin-sharing/README.md b/packages/gitbook-plugin-sharing/README.md new file mode 100644 index 0000000..28ae0d4 --- /dev/null +++ b/packages/gitbook-plugin-sharing/README.md @@ -0,0 +1,38 @@ +# plugin-sharing + +This plugin adds sharing buttons in the GitBook website toolbar to share book on social networks. + +### Disable this plugin + +This is a default plugin and it can be disabled using a `book.json` configuration: + +``` +{ + plugins: ["-sharing"] +} +``` + +### Configuration + +This plugin can be configured in the `book.json`: + +Default configuration is: + +```js +{ + "pluginsConfig": { + "sharing": { + "facebook": true, + "twitter": true, + "google": false, + "weibo": false, + "instapaper": false, + "vk": false, + "all": [ + "facebook", "google", "twitter", + "weibo", "instapaper" + ] + } + } +} +``` diff --git a/packages/gitbook-plugin-sharing/package.json b/packages/gitbook-plugin-sharing/package.json index 7f85575..b0540e8 100644 --- a/packages/gitbook-plugin-sharing/package.json +++ b/packages/gitbook-plugin-sharing/package.json @@ -4,6 +4,54 @@ "main": "index.js", "browser": "./_assets/plugin.js", "version": "4.0.0", + "gitbook": { + "properties": { + "facebook": { + "type": "boolean", + "default": true, + "title": "Facebook" + }, + "twitter": { + "type": "boolean", + "default": true, + "title": "Twitter" + }, + "google": { + "type": "boolean", + "default": false, + "title": "Google" + }, + "weibo": { + "type": "boolean", + "default": false, + "description": "Weibo" + }, + "instapaper": { + "type": "boolean", + "default": false, + "description": "Instapaper" + }, + "vk": { + "type": "boolean", + "default": false, + "description": "VK" + }, + "all": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "facebook", + "google", + "twitter", + "weibo", + "instapaper" + ], + "uniqueItems": true + } + } + }, "dependencies": { "gitbook-core": "4.0.0" }, @@ -24,5 +72,6 @@ }, "bugs": { "url": "https://github.com/GitbookIO/gitbook/issues" - } -}
\ No newline at end of file + }, + "license": "Apache-2.0" +} diff --git a/packages/gitbook-plugin-sharing/src/SITES.js b/packages/gitbook-plugin-sharing/src/SITES.js new file mode 100644 index 0000000..86eae74 --- /dev/null +++ b/packages/gitbook-plugin-sharing/src/SITES.js @@ -0,0 +1,72 @@ +// All the sharing platforms +const SITES = { + + // One sharing platform + 'facebook': { + // Displayed name + label: 'Facebook', + + // Font-awesome icon id + icon: 'facebook', + + /** + * Share a page on this platform + * @param {String} url The url to share + * @param {String} title The title of the url page + */ + onShare(url, title) { + url = encodeURIComponent(url); + window.open(`http://www.facebook.com/sharer/sharer.php?s=100&p[url]=${url}`); + } + }, + + 'twitter': { + label: 'Twitter', + icon: 'twitter', + onShare(url, title) { + const status = encodeURIComponent(title + ' ' + url); + window.open(`http://twitter.com/home?status=${status}`); + } + }, + + 'google': { + label: 'Google+', + icon: 'google-plus', + onShare(url, title) { + url = encodeURIComponent(url); + window.open(`https://plus.google.com/share?url=${url}`); + } + }, + + 'weibo': { + label: 'Weibo', + icon: 'weibo', + onShare(url, title) { + url = encodeURIComponent(url); + title = encodeURIComponent(title); + window.open(`http://service.weibo.com/share/share.php?content=utf-8&url=${url}&title=${title}`); + } + }, + + 'instapaper': { + label: 'Instapaper', + icon: 'instapaper', + onShare(url, title) { + url = encodeURIComponent(url); + window.open(`http://www.instapaper.com/text?u=${url}`); + } + }, + + 'vk': { + label: 'VK', + icon: 'vk', + onShare(url, title) { + url = encodeURIComponent(url); + window.open(`http://vkontakte.ru/share.php?url=${url}`); + } + } +}; + +SITES.ALL = Object.keys(SITES); + +module.exports = SITES; diff --git a/packages/gitbook-plugin-sharing/src/index.js b/packages/gitbook-plugin-sharing/src/index.js index 4821de2..357ccbb 100644 --- a/packages/gitbook-plugin-sharing/src/index.js +++ b/packages/gitbook-plugin-sharing/src/index.js @@ -1,10 +1,22 @@ const GitBook = require('gitbook-core'); -const { React } = GitBook; +const { + React, + Dropdown +} = GitBook; +const { string, arrayOf, shape, func } = React.PropTypes; + +const SITES = require('./SITES'); +const optionsShape = require('./optionsShape'); +const siteShape = shape({ + label: string.isRequired, + icon: string.isRequired, + onShare: func.isRequired +}); module.exports = GitBook.createPlugin({ activate: (dispatch, getState, { Components }) => { // Dispatch initialization actions - dispatch(Components.registerComponent(SharingButton, { role: 'toolbar:buttons:right' })) + dispatch(Components.registerComponent(Sharing, { role: 'toolbar:buttons:right' })); }, deactivate: (dispatch, getState) => { // Dispatch cleanup actions @@ -12,23 +24,119 @@ module.exports = GitBook.createPlugin({ reduce: (state, action) => state }); -let SharingButton = React.createClass({ +/** + * Displays the group of sharing buttons + */ +let Sharing = React.createClass({ + propTypes: { + options: optionsShape.isRequired, + page: GitBook.Shapes.Page.isRequired + }, + + onShare(site) { + site.onShare(location.href, this.props.page.title); + }, + + render() { + const { options } = this.props; + + // Highlighted sites + const mainButtons = SITES + .ALL + .filter(id => options[id]) + .map(id => <SiteButton key={id} onShare={this.onShare} site={SITES[id]} />); + + // Other sites + let shareButton = undefined; + if (options.all.length > 0) { + shareButton = ( + <ShareButton siteIds={options.all} + onShare={this.onShare} /> + ); + } + + return ( + <GitBook.ButtonGroup> + { mainButtons } + { shareButton } + </GitBook.ButtonGroup> + ); + } +}); + +function mapStateToProps(state) { + let options = state.config.getIn(['pluginsConfig', 'sharing']); + if (options) { + options = options.toJS(); + } else { + options = { all: [] }; + } + + return { + page: state.page, + options + }; +} + +Sharing = GitBook.connect(Sharing, mapStateToProps); + +// An individual site sharing button +const SiteButton = React.createClass({ propTypes: { - page: GitBook.Shapes.Page + site: siteShape.isRequired, + onShare: func.isRequired }, - onClick() { - alert(this.props.page.title) + onClick(e) { + e.preventDefault(); + this.props.onShare(this.props.site); }, render() { + const { site } = this.props; + return ( <GitBook.Button onClick={this.onClick}> - <GitBook.Icon id="facebook"/> + <GitBook.Icon id={site.icon}/> </GitBook.Button> - ) + ); + } +}); + +// Share button with dropdown list of sites +const ShareButton = React.createClass({ + propTypes: { + siteIds: arrayOf(string).isRequired, + onShare: func.isRequired + }, + + getInitialState() { + return { open: false }; + }, + + onToggle() { + this.setState({ open: !this.state.open }); + }, + + render() { + const { siteIds, onShare } = this.props; + + const items = siteIds.map((id) => ( + <Dropdown.Item onClick={() => onShare(SITES[id])} key={id}> + {SITES[id].label} + </Dropdown.Item> + )); + + return ( + <Dropdown.Container> + <GitBook.Button onClick={this.onToggle}> + <GitBook.Icon id="share-alt" /> + </GitBook.Button> + + <Dropdown.Menu open={this.state.open}> + {items} + </Dropdown.Menu> + </Dropdown.Container> + ); } -}) -SharingButton = GitBook.connect(SharingButton, function mapStateToProps(state) { - return { page: state.page } -}) +}); diff --git a/packages/gitbook-plugin-sharing/src/optionsShape.js b/packages/gitbook-plugin-sharing/src/optionsShape.js new file mode 100644 index 0000000..dd51016 --- /dev/null +++ b/packages/gitbook-plugin-sharing/src/optionsShape.js @@ -0,0 +1,20 @@ +const { + bool, + arrayOf, + oneOf, + shape +} = require('gitbook-core').React.PropTypes; + +const { ALL } = require('./SITES'); + +const optionsShape = shape({ + facebook: bool, + twitter: bool, + google: bool, + weibo: bool, + instapaper: bool, + vk: bool, + all: arrayOf(oneOf(ALL)).isRequired +}); + +module.exports = optionsShape; |