finish drag and drop system

This commit is contained in:
Katalina / stardust 2017-12-27 13:16:26 -06:00
parent e36be9e381
commit 7806219464
19 changed files with 465 additions and 90 deletions

View file

@ -1,21 +1,41 @@
import React, { Component } from 'react'
import { DropTarget } from 'react-dnd'
import Role from '../role'
import Role from '../role/draggable'
import CategoryEditor from './CategoryEditor'
@DropTarget(Symbol.for('dnd: role'), {
drop (props, monitor, element) {
props.onDrop(monitor.getItem())
},
canDrop (props) {
return props.mode !== Symbol.for('edit')
}
}, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
}))
class Category extends Component {
render () {
const { category, name } = this.props
const { category, name, isOver, connectDropTarget, mode, ...rest } = this.props
return <div key={name} className="role-picker__category">
if (mode === Symbol.for('edit')) {
return <CategoryEditor category={category} name={name} {...rest} />
}
return connectDropTarget(<div key={name} className={`role-picker__category ${(isOver) ? 'is-over' : ''}`}>
<h4>{ name }</h4>
{
category.get('roles_map')
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} role={r} type='drag' />)
.map((r, k) => <Role key={k} role={r} categoryId={name} />)
.toArray()
}
</div>
</div>)
}
}
export default Category

View file

@ -0,0 +1,41 @@
import React, { Component } from 'react'
export default class CategoryEditor extends Component {
render () {
const {
name,
category
} = this.props
return <div className="role-editor__category editor">
<form onSubmit={(e) => e.preventDefault()} className="uk-form-stacked uk-light">
<div>
<label className="uk-form-label">Category Name</label>
<div className="uk-form-controls">
<input type="text" className="uk-input" placeholder='' value={name} onChange={this.props.onEdit('name', Symbol.for('edit: text'))} />
</div>
</div>
<div style={{ marginTop: 10 }}>
<div className="uk-form-controls">
<label uk-tooltip="delay: 1s" title="Hides and disables roles in this category from being used.">
<input
style={{ marginRight: 5 }}
type="checkbox"
className="uk-checkbox"
checked={category.get('hidden')}
onChange={this.props.onEdit('hidden', Symbol.for('edit: bool'))}
/>
Hidden
</label>
</div>
</div>
<div className='role-editor__actions'>
<button className="uk-button rp-button secondary role-editor__actions_delete" onClick={this.props.onDelete}>
<i uk-icon="icon: trash" />
</button>
<button className="uk-button rp-button primary role-editor__actions_save" onClick={this.props.onSave}>Save</button>
</div>
</form>
</div>
}
}

View file

@ -2,5 +2,45 @@
&__grid
display: grid
grid-template-areas: 'left right'
grid-template-columns: 1fr
grid-template-rows: 1fr 1fr
grid-template-columns: 1fr 1fr
grid-template-rows: 1fr
&__actions
display: flex
margin-top: 10px
button
padding: 0
&_delete
flex: 1
margin-right: 5px
&_save
flex: 4
.role-editor__category
box-sizing: border-box
background-color: var(--c-1)
padding: 15px
margin: 10px
min-width: 220px - 30px
&.add-button
height: 100px
display: flex
align-items: center
justify-content: center
text-align: center
color: var(--c-5)
font-size: 2em
i
transition: transform 0.15s ease-in-out, color 0.15s ease-in-out
&:hover
i
transform: scale(1.1)
color: var(--c-7)

View file

@ -0,0 +1,88 @@
import { Set } from 'immutable'
import * as UIActions from '../../actions/ui'
import { getViewMap } from '../role-picker/actions'
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
let { viewMap } = getViewMap(server)
viewMap = viewMap.map(c => c.set('mode', Symbol.for('drop')))
dispatch({
type: Symbol.for('re: setup'),
data: {
viewMap: viewMap
}
})
dispatch(UIActions.fadeIn)
}
export const addRoleToCategory = (name, oldName, role, flip = true) => (dispatch) => {
dispatch({
type: Symbol.for('re: add role to category'),
data: {
name,
role
}
})
if (flip) {
dispatch(removeRoleFromCategory(oldName, name, role, false))
}
}
export const removeRoleFromCategory = (name, oldName, role, flip = true) => (dispatch) => {
dispatch({
type: Symbol.for('re: remove role from category'),
data: {
name,
role
}
})
if (flip) {
dispatch(addRoleToCategory(oldName, name, role, false))
}
}
export const editCategory = (stuff) => dispatch => {
}
export const saveCategory = (name) => ({
type: Symbol.for('re: switch category mode'),
data: {
name,
mode: Symbol.for('drop')
}
})
export const deleteCategory = (name) => ({
type: Symbol.for('re: delete category'),
data: name
})
export const createCategory = (dispatch, getState) => {
const { roleEditor } = getState()
const vm = roleEditor.get('viewMap')
let name = 'New Category'
let idx = 1
while (vm.has(name)) {
idx++
name = `New Category ${idx}`
}
dispatch({
type: Symbol.for('re: set category'),
data: {
name,
roles: Set([]),
roles_map: Set([]),
hidden: true,
mode: Symbol.for('edit')
}
})
}

