diff --git a/src/components/add-page-button/demo.tsx b/src/components/add-page-button/demo.tsx new file mode 100644 index 000000000..0fbdfeeca --- /dev/null +++ b/src/components/add-page-button/demo.tsx @@ -0,0 +1,11 @@ +import DemoContainer from '../demo-container'; +import * as React from 'react'; +import { AddPageButton } from '.'; + +const AddPageButtonDemo: React.StatelessComponent = (): JSX.Element => ( + + + +); + +export default AddPageButtonDemo; diff --git a/src/components/add-page-button/index.tsx b/src/components/add-page-button/index.tsx new file mode 100644 index 000000000..ff361dfb1 --- /dev/null +++ b/src/components/add-page-button/index.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Color } from '../colors'; +import { Copy } from '../copy'; +import { getSpace, SpaceSize } from '../space'; +import { Icon, IconName, IconSize } from '../icons'; +import styled from 'styled-components'; + +export interface AddPageButtonProps { + onClick?: React.MouseEventHandler; +} + +const StyledAddPageButton = styled.button` + position: relative; + box-sizing: border-box; + height: 60px; + width: 100%; + border: 1px solid ${Color.Grey80}; + border-radius: 6px; + background-color: transparent; + margin: ${getSpace(SpaceSize.S)}px; + display: flex; + align-items: center; + justify-content: center; + transition: border 0.2s; + user-select: none; + + &:focus { + outline: none; + } + + &:hover { + border: 1px solid ${Color.Grey60}; + } +`; + +const StyledIcon = styled(Icon)` + margin-right: ${getSpace(SpaceSize.XS)}px; +`; + +export const AddPageButton: React.SFC = props => ( + + + Add Page + +); diff --git a/src/components/add-page-button/pattern.json b/src/components/add-page-button/pattern.json new file mode 100644 index 000000000..704647c0a --- /dev/null +++ b/src/components/add-page-button/pattern.json @@ -0,0 +1,8 @@ +{ + "name": "add-page-button", + "description": "Button to add pages", + "displayName": "Add Page Button", + "version": "1.0.0", + "tags": ["atom", "button"], + "flag": "alpha" +} diff --git a/src/components/colors/index.tsx b/src/components/colors/index.tsx index e8582b16f..f31d33270 100644 --- a/src/components/colors/index.tsx +++ b/src/components/colors/index.tsx @@ -5,6 +5,7 @@ export enum Color { BlueAlpha40 = 'rgba(0, 112, 214, .4)', Blue20 = 'rgb(51, 141, 222)', Blue40 = 'rgb(102, 169, 230)', + Blue60 = 'rgb(153, 198, 239)', Blue80 = 'rgb(212, 226, 242)', Green = 'rgb(91, 226, 122)', Grey20 = 'rgb(52, 61, 69)', diff --git a/src/components/element/demo.tsx b/src/components/element/demo.tsx index 5ef189f21..0189f0a88 100644 --- a/src/components/element/demo.tsx +++ b/src/components/element/demo.tsx @@ -1,5 +1,5 @@ import DemoContainer from '../demo-container'; -import { Element, ElementState } from './index'; +import { Element, ElementCapability, ElementState } from './index'; import * as React from 'react'; // tslint:disable-next-line:no-empty @@ -9,8 +9,7 @@ const CHILD = ( = (): JSX.Element => ( = (): JSX.Element => ( Active = (): JSX.Element => ( Highlighted = (): JSX.Element => ( Placeholder Highlighted = (): JSX.Element => ( Editable = (): JSX.Element => ( May open, closed = (): JSX.Element => ( May open, opened = (): JSX.Element => ( With child, active and open ; open: boolean; placeholderHighlighted?: boolean; @@ -45,6 +51,7 @@ interface StyledIconProps { interface LabelContentProps { active: boolean; + editable?: boolean; } export interface StyledElementChildProps { @@ -178,7 +185,8 @@ const LabelContent = styled.div` text-overflow: ellipsis; white-space: nowrap; width: 100%; - cursor: ${(props: LabelContentProps) => (props.active ? 'text' : 'default')}; + cursor: ${(props: LabelContentProps) => (props.active && props.editable ? 'text' : 'default')}; + user-select: none; `; const StyledSeamlessInput = styled.input` @@ -227,7 +235,10 @@ class SeamlessInput extends React.Component { } export const Element: React.StatelessComponent = props => ( - + {props.dragging && ( = props => ( /> )} - {props.mayOpen && ( + {props.capabilities.includes(ElementCapability.Openable) && ( = props => ( active={props.state === ElementState.Active} /> )} - {props.state === ElementState.Editable ? ( + {props.state === ElementState.Editable && + props.capabilities.includes(ElementCapability.Editable) ? ( = props => ( ) : ( {props.title} diff --git a/src/components/floating-button/demo.tsx b/src/components/floating-button/demo.tsx deleted file mode 100644 index 95e4604de..000000000 --- a/src/components/floating-button/demo.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import DemoContainer from '../demo-container'; -import { Icon, IconName } from '../icons'; -import * as React from 'react'; -import { FloatingButton } from '.'; - -const FloatingButtonDemo: React.StatelessComponent = (): JSX.Element => ( - - } /> - -); - -export default FloatingButtonDemo; diff --git a/src/components/floating-button/index.md b/src/components/floating-button/index.md deleted file mode 100644 index b75f790d1..000000000 --- a/src/components/floating-button/index.md +++ /dev/null @@ -1,12 +0,0 @@ -[Floating Action Button](https://material.io/guidelines/components/buttons-floating-action-button.html) commonly found in Material Design. - -The Alva implemation limits the content to instances of [Icon](./pattern/icons). - -* Minimal width is 56px -* Height directly proportional to width `1:1` -* Centers provided [Icon](./pattern/icons) horizontally and vertically -* Positioning is static - -## Usage in Alva - -* Primary action on page list view for adding new pages diff --git a/src/components/floating-button/index.tsx b/src/components/floating-button/index.tsx deleted file mode 100644 index 5cc6549fe..000000000 --- a/src/components/floating-button/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Color } from '../colors'; -import * as React from 'react'; -import styled from 'styled-components'; - -export interface FloatingButtonProps { - icon: React.ReactNode; - onClick?: React.MouseEventHandler; -} - -const StyledFloatingButton = styled.button` - background: transparent; - border: none; - box-sizing: border-box; - color: ${Color.White}; - cursor: pointer; - margin: 0; - min-width: 56px; - overflow: hidden; - padding: 0; - position: relative; - &::before { - content: ''; - display: block; - padding-top: 100%; - background: ${Color.Blue}; - border-radius: 50%; - } - &:focus { - outline: none; - } - &:hover { - &::before { - content: ''; - display: block; - padding-top: 100%; - background: ${Color.Blue20}; - border-radius: 50%; - } - } -`; - -const StyledFloatingButtonContent = styled.div` - box-sizing: border-box; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -`; - -export const FloatingButton: React.SFC = props => ( - - {props.icon} - -); diff --git a/src/components/floating-button/pattern.json b/src/components/floating-button/pattern.json deleted file mode 100644 index 65369b528..000000000 --- a/src/components/floating-button/pattern.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "floating-button", - "description": "Always available interaction target", - "displayName": "Floating Button", - "version": "1.0.0", - "tags": ["atom", "button"], - "flag": "alpha" -} diff --git a/src/components/index.ts b/src/components/index.ts index 1b43542ea..1366d58a3 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ +export * from './add-page-button'; export * from './bug-report'; export * from './button'; export * from './chrome'; @@ -5,7 +6,6 @@ export * from './colors'; export * from './copy'; export * from './create-select'; export * from './element'; -export * from './floating-button'; export * from './fonts'; export * from './global-styles'; export * from './headline'; @@ -15,9 +15,9 @@ export * from './layout'; export * from './link'; export * from './list'; export * from './overlay'; +export * from './page-tile'; export * from './panes'; export * from './pattern-list'; -export * from './preview-tile'; export * from './property-input'; export * from './property-item'; export * from './property-item-asset'; diff --git a/src/components/preview-tile/demo.tsx b/src/components/page-tile/demo.tsx similarity index 84% rename from src/components/preview-tile/demo.tsx rename to src/components/page-tile/demo.tsx index ecf26f074..2fdb91856 100644 --- a/src/components/preview-tile/demo.tsx +++ b/src/components/page-tile/demo.tsx @@ -5,14 +5,14 @@ import DemoContainer from '../demo-container'; import { Headline } from '../headline'; import { Layout } from '../layout'; import { Space, SpaceSize } from '../space'; -import { EditState, PreviewTile } from '.'; +import { EditState, PageTile } from '.'; const handleChange = (e: React.ChangeEvent): string => e.target.value; const currentDate = new Date(); export default (): JSX.Element => ( - + Project Name @@ -22,40 +22,45 @@ export default (): JSX.Element => ( - - - - - ; onChange?: React.ChangeEventHandler; onClick?: React.MouseEventHandler; - onDoubleClick?: React.MouseEventHandler; onFocus?: React.FocusEventHandler; onKeyDown?: React.KeyboardEventHandler; } @@ -25,7 +25,7 @@ export interface PreviewTileProps { interface EditableTitleProps { autoFocus: boolean; autoSelect: boolean; - focused: boolean; + highlighted: boolean; onBlur?: React.FocusEventHandler; onChange?: React.ChangeEventHandler; onClick?: React.MouseEventHandler; @@ -35,40 +35,51 @@ interface EditableTitleProps { value: string; } -interface StyledPreviewTileProps { +interface StyledPageTileProps { + highlighted: boolean; focused: boolean; } -interface StyledPreviewTitleProps { +interface StyledPageTitleProps { children: React.ReactNode; editable: boolean; } -const StyledPreview = styled.section` - width: 245px; - text-align: center; -`; +const BORDER_COLOR = (props: StyledPageTileProps) => (props.focused ? Color.Blue20 : Color.Blue60); -const StyledPreviewTile = styled.div` +const StyledPageTile = styled.div` position: relative; box-sizing: border-box; - width: inherit; - height: 340px; - border: 4px solid; - border-color: ${(props: StyledPreviewTileProps) => - props.focused ? Color.Blue40 : 'transparent'}; + height: 60px; + width: 100%; + border: 3px solid; + border-color: ${(props: StyledPageTileProps) => + props.highlighted ? BORDER_COLOR : 'transparent'}; border-radius: 6px; - box-shadow: 0 3px 12px ${Color.BlackAlpha13}; + box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.15); background-color: ${Color.White}; overflow: hidden; + margin: ${getSpace(SpaceSize.S)}px; + margin-bottom: 0; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: box-shadow 0.2s, color 0.2s; + color: ${Color.Grey20}; + + &:hover { + box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.3); + color: ${Color.Black}; + } `; -const StyledTitle = (props: StyledPreviewTitleProps): JSX.Element => { +const StyledTitle = (props: StyledPageTitleProps): JSX.Element => { const Strong = styled.strong` display: inline-block; width: 100%; margin: 0; - font-size: 12px; + font-size: inherit; font-weight: normal; text-align: center; cursor: ${props.editable ? 'text' : 'default'}; @@ -76,6 +87,8 @@ const StyledTitle = (props: StyledPreviewTitleProps): JSX.Element => { white-space: nowrap; text-overflow: ellipsis; padding: 0; + color: inherit; + user-select: none; `; return {props.children}; }; @@ -99,15 +112,6 @@ const StyledEditableTitle = styled.input` } `; -const StyledContainer = styled.div` - position: absolute; - bottom: 0; - left: 0; - width: 100%; - padding: ${getSpace(SpaceSize.S)}px 0; - background: ${Color.White}; -`; - class EditableTitle extends React.Component { public componentDidMount(): void { const node = ReactDOM.findDOMNode(this); @@ -141,26 +145,27 @@ class EditableTitle extends React.Component { } } -export const PreviewTile: React.StatelessComponent = (props): JSX.Element => ( - - - - {props.nameState === EditState.Editing ? ( - - ) : ( - {props.name} - )} - - - +export const PageTile: React.StatelessComponent = (props): JSX.Element => ( + + {props.nameState === EditState.Editing ? ( + + ) : ( + {props.name} + )} + ); diff --git a/src/components/preview-tile/pattern.json b/src/components/page-tile/pattern.json similarity index 63% rename from src/components/preview-tile/pattern.json rename to src/components/page-tile/pattern.json index 0358f3b30..f8f5d37ae 100644 --- a/src/components/preview-tile/pattern.json +++ b/src/components/page-tile/pattern.json @@ -1,6 +1,6 @@ { - "name": "preview-tile", - "displayName": "Preview Tile", + "name": "page-tile", + "displayName": "Page Tile", "description": "Represent a page", "flag": "alpha", "version": "1.0.0", diff --git a/src/components/view-switch/index.tsx b/src/components/view-switch/index.tsx index 42d42f919..5988eea37 100644 --- a/src/components/view-switch/index.tsx +++ b/src/components/view-switch/index.tsx @@ -23,6 +23,7 @@ export interface StyledViewButtonProps { export interface ViewButtonProps { onClick?: React.MouseEventHandler; + rotateIcon?: boolean; title: string; } @@ -33,6 +34,7 @@ export interface ViewTitleProps { } interface StyledIconProps extends IconProps { + rotate?: boolean; visible: boolean; } @@ -64,6 +66,9 @@ const StyledViewButton: any = styled.div` &:hover { background: ${Color.Grey90}; } + &:active { + background: ${Color.Grey80}; + } `; const StyledTitle = styled.strong` @@ -87,10 +92,15 @@ const StyledIcons = styled(Icon)` visibility: ${(props: StyledIconProps) => (props.visible ? 'visible' : 'hidden')}; cursor: pointer; pointer-events: auto; + transform: rotate(${(props: StyledIconProps) => (props.rotate ? '180deg' : '0')}); + transition: transform 0.2s; &:hover { background: ${Color.Grey90}; } + &:active { + background: ${Color.Grey80}; + } `; export const ViewTitle: React.SFC = (props): JSX.Element => ( @@ -105,6 +115,7 @@ export const ViewButton: React.SFC = (props): JSX.Element => ( color={Color.Grey60} size={IconSize.XS} name={IconName.ArrowLeft} + rotate={props.rotateIcon} visible={true} /> {props.title} diff --git a/src/container/app.tsx b/src/container/app.tsx index cbd545e0b..8614268b8 100644 --- a/src/container/app.tsx +++ b/src/container/app.tsx @@ -19,7 +19,6 @@ import { ElementList } from './element-list'; import { ServerMessageType } from '../message'; import * as MobxReact from 'mobx-react'; import { PageListContainer } from './page-list/page-list-container'; -import { PageListPreview } from './page-list/page-list-preview'; import { PatternListContainer } from './pattern-list'; import { PreviewPaneWrapper } from './preview-pane-wrapper'; import { PropertyListContainer } from './property-list'; @@ -71,13 +70,24 @@ export class App extends React.Component { }} /> )} - {props.store.getActiveAppView() === Types.AlvaView.Pages && ( - - - - )} {props.store.getActiveAppView() === Types.AlvaView.PageDetail && ( + {props.store.getShowPages() && ( + + + + + + )} {props.store.getShowLeftSidebar() && ( )} - {store.getActiveAppView() === Types.AlvaView.Pages && ( - - )} { diff --git a/src/container/chrome/overview-switch-container.tsx b/src/container/chrome/overview-switch-container.tsx index f1822db0c..1d844a93b 100644 --- a/src/container/chrome/overview-switch-container.tsx +++ b/src/container/chrome/overview-switch-container.tsx @@ -2,7 +2,6 @@ import { ViewButton } from '../../components'; import * as MobxReact from 'mobx-react'; import * as React from 'react'; import { ViewStore } from '../../store'; -import * as Types from '../../types'; @MobxReact.inject('store') @MobxReact.observer @@ -16,14 +15,14 @@ export class OverviewSwitchContainer extends React.Component { return null; } - const title = - store.getActiveAppView() === Types.AlvaView.Pages ? `Show "${page.getName()}"` : 'Pages'; + const title = store.getShowPages() ? 'Hide Pages' : 'Show Pages'; - const next = - store.getActiveAppView() === Types.AlvaView.Pages - ? Types.AlvaView.PageDetail - : Types.AlvaView.Pages; - - return store.setActiveAppView(next)} title={title} />; + return ( + store.setShowPages(!store.getShowPages())} + rotateIcon={!store.getShowPages()} + /> + ); } } diff --git a/src/container/element-list/element-container.tsx b/src/container/element-list/element-container.tsx index 86649fee6..ff3c9a311 100644 --- a/src/container/element-list/element-container.tsx +++ b/src/container/element-list/element-container.tsx @@ -1,5 +1,6 @@ import * as AlvaUtil from '../../alva-util'; import * as Components from '../../components'; +import { ElementCapability } from '../../components'; import { ElementContentContainer } from './element-content-container'; import * as MobxReact from 'mobx-react'; import * as Model from '../../model'; @@ -28,17 +29,20 @@ export class ElementContainer extends React.Component { return ( Boolean(item))} dragging={store.getDragging()} id={props.element.getId()} - mayOpen={ - props.element.acceptsChildren() && props.element.getRole() !== ElementRole.Root - } open={open} onChange={AlvaUtil.noop} placeholderHighlighted={props.element.getPlaceholderHighlighted()} state={getState(props.element, store)} - title={props.element.getName()} + title={props.element.getRole() === ElementRole.Root ? 'Page' : props.element.getName()} > {open ? props.element diff --git a/src/container/page-list/page-add-button.tsx b/src/container/page-list/page-add-button.tsx deleted file mode 100644 index d22e941cb..000000000 --- a/src/container/page-list/page-add-button.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as Sender from '../../message/client'; -import { FloatingButton, Icon, IconName, Space, SpaceSize } from '../../components'; -import { ServerMessageType } from '../../message'; -import * as React from 'react'; -import styled from 'styled-components'; -import * as uuid from 'uuid'; - -export class PageAddButton extends React.Component { - private handleClick(e: React.MouseEvent): void { - e.preventDefault(); - Sender.send({ - id: uuid.v4(), - payload: undefined, - type: ServerMessageType.CreateNewPage - }); - } - - public render(): JSX.Element { - return ( - - } - onClick={e => this.handleClick(e)} - /> - - ); - } -} - -const FixedContainer = styled(Space)` - position: fixed; - bottom: ${SpaceSize.XXL}px; - right: ${SpaceSize.XXXL}px; -`; diff --git a/src/container/page-list/page-list-container.tsx b/src/container/page-list/page-list-container.tsx index dc41d1e19..c48319c18 100644 --- a/src/container/page-list/page-list-container.tsx +++ b/src/container/page-list/page-list-container.tsx @@ -1,9 +1,11 @@ -import { Layout, LayoutWrap, Space, SpaceSize } from '../../components'; +import * as Sender from '../../message/client'; +import { AddPageButton, Layout, LayoutWrap } from '../../components'; +import { ServerMessageType } from '../../message'; import * as MobxReact from 'mobx-react'; -import { PageAddButton } from './page-add-button'; import { PageTileContainer } from './page-tile-container'; import * as React from 'react'; import { ViewStore } from '../../store'; +import * as uuid from 'uuid'; export const PageListContainer: React.StatelessComponent = MobxReact.inject('store')( MobxReact.observer((props): JSX.Element | null => { @@ -17,20 +19,27 @@ export const PageListContainer: React.StatelessComponent = MobxReact.inject('sto } return ( - - - {project - .getPages() - .map((page, i) => ( - - ))} - - - + + {project + .getPages() + .map(page => ( + + ))} + + Sender.send({ + id: uuid.v4(), + payload: undefined, + type: ServerMessageType.CreateNewPage + }) + } + /> + ); }) ); diff --git a/src/container/page-list/page-list-preview.tsx b/src/container/page-list/page-list-preview.tsx deleted file mode 100644 index 2361d43c8..000000000 --- a/src/container/page-list/page-list-preview.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Color, Headline, Space, SpaceSize } from '../../components'; -import * as MobxReact from 'mobx-react'; -import * as React from 'react'; -import { ViewStore } from '../../store'; -import styled from 'styled-components'; - -export const PageListPreview: React.StatelessComponent = MobxReact.inject('store')( - MobxReact.observer(props => { - const { store } = props as { store: ViewStore }; - const project = store.getProject(); - if (!project) { - return <>props.children; - } - return ( - - - - - {project.getName()} - - - {props.children} - - - ); - }) -); - -const StyledPageListPreview = styled.div` - box-sizing: border-box; - overflow: auto; - width: 100%; - min-height: 100vh; -`; diff --git a/src/container/page-list/page-tile-container.tsx b/src/container/page-list/page-tile-container.tsx index 0cd244569..5776708e1 100644 --- a/src/container/page-list/page-tile-container.tsx +++ b/src/container/page-list/page-tile-container.tsx @@ -1,4 +1,4 @@ -import { PreviewTile, Space, SpaceSize } from '../../components'; +import { PageTile } from '../../components'; import * as MobxReact from 'mobx-react'; import { Page } from '../../model'; import * as React from 'react'; @@ -7,6 +7,7 @@ import * as Types from '../../types'; export interface PageTileContainerProps { focused: boolean; + highlighted: boolean; page: Page; } @@ -55,31 +56,13 @@ export class PageTileContainer extends React.Component { const target = e.target as HTMLElement; - if (!this.props.focused) { - store.setActivePage(this.props.page); - } + store.setActivePage(this.props.page); - if (this.props.focused && target.matches('[data-title]')) { + if (this.props.highlighted && target.matches('[data-title]')) { this.props.page.setNameState(Types.EditState.Editing); } } - protected handleDoubleClick(e: React.MouseEvent): void { - if (this.props.page.getNameState() === Types.EditState.Editing) { - return; - } - - const { store } = this.props as PageTileContainerProps & { store: ViewStore }; - - const next = - store.getActiveAppView() === Types.AlvaView.Pages - ? Types.AlvaView.PageDetail - : Types.AlvaView.Pages; - - store.setActivePage(this.props.page); - store.setActiveAppView(next); - } - protected handleFocus(): void { this.props.page.setNameState(Types.EditState.Editing); } @@ -107,7 +90,7 @@ export class PageTileContainer extends React.Component { } if ( e.target === document.body && - this.props.focused && + this.props.highlighted && this.props.page.getNameState() === Types.EditState.Editable ) { this.props.page.setNameState(Types.EditState.Editing); @@ -119,23 +102,21 @@ export class PageTileContainer extends React.Component { public render(): JSX.Element { const { props } = this; return ( - - this.handleBlur()} - onChange={e => this.handleChange(e)} - onClick={e => this.handleClick(e)} - onDoubleClick={e => this.handleDoubleClick(e)} - onFocus={e => this.handleFocus()} - onKeyDown={e => { - e.stopPropagation(); - this.handleKeyDown(e.nativeEvent); - }} - nameState={props.page.getNameState()} - name={props.page.getName()} - /> - + this.handleBlur()} + onChange={e => this.handleChange(e)} + onClick={e => this.handleClick(e)} + onFocus={e => this.handleFocus()} + onKeyDown={e => { + e.stopPropagation(); + this.handleKeyDown(e.nativeEvent); + }} + nameState={props.page.getNameState()} + name={props.page.getName()} + /> ); } } diff --git a/src/electron/create-menu.ts b/src/electron/create-menu.ts index daf3b2cd2..034cddfd1 100644 --- a/src/electron/create-menu.ts +++ b/src/electron/create-menu.ts @@ -385,7 +385,7 @@ export function createMenu(ctx: MenuContext): void { type: 'separator' }, { - label: '&Show Left Sidebar', + label: '&Show Elements & Components', type: 'checkbox', checked: true, enabled: ctx.store.getActiveAppView() === Types.AlvaView.PageDetail, @@ -395,7 +395,7 @@ export function createMenu(ctx: MenuContext): void { } }, { - label: '&Show Right Sidebar', + label: '&Show Properties', type: 'checkbox', checked: true, enabled: ctx.store.getActiveAppView() === Types.AlvaView.PageDetail, diff --git a/src/electron/renderer.tsx b/src/electron/renderer.tsx index 409eeb404..827f9becb 100644 --- a/src/electron/renderer.tsx +++ b/src/electron/renderer.tsx @@ -9,7 +9,7 @@ import { AlvaApp, EditHistory, PatternLibrary, Project } from '../model'; import * as Path from 'path'; import * as React from 'react'; import * as ReactDom from 'react-dom'; -import { ViewStore } from '../store'; +import { FocusedItemType, ViewStore } from '../store'; import * as Types from '../types'; import * as uuid from 'uuid'; @@ -59,14 +59,10 @@ Sender.receive(message => { newProject.setPath(message.payload.path); store.setProject(newProject); - const view = - newProject.getPages().length === 0 - ? Types.AlvaView.Pages - : Types.AlvaView.PageDetail; + app.setActiveView(Types.AlvaView.PageDetail); + store.setFocusedItem(FocusedItemType.Page, store.getCurrentPage()); + store.commit(); - app.setActiveView(view); - store.commit(); - const patternLibrary = newProject.getPatternLibrary(); if (patternLibrary.getState() !== Types.PatternLibraryState.Pristine) { @@ -126,10 +122,8 @@ Sender.receive(message => { } store.setActivePage(page); + page.setNameState(Types.EditState.Editing); - if (app.getActiveView() === Types.AlvaView.Pages) { - page.setNameState(Types.EditState.Editing); - } break; } case ServerMessageType.ConnectPatternLibraryResponse: { @@ -209,10 +203,10 @@ Sender.receive(message => { break; } case ServerMessageType.Cut: { - if (app.getActiveView() === Types.AlvaView.Pages) { + /*if (app.getActiveView() === Types.AlvaView.Pages) { // TODO: implement this // store.cutSelectedPage(); - } + }*/ if (app.getActiveView() === Types.AlvaView.PageDetail) { store.executeElementCutSelected(); } @@ -223,10 +217,17 @@ Sender.receive(message => { break; } case ServerMessageType.Delete: { - if (app.getActiveView() === Types.AlvaView.Pages) { + if ( + app.getActiveView() === Types.AlvaView.PageDetail && + store.getFocusedItemType() === FocusedItemType.Page + ) { store.executePageRemoveSelected(); } - if (app.getActiveView() === Types.AlvaView.PageDetail) { + + if ( + app.getActiveView() === Types.AlvaView.PageDetail && + store.getFocusedItemType() === FocusedItemType.Element + ) { store.executeElementRemoveSelected(); } break; @@ -236,10 +237,10 @@ Sender.receive(message => { break; } case ServerMessageType.Copy: { - if (app.getActiveView() === Types.AlvaView.Pages) { + /*if (app.getActiveView() === Types.AlvaView.Pages) { // TODO: implement this // store.copySelectedPage(); - } + }*/ if (app.getActiveView() === Types.AlvaView.PageDetail) { store.copySelectedElement(); } @@ -250,10 +251,10 @@ Sender.receive(message => { break; } case ServerMessageType.Paste: { - if (app.getActiveView() === Types.AlvaView.Pages) { + /*if (app.getActiveView() === Types.AlvaView.Pages) { // TODO: implement this // store.pasteAfterSelectedPage(); - } + }*/ if (app.getActiveView() === Types.AlvaView.PageDetail) { store.executeElementPasteAfterSelected(); } diff --git a/src/model/alva-app.ts b/src/model/alva-app.ts index 0edac0b41..4652ad9bb 100644 --- a/src/model/alva-app.ts +++ b/src/model/alva-app.ts @@ -78,8 +78,6 @@ function deserializeView(state: Types.SerializedAlvaView): Types.AlvaView { switch (state) { case 'SplashScreen': return Types.AlvaView.SplashScreen; - case 'Pages': - return Types.AlvaView.Pages; case 'PageDetail': return Types.AlvaView.PageDetail; } @@ -100,8 +98,6 @@ function serializeView(view: Types.AlvaView): Types.SerializedAlvaView { switch (view) { case Types.AlvaView.SplashScreen: return 'SplashScreen'; - case Types.AlvaView.Pages: - return 'Pages'; case Types.AlvaView.PageDetail: return 'PageDetail'; } diff --git a/src/model/element/element.ts b/src/model/element/element.ts index 099aec569..89c11cf22 100644 --- a/src/model/element/element.ts +++ b/src/model/element/element.ts @@ -12,6 +12,7 @@ export interface ElementInit { containerId?: string; contentIds: string[]; dragged: boolean; + focused: boolean; forcedOpen: boolean; highlighted: boolean; id?: string; @@ -39,6 +40,8 @@ export class Element { @Mobx.observable private editedName: string; + @Mobx.observable private focused: boolean; + @Mobx.observable private forcedOpen: boolean; @Mobx.observable private highlighted: boolean; @@ -71,6 +74,7 @@ export class Element { public constructor(init: ElementInit, context: ElementContext) { this.dragged = init.dragged; + this.focused = init.focused; this.highlighted = init.highlighted; this.id = init.id ? init.id : uuid.v4(); this.patternId = init.patternId; @@ -129,6 +133,7 @@ export class Element { return new Element( { dragged: serialized.dragged, + focused: serialized.focused, highlighted: serialized.highlighted, id: serialized.id, name: serialized.name, @@ -173,6 +178,7 @@ export class Element { const clone = new Element( { dragged: false, + focused: false, highlighted: false, id: uuid.v4(), containerId: undefined, @@ -313,6 +319,10 @@ export class Element { return this.highlighted; } + public getFocused(): boolean { + return this.focused; + } + public getId(): string { return this.id; } @@ -425,6 +435,11 @@ export class Element { this.highlighted = highlighted; } + @Mobx.action + public setFocused(focused: boolean): void { + this.focused = focused; + } + @Mobx.action public setIndex(index: number): void { const container = this.getContainer(); @@ -553,6 +568,7 @@ export class Element { containerId: this.containerId, contentIds: Array.from(this.contentIds), dragged: this.dragged, + focused: this.focused, highlighted: this.highlighted, id: this.id, name: this.name, diff --git a/src/model/page/page.ts b/src/model/page/page.ts index af1cfb823..9af4e41b1 100644 --- a/src/model/page/page.ts +++ b/src/model/page/page.ts @@ -27,6 +27,8 @@ export interface PageContext { export class Page { @Mobx.observable private active: boolean; + @Mobx.observable private focused: boolean; + /** * Intermediary edited name */ @@ -98,6 +100,7 @@ export class Page { dragged: false, open: true, forcedOpen: false, + focused: false, patternId: rootPattern.getId(), placeholderHighlighted: false, properties: [], @@ -166,6 +169,10 @@ export class Page { return rootElement.getContentById(id); } + public getFocused(): boolean { + return this.focused; + } + public getEditedName(): string { return this.editedName; } @@ -222,6 +229,11 @@ export class Page { this.active = active; } + @Mobx.action + public setFocused(focused: boolean): void { + this.focused = focused; + } + @Mobx.action public setName(name: string): void { if (this.nameState === Types.EditState.Editing) { diff --git a/src/store/view-store.ts b/src/store/view-store.ts index dedaee2b0..88b48ae7c 100644 --- a/src/store/view-store.ts +++ b/src/store/view-store.ts @@ -29,6 +29,11 @@ export interface ClipboardElement { type: ClipBoardType.Element; } +export enum FocusedItemType { + Page, + Element +} + /** * The central entry-point for all view-related application state, managed by MobX. * Use this object and its properties in your React components, @@ -46,6 +51,8 @@ export class ViewStore { private editHistory: Model.EditHistory; + @Mobx.observable private focusedItemType: FocusedItemType; + @Mobx.observable private metaDown: boolean = false; @Mobx.observable private project: Model.Project; @@ -54,6 +61,8 @@ export class ViewStore { @Mobx.observable private showLeftSidebar: boolean = true; + @Mobx.observable private showPages: boolean = true; + @Mobx.observable private showRightSidebar: boolean = true; @Mobx.observable private serverPort: number; @@ -161,6 +170,7 @@ export class ViewStore { { contentIds: elementContents.map(e => e.getId()), dragged: init.dragged || false, + focused: false, highlighted: false, forcedOpen: false, open: false, @@ -591,6 +601,24 @@ export class ViewStore { return this.project.getElements().find(element => element.getHighlighted()); } + public getFocusedItem(): Model.Element | Model.Page | undefined { + if (!this.project) { + return; + } + + if (this.focusedItemType === FocusedItemType.Element) { + return this.project.getElements().find(element => element.getFocused()); + } else if (this.focusedItemType === FocusedItemType.Page) { + return this.project.getPages().find(page => page.getFocused()); + } else { + return undefined; + } + } + + public getFocusedItemType(): FocusedItemType { + return this.focusedItemType; + } + public getMetaDown(): boolean { return this.metaDown; } @@ -680,9 +708,9 @@ export class ViewStore { return this.hasClipboardItem(ClipBoardType.Element); } - if (view === Types.AlvaView.Pages) { + /*if (view === Types.AlvaView.Pages) { return this.hasClipboardItem(ClipBoardType.Element); - } + }*/ return false; } @@ -821,14 +849,15 @@ export class ViewStore { public removePage(page: Model.Page): void { const index = this.project.getPageIndex(page); - if (index === 0) { - this.unsetActivePage(); - this.setActiveAppView(Types.AlvaView.Pages); - } else { - this.setActivePageByIndex(index - 1); - } + if (this.project.getPages().length > 1) { + if (index !== 0) { + this.setActivePageByIndex(index - 1); + } else { + this.setActivePageByIndex(index + 1); + } - this.project.removePage(page); + this.project.removePage(page); + } } @Mobx.action @@ -873,6 +902,14 @@ export class ViewStore { this.showRightSidebar = show; } + public getShowPages(): boolean { + return this.showPages; + } + + public setShowPages(show: boolean): void { + this.showPages = show; + } + @Mobx.action public setActiveAppView(appView: Types.AlvaView): void { this.app.setActiveView(appView); @@ -887,6 +924,11 @@ export class ViewStore { this.unsetActivePage(); page.setActive(true); + this.setFocusedItem(FocusedItemType.Page, page); + + this.unsetSelectedElement(); + + console.log('setActivePage'); } @Mobx.action @@ -963,6 +1005,22 @@ export class ViewStore { }); } + @Mobx.action + public setFocusedItem( + type: FocusedItemType, + payload: Model.Element | Model.Page | undefined + ): void { + const previousFocusItem = this.getFocusedItem(); + + if (previousFocusItem) { + previousFocusItem.setFocused(false); + } + this.focusedItemType = type; + if (payload) { + payload.setFocused(true); + } + } + @Mobx.action public setMetaDown(metaDown: boolean): void { this.metaDown = metaDown; @@ -1018,6 +1076,7 @@ export class ViewStore { } selectedElement.setSelected(true); + this.setFocusedItem(FocusedItemType.Element, selectedElement); selectedElement.getAncestors().forEach(ancestor => { ancestor.setForcedOpen(true); diff --git a/src/types/types.ts b/src/types/types.ts index 93d13a4eb..8ac6211f8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -36,6 +36,7 @@ export interface SerializedElement { containerId?: string; contentIds: string[]; dragged: boolean; + focused: boolean; forcedOpen: boolean; highlighted: boolean; id: string; @@ -124,12 +125,11 @@ export interface SerializedPatternSlot { } export enum AlvaView { - Pages = 'Pages', PageDetail = 'PageDetail', SplashScreen = 'SplashScreen' } -export type SerializedAlvaView = 'Pages' | 'PageDetail' | 'SplashScreen'; +export type SerializedAlvaView = 'PageDetail' | 'SplashScreen'; export enum EditState { Editable = 'Editable',