diff --git a/.babelrc b/.babelrc index 65b44c5d..b3ae6f35 100644 --- a/.babelrc +++ b/.babelrc @@ -5,7 +5,8 @@ "stage-0" ], "plugins": [ - "transform-runtime" + "transform-runtime", + "transform-decorators-legacy" ], "env": { "development": { diff --git a/app/components/DragAndDrop/actions.js b/app/components/DragAndDrop/actions.js new file mode 100644 index 00000000..e9829a09 --- /dev/null +++ b/app/components/DragAndDrop/actions.js @@ -0,0 +1,32 @@ +/* @flow weak */ +export const DND_DRAG_START = 'DND_DRAG_START' +export function dragStart ({sourceType, sourceId}) { + return { + type: DND_DRAG_START, + payload: {sourceType, sourceId} + } +} + +export const DND_DRAG_OVER = 'DND_DRAG_OVER' +export function dragOverTarget (payload) { + return { + type: DND_DRAG_OVER, + payload: payload + } +} + +export const DND_UPDATE_DRAG_OVER_META = 'DND_UPDATE_DRAG_OVER_META' +export function updateDragOverMeta (payload) { + return { + type: DND_UPDATE_DRAG_OVER_META, + payload: payload + } +} + +export const DND_DRAG_END = 'DND_DRAG_END' +export function dragEnd (payload) { + return { + type: DND_DRAG_END, + payload: payload + } +} diff --git a/app/components/DragAndDrop/index.jsx b/app/components/DragAndDrop/index.jsx new file mode 100644 index 00000000..7afdd135 --- /dev/null +++ b/app/components/DragAndDrop/index.jsx @@ -0,0 +1,170 @@ +/* @flow weak */ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import _ from 'lodash' +import { dragOverTarget, updateDragOverMeta, dragEnd } from './actions' +import * as PaneActions from '../Pane/actions' + +@connect(state => state.DragAndDrop) +class DragAndDrop extends Component { + + constructor (props) { + super(props) + } + + render () { + const {isDragging, meta, target} = this.props + if (!isDragging) return null + + if (meta && meta.paneLayoutOverlay) { + const {top, left, width, height} = meta.paneLayoutOverlay + var overlayStyle = { + position: 'fixed', + top: top, + left: left, + width: width, + height: height, + opacity: 0.2 + } + } + + return meta && meta.paneLayoutOverlay + ?
+ : null + } + + componentDidMount () { + window.ondragover = this.onDragOver + window.ondrop = this.onDrop + window.ondragend = this.onDragEnd + } + + onDragOver = (e) => { + e.preventDefault() + const {source, droppables, dispatch, meta} = this.props + const prevTarget = this.props.target + + const [oX, oY] = [e.pageX, e.pageY] + const target = droppables.reduce((result, droppable) => { + const {top, left, right, bottom} = droppable.rect + if (left <= oX && oX <= right && top <= oY && oY <= bottom) { + if (!result) return droppable + return result.DOMNode.contains(droppable.DOMNode) ? droppable : result + } else { + return result + } + }, null) + + if (!target) return + if (!prevTarget || target.id !== prevTarget.id) { + dispatch(dragOverTarget({id: target.id, type: target.type})) + } + + switch (source.type) { + case 'TAB': + return this.dragTabOverPane(e, target) + + default: + } + } + + onDrop = (e) => { + e.preventDefault() + const {source, target, meta, dispatch} = this.props + if (!source || !target) return + switch (`${source.type}_to_${target.type}`) { + case 'TAB_to_PANE': + if (meta.paneSplitDirection === 'center') { + + } else { + dispatch(PaneActions.splitTo(target.id, meta.paneSplitDirection)) + } + } + dispatch(dragEnd()) + } + + onDragEnd = (e) => { + e.preventDefault() + const {dispatch} = this.props + dispatch(dragEnd()) + } + + dragTabOverPane (e, target) { + if (target.type !== 'PANE') return + const {meta, dispatch} = this.props + const [oX, oY] = [e.pageX, e.pageY] + + const {top, left, right, bottom, height, width} = target.rect + const leftRule = left + width/3 + const rightRule = right - width/3 + const topRule = top + height/3 + const bottomRule = bottom - height/3 + + let overlayPos + if (oX < leftRule) { + overlayPos = 'left' + } else if (oX > rightRule) { + overlayPos = 'right' + } else if (oY < topRule) { + overlayPos = 'top' + } else if (oY > bottomRule) { + overlayPos = 'bottom' + } else { + overlayPos = 'center' + } + + // nothing changed, stop here + if (meta && meta.paneSplitDirection === overlayPos) return + + const heightTabBar = target.DOMNode.querySelector('.tab-bar').offsetHeight + let overlay + switch (overlayPos) { + case 'left': + overlay = { + top: top + heightTabBar, + left: left, + width: width/2, + height: height - heightTabBar + } + break + case 'right': + overlay = { + top: top + heightTabBar, + left: left + width/2, + width: width/2, + height: height - heightTabBar + } + break + case 'top': + overlay = { + top: top + heightTabBar, + left: left, + width: width, + height: (height - heightTabBar)/2 + } + break + case 'bottom': + overlay = { + top: top + (height + heightTabBar)/2, + left: left, + width: width, + height: (height - heightTabBar)/2 + } + break + default: + overlay = { + top: top + heightTabBar, + left: left, + width: width, + height: height - heightTabBar + } + } + + dispatch(updateDragOverMeta({ + paneSplitDirection: overlayPos, + paneLayoutOverlay: overlay + })) + } +} + +export default DragAndDrop diff --git a/app/components/DragAndDrop/reducer.js b/app/components/DragAndDrop/reducer.js new file mode 100644 index 00000000..e05aec7e --- /dev/null +++ b/app/components/DragAndDrop/reducer.js @@ -0,0 +1,53 @@ +/* @flow weak */ +import _ from 'lodash' +import { + DND_DRAG_START, + DND_DRAG_OVER, + DND_UPDATE_DRAG_OVER_META, + DND_DRAG_END +} from './actions' + +function getDroppables () { + var droppables = _.map(document.querySelectorAll('[data-droppable]'), (DOMNode) => { + return { + id: DOMNode.id, + DOMNode: DOMNode, + type: DOMNode.getAttribute('data-droppable'), + rect: DOMNode.getBoundingClientRect() + } + }) + return droppables +} + +export default function DragAndDropReducer (state={isDragging: false}, action) { + switch (action.type) { + case DND_DRAG_START: + var {sourceType, sourceId} = action.payload + return { + isDragging: true, + source: { + type: sourceType, + id: sourceId + }, + droppables: getDroppables() + } + + case DND_DRAG_OVER: + return { + ...state, + target: action.payload + } + + case DND_UPDATE_DRAG_OVER_META: + return { + ...state, + meta: action.payload + } + + case DND_DRAG_END: + return {isDragging: false} + + default: + return state + } +} diff --git a/app/components/Modal/index.jsx b/app/components/Modal/index.jsx index 669121fd..1afe5c6d 100644 --- a/app/components/Modal/index.jsx +++ b/app/components/Modal/index.jsx @@ -21,11 +21,7 @@ var ModalContainer = (props) => { ) : null } - -ModalContainer = connect( - state => state.ModalState -, null -)(ModalContainer); +ModalContainer = connect(state => state.ModalState, null)(ModalContainer) class Modal extends Component { diff --git a/app/components/Pane/PaneAxis.jsx b/app/components/Pane/PaneAxis.jsx index 3537d6db..5d197e8d 100644 --- a/app/components/Pane/PaneAxis.jsx +++ b/app/components/Pane/PaneAxis.jsx @@ -7,34 +7,74 @@ import * as PaneActions from './actions' import TabViewContainer from '../Tab' import AceEditor from '../AceEditor' -const Pane = (props) => { - const {id, views, size, flexDirection, parentFlexDirection, resizingListeners} = props - var content - if (views.length > 1) { - content = - } else if (typeof views[0] === 'string') { - var tabGroupId = views[0] - console.log(tabGroupId) - content = ( -
- + +@connect(state => state.Panes) +class Pane extends Component { + constructor (props) { + super(props) + this.state = {} + } + + render () { + const {id, views, size, flexDirection, parentFlexDirection, resizingListeners, dropArea} = this.props + var content + if (views.length > 1) { + content = + } else if (typeof views[0] === 'string') { + var tabGroupId = views[0] + content = ( +
+ +
+ ) + } else { + content = null + } + + var style = {flexGrow: size, display: this.props.display} + return ( +
this.paneDOM = r} + > { content } +
) - } else { - content = null } - var style = {flexGrow: size, display: props.display} - return ( -
- { content } - -
- ) + startResize = (sectionId, e) => { + if (e.button !== 0) return // do nothing unless left button pressed + e.preventDefault() + + // dispatch(PaneActions.setCover(true)) + var [oX, oY] = [e.pageX, e.pageY] + + const handleResize = (e) => { + var [dX, dY] = [oX - e.pageX, oY - e.pageY] + ;[oX, oY] = [e.pageX, e.pageY] + this.props.dispatch(PaneActions.resize(sectionId, dX, dY)) + this.props.resizingListeners.forEach(listener => listener()) + } + + const stopResize = () => { + window.document.removeEventListener('mousemove', handleResize) + window.document.removeEventListener('mouseup', stopResize) + this.props.dispatch(PaneActions.confirmResize()) + } + + window.document.addEventListener('mousemove', handleResize) + window.document.addEventListener('mouseup', stopResize) + } + } -let ResizeBar = ({parentFlexDirection, sectionId, startResize}) => { + +const ResizeBar = ({parentFlexDirection, sectionId, startResize}) => { var barClass = (parentFlexDirection == 'row') ? 'col-resize' : 'row-resize' return (
{ ) } -ResizeBar = connect(null, (dispatch, ownProps) => { - return { - startResize: (sectionId, e) => { - if (e.button !== 0) return // do nothing unless left button pressed - e.preventDefault() - - // dispatch(PaneActions.setCover(true)) - var [oX, oY] = [e.pageX, e.pageY] - - const handleResize = (e) => { - var [dX, dY] = [oX - e.pageX, oY - e.pageY] - ;[oX, oY] = [e.pageX, e.pageY] - dispatch(PaneActions.resize(sectionId, dX, dY)) - ownProps.resizingListeners.forEach(listener => listener()) - } - - const stopResize = () => { - window.document.removeEventListener('mousemove', handleResize) - window.document.removeEventListener('mouseup', stopResize) - dispatch(PaneActions.confirmResize()) - } - - window.document.addEventListener('mousemove', handleResize) - window.document.addEventListener('mouseup', stopResize) - } - } -})(ResizeBar) - class PaneAxis extends Component { static get propTypes () { @@ -103,7 +115,7 @@ class PaneAxis extends Component { render () { let props = this.props.hasOwnProperty('root') ? this.props.root : this.props - var { views, flexDirection, className, style } = props + let { views, flexDirection } = props if (views.length === 1 && !Array.isArray(views[0].views) ) views = [props] var Subviews = views.map( _props => { return { Subviews }
+
{ Subviews }
) } } diff --git a/app/components/Pane/PanesContainer.jsx b/app/components/Pane/PanesContainer.jsx index 741d8742..357e74f5 100644 --- a/app/components/Pane/PanesContainer.jsx +++ b/app/components/Pane/PanesContainer.jsx @@ -9,7 +9,7 @@ var PrimaryPaneAxis = connect(state => { })(PaneAxis) var PanesContainer = (props) => { - return + return } export default PanesContainer diff --git a/app/components/Pane/actions.js b/app/components/Pane/actions.js index 43a9257b..469b3033 100644 --- a/app/components/Pane/actions.js +++ b/app/components/Pane/actions.js @@ -24,11 +24,18 @@ export function confirmResize () { return {type: PANE_CONFIRM_RESIZE} } -export const PANE_SPLIT = 'PANE_SPLIT' +export const PANE_SPLIT_WITH_KEY = 'PANE_SPLIT_WITH_KEY' export function split (splitCount, flexDirection='row') { + return { + type: PANE_SPLIT_WITH_KEY, + payload: {flexDirection, splitCount} + } +} + +export const PANE_SPLIT = 'PANE_SPLIT' +export function splitTo (paneId, splitDirection) { return { type: PANE_SPLIT, - flexDirection: flexDirection, - splitCount: splitCount + payload: {paneId, splitDirection} } } diff --git a/app/components/Pane/reducer.js b/app/components/Pane/reducer.js index 925bfe60..f0dc8b80 100644 --- a/app/components/Pane/reducer.js +++ b/app/components/Pane/reducer.js @@ -6,7 +6,8 @@ import { PANE_UNSET_COVER, PANE_RESIZE, PANE_CONFIRM_RESIZE, - PANE_SPLIT + PANE_SPLIT, + PANE_SPLIT_WITH_KEY } from './actions' // removeSingleChildedInternalNode 是为了避免无限级嵌套的 views.length === 1 出现 @@ -71,6 +72,61 @@ class Pane { Pane.indexes[this.id] = this } + addSibling (siblingPane, toTheLeft) { + let parent = getPaneById(this.parentId) + let atIndex = parent.views.indexOf(this) + 1 + if (toTheLeft === -1) atIndex -= 1 + parent.views.splice(atIndex, 0, siblingPane) + } + + addChild (childPane, atIndex) { + if (typeof atIndex !== 'number') atIndex = this.views.length + this.views.splice(atIndex, 0, childPane) + } + /** + * Mutate a pane instance's "views" property to reflect a splitted pane view + * @param {string} splitDirection + * @param {string} splitCount + */ + splitToDirection (splitDirection) { + let flexDirection + switch (splitDirection) { + case 'right': + case 'left': + flexDirection = 'row' + break + case 'top': + case 'bottom': + flexDirection = 'column' + break + default: + throw 'Pane.splitToDirection method requires param "splitDirection"' + } + this.flexDirection = flexDirection + + let parent = getPaneById(this.parentId) + // If flexDirection is same as parent's, + // then we can simply push the newly splitted view into parent's "views" array + if (parent && parent.flexDirection === flexDirection) { + let newPane = new Pane({views: ['']}, parent) + if (splitDirection === 'right' || splitDirection === 'bottom') { + this.addSibling(newPane) + } else { + this.addSibling(newPane, -1) + } + } else { + let curTabGroupId = this.views.pop() + this.views.push(new Pane({views:[curTabGroupId]}, this)) + + let childPane = new Pane({views: ['']}, this) + if (splitDirection === 'right' || splitDirection === 'bottom') { + this.addChild(childPane, 1) + } else { + this.addChild(childPane, 0) + } + } + } + splitPane (splitCount=this.views.length+1, flexDirection=this.flexDirection) { this.flexDirection = flexDirection if (splitCount <= 0) splitCount = 1 @@ -130,7 +186,7 @@ class Pane { Pane.indexes = {} -const getViewById = (id) => Pane.indexes[id] +const getPaneById = (id) => Pane.indexes[id] const debounced = _.debounce(function (func) { func() }, 50) const _state = { @@ -144,8 +200,8 @@ const _state = { export default function PaneReducer (state = _state, action) { switch (action.type) { case PANE_RESIZE: - let section_A = getViewById(action.sectionId) - let parent = getViewById(section_A.parentId) + let section_A = getPaneById(action.sectionId) + let parent = getPaneById(section_A.parentId) let section_B = parent.views[parent.views.indexOf(section_A) + 1] let section_A_Dom = document.getElementById(section_A.id) let section_B_Dom = document.getElementById(section_B.id) @@ -180,8 +236,6 @@ export default function PaneReducer (state = _state, action) { case PANE_CONFIRM_RESIZE: return state - - default: return state } @@ -189,19 +243,21 @@ export default function PaneReducer (state = _state, action) { export function PaneCrossReducer (allStates, action) { switch (action.type) { - case PANE_SPLIT: - const {Panes, TabState} = allStates - var pane = Panes.root - if (action.splitCount === pane.views.length && - action.flexDirection === pane.flexDirection) { + case PANE_SPLIT_WITH_KEY: + var {Panes, TabState} = allStates + var {pane, splitCount, flexDirection} = action.payload + if (!pane) pane = Panes.root + + if (splitCount === pane.views.length && + flexDirection === pane.flexDirection) { return allStates } pane = new Pane(pane) - var tabGroupIds = pane.splitPane(action.splitCount, action.flexDirection) + var tabGroupIds = pane.splitPane(splitCount, flexDirection) if (tabGroupIds.length > 1) { - const tabGroupIdToMergeInto = tabGroupIds[0] - const tabGroupIdsToBeMerged = tabGroupIds.slice(1) + var tabGroupIdToMergeInto = tabGroupIds[0] + var tabGroupIdsToBeMerged = tabGroupIds.slice(1) var mergedTabs = tabGroupIdsToBeMerged.reduceRight((acc, tabGroupId) => { var tabGroup = TabState.getGroupById(tabGroupId) tabGroup.deactivateAllTabsInGroup() @@ -214,6 +270,19 @@ export function PaneCrossReducer (allStates, action) { Panes: {root: new Pane(pane)}, TabState: TabState.normalizeState(TabState) } + + case PANE_SPLIT: + var {Panes, TabState} = allStates + var {paneId, splitDirection} = action.payload + var pane = getPaneById(paneId) + console.log(paneId+': '+splitDirection); + pane.splitToDirection(splitDirection) + + return { ...allStates, + Panes: {root: Pane.root, timestamp: Date.now()} + } + + default: return allStates } diff --git a/app/components/Tab/index.jsx b/app/components/Tab/index.jsx index 74c71e86..687bfb8a 100644 --- a/app/components/Tab/index.jsx +++ b/app/components/Tab/index.jsx @@ -5,10 +5,11 @@ import { createStore } from 'redux' import { connect } from 'react-redux' import cx from 'classnames' import * as TabActions from './actions' +import { dragStart } from '../DragAndDrop/actions' import AceEditor from '../AceEditor' -const TabView = ({TabState, groupId, ...otherProps}) => { - let tabGroup = TabState.getGroupById(groupId) +const TabView = ({getGroupById, groupId, ...otherProps}) => { + let tabGroup = getGroupById(groupId) return (
@@ -33,7 +34,7 @@ const TabBar = ({tabs, groupId, addTab, ...otherProps}) => { ) } -const Tab = ({tab, removeTab, activateTab}) => { +const Tab = ({tab, removeTab, dispatch, activateTab}) => { const possibleStatus = { 'modified': '*', 'warning': '!', @@ -48,6 +49,8 @@ const Tab = ({tab, removeTab, activateTab}) => { modified: tab.flags.modified })} onClick={e => activateTab(tab.id)} + draggable='true' + onDragStart={e => dispatch(dragStart({sourceType: 'TAB', sourceId: tab.id}))} >
{tab.title}
@@ -90,7 +93,7 @@ class TabViewContainer extends Component { } componentWillMount () { - let tabGroup = this.props.TabState.getGroupById(this.props.tabGroupId) + let tabGroup = this.props.getGroupById(this.props.tabGroupId) if (!tabGroup) this.props.dispatch(TabActions.createGroup(this.state.groupId, this.props.defaultContentType)) } @@ -110,11 +113,7 @@ class TabViewContainer extends Component { } TabViewContainer = connect( - state => { - return { - TabState: state.TabState - } - }, + state => state.TabState, dispatch => { return { addTab: (groupId) => dispatch(TabActions.createTabInGroup(groupId)), diff --git a/app/components/Tab/reducer.js b/app/components/Tab/reducer.js index 9182ed10..6f6dbd50 100644 --- a/app/components/Tab/reducer.js +++ b/app/components/Tab/reducer.js @@ -98,7 +98,7 @@ const normalizeState = (prevState) => { tabGroups.forEach(tabGroup => { tabGroup.tabs.forEach(tab => tab.group = tabGroup) }) - return {tabGroups, getGroupById, getActiveGroup, activateGroup, activatePrevGroup, normalizeState} + return {_timestamp: Date.now(), tabGroups, getGroupById, getActiveGroup, activateGroup, activatePrevGroup, normalizeState} } function getGroupById (id) { diff --git a/app/containers/Utilities.jsx b/app/containers/Utilities.jsx index 00c9ee2b..e78deca5 100644 --- a/app/containers/Utilities.jsx +++ b/app/containers/Utilities.jsx @@ -1,13 +1,16 @@ /* @flow weak */ import React, { Component } from 'react' +import { connect } from 'react-redux' import ModalContainer from '../components/Modal' import Notification from '../components/Notification' +import DragAndDrop from '../components/DragAndDrop' const Utilities = () => { return (
+
) } diff --git a/app/store.js b/app/store.js index a77b005d..e7f12928 100644 --- a/app/store.js +++ b/app/store.js @@ -13,6 +13,7 @@ import NotificationReducer from './components/Notification/reducer' import TerminalReducer from './components/Terminal/reducer' import GitReducer from './components/Git/reducer' import WorkspaceReducer from './components/Workspace/reducer' +import DragAndDropReducer from './components/DragAndDrop/reducer' const combinedReducers = combineReducers({ WindowPaneState: PanelReducer, @@ -24,7 +25,8 @@ const combinedReducers = combineReducers({ TerminalState: TerminalReducer, GitState: GitReducer, NotificationState: NotificationReducer, - WorkspaceState: WorkspaceReducer + WorkspaceState: WorkspaceReducer, + DragAndDrop: DragAndDropReducer }) const crossReducers = composeReducers(PaneCrossReducer) diff --git a/app/styles/components/DragAndDrop.styl b/app/styles/components/DragAndDrop.styl new file mode 100644 index 00000000..566615ef --- /dev/null +++ b/app/styles/components/DragAndDrop.styl @@ -0,0 +1,6 @@ +.pane-layout-overlay { + z-index: z(pane-layout-overlay); + transition: all ease 0.2s; + opacity: 0; + background-color: white; +} diff --git a/app/styles/components/PaneView.styl b/app/styles/components/PaneView.styl index 13d52661..b0ab51f9 100644 --- a/app/styles/components/PaneView.styl +++ b/app/styles/components/PaneView.styl @@ -41,7 +41,7 @@ } .resize-bar { - z-index: 1000; + z-index: z(pane-resize-bar); &.col-resize { cursor: col-resize; absolute(right 0 top 0 bottom 0); diff --git a/app/styles/components/index.styl b/app/styles/components/index.styl index 2fa945bd..97323c58 100644 --- a/app/styles/components/index.styl +++ b/app/styles/components/index.styl @@ -10,3 +10,4 @@ @import "./StatusBar"; @import "./Workspace"; @import "./CommandPalette"; +@import "./DragAndDrop"; diff --git a/app/styles/zindex.styl b/app/styles/zindex.styl index 659eb269..9adaab24 100644 --- a/app/styles/zindex.styl +++ b/app/styles/zindex.styl @@ -17,10 +17,13 @@ z($e) { } $z-index-list = \ + (placeholder), (main-pane-view modal-container), + (pane-layout-overlay pane-resize-bar), (filetree-node-bg filetree-node-label), (modal-backdrop modal), (context-menu menu menu-bar-item-container), (utilities-container), (workspace-list); + diff --git a/package.json b/package.json index 1da76761..be4a6172 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "devDependencies": { "babel-core": "^6.13.2", "babel-loader": "^6.2.4", + "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.12.0", "babel-polyfill": "^6.13.0", "babel-preset-es2015": "^6.13.2",