Files
freeCodeCamp/common/app/Panes/redux/index.js
Berkeley Martinez 8498abc95a fix(Panes): Fix rightPane isHidden shadow bound
a rightpane that is hidden would also provide a shadow barrier to
minimizing a pane just left of it.
2018-01-05 21:24:30 -08:00

294 lines
8.1 KiB
JavaScript

import _ from 'lodash';
import invariant from 'invariant';
import {
composeReducers,
createAction,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import { types as challengeTypes } from '../../routes/Challenges/redux';
import ns from '../ns.json';
import windowEpic from './window-epic.js';
import dividerEpic from './divider-epic.js';
export const epics = [
windowEpic,
dividerEpic
];
export const types = createTypes([
'panesMapUpdated',
'panesMounted',
'panesUpdated',
'panesWillMount',
'panesWillUnmount',
'updateSize',
'dividerClicked',
'dividerMoved',
'mouseReleased',
'windowResized',
// commands
'hidePane',
'updateNavHeight'
], ns);
export const panesMapUpdated = createAction(
types.panesMapUpdated,
null,
(type, panesMap) => ({ trigger: type, panesMap })
);
export const panesMounted = createAction(types.panesMounted);
export const panesUpdated = createAction(types.panesUpdated);
export const panesWillMount = createAction(types.panesWillMount);
export const panesWillUnmount = createAction(types.panesWillUnmount);
export const dividerClicked = createAction(types.dividerClicked);
export const dividerMoved = createAction(types.dividerMoved);
export const mouseReleased = createAction(types.mouseReleased);
export const windowResized = createAction(types.windowResized);
// commands
export const hidePane = createAction(types.hidePane);
export const updateNavHeight = createAction(types.updateNavHeight);
const defaultState = {
height: 600,
width: 800,
navHeight: 50,
isMapPaneHidden: false,
panes: [],
panesByName: {},
pressedDivider: null,
panesMap: {}
};
export const getNS = state => state[ns];
export const heightSelector = state => {
const { navHeight, height } = getNS(state);
return height - navHeight;
};
export const panesSelector = state => getNS(state).panes;
export const panesByNameSelector = state => getNS(state).panesByName;
export const pressedDividerSelector =
state => getNS(state).pressedDivider;
export const widthSelector = state => getNS(state).width;
export const panesMapSelector = state => getNS(state).panesMap;
function isPanesAction({ type } = {}, panesMap) {
return !!panesMap[type];
}
function getDividerLeft(numOfPanes, index) {
let dividerLeft = null;
if (numOfPanes > 1 && numOfPanes !== index + 1) {
dividerLeft = (100 / numOfPanes) * (index + 1);
}
return dividerLeft;
}
function checkForTypeKeys(panesMap) {
_.forEach(panesMap, (_, actionType) => {
invariant(
actionType !== 'undefined',
`action type for ${panesMap[actionType]} is undefined`
);
});
return panesMap;
}
const getPane = (panesByName, panes, index) => _.get(
panesByName,
getPaneName(panes, index),
null
);
const getPaneName = (panes, index) => _.get(
panes,
index,
''
);
const createGetBound = isRight => (pane, buffer) =>
(pane && !pane.isHidden && pane.dividerLeft || (isRight ? 100 : 0)) - buffer;
const getRightBound = createGetBound(true);
const getLeftBound = createGetBound(false);
function normalizePanesMapCreator(createPanesMap) {
invariant(
_.isFunction(createPanesMap),
'createPanesMap should be a function but got %s',
createPanesMap
);
const panesMap = createPanesMap({}, { type: '@@panes/test' });
if (typeof panesMap === 'function') {
return normalizePanesMapCreator(panesMap);
}
invariant(
!panesMap,
'panesMap test should return undefined or null on test action but got %s',
panesMap
);
return createPanesMap;
}
export default function createPanesAspects({ createPanesMap }) {
createPanesMap = normalizePanesMapCreator(createPanesMap);
function middleware({ getState }) {
return next => action => {
let finalAction = action;
const panesMap = panesMapSelector(getState());
if (isPanesAction(action, panesMap)) {
finalAction = {
...action,
meta: {
...action.meta,
isPaneAction: true,
paneName: panesMap[action.type]
}
};
}
const result = next(finalAction);
const nextPanesMap = createPanesMap(getState(), action);
if (nextPanesMap) {
checkForTypeKeys(nextPanesMap);
next(panesMapUpdated(action.type, nextPanesMap));
}
return result;
};
}
const reducer = composeReducers(
ns,
handleActions(
() => ({
[types.dividerClicked]: (state, { payload: name }) => ({
...state,
pressedDivider: name
}),
[types.dividerMoved]: (state, { payload: clientX }) => {
const {
panes,
panesByName,
pressedDivider: paneName,
width
} = state;
const dividerBuffer = (200 / width) * 100;
const paneIndex =
_.findIndex(state.panes, ({ name }) => paneName === name);
const currentPane = panesByName[paneName];
const rightPane = getPane(panesByName, panes, paneIndex + 1);
const leftPane = getPane(panesByName, panes, paneIndex - 1);
const rightBound = getRightBound(rightPane, dividerBuffer);
const leftBound = getLeftBound(leftPane, dividerBuffer);
const newPosition = _.clamp(
(clientX / width) * 100,
leftBound,
rightBound
);
return {
...state,
panesByName: {
...state.panesByName,
[currentPane.name]: {
...currentPane,
dividerLeft: newPosition
}
}
};
},
[types.mouseReleased]: state => ({ ...state, pressedDivider: null }),
[types.windowResized]: (state, { payload: { height, width } }) => ({
...state,
height,
width
}),
// used to clear bin buttons
[types.panesWillUnmount]: state => ({
...state,
panes: [],
panesByName: {},
pressedDivider: null
}),
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
...state,
navHeight
}),
[challengeTypes.toggleMap]: state => ({
...state,
isMapPaneHidden: !state.isMapPaneHidden
})
}),
defaultState
),
function metaReducer(state = defaultState, action) {
if (action.meta && action.meta.panesMap) {
const panesMap = action.meta.panesMap;
const panes = _.map(panesMap, (name, type) => ({ name, type }));
const numOfPanes = Object.keys(panes).length;
return {
...state,
panesMap,
panes,
panesByName: panes.reduce((panes, { name }, index) => {
const dividerLeft = getDividerLeft(numOfPanes, index);
panes[name] = {
name,
dividerLeft,
isHidden: name === 'Map' ? state.isMapPaneHidden : false
};
return panes;
}, {})
};
}
if (action.meta && action.meta.isPaneAction) {
const name = action.meta.paneName;
const oldPane = state.panesByName[name];
const pane = {
...oldPane,
isHidden: !oldPane.isHidden
};
const panesByName = {
...state.panesByName,
[name]: pane
};
const numOfPanes = state.panes.reduce((sum, { name }) => {
return panesByName[name].isHidden ? sum : sum + 1;
}, 0);
let numOfHidden = 0;
return {
...state,
panesByName: state.panes.reduce(
(panesByName, { name }, index) => {
if (!panesByName[name].isHidden) {
const dividerLeft = getDividerLeft(
numOfPanes,
index - numOfHidden
);
panesByName[name] = {
...panesByName[name],
dividerLeft
};
} else {
numOfHidden = numOfHidden + 1;
}
return panesByName;
},
panesByName
)
};
}
return state;
}
);
return {
reducer,
middleware
};
}