View file

@ -1,13 +1,15 @@
import React, { Component } from 'react'
import { Set } from 'immutable'
import { connect } from 'react-redux'
import { DropTarget } from 'react-dnd'
import * as Actions from './actions'
import * as PickerActions from '../role-picker/actions'
import * as UIActions from '../../actions/ui'
import './RoleEditor.sass'
import Category from './Category'
import Role from '../role'
import CategoryEditor from './CategoryEditor'
import Role from '../role/draggable'
import { Scrollbars } from 'react-custom-scrollbars';
const mapState = ({ rolePicker, roleEditor, servers }, ownProps) => ({
@ -17,43 +19,110 @@ const mapState = ({ rolePicker, roleEditor, servers }, ownProps) => ({
})
@connect(mapState)
@DropTarget(Symbol.for('dnd: role'), {
drop (props, monitor, element) {
element.dropRole({}, 'Uncategorized')(monitor.getItem())
}
}, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
isOverCurrent: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
}))
class RoleEditor extends Component {
componentWillMount () {
const { dispatch, match: { params: { server } } } = this.props
dispatch(PickerActions.setup(server))
dispatch(Actions.constructView(server))
}
componentWillReceiveProps (nextProps) {
if (this.props.match.params.server !== nextProps.match.params.server) {
const { dispatch } = this.props
dispatch(UIActions.fadeOut(() => dispatch(PickerActions.setup(nextProps.match.params.server))))
dispatch(UIActions.fadeOut(() => dispatch(Actions.constructView(nextProps.match.params.server))))
}
}
dropRole = (category, name) => ({role, category}) => {
const { dispatch } = this.props
console.log(role)
dispatch(Actions.addRoleToCategory(name, category, role))
}
createCategory = () => {
const { dispatch } = this.props
dispatch(Actions.createCategory)
}
saveCategory = (category, name) => () => {
const { dispatch } = this.props
dispatch(Actions.saveCategory(name))
}
deleteCategory = (category, name) => () => {
const { dispatch } = this.props
dispatch(Actions.deleteCategory(name))
}
editCategory = (category, name) => (key, type) => event => {
const { dispatch } = this.props
let value
switch (type) {
case Symbol.for('edit: text'):
value = event.target.value
break
case Symbol.for('edit: bool'):
value = event.target.checked
break
default:
value = null
}
dispatch(Actions.editCategory({ category, name, key, type, value }))
}
render () {
const vm = this.props.rp.get('viewMap')
console.log(vm.toJS())
const vm = this.props.editor.get('viewMap')
return <div className="inner role-editor">
<div className="role-editor__grid">
<div className="role-editor__grid__left">
<Scrollbars autoHeight autoHeightMax='calc(100vh - 110px)'>
{
vm
.filter((_, k) => k !== 'Unassigned')
.map((c, name) => <Category key={name} name={name} category={c} />)
.toArray()
.filter((_, k) => k !== 'Uncategorized')
.map((c, name) => <Category
key={name}
name={name}
category={c}
mode={c.get('mode')}
onDrop={this.dropRole(c, name)}
onEdit={this.editCategory(c, name)}
// onEditOpen={this.openEditor(c, name)}
onSave={this.saveCategory(c, name)}
onDelete={this.deleteCategory(c, name)}
/>)
.toArray()
}
<div onClick={this.createCategory} uk-tooltip="pos: bottom" title="Add new category" className="role-editor__category add-button">
<i uk-icon="icon: plus" />
</div>
</Scrollbars>
</div>
<div className="role-editor__grid__right">
{
(vm.getIn(['Uncategorized', 'roles_map']) || Set())
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} role={r} type='drag' />)
.toArray()
this.props.connectDropTarget(
<div className="role-editor__grid__right">
{
(vm.getIn(['Uncategorized', 'roles_map']) || Set())
.sortBy(r => r.get('position'))
.reverse()
.map((r, k) => <Role key={k} categoryId='Uncategorized' role={r} />)
.toArray()
}
</div>)
}
</div>
</div>
</div>
}

View file

