diff --git a/package.json b/package.json index 5c81db2153..943c443c59 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "querystring": "^0.2.0", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", + "react-dnd": "^2.1.4", + "react-dnd-html5-backend": "^2.1.2", "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.14.1", diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index a853a59212..2c498e9eee 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -15,82 +15,17 @@ limitations under the License. */ import React from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import classNames from 'classnames'; import FilterStore from '../../stores/FilterStore'; import FlairStore from '../../stores/FlairStore'; import TagOrderStore from '../../stores/TagOrderStore'; import sdk from '../../index'; import dis from '../../dispatcher'; -import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; -const TagTile = React.createClass({ - displayName: 'TagTile', - - propTypes: { - groupProfile: PropTypes.object, - }, - - contextTypes: { - matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, - }, - - getInitialState() { - return { - hover: false, - }; - }, - - onClick: function(e) { - e.preventDefault(); - e.stopPropagation(); - dis.dispatch({ - action: 'select_tag', - tag: this.props.groupProfile.groupId, - ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e), - shiftKey: e.shiftKey, - }); - }, - - onMouseOver: function() { - this.setState({hover: true}); - }, - - onMouseOut: function() { - this.setState({hover: false}); - }, - - render: function() { - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); - const profile = this.props.groupProfile || {}; - const name = profile.name || profile.groupId; - const avatarHeight = 35; - - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop", - ) : null; - - const className = classNames({ - mx_TagTile: true, - mx_TagTile_selected: this.props.selected, - }); - - const tip = this.state.hover ? - : -
; - return -
- - { tip } -
-
; - }, -}); - -export default React.createClass({ +const TagPanel = React.createClass({ displayName: 'TagPanel', contextTypes: { @@ -166,14 +101,16 @@ export default React.createClass({ render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); + const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const orderedGroupProfiles = this.state.orderedTags ? this.state.joinedGroupProfiles.sort((a, b) => this.state.orderedTags.indexOf(a.groupId) - this.state.orderedTags.indexOf(b.groupId), ) : this.state.joinedGroupProfiles; + const tags = orderedGroupProfiles.map((groupProfile, index) => { - return ; }, }); +export default DragDropContext(HTML5Backend)(TagPanel); diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js new file mode 100644 index 0000000000..6715bd8dd3 --- /dev/null +++ b/src/components/views/elements/DNDTagTile.js @@ -0,0 +1,86 @@ +/* +Copyright 2017 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DragSource, DropTarget } from 'react-dnd'; + +import TagTile from './TagTile'; +import dis from '../../../dispatcher'; +import { findDOMNode } from 'react-dom'; + +const tagTileSource = { + canDrag: function(props, monitor) { + return true; + }, + + beginDrag: function(props) { + // Return the data describing the dragged item + return { + tag: props.groupProfile.groupId, + }; + }, + + endDrag: function(props, monitor, component) { + const dropResult = monitor.getDropResult(); + if (!monitor.didDrop() || !dropResult) { + return; + } + dis.dispatch({ + action: 'commit_tags', + }); + }, +}; + +const tagTileTarget = { + canDrop(props, monitor) { + return true; + }, + + hover(props, monitor, component) { + if (!monitor.canDrop()) return; + const draggedY = monitor.getClientOffset().y; + const {top, bottom} = findDOMNode(component).getBoundingClientRect(); + const targetY = (top + bottom) / 2; + dis.dispatch({ + action: 'order_tag', + tag: monitor.getItem().tag, + targetTag: props.groupProfile.groupId, + // Note: we indicate that the tag should be after the target when + // it's being dragged over the top half of the target. + after: draggedY < targetY, + }); + }, + + drop(props) { + // Return the data to be returned by getDropResult + return { + tag: props.groupProfile.groupId, + }; + }, +}; + +export default + DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + }))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + }))((props) => { + const { connectDropTarget, connectDragSource, ...otherProps } = props; + return connectDropTarget(connectDragSource( +
+ +
, + )); + })); diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js new file mode 100644 index 0000000000..124559a838 --- /dev/null +++ b/src/components/views/elements/TagTile.js @@ -0,0 +1,88 @@ +/* +Copyright 2017 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { MatrixClient } from 'matrix-js-sdk'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard'; + +export default React.createClass({ + displayName: 'TagTile', + + propTypes: { + groupProfile: PropTypes.object, + }, + + contextTypes: { + matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, + }, + + getInitialState() { + return { + hover: false, + }; + }, + + onClick: function(e) { + e.preventDefault(); + e.stopPropagation(); + dis.dispatch({ + action: 'select_tag', + tag: this.props.groupProfile.groupId, + ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e), + shiftKey: e.shiftKey, + }); + }, + + onMouseOver: function() { + this.setState({hover: true}); + }, + + onMouseOut: function() { + this.setState({hover: false}); + }, + + render: function() { + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); + const profile = this.props.groupProfile || {}; + const name = profile.name || profile.groupId; + const avatarHeight = 35; + + const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( + profile.avatarUrl, avatarHeight, avatarHeight, "crop", + ) : null; + + const className = classNames({ + mx_TagTile: true, + mx_TagTile_selected: this.props.selected, + }); + + const tip = this.state.hover ? + : +
; + return +
+ + { tip } +
+
; + }, +});