From bf88c4d5349ca2d34d2febaeee83d448610a24b1 Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Sun, 29 Nov 2015 13:05:02 -0800 Subject: [PATCH 1/4] Import tab bar component and stylesheet. --- js/components/main.js | 8 +- js/components/tabs.js | 220 ++++++++++++++++++++++++++++++++++++++++++ js/entry.js | 1 + js/lib/appUrlUtil.js | 74 ++++++++++++++ js/lib/faviconUtil.js | 32 ++++++ less/tabs.less | 155 +++++++++++++++++++++++++++++ 6 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 js/components/tabs.js create mode 100644 js/lib/appUrlUtil.js create mode 100644 js/lib/faviconUtil.js create mode 100644 less/tabs.less diff --git a/js/components/main.js b/js/components/main.js index 746a1f628e6..9c3385d0bab 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -8,6 +8,7 @@ const AppActions = require('../actions/appActions') // Components const NavigationBar = require('./navigationBar') const Frame = require('./frame') +const Tabs = require('./tabs') // Constants const Config = require('../constants/config') @@ -34,11 +35,16 @@ class Main extends ImmutableComponent { ? 1 : b.get('key') > a.get('key') ? -1 : 0 return
-
+
+
diff --git a/js/components/tabs.js b/js/components/tabs.js new file mode 100644 index 00000000000..74d3f88d27c --- /dev/null +++ b/js/components/tabs.js @@ -0,0 +1,220 @@ +const React = require('react') +const ReactDOM = require('react-dom') + +import Immutable from 'immutable' +const ImmutableComponent = require('./immutableComponent') +const cx = require('../lib/classSet.js') + +const getFavicon = require('../lib/faviconUtil.js') + +class DragIndicator extends ImmutableComponent { + constructor (props) { + super(props) + } + + render () { + return
+ } +} + +class Tab extends ImmutableComponent { + constructor (props) { + super(props) + this.state = Immutable.fromJS({ + isDragging: false, + isDraggingOn: false, + isDraggingOverLeftHalf: false, + isDraggingOverRightHalf: false + }) + } + + get displayValue () { + // YouTube tries to change the title to add a play icon when + // there is audio. Since we have our own audio indicator we get + // rid of it. + return (this.props.frameProps.get('title') || + this.props.frameProps.get('location')).replace('▶ ', '') + } + + onDragStart (e) { + e.dataTransfer.setData('frameProps', + JSON.stringify(this.props.frameProps.toJS())) + this.setState('isDragging', true) + } + + onDragEnd () { + this.setState('isDragging', false) + } + + onDragOver (e) { + e.preventDefault() + + // Otherise, only accept it if we have some frameProps + if (!e.dataTransfer.getData('frameProps')) { + this.mergeState({ + isDraggingOn: true, + isDraggingOverLeftHalf: false, + isDraggingOverRightHalf: false + }) + return + } + + let rect = ReactDOM.findDOMNode(this.refs.tab).getBoundingClientRect() + if (e.clientX > rect.left && e.clientX < rect.left + rect.width / 2 && + !this.state.get('isDraggingOverLeftHalf')) { + this.mergeState({ + isDraggingOn: false, + isDraggingOverLeftHalf: true, + isDraggingOverRightHalf: false + }) + } else if (e.clientX < rect.right && e.clientX >= rect.left + rect.width / 2 && + !this.state.get('isDraggingOverRightHalf')) { + this.mergeState({ + isDraggingOn: false, + isDraggingOverLeftHalf: false, + isDraggingOverRightHalf: true + }) + } + } + + onDragLeave () { + if (this.state.get('isDraggingOverLeftHalf') || + this.state.get('isDraggingOn') || + this.state.get('isDraggingOverLeftHalf')) { + this.mergeState({ + isDraggingOn: false, + isDraggingOverLeftHalf: false, + isDraggingOverRightHalf: false + }) + } else if (this.state.get('isDraggingOverRightHalf')) { + this.setState('isDraggingOverRightHalf', false) + } + } + + onDrop (e) { + var dropText = e.dataTransfer.getData('text/plain') + if (dropText) { + this.props.onNavigate(dropText) + return + } + + let dataTransferString = e.dataTransfer.getData('frameProps') + if (!dataTransferString) { + return + } + + let sourceFrameProps = Immutable.fromJS(JSON.parse(dataTransferString)) + if (this.state.get('isDraggingOverLeftHalf')) { + this.props.onMoveFrame(sourceFrameProps, this.props.frameProps, true) + } else { + this.props.onMoveFrame(sourceFrameProps, this.props.frameProps, false) + } + this.mergeState({ + isDraggingOn: false, + isDraggingOverLeftHalf: false, + isDraggingOverRightHalf: false + }) + } + + render () { + const thumbnailWidth = 160 + const thumbnailHeight = 100 + + let thumbnailStyle = { + backgroundSize: `${thumbnailWidth} ${thumbnailHeight}`, + width: thumbnailWidth, + height: thumbnailHeight + } + if (this.props.frameProps.get('thumbnailUrl')) { + thumbnailStyle.backgroundImage = `url(${this.props.frameProps.get('thumbnailUrl')})` + } + + // Style based on theme-color + var activeTabStyle = {} + if (this.props.isActive && (this.props.frameProps.get('themeColor') || this.props.frameProps.get('computedThemeColor'))) { + activeTabStyle.backgroundColor = this.props.frameProps.get('themeColor') || this.props.frameProps.get('computedThemeColor') + } + + const iconStyle = { + backgroundImage: `url(${getFavicon(this.props.frameProps)})`, + backgroundSize: 16, + width: 16, + height: 16 + } + + let playIcon = null + if (this.props.frameProps.get('audioPlaybackActive') || + this.props.frameProps.get('audioMuted')) { + playIcon = + } + + return
+ +
+
+ +
+
+ {playIcon} + {this.displayValue} +
+
+ +
+ } +} + +class Tabs extends ImmutableComponent { + constructor () { + super() + } + + render () { + var tabWidth = 100 / this.props.frames.size + + return
+
+ { + this.props.frames.map(frameProps => ) + } +
+
+ } +} + +module.exports = Tabs diff --git a/js/entry.js b/js/entry.js index d0eca113267..a9c3483bf27 100644 --- a/js/entry.js +++ b/js/entry.js @@ -2,6 +2,7 @@ require('../less/browser.less') require('../less/main.less') require('../less/navigationBar.less') +require('../less/tabs.less') const React = require('react') const ReactDOM = require('react-dom') diff --git a/js/lib/appUrlUtil.js b/js/lib/appUrlUtil.js new file mode 100644 index 00000000000..3b567bf9282 --- /dev/null +++ b/js/lib/appUrlUtil.js @@ -0,0 +1,74 @@ +const Immutable = require('immutable') + +/** + * Determines the path of a relative URL from the hosted app + */ +export function getAppUrl (relativeUrl = './') { + return new window.URL(relativeUrl, window.location).href +} + +/** + * Returns the URL to the application's manifest + */ +export function getManifestUrl () { + return getAppUrl('./manifest.webapp') +} + +// Map of source about: URLs mapped to target URLs +export const aboutUrls = new Immutable.Map({ + 'about:about': getAppUrl('./about-about.html'), + 'about:blank': getAppUrl('./about-blank.html'), + 'about:history': getAppUrl('./about-history.html'), + 'about:newtab': getAppUrl('./about-newtab.html'), + 'about:preferences': getAppUrl('./about-preferences.html'), + 'about:settings': getAppUrl('./about-settings.html') +}) + +// Map of target URLs mapped to source about: URLs +const aboutUrlsReverse = new Immutable.Map(aboutUrls.reduce((obj, v, k) => { + obj[v] = k + return obj +}, {})) + +/** + * Obtains the target URL associated with an about: source URL + * Example: + * about:blank -> http://localhost:8000/about-blank/index.html + */ +export function getTargetAboutUrl (input) { + return aboutUrls.get(input) +} + +/** + * Obtains the source about: URL associated with a target URL + * Example: + * http://localhost:8000/about-blank.html -> about:blank + */ +export function getSourceAboutUrl (input) { + return aboutUrlsReverse.get(input) +} + +/** + * Determines if the passed in string is a source about: URL + * Example: isSourceAboutUrl('about:blank') -> true + */ +export function isSourceAboutUrl (input) { + return !!getTargetAboutUrl(input) +} + +/** + * Determines if the passed in string is the target of a source about: URL + * Example: isTargetAboutUrl('http://localhost:8000/about-blank/index.html') -> true + */ +export function isTargetAboutUrl (input) { + return !!getSourceAboutUrl(input) +} + +/** + * Determines whether the passed in string is pointing to a URL that + * should be privileged (mozapp attribute on the iframe) + * For now this is the same as an about URL. + */ +export function isPrivilegedUrl (input) { + return isSourceAboutUrl(input) +} diff --git a/js/lib/faviconUtil.js b/js/lib/faviconUtil.js new file mode 100644 index 00000000000..6fc16bd9f10 --- /dev/null +++ b/js/lib/faviconUtil.js @@ -0,0 +1,32 @@ +import { isSourceAboutUrl } from './appUrlUtil.js' +const UrlUtil = require('../../node_modules/urlutil.js/dist/node-urlutil.js') + +module.exports = function getFavicon (frameProps) { + if (!frameProps.get('location')) { + return null + } + + var size = window.devicePixelRatio * 16 + var resolution = '#-moz-resolution=' + size + ',' + size + var iconHref = frameProps.get('icon') + + // Default to favicon.ico if we can't find an icon. + if (!iconHref) { + var loc = frameProps.get('location') + if (UrlUtil.isViewSourceUrl(loc)) { + loc = loc.substring('view-source:'.length) + } else if (UrlUtil.isImageDataUrl(loc)) { + return loc + } else if (isSourceAboutUrl(loc) || UrlUtil.isDataUrl(loc)) { + return '' + } + + try { + var defaultIcon = new window.URL('/favicon.ico' + resolution, loc) + iconHref = defaultIcon.toString() + } catch (e) { + return '' + } + } + return iconHref + resolution +} diff --git a/less/tabs.less b/less/tabs.less new file mode 100644 index 00000000000..ca93290c687 --- /dev/null +++ b/less/tabs.less @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@import "variables.less"; + +.tabs { + position: relative; + z-index: 310; + padding: 8px 4px 0 6px; + height: 40px; + white-space: nowrap; + background: @chromeSecondary; + box-sizing: border-box; +} + +.tab { + background: -moz-linear-gradient(to bottom, #BEBFC2, #AFB0B4); + border-top-left-radius: @tabBorderRadius; + border-top-right-radius: @tabBorderRadius; + box-sizing: border-box; + margin: 0; + position: relative; + transition: transform 200ms ease; + border: 1px solid @chromeBorderColor; + border-width: 1px 1px 0; + left: 0; + opacity: 1.0; + padding: 4px 0 0; + height: 32px; + width: 100%; + line-height: 24px; + -moz-window-dragging: no-drag; + color: #3B3B3B; + + .tabTitle { + -moz-user-select: none; + box-sizing: border-box; + display: inline-block; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: calc(~'100% - 26px'); + } + .tabIcon { + display: inline-block; + margin: 0 4px; + position: relative; + top: -5px; + background-position: center; + background-repeat: no-repeat; + } + .playIcon { + cursor: default; + margin-right: 4px; + } + + .thumbnail { + display: none; + position: absolute; + top: 32px; + left: 0; + border: 1px solid #000; + padding: 10px; + background: #fff; + pointer-events: none; + z-index: 312; + } + + &.active { + color: #fff; + background: @hoverBlue; + } + + &.private { + background: @privateTabBackground; + color: #fff; + } + + &:hover { + .closeTab { + opacity: 1; + } + + .thumbnail { + display: block; + } + } + + &:not(.active):hover { + background: -moz-linear-gradient(to bottom, #DFDFDF, #AFB0B4); + } + + &.dragging { + &:hover { + .closeTab { + opacity: 0; + } + } + } + + .closeTab { + color: white; + cursor: pointer; + font-size: 24px; + height: 24px; + opacity: 0; + position: absolute; + right: -4px; + text-align: center; + top: -8px; + width: 24px; + + &:hover { + color: @hoverBlue; + } + + background-color: black; + border: 0px solid white; + border-radius: 50%; + z-index: 3; + } +} + +.tabArea { + position: relative; + display: inline-block; + height: 100%; + max-width: 184px; + box-sizing: border-box; + padding: 0 2px 0 0; + + hr.dragIndicator { + position: absolute; + top: 0; + left: -1px; + z-index: 100; + height: 100%; + width: 1px; + + &.dragIndicatorEnd { + bottom: -5px; + top: 0; + right: -3px; + left: unset; + } + + display: none; + &.dragActive { + display: block; + color: @hoverBlue; + } + } +} From fa6543b1f48e2ad1b419f5d2ee06e30fea9161f3 Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Sun, 29 Nov 2015 13:15:19 -0800 Subject: [PATCH 2/4] Add APP_SET_ACTIVE_FRAME action and trigger frame change on tab click. --- js/actions/appActions.js | 7 +++++++ js/components/tabs.js | 12 ++++++++++++ js/constants/appConstants.js | 3 ++- js/stores/appStore.js | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/js/actions/appActions.js b/js/actions/appActions.js index fb7cb6984ff..d0a5b067c53 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -26,6 +26,13 @@ const AppActions = { frameOpts: frameOpts, openInForeground }) + }, + + setActiveFrame: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_SET_ACTIVE_FRAME, + frameProps: frameProps + }) } } diff --git a/js/components/tabs.js b/js/components/tabs.js index 74d3f88d27c..ca9eef2ff9d 100644 --- a/js/components/tabs.js +++ b/js/components/tabs.js @@ -3,6 +3,8 @@ const ReactDOM = require('react-dom') import Immutable from 'immutable' const ImmutableComponent = require('./immutableComponent') + +const AppActions = require('../actions/appActions') const cx = require('../lib/classSet.js') const getFavicon = require('../lib/faviconUtil.js') @@ -120,6 +122,10 @@ class Tab extends ImmutableComponent { }) } + setActiveFrame () { + AppActions.setActiveFrame(this.props.frameProps) + } + render () { const thumbnailWidth = 160 const thumbnailHeight = 100 @@ -176,6 +182,12 @@ class Tab extends ImmutableComponent { ref='tab' draggable='true' title={this.props.frameProps.get('title')} + onDragStart={this.onDragStart.bind(this)} + onDragEnd={this.onDragEnd.bind(this)} + onDragLeave={this.onDragLeave.bind(this)} + onDragOver={this.onDragOver.bind(this)} + onDrop={this.onDrop.bind(this)} + onClick={this.setActiveFrame.bind(this)} style={activeTabStyle}>
diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index eb14c223163..3553747a244 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -1,5 +1,6 @@ module.exports = { APP_SET_URL: 1, APP_SET_NAVBAR_INPUT: 2, - APP_NEW_FRAME: 3 + APP_NEW_FRAME: 3, + APP_SET_ACTIVE_FRAME: 4 } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 62dd09cc64f..529f237da47 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -73,6 +73,12 @@ AppDispatcher.register((action) => { nextKey, action.openInForeground ? nextKey : appState.get('activeFrameKey'))) appStore.emitChange() break + case AppConstants.APP_SET_ACTIVE_FRAME: + appState = appState.merge({ + activeFrameKey: action.frameProps.get('key') + }) + appStore.emitChange() + break default: } }) From 3959c8644e9fa61189ac751dbf49bb04af61628d Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Mon, 30 Nov 2015 13:50:44 -0800 Subject: [PATCH 3/4] Fix activeFrame background. --- js/components/main.js | 2 +- js/state/frameStateUtil.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/js/components/main.js b/js/components/main.js index 9c3385d0bab..0ba7f6e5402 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -43,7 +43,7 @@ class Main extends ImmutableComponent {
diff --git a/js/state/frameStateUtil.js b/js/state/frameStateUtil.js index f2d324a1f8c..39f0223dc5d 100644 --- a/js/state/frameStateUtil.js +++ b/js/state/frameStateUtil.js @@ -17,6 +17,11 @@ export function getFrameByIndex (appState, i) { return appState.getIn(['frames', i]) } +export function getActiveFrame(appState) { + const activeFrameIndex = getActiveFrameIndex(appState) + return appState.get('frames').get(activeFrameIndex) +} + export function setActiveFrameIndex (appState, i) { const frame = getFrameByIndex(appState, i) if (!frame) { From de4a34d7347d539a7cbfccb53e19e80a9996caad Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Mon, 30 Nov 2015 14:46:29 -0800 Subject: [PATCH 4/4] Move drag & drop state handling outside of component. --- js/actions/appActions.js | 58 +++++++++++++++++++++++ js/components/main.js | 1 + js/components/tabs.js | 89 +++++++++++------------------------- js/constants/appConstants.js | 10 +++- js/state/frameStateUtil.js | 2 +- js/stores/appStore.js | 66 ++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 64 deletions(-) diff --git a/js/actions/appActions.js b/js/actions/appActions.js index d0a5b067c53..568db210f95 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -33,6 +33,64 @@ const AppActions = { actionType: AppConstants.APP_SET_ACTIVE_FRAME, frameProps: frameProps }) + }, + + tabDragStart: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAG_START, + frameProps + }) + }, + + tabDragStop: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAG_STOP, + frameProps + }) + }, + + tabDragDraggingOverLeftHalf: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAGGING_OVER_LEFT, + frameProps + }) + }, + + tabDragDraggingOverRightHalf: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAGGING_OVER_RIGHT, + frameProps + }) + }, + + tabDragExit: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAG_EXIT, + frameProps + }) + }, + + tabDragExitRightHalf: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAG_EXIT_RIGHT, + frameProps + }) + }, + + tabDraggingOn: function (frameProps) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_DRAGGING_ON, + frameProps + }) + }, + + moveTab: function (sourceFrameProps, destinationFrameProps, prepend) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_TAB_MOVE, + sourceFrameProps, + destinationFrameProps, + prepend + }) } } diff --git a/js/components/main.js b/js/components/main.js index 0ba7f6e5402..01900297359 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -41,6 +41,7 @@ class Main extends ImmutableComponent { activeFrame={this.props.browser.get('frame')} /> rect.left && e.clientX < rect.left + rect.width / 2 && - !this.state.get('isDraggingOverLeftHalf')) { - this.mergeState({ - isDraggingOn: false, - isDraggingOverLeftHalf: true, - isDraggingOverRightHalf: false - }) + !this.props.frameProps.get('tabIsDraggingOverLeftHalf')) { + AppActions.tabDragDraggingOverLeftHalf(this.props.frameProps) } else if (e.clientX < rect.right && e.clientX >= rect.left + rect.width / 2 && - !this.state.get('isDraggingOverRightHalf')) { - this.mergeState({ - isDraggingOn: false, - isDraggingOverLeftHalf: false, - isDraggingOverRightHalf: true - }) + !this.props.frameProps.get('tabIsDraggingOverRightHalf')) { + AppActions.tabDragDraggingOverRightHalf(this.props.frameProps) } } onDragLeave () { - if (this.state.get('isDraggingOverLeftHalf') || - this.state.get('isDraggingOn') || - this.state.get('isDraggingOverLeftHalf')) { - this.mergeState({ - isDraggingOn: false, - isDraggingOverLeftHalf: false, - isDraggingOverRightHalf: false - }) - } else if (this.state.get('isDraggingOverRightHalf')) { - this.setState('isDraggingOverRightHalf', false) + if (this.props.frameProps.get('tabIsDraggingOverLeftHalf') || + this.props.frameProps.get('tabIsDraggingOn') || + this.props.frameProps.get('tabIsDraggingOverLeftHalf')) { + AppActions.tabDragExit(this.props.frameProps) + } else if (this.props.frameProps.get('tabIsDraggingOverRightHalf')) { + AppActions.tabDragExitRightHalf(this.props.frameProps) } } onDrop (e) { - var dropText = e.dataTransfer.getData('text/plain') - if (dropText) { - this.props.onNavigate(dropText) + let sourceFrameProps = this.props.activeDraggedTab + if (!sourceFrameProps) { return } - let dataTransferString = e.dataTransfer.getData('frameProps') - if (!dataTransferString) { - return - } - - let sourceFrameProps = Immutable.fromJS(JSON.parse(dataTransferString)) - if (this.state.get('isDraggingOverLeftHalf')) { - this.props.onMoveFrame(sourceFrameProps, this.props.frameProps, true) + if (this.props.frameProps.get('tabIsDraggingOverLeftHalf')) { + AppActions.moveTab(sourceFrameProps, this.props.frameProps, true) } else { - this.props.onMoveFrame(sourceFrameProps, this.props.frameProps, false) + AppActions.moveTab(sourceFrameProps, this.props.frameProps, false) } - this.mergeState({ - isDraggingOn: false, - isDraggingOverLeftHalf: false, - isDraggingOverRightHalf: false - }) + AppActions.tabDragExit(this.props.frameProps) } setActiveFrame () { @@ -169,15 +133,15 @@ class Tab extends ImmutableComponent { style={{ width: `${this.props.tabWidth}%` }}> - +
+ active={this.props.frameProps.get('tabIsDraggingOverRightHalf')}/>
} } @@ -218,6 +182,7 @@ class Tabs extends ImmutableComponent {
{ this.props.frames.map(frameProps => { }) appStore.emitChange() break + case AppConstants.APP_TAB_DRAG_START: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDragging: true + }) + appState = appState.setIn(['ui', 'tabs', 'activeDraggedTab'], action.frameProps) + appStore.emitChange() + break + case AppConstants.APP_TAB_DRAG_STOP: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDragging: false + }) + appState = appState.setIn(['ui', 'tabs', 'activeDraggedTab'], null) + appStore.emitChange() + break + case AppConstants.APP_TAB_DRAGGING_OVER_LEFT: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDraggingOn: false, + tabIsDraggingOverLeftHalf: true, + tabIsDraggingOverRightHalf: false + }) + appStore.emitChange() + break + case AppConstants.APP_TAB_DRAGGING_OVER_RIGHT: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDraggingOn: false, + tabIsDraggingOverLeftHalf: false, + tabIsDraggingOverRightHalf: true + }) + appStore.emitChange() + break + case AppConstants.APP_TAB_DRAG_EXIT: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDraggingOn: false, + tabIsDraggingOverLeftHalf: false, + tabIsDraggingOverRightHalf: false + }) + appStore.emitChange() + break + case AppConstants.APP_TAB_DRAG_EXIT_RIGHT: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDraggingOverRightHalf: false + }) + appStore.emitChange() + break + case AppConstants.APP_TAB_DRAGGING_ON: + appState = appState.mergeIn(['frames', FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.frameProps)], { + tabIsDraggingOn: true, + tabIsDraggingOverLeftHalf: false, + tabIsDraggingOverRightHalf: false + }) + appStore.emitChange() + break + case AppConstants.APP_TAB_MOVE: + let sourceFramePropsIndex = FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.sourceFrameProps) + let newIndex = FrameStateUtil.getFramePropsIndex(appState.get('frames'), action.destinationFrameProps) + (action.prepend ? 0 : 1) + let frames = appState.get('frames').splice(sourceFramePropsIndex, 1) + if (newIndex > sourceFramePropsIndex) { + newIndex-- + } + frames = frames.splice(newIndex, 0, action.sourceFrameProps) + appState = appState.set('frames', frames) + appStore.emitChange() + break default: } })