@ -29,6 +29,10 @@ class Category extends Component {
return null
}
if (category.get('roles').count() === 0) {
return null
}
return <div key={name} className="role-picker__category">
<h4>{ name }</h4>
{

View file

@ -49,40 +49,4 @@
&.hidden
opacity: 0
// display: none
.action__button
border: 0
border-radius: 2px
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out
position: relative
&::after
content: ""
position: absolute
top: 0
bottom: 0
right: 0
left: 0
background-color: rgba(0,0,0,0.1)
border-radius: 2px
opacity: 0
transition: opacity 0.15s ease-in-out
&:hover
transform: translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
&::after
opacity: 0.7
&:active
transform: translateY(0px)
box-shadow: none
&::after
opacity: 1
&.primary
background-color: var(--c-5)
&.secondary
background-color: var(--c-3)
// display: none

View file

@ -16,8 +16,7 @@ export const setup = id => async dispatch => {
dispatch(constructView(id))
}
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
export const getViewMap = server => {
const roles = server.get('roles')
const categories = server.get('categories')
@ -28,7 +27,7 @@ export const constructView = id => (dispatch, getState) => {
// console.log('roles', allRoles.toJS(), accountedRoles.toJS(), unaccountedRoles.toJS())
const vm = categories.set('Uncategorized', fromJS({
const viewMap = categories.set('Uncategorized', fromJS({
roles: unaccountedRoles,
hidden: false,
type: 'multi'
@ -43,11 +42,21 @@ export const constructView = id => (dispatch, getState) => {
const selected = roles.reduce((acc, r) => acc.set(r.get('id'), r.get('selected')), Map())
console.log(categories, selected)
return {
viewMap,
selected
}
}
export const constructView = id => (dispatch, getState) => {
const server = getState().servers.get(id)
const { viewMap, selected } = getViewMap(server)
dispatch({
type: Symbol.for('setup role picker'),
data: {
viewMap: vm,
viewMap: viewMap,
rolesSelected: selected,
originalRolesSelected: selected,
hidden: false

View file

@ -77,10 +77,10 @@ class RolePicker extends Component {
<h3>Roles</h3>
<div className="role-picker__spacer"></div>
<div className={`role-picker__actions ${(!this.rolesHaveChanged) ? 'hidden' : ''}`}>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.resetSelected)} className="uk-button action__button secondary">
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.resetSelected)} className="uk-button rp-button secondary">
Reset
</button>
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.submitSelected(this.props.match.params.server))} className="uk-button action__button primary">
<button disabled={!this.rolesHaveChanged} onClick={() => dispatch(Actions.submitSelected(this.props.match.params.server))} className="uk-button rp-button primary">
Save Changes
</button>
</div>

View file

@ -3,7 +3,6 @@
border-radius: 32px
box-sizing: border-box
height: 32px
margin: 0 6px 6px 0
padding: 4px
display: inline-flex
font-weight: 600
@ -13,8 +12,11 @@
vertical-align: baseline
transition: background-color 0.15s ease-in-out, transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out
transform: translateZ(0)
margin: 0 6px 6px 0
position: relative
&.role__button
.role__option
&:hover
cursor: inherit
@ -67,13 +69,34 @@
&.role__drag
box-shadow: none
&:hover
&:hover, &.is-dragging
transform: translateZ(0) translateY(-1px)
box-shadow: 0 1px 2px rgba(0,0,0,0.3)
background-color: rgba(#ccc,0.03)
cursor: grab
&:active
cursor: grabbing
&::after
position: absolute
top: -1px
bottom: -1px
left: -1px
right: -1px
border-radius: 32px
background-color: var(--c-3)
border: 2px dashed var(--c-7)
content: ""
opacity: 0
transition: opacity 0.15s ease-in-out
box-sizing: border-box
&.is-dragging
transition: border-color 0s ease-out 0.15s
border-color: transparent
&::after
opacity: 1
.role__option:active, .role:active .role__option:not(:active)
transform: translateY(0) translateZ(0px) !important

View file

@ -0,0 +1,19 @@
import React, { Component } from 'react'
import { DragSource } from 'react-dnd'
import Role from './index'
@DragSource(Symbol.for('dnd: role'), {
beginDrag ({ role, categoryId }) {
return { role, category: categoryId }
}
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}))
export default class DraggableRole extends Component {
render () {
return <Role {...this.props} type='drag' />
}
}

View file

@ -15,9 +15,11 @@ class Role extends Component {
}
render () {
let { role, selected, disabled, type, provided } = this.props
let { role, selected, disabled, type, isDragging } = this.props
type = type || 'button'
// console.log(this.props)
let color = Color(role.get('color'))
if (color.rgbNumber() === 0) {
@ -27,14 +29,14 @@ class Role extends Component {
const c = color
let hc = color.lighten(0.1)
return <div
const out = <div
onClick={() => {
if (!disabled && this.props.onToggle != null) {
this.props.onToggle(!selected, selected) }
}
}
{...((disabled) ? { 'uk-tooltip': '', title: "I don't have permissions to grant this." } : {})}
className={`role font-sans-serif ${(disabled) ? 'disabled' : ''} role__${type}`}
className={`role font-sans-serif ${(disabled) ? 'disabled' : ''} ${(isDragging) ? 'is-dragging' : ''} role__${type}`}
style={{
'--role-color-hex': c.string(),
'--role-color-hover': hc.string(),
@ -45,6 +47,12 @@ class Role extends Component {
{role.get('name')}
</div>
</div>
if (type === 'drag' && this.props.connectDragSource != null) {
return this.props.connectDragSource(out)
}
return out
}
}