Merge branch 'develop' into bug/sidebar-name-overflow
This commit is contained in:
commit
223d71688e
12 changed files with 265 additions and 43 deletions
|
@ -12,6 +12,7 @@ import Exit from '../images/exit.svg';
|
|||
import DropdownArrow from '../images/down-filled-triangle.svg';
|
||||
import Preferences from '../images/preferences.svg';
|
||||
import Play from '../images/triangle-arrow-right.svg';
|
||||
import More from '../images/more.svg';
|
||||
import Code from '../images/code.svg';
|
||||
import Terminal from '../images/terminal.svg';
|
||||
|
||||
|
@ -77,4 +78,6 @@ export const ExitIcon = withLabel(Exit);
|
|||
export const DropdownArrowIcon = withLabel(DropdownArrow);
|
||||
export const PreferencesIcon = withLabel(Preferences);
|
||||
export const PlayIcon = withLabel(Play);
|
||||
export const MoreIcon = withLabel(More);
|
||||
export const TerminalIcon = withLabel(Terminal);
|
||||
export const CodeIcon = withLabel(Code);
|
||||
|
|
92
client/components/Dropdown.jsx
Normal file
92
client/components/Dropdown.jsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
import { remSize, prop, common } from '../theme';
|
||||
import IconButton from './mobile/IconButton';
|
||||
import Button from '../common/Button';
|
||||
|
||||
const DropdownWrapper = styled.ul`
|
||||
background-color: ${prop('Modal.background')};
|
||||
border: 1px solid ${prop('Modal.border')};
|
||||
box-shadow: 0 0 18px 0 ${prop('shadowColor')};
|
||||
color: ${prop('primaryTextColor')};
|
||||
|
||||
position: absolute;
|
||||
right: ${props => (props.right ? 0 : 'initial')};
|
||||
left: ${props => (props.left ? 0 : 'initial')};
|
||||
|
||||
${props => (props.align === 'right' && 'right: 0;')}
|
||||
${props => (props.align === 'left' && 'left: 0;')}
|
||||
|
||||
|
||||
text-align: left;
|
||||
width: ${remSize(180)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
z-index: 9999;
|
||||
border-radius: ${remSize(6)};
|
||||
|
||||
& li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; }
|
||||
& li:last-child { border-radius: 0 0 ${remSize(5)} ${remSize(5)}; }
|
||||
|
||||
& li:hover {
|
||||
|
||||
background-color: ${prop('Button.hover.background')};
|
||||
color: ${prop('Button.hover.foreground')};
|
||||
|
||||
& button, & a {
|
||||
color: ${prop('Button.hover.foreground')};
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
height: ${remSize(36)};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& button,
|
||||
& a {
|
||||
color: ${prop('primaryTextColor')};
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: ${remSize(8)} ${remSize(16)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: Add Icon to the left of the items in the menu
|
||||
// const MaybeIcon = (Element, label) => Element && <Element aria-label={label} />;
|
||||
|
||||
const Dropdown = ({ items, align }) => (
|
||||
<DropdownWrapper align={align} >
|
||||
{/* className="nav__items-left" */}
|
||||
{items && items.map(({ title, icon, href }) => (
|
||||
<li key={`nav-${title && title.toLowerCase()}`}>
|
||||
<Link to={href}>
|
||||
{/* {MaybeIcon(icon, `Navigate to ${title}`)} */}
|
||||
{title}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</DropdownWrapper>
|
||||
);
|
||||
|
||||
Dropdown.propTypes = {
|
||||
align: PropTypes.oneOf(['left', 'right']),
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
action: PropTypes.func,
|
||||
icon: PropTypes.func,
|
||||
href: PropTypes.string
|
||||
})),
|
||||
};
|
||||
|
||||
Dropdown.defaultProps = {
|
||||
items: [],
|
||||
align: null
|
||||
};
|
||||
|
||||
export default Dropdown;
|
27
client/components/OverlayManager.jsx
Normal file
27
client/components/OverlayManager.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const OverlayManager = ({ overlay, hideOverlay }) => {
|
||||
// const [visible, trigger, setRef] = useModalBehavior();
|
||||
|
||||
const jsx = (
|
||||
<React.Fragment>
|
||||
{/* <div ref={setRef} >
|
||||
{visible && <Dropdown items={headerNavOptions} />}
|
||||
</div> */}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return jsx && createPortal(jsx, document.body);
|
||||
};
|
||||
|
||||
|
||||
OverlayManager.propTypes = {
|
||||
overlay: PropTypes.string,
|
||||
hideOverlay: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
OverlayManager.defaultProps = { overlay: null };
|
||||
|
||||
export default OverlayManager;
|
|
@ -31,7 +31,9 @@ const HeaderDiv = styled.div`
|
|||
|
||||
const IconContainer = styled.div`
|
||||
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: horizontal;
|
||||
`;
|
||||
|
||||
|
||||
|
|
10
client/components/useAsModal.jsx
Normal file
10
client/components/useAsModal.jsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useModalBehavior } from '../utils/custom-hooks';
|
||||
|
||||
export default (component) => {
|
||||
const [visible, trigger, setRef] = useModalBehavior();
|
||||
|
||||
const wrapper = () => <div ref={setRef}> {visible && component} </div>; // eslint-disable-line
|
||||
|
||||
return [trigger, wrapper];
|
||||
};
|
5
client/images/more.svg
Normal file
5
client/images/more.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.28004 13.76C10.5207 13.76 11.52 14.7324 11.52 16.0174C11.52 17.2676 10.5207 18.24 9.28004 18.24C8.03942 18.24 7.04004 17.2676 7.04004 16.0174C7.04004 14.7324 8.03942 13.76 9.28004 13.76Z" fill="#333333"/>
|
||||
<path d="M18.24 16C18.24 14.7629 17.2371 13.76 16 13.76C14.7629 13.76 13.76 14.7629 13.76 16C13.76 17.2371 14.7629 18.24 16 18.24C17.2371 18.24 18.24 17.2371 18.24 16Z" fill="#333333"/>
|
||||
<path d="M22.72 13.76C23.9606 13.76 24.96 14.705 24.96 15.965C24.96 17.295 23.9606 18.24 22.72 18.24C21.4794 18.24 20.48 17.295 20.48 15.965C20.48 14.74 21.4794 13.76 22.72 13.76Z" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 704 B |
|
@ -88,8 +88,6 @@ const Console = () => {
|
|||
|
||||
const cm = useRef({});
|
||||
|
||||
useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; });
|
||||
|
||||
const consoleClass = classNames({
|
||||
'preview-console': true,
|
||||
'preview-console--collapsed': !isExpanded
|
||||
|
|
|
@ -7,6 +7,7 @@ import styled from 'styled-components';
|
|||
|
||||
// Imports to be Refactored
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import * as FileActions from '../actions/files';
|
||||
import * as IDEActions from '../actions/ide';
|
||||
import * as ProjectActions from '../actions/project';
|
||||
|
@ -19,7 +20,7 @@ import { getHTMLFile } from '../reducers/files';
|
|||
|
||||
// Local Imports
|
||||
import Editor from '../components/Editor';
|
||||
import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons';
|
||||
import { PlayIcon, ExitIcon, MoreIcon } from '../../../common/icons';
|
||||
|
||||
import IconButton from '../../../components/mobile/IconButton';
|
||||
import Header from '../../../components/mobile/Header';
|
||||
|
@ -28,7 +29,11 @@ import Footer from '../../../components/mobile/Footer';
|
|||
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
||||
import Console from '../components/Console';
|
||||
import { remSize } from '../../../theme';
|
||||
// import OverlayManager from '../../../components/OverlayManager';
|
||||
import ActionStrip from '../../../components/mobile/ActionStrip';
|
||||
import useAsModal from '../../../components/useAsModal';
|
||||
import { PreferencesIcon } from '../../../common/icons';
|
||||
import Dropdown from '../../../components/Dropdown';
|
||||
|
||||
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
|
||||
|
||||
|
@ -37,17 +42,30 @@ const Expander = styled.div`
|
|||
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
||||
`;
|
||||
|
||||
const NavItem = styled.li`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const headerNavOptions = [
|
||||
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
|
||||
{ icon: PreferencesIcon, title: 'Examples', href: '/mobile/examples' },
|
||||
{ icon: PreferencesIcon, title: 'Original Editor', href: '/', },
|
||||
];
|
||||
|
||||
|
||||
const MobileIDEView = (props) => {
|
||||
const {
|
||||
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||
selectedFile, updateFileContent, files,
|
||||
closeEditorOptions, showEditorOptions, showKeyboardShortcutModal, setUnsavedChanges,
|
||||
closeEditorOptions, showEditorOptions,
|
||||
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
|
||||
} = props;
|
||||
|
||||
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
||||
const [overlay, setOverlay] = useState(null); // eslint-disable-line
|
||||
|
||||
|
||||
const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown align="right" items={headerNavOptions} />);
|
||||
|
||||
return (
|
||||
<Screen fullscreen>
|
||||
|
@ -58,13 +76,17 @@ const MobileIDEView = (props) => {
|
|||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||
}
|
||||
>
|
||||
<NavItem>
|
||||
<IconButton
|
||||
to="/mobile/preferences"
|
||||
onClick={() => setOverlay('preferences')}
|
||||
icon={PreferencesIcon}
|
||||
aria-label="Open preferences menu"
|
||||
onClick={triggerNavDropdown}
|
||||
icon={MoreIcon}
|
||||
aria-label="Options"
|
||||
/>
|
||||
<NavDropDown />
|
||||
</NavItem>
|
||||
<li>
|
||||
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
|
||||
</li>
|
||||
</Header>
|
||||
|
||||
<IDEWrapper>
|
||||
|
@ -82,9 +104,7 @@ const MobileIDEView = (props) => {
|
|||
editorOptionsVisible={ide.editorOptionsVisible}
|
||||
showEditorOptions={showEditorOptions}
|
||||
closeEditorOptions={closeEditorOptions}
|
||||
showKeyboardShortcutModal={showKeyboardShortcutModal}
|
||||
setUnsavedChanges={setUnsavedChanges}
|
||||
isPlaying={ide.isPlaying}
|
||||
showKeyboard={ide.isPlaying}
|
||||
theme={preferences.theme}
|
||||
startRefreshSketch={startRefreshSketch}
|
||||
stopSketch={stopSketch}
|
||||
|
@ -103,6 +123,7 @@ const MobileIDEView = (props) => {
|
|||
provideController={setTmController}
|
||||
/>
|
||||
</IDEWrapper>
|
||||
|
||||
<Footer>
|
||||
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>}
|
||||
<ActionStrip />
|
||||
|
@ -111,7 +132,6 @@ const MobileIDEView = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
MobileIDEView.propTypes = {
|
||||
|
||||
preferences: PropTypes.shape({
|
||||
|
@ -130,7 +150,7 @@ MobileIDEView.propTypes = {
|
|||
ide: PropTypes.shape({
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||
consoleEvent: PropTypes.array,
|
||||
consoleEvent: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
modalIsVisible: PropTypes.bool.isRequired,
|
||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||
|
@ -156,7 +176,7 @@ MobileIDEView.propTypes = {
|
|||
}).isRequired,
|
||||
|
||||
editorAccessibility: PropTypes.shape({
|
||||
lintMessages: PropTypes.array.isRequired,
|
||||
lintMessages: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
}).isRequired,
|
||||
|
||||
project: PropTypes.shape({
|
||||
|
@ -193,10 +213,6 @@ MobileIDEView.propTypes = {
|
|||
|
||||
showEditorOptions: PropTypes.func.isRequired,
|
||||
|
||||
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||
|
||||
setUnsavedChanges: PropTypes.func.isRequired,
|
||||
|
||||
startRefreshSketch: PropTypes.func.isRequired,
|
||||
|
||||
stopSketch: PropTypes.func.isRequired,
|
||||
|
|
|
@ -45,7 +45,7 @@ const initialState = () => {
|
|||
name: 'root',
|
||||
id: r,
|
||||
_id: r,
|
||||
children: [a, b, c],
|
||||
children: [b, a, c],
|
||||
fileType: 'folder',
|
||||
content: ''
|
||||
},
|
||||
|
@ -110,6 +110,32 @@ function deleteMany(state, ids) {
|
|||
return newState;
|
||||
}
|
||||
|
||||
function sortedChildrenId(state, children) {
|
||||
const childrenArray = state.filter(file => children.includes(file.id));
|
||||
childrenArray.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
return childrenArray.map(child => child.id);
|
||||
}
|
||||
|
||||
function updateParent(state, action) {
|
||||
return state.map((file) => {
|
||||
if (file.id === action.parentId) {
|
||||
const newFile = Object.assign({}, file);
|
||||
newFile.children = [...newFile.children, action.id];
|
||||
return newFile;
|
||||
}
|
||||
return file;
|
||||
});
|
||||
}
|
||||
|
||||
function renameFile(state, action) {
|
||||
return state.map((file) => {
|
||||
if (file.id !== action.id) {
|
||||
return file;
|
||||
}
|
||||
return Object.assign({}, file, { name: action.name });
|
||||
});
|
||||
}
|
||||
|
||||
const files = (state, action) => {
|
||||
if (state === undefined) {
|
||||
state = initialState(); // eslint-disable-line
|
||||
|
@ -138,15 +164,8 @@ const files = (state, action) => {
|
|||
return initialState();
|
||||
case ActionTypes.CREATE_FILE: // eslint-disable-line
|
||||
{
|
||||
const newState = state.map((file) => {
|
||||
if (file.id === action.parentId) {
|
||||
const newFile = Object.assign({}, file);
|
||||
newFile.children = [...newFile.children, action.id];
|
||||
return newFile;
|
||||
}
|
||||
return file;
|
||||
});
|
||||
return [...newState,
|
||||
const newState = [
|
||||
...updateParent(state, action),
|
||||
{
|
||||
name: action.name,
|
||||
id: action.id,
|
||||
|
@ -156,15 +175,23 @@ const files = (state, action) => {
|
|||
children: action.children,
|
||||
fileType: action.fileType || 'file'
|
||||
}];
|
||||
return newState.map((file) => {
|
||||
if (file.id === action.parentId) {
|
||||
file.children = sortedChildrenId(newState, file.children);
|
||||
}
|
||||
return file;
|
||||
});
|
||||
}
|
||||
case ActionTypes.UPDATE_FILE_NAME:
|
||||
return state.map((file) => {
|
||||
if (file.id !== action.id) {
|
||||
return file;
|
||||
{
|
||||
const newState = renameFile(state, action);
|
||||
return newState.map((file) => {
|
||||
if (file.children.includes(action.id)) {
|
||||
file.children = sortedChildrenId(newState, file.children);
|
||||
}
|
||||
|
||||
return Object.assign({}, file, { name: action.name });
|
||||
return file;
|
||||
});
|
||||
}
|
||||
case ActionTypes.DELETE_FILE:
|
||||
{
|
||||
const newState = deleteMany(state, [action.id, ...getAllDescendantIds(state, action.id)]);
|
||||
|
@ -200,7 +227,10 @@ const files = (state, action) => {
|
|||
return file;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
return state.map((file) => {
|
||||
file.children = sortedChildrenId(state, file.children);
|
||||
return file;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, useSelector, useDispatch } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import Header from '../../components/mobile/Header';
|
||||
import IconButton from '../../components/mobile/IconButton';
|
||||
|
@ -25,7 +24,7 @@ const Content = styled.div`
|
|||
margin-top: ${remSize(68)};
|
||||
`;
|
||||
|
||||
const MobileSketchView = (props) => {
|
||||
const MobileSketchView = () => {
|
||||
const { files, ide, preferences } = useSelector(state => state);
|
||||
|
||||
const htmlFile = useSelector(state => getHTMLFile(state.files));
|
||||
|
|
|
@ -41,7 +41,8 @@ export const grays = {
|
|||
};
|
||||
|
||||
export const common = {
|
||||
baseFontSize: 12
|
||||
baseFontSize: 12,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.16)'
|
||||
};
|
||||
|
||||
export const remSize = size => `${size / common.baseFontSize}rem`;
|
||||
|
@ -97,6 +98,10 @@ export default {
|
|||
border: grays.middleLight,
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
background: grays.light,
|
||||
border: grays.middleLight
|
||||
},
|
||||
Separator: grays.middleLight,
|
||||
},
|
||||
[Theme.dark]: {
|
||||
|
@ -138,6 +143,10 @@ export default {
|
|||
border: grays.middleDark,
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
background: grays.dark,
|
||||
border: grays.middleDark
|
||||
},
|
||||
Separator: grays.middleDark,
|
||||
},
|
||||
[Theme.contrast]: {
|
||||
|
@ -179,6 +188,10 @@ export default {
|
|||
border: grays.middleDark,
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
background: grays.dark,
|
||||
border: grays.middleDark
|
||||
},
|
||||
Separator: grays.middleDark,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export const noop = () => {};
|
||||
|
||||
|
@ -13,3 +13,30 @@ export const useDidUpdate = (callback, deps) => {
|
|||
}
|
||||
}, deps);
|
||||
};
|
||||
|
||||
// Usage: const ref = useModalBehavior(() => setSomeState(false))
|
||||
// place this ref on a component
|
||||
export const useModalBehavior = (hideOverlay) => {
|
||||
const ref = useRef({});
|
||||
|
||||
// Return values
|
||||
const setRef = (r) => { ref.current = r; };
|
||||
const [visible, setVisible] = useState(false);
|
||||
const trigger = () => setVisible(!visible);
|
||||
|
||||
const hide = () => setVisible(false);
|
||||
|
||||
|
||||
const handleClickOutside = ({ target }) => {
|
||||
if (ref && ref.current && !ref.current.contains(target)) {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [ref]);
|
||||
|
||||
return [visible, trigger, setRef];
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue