Skip to content

Commit

Permalink
ментальная карта в виде дерева
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandr Karachev committed Jan 20, 2025
1 parent 9ad4323 commit f2e98f4
Show file tree
Hide file tree
Showing 20 changed files with 1,381 additions and 12 deletions.
34 changes: 34 additions & 0 deletions asset/mental_map/Lib/TreeReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default function TreeReducer(state, action) {
switch(action.type) {
case 'tree_loaded': {
return action.tree
}
case 'update_tree': {
console.log(action.treeData)
return {...state, treeData: action.treeData}
}
case 'add_node': {
return {
...state,
treeData: [...state.treeData, action.payload]
}
}
case 'update_image_item': {
return [...state].map(i => {
if (i.id === action.payload.id) {
return {...i, ...action.payload}
}
return i
})
}
case 'update_images': {
return Array.from(action.payload)
}
case 'delete_image': {
return [...state].filter(i => i.id !== action.imageId)
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
22 changes: 14 additions & 8 deletions asset/mental_map/components/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import ImagesReducer from "../../Lib/ImagesReducer";
import Dialog from "../Dialog";
import {CSSTransition} from "react-transition-group";
import ScheduleReducer from "../../Lib/ScheduleReducer";
import TreeView from "../TreeView";

export const MentalMapContext = createContext({});
export const ImagesContext = createContext({});
export const SchedulesContext = createContext({});

export default function App({mentalMapId}) {
const [loading, setLoading] = useState(true)
const [mentalMap, setMentalMap] = useState({})
//const [mentalMap, setMentalMap] = useState({})
const [error, setError] = useState(null)
const [state, dispatch] = useReducer(MentalMapReducer, {})
const [imagesState, imagesDispatch] = useReducer(ImagesReducer, {})
Expand All @@ -30,13 +31,14 @@ export default function App({mentalMapId}) {
const [formattedMapText, setFormattedMapText] = useState()
const [settings, setSettings] = useState(state?.settings || {})
const checkId = useId()
const [isTreeView, setIsTreeView] = useState(false)

useEffect(() => {
api
.get(`/admin/index.php?r=mental-map/get&id=${mentalMapId}`)
.then((response) => {
setLoading(false);
setMentalMap(response.course);
//setMentalMap(response.course);
dispatch({
type: 'mental_map_loaded',
mentalMap: response.mentalMap
Expand All @@ -46,6 +48,7 @@ export default function App({mentalMapId}) {
images: response.images
})
setSchedules(response.schedules)
setIsTreeView(Boolean(response?.mentalMap?.treeView))
})
.catch(async (error) => setError(await parseError(error)))
}, [])
Expand Down Expand Up @@ -76,7 +79,7 @@ export default function App({mentalMapId}) {
const timeoutId = setTimeout(() => api
.post('/admin/index.php?r=mental-map/update-settings', {
payload: {
id: state.id,
id: mentalMapId,
settings: state?.settings || {}
}
}), 500);
Expand Down Expand Up @@ -120,11 +123,14 @@ export default function App({mentalMapId}) {
{loading
? <AppLoader/>
: (
<MentalMapContext.Provider value={mentalMapContext}>
<ImagesContext.Provider value={imagesContext}>
<Editor/>
</ImagesContext.Provider>
</MentalMapContext.Provider>
isTreeView
? <TreeView texts={formattedMapText}/>
:
<MentalMapContext.Provider value={mentalMapContext}>
<ImagesContext.Provider value={imagesContext}>
<Editor/>
</ImagesContext.Provider>
</MentalMapContext.Provider>
)
}

Expand Down
44 changes: 44 additions & 0 deletions asset/mental_map/components/TreeView/TreeView.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.author-layout__content {
word-wrap: break-word;
background: #fff;
min-height: 100vh;
overflow: hidden;
padding-top: 5rem;
position: relative;
transition: background .3s;
word-break: normal;
}

.rst__nodeContent {
right: 0;
}

.rst__rowLabel {
flex: 1 !important;
}

.rst__rowTitle {
display: flex;
width: 100%;
}

.rst__moveHandle, .rst__loadingHandle {
background-repeat: no-repeat;
}

.rst__rowContents {
align-items: start !important;
padding: 8px !important;
}

.rst__rowTitle textarea {
font-size: 16px;
font-weight: normal;
padding: 4px;
border: 0;
}

.rst__rowToolbar {
height: 100%;
align-items: center;
}
159 changes: 159 additions & 0 deletions asset/mental_map/components/TreeView/TreeView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, {createContext, useEffect, useReducer, useState} from "react";
import {changeNodeAtPath, SortableTree} from "@nosferatu500/react-sortable-tree";
import TextareaAutosize from 'react-textarea-autosize';
import "./TreeView.css";
import {addNodeUnderParent, removeNodeAtPath} from "./tree";
import api, {parseError} from "../../Api";
import TreeReducer from "../../Lib/TreeReducer";
import {v4 as uuidv4} from 'uuid'

export const TreeContext = createContext({});

let sendSaveRequest = false

export default function TreeView({texts}) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [state, dispatch] = useReducer(TreeReducer, {treeData: []})

const getNodeKey = ({treeIndex}) => treeIndex;

useEffect(() => {
api
.get(`/admin/index.php?r=mental-map/tree-init&id=${mentalMapId}`)
.then((response) => {
setLoading(false);
dispatch({
type: 'tree_loaded',
tree: response.payload
})
})
.catch(async (error) => setError(await parseError(error)))
}, [])

const treeContext = {state, dispatch}

useEffect(() => {
if (!sendSaveRequest) {
sendSaveRequest = true
return
}
const timeoutId = setTimeout(() => api
.post('/admin/index.php?r=mental-map/tree-save', {
payload: {
id: mentalMapId,
treeData: state.treeData
}
}), 500);
return () => clearTimeout(timeoutId);
}, [JSON.stringify(state.treeData)]);

const createNodesFromTextHandler = () => {
texts.map(t => dispatch({
type: 'add_node',
payload: {id: uuidv4(), title: t}
}))
}

return (
<div className="author-layout__content">
<TreeContext.Provider value={treeContext}>
<div
style={{paddingLeft: '3rem', paddingRight: '3rem', height: '100%', display: 'flex', flexDirection: 'column'}}>
<div>
<p>&nbsp;</p>
</div>
<div style={{flex: '1', width: '80%', margin: '0 auto', display: 'flex', flexDirection: 'column'}}>
<SortableTree
treeData={state.treeData}
rowHeight={100}
onChange={treeData => dispatch({type: 'update_tree', treeData})}
generateNodeProps={({node, path}) => ({
title: (
<TextareaAutosize
style={{width: '100%', resize: 'none'}}
onChange={
event => {
const title = event.target.value;
const data = changeNodeAtPath({
treeData: state.treeData,
path,
getNodeKey,
newNode: {...node, title},
})
dispatch({type: 'update_tree', treeData: data})
}
} value={node.title}/>
),
buttons: [
<button
style={{width: '30px', padding: '6px'}}
onClick={() => {
const treeData = addNodeUnderParent({
treeData: state.treeData,
parentKey: path[path.length - 1],
expandParent: true,
getNodeKey,
newNode: {
id: uuidv4(),
title: '',
},
addAsFirstChild: state.addAsFirstChild,
}).treeData
dispatch({
type: 'update_tree',
treeData,
})
}}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5"
stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
</svg>
</button>,
<button
style={{width: '30px', padding: '6px'}}
onClick={() => {
const treeData = removeNodeAtPath({
treeData: state.treeData,
path,
getNodeKey,
})
dispatch({
type: 'update_tree',
treeData,
})
}}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5"
stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
</svg>
</button>,
],
})}
/>

<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '10px'}}>
<button
className="button button--default button--header-done"
onClick={() =>
dispatch({
type: 'add_node',
payload: {id: uuidv4(), title: ''}
})
}
>
Добавить
</button>
{state.treeData.length === 0 && (
<button onClick={createNodesFromTextHandler} className="button button--default button--header-done" type="button">Создать из текста</button>
)}
</div>
</div>
</div>
</TreeContext.Provider>
</div>
)
}
1 change: 1 addition & 0 deletions asset/mental_map/components/TreeView/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default} from './TreeView'
Loading

0 comments on commit f2e98f4

Please sign in to comment.