This is snapshot of custom node renderer for react-sortable-tree.

It likely won't be needed however.
This commit is contained in:
Tomas Bures 2017-08-16 16:10:30 +02:00
parent 6a7dab52eb
commit baf9f61465
8 changed files with 715 additions and 193 deletions

View file

@ -30,6 +30,7 @@
"react-dom": "^15.6.1",
"react-i18next": "^4.6.1",
"react-router-dom": "^4.1.1",
"react-sortable-tree": "^1.2.0",
"slugify": "^1.1.0",
"url-parse": "^1.1.9"
},
@ -43,6 +44,8 @@
"babel-preset-stage-1": "^6.24.1",
"css-loader": "^0.28.4",
"i18next-conv": "^3.0.3",
"node-sass": "^4.5.3",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.2",
"url-loader": "^0.5.9",
"webpack": "^2.6.1"

View file

@ -1,20 +1,3 @@
.mt-treetable-container .fancytree-container span.fancytree-node.mt-tree-end-drop {
height: 10px;
margin-top: 0px;
margin-bottom: -10px;
overflow: hidden;
min-height: 0px;
}
.mt-treetable-container .fancytree-container span.fancytree-node.mt-tree-end-drop span.fancytree-title {
width: 15px;
margin-left: 28px;
}
.mt-treetable-container .fancytree-container span.fancytree-node.mt-tree-end-drop.mt-tree-end-drop-wide span.fancytree-title {
width: 100%;
}
.mt-treetable-container .fancytree-container {
border: none;
}
@ -71,31 +54,6 @@
.mt-treetable-container span.fancytree-node.fancytree-drag-source {
background-color: transparent !important;
}
.mt-treetable-container #fancytree-drop-marker {
background-image: url("../../public/fancytree/skin-bootstrap/icons.gif");
height: 12px;
}
.mt-treetable-container #fancytree-drop-marker.fancytree-drop-over {
background-position: 0px -130px;
width: 22px;
}
.mt-treetable-container #fancytree-drop-marker.fancytree-drop-after, .mt-treetable-container #fancytree-drop-marker.fancytree-drop-before {
background-position: 0px -145px;
width: 64px;
}
.mt-treetable-container span.fancytree-node.fancytree-drop-accept fancytree-expander {
visibility: hidden;
}
.form-group .mt-treetable-container {
border: 1px solid #cccccc;
border-radius: 4px;

View file

@ -71,7 +71,6 @@ class TreeTable extends Component {
withHeader: PropTypes.bool,
withDescription: PropTypes.bool,
noTable: PropTypes.bool,
withDnd: PropTypes.bool,
withIcons: PropTypes.bool
}
@ -176,33 +175,6 @@ class TreeTable extends Component {
};
}
if (this.props.withDnd) {
treeOpts.extensions.push('dnd');
treeOpts.dnd = {
autoExpandMS: 400,
focusOnClick: true,
preventVoidMoves: true,
preventRecursiveMoves: true,
dropMarkerOffsetX: -46, // -22
dropMarkerInsertOffsetX: 0,
dragStart: (node, data) => {
return node.key !== '__mt-tree-end-drop__';
},
dragEnter: (node, data) => {
if (node.folder) {
return ['before', 'over'];
} else {
return ['before'];
}
},
dragDrop: (node, data) => {
console.log(node);
console.log(data);
data.otherNode.moveTo(node, data.hitMode);
}
};
}
this.tree = jQuery(this.domTable).fancytree(treeOpts).fancytree("getTree");
this.updateSelection();

View file

