import React, { Component } from 'react' import { Route, Link } from 'react-router-dom' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { history } from '../../../store' import { session } from '../../../session' import actions from '../../../actions' import * as graphActions from '../graph.actions' import { Loader } from '../../../common' import { clamp, dist, mod } from '../../../util' const defaultState = { dragging: false, bounds: null, mouseX: 0, mouseY: 0, box: { x: 0, y: 0, w: 0, h: 0, }, page: null, } class GraphEditor extends Component { state = { ...defaultState, } constructor() { super() // bind these events in the constructor, so we can remove event listeners later this.handleMouseDown = this.handleMouseDown.bind(this) this.handleMouseMove = this.handleMouseMove.bind(this) this.handleMouseUp = this.handleMouseUp.bind(this) this.handleWindowResize = this.handleWindowResize.bind(this) this.graphRef = React.createRef() } getBoundingClientRect() { if (!this.graphRef.current) return null const rect = this.graphRef.current.getBoundingClientRect() const scrollTop = document.body.scrollTop || document.body.parentNode.scrollTop const scrollLeft = document.body.scrollLeft || document.body.parentNode.scrollLeft const bounds = { top: rect.top + scrollTop, left: rect.left + scrollLeft, width: rect.width, height: rect.height, } // console.log(bounds) return bounds } componentDidMount() { document.body.addEventListener('mousemove', this.handleMouseMove) document.body.addEventListener('mouseup', this.handleMouseUp) window.addEventListener('resize', this.handleWindowResize) this.setState({ bounds: this.getBoundingClientRect() }) } componentDidUpdate(prevProps) { if (!this.state.bounds) { this.setState({ bounds: this.getBoundingClientRect() }) } } handleWindowResize() { this.setState({ bounds: this.getBoundingClientRect() }) } handleMouseDown(e, page) { const bounds = this.getBoundingClientRect() const mouseX = e.pageX const mouseY = e.pageY let w = 128 / bounds.width let h = 16 / bounds.height let { x, y } = page.settings x = clamp(x, 0, 1) y = clamp(y, 0, 1) this.setState({ page, dragging: true, bounds, mouseX, mouseY, box: { x, y, w, h, }, initialBox: { x, y, w, h, } }) } handleMouseMove(e) { const { dragging, bounds, mouseX, mouseY, initialBox, box } = this.state if (dragging) { e.preventDefault() let { x, y, w, h } = initialBox let dx = (e.pageX - mouseX) / bounds.width let dy = (e.pageY - mouseY) / bounds.height this.setState({ box: { x: clamp(x + dx, 0, 1.0 - w), y: clamp(y + dy, 0, 1.0 - h), w, h, } }) } } handleMouseUp(e) { // const { actions } = this.props const { dragging, bounds, initialBox, box, page } = this.state if (!dragging) return e.preventDefault() const { width, height } = bounds const { x, y, w, h } = box let url = window.location.pathname this.setState({ page: null, box: null, initialBox: null, dragging: false, }) if (dist(width * x, height * y, width * initialBox.x, height * initialBox.y) < 3) return const updatedPage = { ...page, settings: { ...page.settings, x, y, } } this.props.graphActions.updateGraphPage(updatedPage) actions.page.update(updatedPage) } render(){ // console.log(this.props.graph.show.res) const { page: currentPage, box } = this.state const { res: graph } = this.props.graph.show // console.log(res.pages) return (
{this.state.bounds && graph.pages.map(page => ( this.handleMouseDown(e, page)} /> ))}
) } } class GraphCanvas extends Component { constructor(props) { super(props) this.canvasRef = React.createRef() } componentDidMount() { if (this.props.bounds) { this.draw({}) } } componentDidUpdate(prevProps) { this.draw(prevProps) } draw(prevProps) { const { current: canvas } = this.canvasRef const { bounds, pages, currentPage, box } = this.props const { width, height } = bounds if (prevProps.bounds !== bounds) { canvas.width = width canvas.height = height } const ctx = canvas.getContext('2d') ctx.clearRect(0, 0, width, height) ctx.lineWidth = 2 const coordsLookup = pages.reduce((a,b) => { if (currentPage && box && b.id === currentPage.id) { a[b.id] = { x: box.x, y: box.y, backlinks: new Set([]), } } else { a[b.id] = { x: b.settings.x, y: b.settings.y, backlinks: new Set([]), } } return a }, {}) pages.map(page => { const sourceCoord = coordsLookup[page.id] page.backlinks.map(tile => { if (tile.target_page_id <= 0) return const targetCoord = coordsLookup[tile.page_id] let xOffset = 16 let yOffset = 16 // console.log(tile.page_id, tile.target_page_id, sourceCoord.backlinks, targetCoord.backlinks) if (targetCoord.backlinks.has(tile.target_page_id)) { xOffset += 10 ctx.strokeStyle = "#88ffff" } else { sourceCoord.backlinks.add(tile.page_id) ctx.strokeStyle = "#ff88ff" } ctx.beginPath() const x1 = targetCoord.x * width + xOffset const y1 = targetCoord.y * height + yOffset const x2 = sourceCoord.x * width + xOffset const y2 = sourceCoord.y * height + yOffset this.arrow(ctx, x1, y1, x2, y2) ctx.stroke() }) }) } arrow(ctx, x1, y1, x2, y2) { const headlen = 10 // length of head in pixels const dx = x2 - x1 const dy = y2 - y1 const angle = Math.atan2(dy, dx) const farOffset = 20 x1 += Math.cos(angle) * 0 x2 -= Math.cos(angle) * farOffset y1 += Math.sin(angle) * 0 y2 -= Math.sin(angle) * farOffset const leftAngle = mod(angle - Math.PI / 6, Math.PI * 2) const rightAngle = mod(angle + Math.PI / 6, Math.PI * 2) ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.lineTo(x2 - headlen * Math.cos(leftAngle), y2 - headlen * Math.sin(leftAngle)) ctx.moveTo(x2, y2) ctx.lineTo(x2 - headlen * Math.cos(rightAngle), y2 - headlen * Math.sin(rightAngle)) } render() { return ( ) } } const PageHandle = ({ graph, page, bounds, box, onMouseDown }) => { let style; if (box) { style = { top: (bounds.height) * box.y, left: (bounds.width) * box.x, } } else { style = { top: (bounds.height) * Math.min(page.settings.y, 0.95), left: (bounds.width) * Math.min(page.settings.x, 0.95), } } const url = '/' + graph.path + '/' + page.path // console.log(style) return (
history.push(url)} style={style} > {page.title} {'>'}
) } const mapStateToProps = state => ({ graph: state.graph, }) const mapDispatchToProps = dispatch => ({ graphActions: bindActionCreators({ ...graphActions }, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(GraphEditor)