dialog-editor: drop and edit items

This commit is contained in:
muerwre 2019-03-01 10:46:18 +07:00
parent a2607b257d
commit 970d0967c8
10 changed files with 279 additions and 150 deletions

View file

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RouteRow } from '$components/maps/RouteRow'; import { RouteRowWrapper } from '$components/maps/RouteRowWrapper';
import { Scroll } from '$components/Scroll'; import { Scroll } from '$components/Scroll';
import { import {
searchSetDistance, searchSetDistance,
@ -21,6 +21,8 @@ import { IRootState, IRouteListItem } from '$redux/user/reducer';
export interface IMapListDialogProps extends IRootState { export interface IMapListDialogProps extends IRootState {
marks: { [x: number]: string }, marks: { [x: number]: string },
routes_sorted: Array<IRouteListItem>, routes_sorted: Array<IRouteListItem>,
routes: IRootState['routes'],
ready: IRootState['ready'],
mapsLoadMore: typeof mapsLoadMore, mapsLoadMore: typeof mapsLoadMore,
searchSetDistance: typeof searchSetDistance, searchSetDistance: typeof searchSetDistance,
@ -30,32 +32,44 @@ export interface IMapListDialogProps extends IRootState {
} }
export interface IMapListDialogState { export interface IMapListDialogState {
selected_item: IRouteListItem['_id'], menu_target: IRouteListItem['_id'],
selected_item_mode: 'menu' | 'edit' | 'drop' | null, editor_target: IRouteListItem['_id'],
is_editing: boolean,
is_dropping: boolean,
} }
class Component extends React.Component<IMapListDialogProps, IMapListDialogState> { class Component extends React.Component<IMapListDialogProps, IMapListDialogState> {
state = { state = {
selected_item: null, menu_target: null,
selected_item_mode: null, editor_target: null,
is_editing: false,
is_dropping: false,
}; };
startEditing = (selected_item: IRouteListItem['_id']): void => this.setState({ startEditing = (editor_target: IRouteListItem['_id']): void => this.setState({
selected_item: ((this.state.selected_item !== selected_item && selected_item) || null), editor_target,
selected_item_mode: 'edit', menu_target: null,
is_editing: true,
is_dropping: false,
}); });
showMenu = (selected_item: IRouteListItem['_id']): void => this.setState({ showMenu = (menu_target: IRouteListItem['_id']): void => this.setState({
selected_item: ((this.state.selected_item !== selected_item && selected_item) || null), menu_target,
selected_item_mode: 'menu',
}); });
showDropCard = (selected_item: IRouteListItem['_id']): void => this.setState({ showDropCard = (editor_target: IRouteListItem['_id']): void => this.setState({
selected_item: ((this.state.selected_item !== selected_item && selected_item) || null), editor_target,
selected_item_mode: 'drop', menu_target: null,
is_editing: false,
is_dropping: true,
}); });
stopEditing = (): void => this.setState({ selected_item: null }); stopEditing = (): void => {
console.log('stop it!');
this.setState({ editor_target: null });
};
setTitle = ({ target: { value } }: { target: { value: string }}): void => { setTitle = ({ target: { value } }: { target: { value: string }}): void => {
this.props.searchSetTitle(value); this.props.searchSetTitle(value);
@ -64,6 +78,8 @@ class Component extends React.Component<IMapListDialogProps, IMapListDialogState
openRoute = (_id: string): void => { openRoute = (_id: string): void => {
if (isMobile()) this.props.setDialogActive(false); if (isMobile()) this.props.setDialogActive(false);
this.stopEditing();
pushPath(`/${_id}/${this.props.editing ? 'edit' : ''}`); pushPath(`/${_id}/${this.props.editing ? 'edit' : ''}`);
}; };
@ -80,6 +96,8 @@ class Component extends React.Component<IMapListDialogProps, IMapListDialogState
} }
}; };
dropRoute = (): void => null;
render() { render() {
const { const {
ready, ready,
@ -97,7 +115,7 @@ class Component extends React.Component<IMapListDialogProps, IMapListDialogState
marks, marks,
}: IMapListDialogProps = this.props; }: IMapListDialogProps = this.props;
const { selected_item, selected_item_mode } = this.state; const { editor_target, menu_target, is_editing, is_dropping } = this.state;
return ( return (
<div className="dialog-content"> <div className="dialog-content">
@ -166,19 +184,21 @@ class Component extends React.Component<IMapListDialogProps, IMapListDialogState
<div className="dialog-maplist"> <div className="dialog-maplist">
{ {
list.map(route => ( list.map(route => (
<RouteRow <RouteRowWrapper
title={route.title} title={route.title}
distance={route.distance} distance={route.distance}
_id={route._id} _id={route._id}
is_public={route.is_public} is_public={route.is_public}
tab={tab} tab={tab}
selected={(selected_item === route._id)} is_editing_mode={is_dropping ? 'drop' : 'edit'}
mode={selected_item_mode} is_editing_target={editor_target === route._id}
is_menu_target={menu_target === route._id}
openRoute={this.openRoute} openRoute={this.openRoute}
startEditing={this.startEditing} startEditing={this.startEditing}
stopEditing={this.stopEditing} stopEditing={this.stopEditing}
showMenu={this.showMenu} showMenu={this.showMenu}
showDropCard={this.showDropCard} showDropCard={this.showDropCard}
dropRoute={this.dropRoute}
key={route._id} key={route._id}
/> />
)) ))

View file

@ -1,69 +0,0 @@
// @flow
import * as React from 'react';
import { Icon } from '$components/panels/Icon';
import classnames from 'classnames';
import { IMapListDialogState, MapListDialog } from "$components/dialogs/MapListDialog";
import { Tooltip } from "$components/panels/Tooltip";
interface Props {
_id: string,
tab: string,
selected: boolean,
title: string,
distance: number,
is_public: boolean,
mode: IMapListDialogState['selected_item_mode'],
openRoute: typeof MapListDialog.openRoute,
startEditing: typeof MapListDialog.startEditing,
stopEditing: typeof MapListDialog.stopEditing,
showMenu: typeof MapListDialog.showMenu,
showDropCard: typeof MapListDialog.showDropCard,
key: string,
}
export const RouteRow = ({
title, distance, _id, openRoute, tab, selected, startEditing, showMenu, showDropCard, mode
}: Props) => (
<div
className={classnames('route-row-wrapper', {
selected,
has_menu: selected && mode === 'menu',
has_drop: selected && mode === 'drop',
has_edit: selected && mode === 'edit',
})}
>
<div
className="route-row"
>
<div onClick={() => openRoute(_id)}>
<div className="route-title">
<span>{(title || _id)}</span>
</div>
<div className="route-description">
<span>
<Icon icon="icon-link-1" />
{_id}
</span>
<span>
<Icon icon="icon-cycle-1" />
{(distance && `${distance} km`) || '0 km'}
</span>
</div>
</div>
</div>
<div className="route-row-edit-button pointer" onClick={() => showMenu(_id)}>
<Icon icon="icon-more-vert" />
<div className="route-row-edit-menu">
<div onClick={() => showDropCard(_id)}>
<Tooltip>Удалить</Tooltip>
<Icon icon="icon-trash-3" size={32} />
</div>
<div onClick={() => startEditing(_id)}>
<Tooltip>Редактировать</Tooltip>
<Icon icon="icon-edit-1" size={32} />
</div>
</div>
</div>
</div>
);

View file

@ -0,0 +1,29 @@
// @flow
import * as React from 'react';
import { Icon } from '$components/panels/Icon';
import { MapListDialog } from "$components/dialogs/MapListDialog";
import { Tooltip } from "$components/panels/Tooltip";
import { ReactElement } from "react";
interface Props {
_id: string,
stopEditing: typeof MapListDialog.stopEditing,
dropRoute: typeof MapListDialog.dropRoute,
}
export const RouteRowDrop = ({
_id, stopEditing, dropRoute,
}: Props): ReactElement<Props, null> => (
<div
className="route-row-drop"
>
<div
className="route-row"
>
<div className="button-group">
<div className="button" onClick={() => dropRoute(_id)}>Удалить</div>
<div className="button primary" onClick={stopEditing}>Отмена</div>
</div>
</div>
</div>
);

View file

@ -5,9 +5,8 @@ import { Switch } from '$components/Switch';
interface Props { interface Props {
title: string; title: string;
is_editing: boolean;
distance: number;
_id: string; _id: string;
is_public: boolean,
} }
interface State { interface State {
@ -32,46 +31,34 @@ export class RouteRowEditor extends React.Component<Props, State> {
render() { render() {
const { const {
state: { title, is_public }, state: { title, is_public },
props: { distance, _id } props: { _id }
} = this; } = this;
return ( return (
<div <div className="route-row-edit">
className="route-row" <div className="route-row">
> <div className="route-title">
<div className="route-title"> <input
<input type="text"
type="text" value={title}
value={title} onChange={this.setTitle}
onChange={this.setTitle} placeholder="Введите название"
placeholder="Введите название" autoFocus
autoFocus />
/> </div>
</div> <div className="route-row-editor">
<div className="route-description"> <div className="route-row-buttons">
<span> <div className="flex_1" onClick={this.setPublic}>
<Icon icon="icon-link-1" /> <Switch active={is_public} />
{_id} {
</span> is_public
<span> ? ' В каталоге карт'
<Icon icon="icon-cycle-1" /> : ' Только по ссылке'
{(distance && `${distance} km`) || '0 km'} }
</span> </div>
</div> <div className="button primary" onClick={this.stopEditing}>
<div className="route-row-editor"> OK
<div className="route-row-buttons"> </div>
<div className="flex_1" onClick={this.setPublic}>
<Switch
active={is_public}
/>
{
is_public
? ' В каталоге карт'
: ' Только по ссылке'
}
</div>
<div className="button primary" onClick={this.stopEditing}>
OK
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,61 @@
// @flow
import * as React from 'react';
import { Icon } from '$components/panels/Icon';
import { MapListDialog } from "$components/dialogs/MapListDialog";
import { Tooltip } from "$components/panels/Tooltip";
import { ReactElement } from "react";
interface Props {
_id: string,
tab: string,
title: string,
distance: number,
is_public: boolean,
openRoute: typeof MapListDialog.openRoute,
startEditing: typeof MapListDialog.startEditing,
stopEditing: typeof MapListDialog.stopEditing,
showMenu: typeof MapListDialog.showMenu,
showDropCard: typeof MapListDialog.showDropCard,
}
export const RouteRowView = ({
title, distance, _id, openRoute, tab, startEditing, showMenu, showDropCard
}: Props): ReactElement<Props, null> => (
<div
className="route-row-view"
>
<div
className="route-row"
onClick={() => openRoute(_id)}
>
<div className="route-title">
<span>{(title || _id)}</span>
</div>
<div className="route-description">
<span>
<Icon icon="icon-link-1" />
{_id}
</span>
<span>
<Icon icon="icon-cycle-1" />
{(distance && `${distance} km`) || '0 km'}
</span>
</div>
</div>
<div className="route-row-edit-button pointer" onClick={() => showMenu(_id)}>
<Icon icon="icon-more-vert" />
</div>
<div className="route-row-edit-menu pointer">
<div onMouseDown={() => showDropCard(_id)}>
<Tooltip>Удалить</Tooltip>
<Icon icon="icon-trash-3" size={32} />
</div>
<div onMouseDown={() => startEditing(_id)}>
<Tooltip>Редактировать</Tooltip>
<Icon icon="icon-edit-1" size={32} />
</div>
</div>
</div>
);

View file

@ -0,0 +1,72 @@
import * as React from 'react';
import classnames from 'classnames';
import { MapListDialog } from "$components/dialogs/MapListDialog";
import { RouteRowView } from "$components/maps/RouteRowView";
import { RouteRowEditor } from "$components/maps/RouteRowEditor";
import { RouteRowDrop } from "$components/maps/RouteRowDrop";
import { ReactElement } from "react";
interface Props {
_id: string,
tab: string,
title: string,
distance: number,
is_public: boolean,
is_editing_target: boolean,
is_menu_target: boolean,
openRoute: typeof MapListDialog.openRoute,
startEditing: typeof MapListDialog.startEditing,
stopEditing: typeof MapListDialog.stopEditing,
showMenu: typeof MapListDialog.showMenu,
showDropCard: typeof MapListDialog.showDropCard,
dropRoute: typeof MapListDialog.dropRoute,
is_editing_mode: 'edit' | 'drop',
}
export const RouteRowWrapper = ({
title, distance, _id, openRoute, tab, startEditing, showMenu,
showDropCard, is_public, is_editing_target, is_menu_target, is_editing_mode,
dropRoute, stopEditing,
}: Props): ReactElement<Props, null> => (
<div
className={classnames('route-row-wrapper', {
is_menu_target,
is_editing_target,
})}
>
{
is_editing_target && is_editing_mode === 'edit' &&
<RouteRowEditor
title={title}
_id={_id}
is_public={is_public}
/>
}
{
is_editing_target && is_editing_mode === 'drop' &&
<RouteRowDrop
_id={_id}
dropRoute={dropRoute}
stopEditing={stopEditing}
/>
}
{
!is_editing_target &&
<RouteRowView
_id={_id}
tab={tab}
title={title}
distance={distance}
is_public={is_public}
openRoute={openRoute}
startEditing={startEditing}
stopEditing={stopEditing}
showMenu={showMenu}
showDropCard={showDropCard}
/>
}
</div>
);

View file

@ -1,11 +1,10 @@
import { Map, LayerGroup, LatLng, LatLngLiteral, LeafletEventHandlerFn, marker, divIcon, Marker } from 'leaflet'; import { Map, LatLng } from 'leaflet';
import { EditablePolyline } from '$utils/EditablePolyline'; import { EditablePolyline } from '$utils/EditablePolyline';
import { simplify } from '$utils/simplify'; import { simplify } from '$utils/simplify';
import { CLIENT } from '$config/frontend'; import { CLIENT } from '$config/frontend';
import { editor, Editor } from "$modules/Editor"; import { editor, Editor } from "$modules/Editor";
import { ILatLng } from "$modules/Stickers"; import { ILatLng } from "$modules/Stickers";
import { InteractivePoly } from "$modules/InteractivePoly"; import { InteractivePoly } from "$modules/InteractivePoly";
import { clusterIcon } from "$utils/clusterIcon";
import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js'; import { MarkerClusterGroup } from 'leaflet.markercluster/dist/leaflet.markercluster-src.js';
import { angleBetweenPoints, dist2, distToSegment, middleCoord } from "$utils/geom"; import { angleBetweenPoints, dist2, distToSegment, middleCoord } from "$utils/geom";
import { arrowClusterIcon, createArrow } from "$utils/arrow"; import { arrowClusterIcon, createArrow } from "$utils/arrow";
@ -23,17 +22,17 @@ export class Poly {
constructor({ constructor({
map, routerMoveStart, lockMapClicks, setDistance, triggerOnChange, editor, map, routerMoveStart, lockMapClicks, setDistance, triggerOnChange, editor,
}: Props) { }: Props) {
this.poly = new InteractivePoly([], { this.poly = new InteractivePoly([ ], {
color: 'url(#activePathGradient)', color: 'url(#activePathGradient)',
weight: 6, weight: 6,
maxMarkers: 100, maxMarkers: 100,
smoothFactor: 3, smoothFactor: 3,
// bubblingMouseEvents: false,
}) })
.on('distancechange', this.onDistanceUpdate) .on('distancechange', this.onDistanceUpdate)
.on('allvertexhide', this.onVertexHide) .on('allvertexhide', this.onVertexHide)
.on('allvertexshow', this.onVertexShow) .on('allvertexshow', this.onVertexShow)
.on('latlngschange', this.updateArrows); .on('latlngschange', this.updateArrows)
.on('latlngschange', triggerOnChange);
this.poly.addTo(map); this.poly.addTo(map);
this.editor = editor; this.editor = editor;
@ -162,7 +161,7 @@ export class Poly {
return (!this.latlngs || Object.values(this.latlngs).length <= 0); return (!this.latlngs || Object.values(this.latlngs).length <= 0);
} }
poly: EditablePolyline; poly;
arrowLayer: MarkerClusterGroup = new MarkerClusterGroup({ arrowLayer: MarkerClusterGroup = new MarkerClusterGroup({
spiderfyOnMaxZoom: false, spiderfyOnMaxZoom: false,
showCoverageOnHover: false, showCoverageOnHover: false,

View file

@ -13,6 +13,7 @@
box-shadow: inset rgba(100,100,100, 0.3) 1px 0, inset rgba(0,0,0, 0.1) -1px 0; box-shadow: inset rgba(100,100,100, 0.3) 1px 0, inset rgba(0,0,0, 0.1) -1px 0;
color: white; color: white;
font-weight: 400; font-weight: 400;
border: none;
&.primary { &.primary {
background: #3c78db; background: #3c78db;

View file

@ -188,13 +188,14 @@
margin-bottom: 10px; margin-bottom: 10px;
transition: all 500ms; transition: all 500ms;
display: flex; display: flex;
flex-direction: column;
&.has_edit { &.has_edit {
//transform: translateY(-2px); //transform: translateY(-2px);
.route-row { background: rgba(255, 100, 100, 0.2); } .route-row { background: fade(@green_secondary, 30%); }
} }
&.has_menu { &.is_menu_target {
.route-row { .route-row {
transform: translateX(-100px); transform: translateX(-100px);
} }
@ -204,18 +205,37 @@
} }
} }
} }
//
//.route-row-editor { .route-row-editor {
// color: white; color: white;
// padding: 20px 0 5px; padding: 5px 0 5px;
//} }
//
//.route-row-buttons { .route-row-buttons {
// flex: 1; flex: 1;
// flex-direction: row; flex-direction: row;
// display: flex; display: flex;
// align-items: center; align-items: center;
//} }
.route-row-view {
overflow: hidden;
transition: height 500ms;
padding-right: 32px;
position: relative;
}
.route-row-edit {
background: fade(@green_secondary, 30%);
}
.route-row-drop {
background: fade(@red_secondary, 20%);
.route-row {
align-items: center;
}
}
.route-row { .route-row {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
@ -226,6 +246,12 @@
transition: background 250ms, transform 500ms; transition: background 250ms, transform 500ms;
position: relative; position: relative;
flex: 1; flex: 1;
min-height: 64px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
&:hover { &:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
@ -246,7 +272,10 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transition: all 250ms; transition: all 250ms;
position: relative; position: absolute;
top: 0;
right: 0;
height: 100%;
&:hover { &:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
@ -256,13 +285,14 @@
.route-row-edit-menu { .route-row-edit-menu {
width: 0; width: 0;
height: 100%; height: 100%;
right: 100%; right: 32px;
bottom: 0; bottom: 0;
position: absolute; position: absolute;
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
transition: all 500ms; transition: all 500ms;
display: flex; display: flex;
fill: fade(white, 30%);
div { div {
width: 50px; width: 50px;

View file

@ -41,7 +41,6 @@ const resolve = {
$utils: join(__dirname, 'src/utils'), $utils: join(__dirname, 'src/utils'),
$modules: join(__dirname, 'src/modules'), $modules: join(__dirname, 'src/modules'),
}, },
extensions: ['*', '.ts', '.tsx', '.js', '.jsx', '.json'] extensions: ['*', '.ts', '.tsx', '.js', '.jsx', '.json']
}; };