'use strict'; import React, {Component} from 'react'; import ReactDOMServer from 'react-dom/server'; import {withTranslation} from './i18n'; import PropTypes from 'prop-types'; import jQuery from 'jquery'; import '../../static/jquery/jquery-ui-1.12.1.min.js'; import '../../static/fancytree/jquery.fancytree-all.min.js'; import '../../static/fancytree/skin-bootstrap/ui.fancytree.min.css'; import './tree.scss'; import axios from './axios'; import {withPageHelpers} from './page' import { withAsyncErrorHandler, withErrorHandling } from './error-handling'; import styles from "./styles.scss"; import {getUrl} from "./urls"; import {withComponentMixins} from "./decorator-helpers"; const TreeSelectMode = { NONE: 0, SINGLE: 1, MULTI: 2 }; @withComponentMixins([ withTranslation, withErrorHandling, withPageHelpers ], ['refresh']) class TreeTable extends Component { constructor(props) { super(props); this.state = { treeData: [] }; if (props.data) { this.state.treeData = props.data; } // Select Mode simply cannot be changed later. This is just to make sure we avoid inconsistencies if someone changes it anyway. this.selectMode = this.props.selectMode; } static defaultProps = { selectMode: TreeSelectMode.NONE } refresh() { if (this.tree) { this.tree.reload(this.sanitizeTreeData(this.state.treeData)); this.updateSelection(); } } @withAsyncErrorHandler async loadData(dataUrl) { const response = await axios.get(getUrl(dataUrl)); const treeData = response.data; for (const root of treeData) { root.expanded = true; for (const child of root.children) { child.expanded = true; } } this.setState({ treeData }); } static propTypes = { dataUrl: PropTypes.string, data: PropTypes.array, selectMode: PropTypes.number, selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]), onSelectionChangedAsync: PropTypes.func, actions: PropTypes.func, withHeader: PropTypes.bool, withDescription: PropTypes.bool, noTable: PropTypes.bool, withIcons: PropTypes.bool, className: PropTypes.string } componentWillReceiveProps(nextProps) { if (nextProps.data) { this.setState({ treeData: nextProps.data }); } else if (nextProps.dataUrl && this.props.dataUrl !== nextProps.dataUrl) { // noinspection JSIgnoredPromiseFromCall this.loadData(next.props.dataUrl); } } shouldComponentUpdate(nextProps, nextState) { return this.props.selection !== nextProps.selection || this.state.treeData != nextState.treeData || this.props.className !== nextProps.className; } // XSS protection sanitizeTreeData(unsafeData) { const data = []; for (const unsafeEntry of unsafeData) { const entry = Object.assign({}, unsafeEntry); entry.unsanitizedTitle = entry.title; entry.title = ReactDOMServer.renderToStaticMarkup(
{entry.title}
); entry.description = ReactDOMServer.renderToStaticMarkup(
{entry.description}
); if (entry.children) { entry.children = this.sanitizeTreeData(entry.children); } data.push(entry); } return data; } componentDidMount() { if (!this.props.data && this.props.dataUrl) { // noinspection JSIgnoredPromiseFromCall this.loadData(this.props.dataUrl); } let createNodeFn; createNodeFn = (event, data) => { const node = data.node; const tdList = jQuery(node.tr).find(">td"); let tdIdx = 1; if (this.props.withDescription) { const descHtml = node.data.description; // This was already sanitized in sanitizeTreeData when the data was loaded tdList.eq(tdIdx).html(descHtml); tdIdx += 1; } if (this.props.actions) { const linksContainer = jQuery(``); const actions = this.props.actions(node); for (const action of actions) { if (action.action) { const html = ReactDOMServer.renderToStaticMarkup({action.label}); const elem = jQuery(html); elem.click((evt) => { evt.preventDefault(); action.action(this) }); linksContainer.append(elem); } else if (action.link) { const html = ReactDOMServer.renderToStaticMarkup({action.label}); const elem = jQuery(html); elem.click((evt) => { evt.preventDefault(); this.navigateTo(action.link) }); linksContainer.append(elem); } else if (action.href) { const html = ReactDOMServer.renderToStaticMarkup({action.label}); const elem = jQuery(html); linksContainer.append(elem); } else { const html = ReactDOMServer.renderToStaticMarkup({action.label}); const elem = jQuery(html); linksContainer.append(elem); } } tdList.eq(tdIdx).html(linksContainer); tdIdx += 1; } }; const treeOpts = { extensions: ['glyph'], glyph: { map: { expanderClosed: 'fas fa-angle-right', expanderLazy: 'fas fa-angle-right', // glyphicon-plus-sign expanderOpen: 'fas fa-angle-down', // glyphicon-collapse-down checkbox: 'fas fa-square', checkboxSelected: 'fas fa-check-square', folder: 'fas fa-folder', folderOpen: 'fas fa-folder-open', doc: 'fas fa-file', docOpen: 'fas fa-file' } }, selectMode: (this.selectMode === TreeSelectMode.MULTI ? 2 : 1), icon: !!this.props.withIcons, autoScroll: true, scrollParent: jQuery(this.domTableContainer), source: this.sanitizeTreeData(this.state.treeData), toggleEffect: false, createNode: createNodeFn, checkbox: this.selectMode === TreeSelectMode.MULTI, activate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null), select: (this.selectMode === TreeSelectMode.MULTI ? ::this.onSelect : null), }; if (!this.props.noTable) { treeOpts.extensions.push('table'); treeOpts.table = { nodeColumnIdx: 0 }; } this.tree = jQuery(this.domTable).fancytree(treeOpts).fancytree("getTree"); this.updateSelection(); } componentDidUpdate(prevProps, prevState) { if (this.props.selection !== prevProps.selection || this.state.treeData != prevState.treeData) { if (this.state.treeData != prevState.treeData) { this.tree.reload(this.sanitizeTreeData(this.state.treeData)); } this.updateSelection(); } } updateSelection() { const tree = this.tree; if (this.selectMode === TreeSelectMode.MULTI) { const selectSet = new Set(this.props.selection.map(key => this.stringifyKey(key))); tree.enableUpdate(false); tree.visit(node => node.setSelected(selectSet.has(node.key))); tree.enableUpdate(true); } else if (this.selectMode === TreeSelectMode.SINGLE) { this.tree.activateKey(this.stringifyKey(this.props.selection)); } } @withAsyncErrorHandler async onSelectionChanged(sel) { if (this.props.onSelectionChangedAsync) { await this.props.onSelectionChangedAsync(sel); } } stringifyKey(key) { if (key !== null && key !== undefined) { return key.toString(); } else { return key; } } destringifyKey(key) { if (/^(\-|\+)?([0-9]+|Infinity)$/.test(key)) { return Number(key); } else { return key; } } // Single-select onActivate(event, data) { const selection = this.destringifyKey(this.tree.getActiveNode().key); if (selection !== this.props.selection) { // noinspection JSIgnoredPromiseFromCall this.onSelectionChanged(selection); } } // Multi-select onSelect(event, data) { const newSel = this.tree.getSelectedNodes().map(node => this.destringifyKey(node.key)).sort(); const oldSel = this.props.selection; let updated = false; const length = oldSel.length; if (length === newSel.length) { for (let i = 0; i < length; i++) { if (oldSel[i] !== newSel[i]) { updated = true; break; } } } else { updated = true; } if (updated) { // noinspection JSIgnoredPromiseFromCall this.onSelectionChanged(selection); } } render() { const t = this.props.t; const props = this.props; const actions = props.actions; const withHeader = props.withHeader; const withDescription = props.withDescription; let containerClass = 'mt-treetable-container ' + (this.props.className || ''); if (this.selectMode === TreeSelectMode.NONE) { containerClass += ' mt-treetable-inactivable'; } else { if (!props.noTable) { containerClass += ' table-hover'; } } if (!this.withHeader) { containerClass += ' mt-treetable-noheader'; } // FIXME: style={{ height: '100px', overflow: 'auto'}} if (props.noTable) { return (
{ this.domTableContainer = domElem; }} >
{ this.domTable = domElem; }}>
); } else { let tableClass = 'table table-striped table-condensed'; if (this.selectMode !== TreeSelectMode.NONE) { tableClass += ' table-hover'; } return (
{ this.domTableContainer = domElem; }} > { this.domTable = domElem; }} className={tableClass}> {props.withHeader && {withDescription && } {actions && } } {withDescription && } {actions && }
{t('name')}{t('description')}
); } } } export { TreeTable, TreeSelectMode }