@ -10,7 +10,9 @@ import {
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
import interoperableErrors from '../../../../shared/interoperable-errors';
import {TreeSelectMode, TreeTable} from "../../lib/tree";
import SortableTree from 'react-sortable-tree';
import { getRuleTreeNodeRenderer } from './RuleTreeNodeRenderer';
@translate()
@withForm
@ -21,7 +23,110 @@ export default class CUD extends Component {
constructor(props) {
super(props);
this.state = {};
this.compoundRuleTypes = [ 'all', 'some', 'none' ];
const allRule = {
type: 'all'
};
const otherRule = {
type: 'eq'
};
this.state = {
rules: [
{
key: 'a',
title: 'A',
expanded: true,
rule: allRule,
children: [
{
key: 'aa',
title: 'AA',
expanded: true,
rule: allRule,
children: [
{
key: 'aaa',
title:
<div><h4>sdfsdf</h4><div>xxx</div><div>yyy<NavButton label="ZZZ" linkTo="/lists"/></div></div>,
rule: otherRule
},
{
key: 'aab',
title: 'AAB',
subtitle: 'sdfwere',
rule: otherRule
}
]
},
{
key: 'ab',
title: 'AB',
expanded: true,
rule: allRule,
children: [
{
key: 'aba',
title: 'ABA',
rule: otherRule
},
{
key: 'abb',
title: 'ABB',
rule: otherRule
}
]
}
]
},
{
key: 'b',
title: 'B',
expanded: true,
rule: allRule,
children: [
{
key: 'ba',
title: 'BA',
expanded: true,
rule: allRule,
children: [
{
key: 'baa',
title: 'BAA',
rule: otherRule
},
{
key: 'bab',
title: 'BAB',
rule: otherRule
}
]
},
{
key: 'bb',
title: 'BB',
expanded: true,
rule: allRule,
children: [
{
key: 'bba',
title: 'BBA',
rule: otherRule
},
{
key: 'bbb',
title: 'BBB',
rule: otherRule
}
]
}
]
}
]
};
this.initForm();
}
@ -100,9 +205,9 @@ export default class CUD extends Component {
}
}
async onRuleSelectionChangedAsync(sel) {
onRuleSelectionClick(data) {
this.setState({
selectedRule: sel
selectedRule: data.node.key
});
}
@ -110,119 +215,6 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
const treeEnd = {
key: '__mt-tree-end-drop__',
icon: false,
unselectable: true,
extraClasses: 'mt-tree-end-drop',
beforeActivate: () => false
};
const treeEndWide = { // This one is used after a non-folder sibling that has no children
key: '__mt-tree-end-drop__',
icon: false,
unselectable: true,
extraClasses: 'mt-tree-end-drop mt-tree-end-drop-wide',
beforeActivate: () => false
}
const sampleTreeData = [
{
key: 'a',
title: 'A',
expanded: true,
folder: true,
children: [
{
key: 'aa',
title: 'AA',
expanded: true,
folder: true,
children: [
{
key: 'aaa',
title: 'AAA',
},
{
key: 'aab',
title: 'AAB',
},
{
key: 'aab',
title: 'AAB',
folder: true
},
treeEnd
]
},
{
key: 'ab',
title: 'AB',
expanded: true,
folder: true,
children: [
{
key: 'aba',
title: 'ABA'
},
{
key: 'abb',
title: 'ABB'
},
treeEndWide
]
},
treeEnd
]
},
{
key: 'b',
title: 'B',
expanded: true,
folder: true,
children: [
{
key: 'ba',
title: 'BA',
expanded: true,
folder: true,
children: [
{
key: 'baa',
title: 'BAA'
},
{
key: 'bab',
title: 'BAB'
},
treeEndWide
]
},
{
key: 'bb',
title: 'BB',
expanded: true,
folder: true,
children: [
{
key: 'bba',
title: 'BBA'
},
{
key: 'bbb',
title: 'BBB'
},
treeEndWide
]
},
treeEnd
]
},
treeEnd
];
return (
<div>
@ -253,10 +245,19 @@ export default class CUD extends Component {
<hr />
<div className="row">
<div className="col-sm-6">
<TreeTable data={sampleTreeData} noTable withIcons withDnd format="wide" selectMode={TreeSelectMode.SINGLE} selection={this.state.selectedRule} onSelectionChangedAsync={::this.onRuleSelectionChangedAsync} />
<div className="col-md-6" >
<SortableTree
treeData={this.state.rules}
onChange={ rules => this.setState({rules}) }
isVirtualized={false}
nodeContentRenderer={getRuleTreeNodeRenderer({
onClick: ::this.onRuleSelectionClick,
isSelected: data => data.node.key === this.state.selectedRule
})}
canDrop={data => data.nextParent && this.compoundRuleTypes.includes(data.nextParent.rule.type)}
/>
</div>
<div className="col-sm-6">
<div className="col-md-6">
<h3>{t('Selected Rule Options')}</h3>
<InputField id="name" label={t('Name')} format="wide" />
</div>

View file

@ -0,0 +1,252 @@
"use strict";
// Taken and adapted from https://github.com/fritz-c/react-sortable-tree/blob/cbca55b9c9800a114fa2749866fd057fc1eaeb9c/src/node-renderer-default.js
// It adds the onClick listener to .rowContents and wraps the whole class to a function to allow parameterization by the onClickHandler
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import baseStyles from './rule-tree.scss';
function getIEVersion() {
const match = navigator.userAgent.match(/(?:MSIE |Trident\/.*; rv:)(\d+)/);
return match ? parseInt(match[1], 10) : undefined;
}
function isDescendant(older, younger) {
return (
!!older.children &&
typeof older.children !== 'function' &&
older.children.some(
child => child === younger || isDescendant(child, younger)
)
);
}
let styles = baseStyles;
// Add extra classes in browsers that don't support flex
if (getIEVersion < 10) {
styles = {
...baseStyles,
row: `${styles.row} ${styles.row_NoFlex}`,
rowContents: `${styles.rowContents} ${styles.rowContents_NoFlex}`,
rowLabel: `${styles.rowLabel} ${styles.rowLabel_NoFlex}`,
rowToolbar: `${styles.rowToolbar} ${styles.rowToolbar_NoFlex}`,
};
}
export function getRuleTreeNodeRenderer(options) {
class RuleTreeNodeRenderer extends Component {
render() {
const {
scaffoldBlockPxWidth,
toggleChildrenVisibility,
connectDragPreview,
connectDragSource,
isDragging,
canDrop,
canDrag,
node,
title,
subtitle,
draggedNode,
path,
treeIndex,
isSearchMatch,
isSearchFocus,
buttons,
className,
style,
didDrop,
isOver, // Not needed, but preserved for other renderers
parentNode, // Needed for dndManager
...otherProps
} = this.props;
const nodeTitle = title || node.title;
const nodeSubtitle = subtitle || node.subtitle;
let handle;
if (canDrag) {
if (typeof node.children === 'function' && node.expanded) {
// Show a loading symbol on the handle when the children are expanded
// and yet still defined by a function (a callback to fetch the children)
handle = (
<div className={styles.loadingHandle}>
<div className={styles.loadingCircle}>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
<div className={styles.loadingCirclePoint}/>
</div>
</div>
);
} else {
// Show the handle used to initiate a drag-and-drop
handle = connectDragSource(<div className={styles.moveHandle}/>, {
dropEffect: 'copy',
});
}
}
const isDraggedDescendant = draggedNode && isDescendant(draggedNode, node);
const isLandingPadActive = !didDrop && isDragging;
return (
<div style={{height: '100%'}} {...otherProps}>
{toggleChildrenVisibility &&
node.children &&
node.children.length > 0 &&
<div>
<button
type="button"
aria-label={node.expanded ? 'Collapse' : 'Expand'}
className={
node.expanded ? styles.collapseButton : styles.expandButton
}
style={{left: -0.5 * scaffoldBlockPxWidth}}
onClick={() =>
toggleChildrenVisibility({
node,
path,
treeIndex,
})}
/>
{node.expanded &&
!isDragging &&
<div
style={{width: scaffoldBlockPxWidth}}
className={styles.lineChildren}
/>}
</div>}
<div className={styles.rowWrapper}>
{/* Set the row preview to be used during drag and drop */}
{connectDragPreview(
<div
className={
styles.row +
(isLandingPadActive ? ` ${styles.rowLandingPad}` : '') +
(isLandingPadActive && !canDrop
? ` ${styles.rowCancelPad}`
: '') +
(isSearchMatch ? ` ${styles.rowSearchMatch}` : '') +
(isSearchFocus ? ` ${styles.rowSearchFocus}` : '') +
(className ? ` ${className}` : '')
}
style={{
opacity: isDraggedDescendant ? 0.5 : 1,
...style,
}}
>
{handle}
<div
className={
styles.rowContents +
(!canDrag ? ` ${styles.rowContentsDragDisabled}` : '') +
(options && options.isSelected && options.isSelected({path, node}) ? ` ${styles.rowContentsSelected}` : '') // This has been added compared to the original file
}
onClick={() => options && options.onClick && options.onClick({path, node})} // This has been added compared to the original file
>
<div className={styles.rowLabel}>
<span
className={
styles.rowTitle +
(node.subtitle ? ` ${styles.rowTitleWithSubtitle}` : '')
}
>
{typeof nodeTitle === 'function'
? nodeTitle({
node,
path,
treeIndex,
})
: nodeTitle}
</span>
{nodeSubtitle &&
<span className={styles.rowSubtitle}>
{typeof nodeSubtitle === 'function'
? nodeSubtitle({
node,
path,
treeIndex,
})
: nodeSubtitle}
</span>}
</div>
<div className={styles.rowToolbar}>
{buttons.map((btn, index) =>
<div
key={index} // eslint-disable-line react/no-array-index-key
className={styles.toolbarButton}
>
{btn}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}
}
RuleTreeNodeRenderer.defaultProps = {
isSearchMatch: false,
isSearchFocus: false,
canDrag: false,
toggleChildrenVisibility: null,
buttons: [],
className: '',
style: {},
parentNode: null,
draggedNode: null,
canDrop: false,
title: null,
subtitle: null,
};
RuleTreeNodeRenderer.propTypes = {
node: PropTypes.shape({}).isRequired,
title: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
subtitle: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
path: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
).isRequired,
treeIndex: PropTypes.number.isRequired,
isSearchMatch: PropTypes.bool,
isSearchFocus: PropTypes.bool,
canDrag: PropTypes.bool,
scaffoldBlockPxWidth: PropTypes.number.isRequired,
toggleChildrenVisibility: PropTypes.func,
buttons: PropTypes.arrayOf(PropTypes.node),
className: PropTypes.string,
style: PropTypes.shape({}),
// Drag and drop API functions
// Drag source
connectDragPreview: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
parentNode: PropTypes.shape({}), // Needed for dndManager
isDragging: PropTypes.bool.isRequired,
didDrop: PropTypes.bool.isRequired,
draggedNode: PropTypes.shape({}),
// Drop target
isOver: PropTypes.bool.isRequired,
canDrop: PropTypes.bool
};
return RuleTreeNodeRenderer;
}

View file

@ -0,0 +1,300 @@
// Taken from https://github.com/fritz-c/react-sortable-tree/blob/d6a9be9e4931b5f0dac7b87511d309694f42355e/src/node-renderer-default.scss
$row-padding: 10px;
.rowWrapper {
padding: $row-padding $row-padding $row-padding 0;
height: 100%;
box-sizing: border-box;
}
.row {
height: 100%;
white-space: nowrap;
display: flex;
& > * {
box-sizing: border-box;
}
}
/**
* The outline of where the element will go if dropped, displayed while dragging
*/
.rowLandingPad {
border: none !important;
box-shadow: none !important;
outline: none !important;
* {
opacity: 0 !important;
}
&::before {
background-color: lightblue;
border: 3px dashed white;
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
}
}
/**
* Alternate appearance of the landing pad when the dragged location is invalid
*/
.rowCancelPad {
@extend .rowLandingPad;
&::before {
background-color: #e6a8ad;
}
}
/**
* Nodes matching the search conditions are highlighted
*/
.rowSearchMatch {
outline: solid 3px #0080ff;
}
/**
* The node that matches the search conditions and is currently focused
*/
.rowSearchFocus {
outline: solid 3px #fc6421;
}
%rowItem {
display: inline-block;
vertical-align: middle;
}
.rowContents {
@extend %rowItem;
position: relative;
height: 100%;
border: solid #bbb 1px;
border-left: none;
box-shadow: 0 2px 2px -2px;
padding: 0 5px 0 10px;
border-radius: 2px;
min-width: 230px;
flex: 1 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
background-color: white;
}
.rowContentsDragDisabled {
border-left: solid #bbb 1px;
}
.rowLabel {
@extend %rowItem;
flex: 0 1 auto;
padding-right: 20px;
}
.rowToolbar {
@extend %rowItem;
flex: 0 1 auto;
display: flex;
}
.moveHandle {
@extend %rowItem;
height: 100%;
width: 44px;
background: #d9d9d9
url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MiIgaGVpZ2h0PSI0MiI+PGcgc3Ryb2tlPSIjRkZGIiBzdHJva2Utd2lkdGg9IjIuOSIgPjxwYXRoIGQ9Ik0xNCAxNS43aDE0LjQiLz48cGF0aCBkPSJNMTQgMjEuNGgxNC40Ii8+PHBhdGggZD0iTTE0IDI3LjFoMTQuNCIvPjwvZz4KPC9zdmc+')
no-repeat center;
border: solid #aaa 1px;
box-shadow: 0 2px 2px -2px;
cursor: move;
border-radius: 1px;
z-index: 1;
}
.loadingHandle {
@extend .moveHandle;
cursor: default;
background: #d9d9d9;
}
@keyframes pointFade {
0%,
19.999%,
100% {
opacity: 0;
}
20% {
opacity: 1;
}
}
.loadingCircle {
width: 80%;
height: 80%;
margin: 10%;
position: relative;
}
.loadingCirclePoint {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
$point-count: 12;
$spin-animation-time: 800ms;
&:before {
content: '';
display: block;
margin: 0 auto;
width: 11%;
height: 30%;
background-color: #fff;
border-radius: 30%;
animation: pointFade $spin-animation-time infinite ease-in-out both;
}
@for $i from 1 through (($point-count + 1) / 2) {
&:nth-of-type(#{$i}) {
transform: rotate(360deg / $point-count * ($i - 1));
}
&:nth-of-type(#{$i + $point-count / 2}) {
transform: rotate(180deg + 360deg / $point-count * ($i - 1));
}
&:nth-of-type(#{$i}),
&:nth-of-type(#{$i + $point-count / 2}) {
&:before {
animation-delay: - $spin-animation-time + ($spin-animation-time /
$point-count * 2 * ($i - 1));
}
}
}
}
.toolbarButton {
@extend %rowItem;
}
.rowTitle {
font-weight: bold;
}
.rowTitleWithSubtitle {
font-size: 85%;
display: block;
height: 0.8rem;
}
.rowSubtitle {
font-size: 70%;
line-height: 1;
}
.collapseButton,
.expandButton {
appearance: none;
border: none;
position: absolute;
border-radius: 100%;
box-shadow: 0 0 0 1px #000;
width: 16px;
height: 16px;
top: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
&:focus {
outline: none;
box-shadow: 0 0 0 1px #000, 0 0 1px 3px #83bef9;
}
&:hover:not(:active) {
background-size: 24px;
height: 20px;
width: 20px;
}
}
.collapseButton {
background: #fff
url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCI+PGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiIGZpbGw9IiNGRkYiLz48ZyBzdHJva2U9IiM5ODk4OTgiIHN0cm9rZS13aWR0aD0iMS45IiA+PHBhdGggZD0iTTQuNSA5aDkiLz48L2c+Cjwvc3ZnPg==')
no-repeat center;
}
.expandButton {
background: #fff
url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCI+PGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiIGZpbGw9IiNGRkYiLz48ZyBzdHJva2U9IiM5ODk4OTgiIHN0cm9rZS13aWR0aD0iMS45IiA+PHBhdGggZD0iTTQuNSA5aDkiLz48cGF0aCBkPSJNOSA0LjV2OSIvPjwvZz4KPC9zdmc+')
no-repeat center;
}
/**
* Classes for IE9 and below
*/
%fixVertAlign {
&::before {
content: '';
display: inline-block;
vertical-align: middle;
height: 100%;
}
}
.row_NoFlex {
@extend %fixVertAlign;
}
.rowContents_NoFlex {
@extend %fixVertAlign;
display: inline-block;
&::after {
content: '';
display: inline-block;
width: 100%;
}
}
.rowLabel_NoFlex {
@extend %rowItem;
width: 50%;
}
.rowToolbar_NoFlex {
@extend %rowItem;
text-align: right;
width: 50%;
}
/**
* Line for under a node with children
*/
.lineChildren {
height: 100%;
display: inline-block;
position: absolute;
&::after {
content: '';
position: absolute;
background-color: black;
width: 1px;
left: 50%;
bottom: 0;
height: $row-padding;
}
}

View file

@ -0,0 +1,5 @@
@import './node-renderer-default';
.rowContents.rowContentsSelected {
background-color: #d3d3ff;
}

View file

@ -16,9 +16,40 @@ module.exports = {
},
module: {
rules: [
{test: /\.(js|jsx)$/, use: 'babel-loader'},
{test: /\.css$/, loader: 'style-loader!css-loader'},
{test: /\.(png|jpg|gif)$/, loader: 'url-loader?limit=8192' } // inline base64 URLs for <=8k images, direct URLs for the rest
{
test: /\.(js|jsx)$/,
exclude: /(disposables)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
use: [ 'babel-loader' ]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192 // inline base64 URLs for <=8k images, direct URLs for the rest
}
}
]
},
{
test: /\.scss$/,
exclude: path.join(__dirname, 'node_modules'),
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
},
'sass-loader' ]
},
]
},
externals: {