diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..facd180 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react"] +} \ No newline at end of file diff --git a/.buildpacks b/.buildpacks deleted file mode 100644 index 1cf2c2d..0000000 --- a/.buildpacks +++ /dev/null @@ -1,2 +0,0 @@ -https://github.com/heroku/heroku-buildpack-nodejs -https://github.com/aguestuser/heroku-buildpack-webpack diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 27b21bf..0000000 --- a/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "parser": "babel-eslint", - "env": { - "browser": true, - "es6": true, - "node": true, - "mocha": true - }, - "ecmaFeatures": { - "arrowFunctions": true, - "blockBindings": true, - "classes": true, - "defaultParams": true, - "destructuring": true, - "forOf": true, - "generators": true, - "modules": true, - "spread": true, - "templateStrings": true, - "jsx": true - }, - "rules": { - "consistent-return": [0], - "key-spacing": [0], - "quotes": [0], - "new-cap": [0], - "no-multi-spaces": [0], - "no-shadow": [0], - "no-unused-vars": [1], - "no-use-before-define": [2, "nofunc"], - "curly": [2, "multi-line"], - "camelcase": [2, {"properties": "never"}] - } -} diff --git a/.tern-project b/.tern-project deleted file mode 100644 index 081cbd5..0000000 --- a/.tern-project +++ /dev/null @@ -1,13 +0,0 @@ -{ - "defs": [ - "browser", - "ecma5", - "ecma6" - ], - "dontLoad": [ - "node_modules/**" - ], - "plugins": { - "node": true - } -} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aff917c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js + +node_js: + - "6.0.0" \ No newline at end of file diff --git a/app/CaptionDisplaySettings.js b/app/CaptionDisplaySettings.js index dc253fc..084d8b3 100644 --- a/app/CaptionDisplaySettings.js +++ b/app/CaptionDisplaySettings.js @@ -5,8 +5,9 @@ module.exports = { selectFillColor: "#0f0", textOpacity: { "normal": 1, + "editable": 1, "highlighted": 1, - "faded": 0.2 + "faded": 0.5 }, highlightOpacity: 0.5, selectOpacity: 0.5 diff --git a/app/EdgeDisplaySettings.js b/app/EdgeDisplaySettings.js index 6be7627..71b9215 100644 --- a/app/EdgeDisplaySettings.js +++ b/app/EdgeDisplaySettings.js @@ -2,22 +2,26 @@ module.exports = { curveStrength: 0.5, lineColor: { normal: "#999", + editable: "#999", highlighted: "#999", - faded: "#ddd" + faded: "#aaa" }, textColor: { normal: "#999", + editable: "#444", highlighted: "#444", - faded: "#ddd" + faded: "#aaa" }, bgColor: { normal: "#fff", + editable: "#ff9800", highlighted: "#ff0", faded: "#fff" }, selectColor: "#0f0", bgOpacity: { normal: 0, + editable: 0.5, highlighted: 0.5, faded: 0 }, diff --git a/app/NodeDisplaySettings.js b/app/NodeDisplaySettings.js index fb2a8aa..58d550f 100644 --- a/app/NodeDisplaySettings.js +++ b/app/NodeDisplaySettings.js @@ -7,39 +7,46 @@ module.exports = { cornerRadius: 5, circleColor: { normal: "#ccc", + editable: "#ccc", highlighted: "#ccc", faded: "#ccc" }, textColor: { normal: "#000", + editable: "#000", highlighted: "#000", faded: "#000" }, textOpacity: { normal: 1, + editable: 1, highlighted: 1, - faded: 0.2 + faded: 0.5 }, bgColor: { normal: "#fff", + editable: "#ff9800", highlighted: "#ff0", faded: "#fff" }, selectColor: "#0f0", bgOpacity: { normal: 0, - highlighted: 0.5, + editable: 0.75, + highlighted: 0.75, faded: 0 }, imageOpacity: { normal: 1, + editable: 1, highlighted: 1, - faded: 0.2 + faded: 0.5 }, circleOpacity: { normal: 1, + editable: 1, highlighted: 1, - faded: 0.2 + faded: 0.5 }, bgRadiusDiff: 4, selectionRadiusDiff: 10 diff --git a/app/actions.js b/app/actions.js index e5e7e20..9b9f77f 100644 --- a/app/actions.js +++ b/app/actions.js @@ -50,6 +50,7 @@ export const SET_TITLE = 'SET_TITLE'; export const SET_SETTINGS = 'SET_SETTINGS'; export const TOGGLE_HELP_SCREEN = 'TOGGLE_HELP_SCREEN'; export const TOGGLE_SETTINGS = 'TOGGLE_SETTINGS'; +export const TOGGLE_NODE_SELECTABLE = 'TOGGLE_NODE_SELECTABLE'; /* * action creators @@ -184,8 +185,8 @@ export function layoutCircle() { return { type: LAYOUT_CIRCLE }; } -export function setHighlights(highlights, otherwiseFaded = false) { - return { type: SET_HIGHLIGHTS, highlights, otherwiseFaded }; +export function setHighlights(highlights, otherwiseFaded = false, showEditTools, allowEditNodes) { + return { type: SET_HIGHLIGHTS, highlights, otherwiseFaded, showEditTools, allowEditNodes }; } export function clearHighlights() { @@ -247,4 +248,8 @@ export function toggleHelpScreen(value) { export function toggleSettings(value) { return { type: TOGGLE_SETTINGS, value }; +} + +export function toggleNodeSelectable(value) { + return { type: TOGGLE_NODE_SELECTABLE, value }; } \ No newline at end of file diff --git a/app/components/ChangeColorInput.jsx b/app/components/ChangeColorInput.jsx index d0773c9..2bfea99 100644 --- a/app/components/ChangeColorInput.jsx +++ b/app/components/ChangeColorInput.jsx @@ -38,17 +38,17 @@ export default class ChangeColorInput extends BaseComponent { render() { return ( -
-
-
-
-
+
{ this.state.displayColorPicker &&
-
+
} diff --git a/app/components/Edge.jsx b/app/components/Edge.jsx index d472fe7..8b2ec0c 100644 --- a/app/components/Edge.jsx +++ b/app/components/Edge.jsx @@ -21,6 +21,7 @@ export default class Edge extends BaseComponent { let width = 1 + (e.display.scale - 1) * 5; let selected = this.props.selected; let highlighted = e.display.status == "highlighted"; + let editable = e.display.status == "editable"; return ( - + { selected ? : null } - { highlighted ? + ); + isLocked={this.props.isLocked} + showEditTools={this.props.showEditTools} + allowEditNodes={this.props.allowEditNodes} />); } _renderCaptions() { @@ -105,8 +107,8 @@ export default class Graph extends BaseComponent { // VIEWBOX AND ZOOM - _updateViewbox(graph, zoom) { - let viewBox = this._computeViewbox(graph, zoom); + _updateViewbox(graph, zoom, viewOnlyHighlighted) { + let viewBox = this._computeViewbox(graph, zoom, viewOnlyHighlighted); let changed = (viewBox !== this.state.viewBox); let oldViewBox = changed ? this.state.viewBox : null; this.setState({ viewBox, oldViewBox }); @@ -130,9 +132,8 @@ export default class Graph extends BaseComponent { } } - _computeViewbox(graph, zoom = 1.2) { - let onlyHighlighted = this.props.viewOnlyHighlighted; - let rect = this._computeRect(graph, onlyHighlighted); + _computeViewbox(graph, zoom = 1, viewOnlyHighlighted = true) { + let rect = this._computeRect(graph, viewOnlyHighlighted); let w = rect.w / zoom; let h = rect.h / zoom; let x = rect.x + rect.w/2 - (w/2); diff --git a/app/components/GraphAnnotationForm.jsx b/app/components/GraphAnnotationForm.jsx deleted file mode 100644 index 8a36f09..0000000 --- a/app/components/GraphAnnotationForm.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import BaseComponent from './BaseComponent'; -import merge from 'lodash/object/merge'; -import pick from 'lodash/object/pick'; -import Editor from 'react-medium-editor'; - -export default class GraphAnnotationForm extends BaseComponent { - constructor(props) { - super(props); - this.bindAll('_handleHeaderChange', '_handleTextChange', '_handleRemove'); - this.state = pick(this.props.annotation, ['header', 'text']); - } - - render() { - let editorOptions = { - toolbar: { buttons: [ - 'bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote', 'unorderedlist', 'orderedlist' - ] }, - targetBlank: true, - placeholder: { text: "annotation text" } - } - - - return ( -
- - - -
- ); - } - - componentDidMount() { - this.refs.editor.medium.subscribe("blur", () => { - this.saveText(); - }); - } - - componentWillReceiveProps(props) { - this.setState(merge({ header: null, text: null }, pick(props.annotation, ['header', 'text']))); - } - - _handleRemove() { - if (confirm("Are you sure you want to delete this annotation?")) { - this.props.remove(this.props.currentIndex); - } - } - - _handleHeaderChange(event) { - this.setState({ header: event.target.value }); - this.props.update(this.props.currentIndex, { header: event.target.value, text: this.state.text }); - } - - _handleTextChange(value, medium) { - this.setState({ text: value }); - } - - saveText() { - this.props.update(this.props.currentIndex, this.state); - } - - _handleChange(field, value) { - this.setState({ [field]: value }); - this._apply(); - } - - _apply() { - let header = this.refs.header.value; - let text = this.refs.text.value; - this.props.update(this.props.currentIndex, { header, text }); - } -} \ No newline at end of file diff --git a/app/components/GraphAnnotationList.jsx b/app/components/GraphAnnotationList.jsx index 3a09aae..907389d 100644 --- a/app/components/GraphAnnotationList.jsx +++ b/app/components/GraphAnnotationList.jsx @@ -1,91 +1,125 @@ import React, { Component, PropTypes } from 'react'; import BaseComponent from './BaseComponent'; +import GraphAnnotationListItem from './GraphAnnotationListItem'; export default class GraphAnnotationList extends BaseComponent { constructor(props) { super(props); - this.bindAll('_handleClick', '_handleDragOver', '_handleDragStart', '_handleDragEnd'); + this.bindAll('_handleDragOver', '_handleDragStart', '_handleDragEnd', '_handleChange', '_handleMakeEditable', '_handleDisableEditable', '_setEditIndex', '_handleRemoveEditTools'); this._placeholder = document.createElement("li"); - this._placeholder.className = "placeholder"; + this._placeholder.className = "placeholder annotationParent"; + this.state = { + editIndex: null + }; } render() { return (
-
    - { this.props.annotations.map((annotation, index) => -
  • + { this.props.annotations.map(function(annotation, index) { + return this._handleChange(index)} + sendClass={index == this.props.currentIndex ? "active" : null} key={annotation.id} - data-id={index} - className={index == this.props.currentIndex ? "active" : null} - draggable={true} - onClick={this._handleClick} - onDragStart={this._handleDragStart} - onDragEnd={this._handleDragEnd}> - {annotation.header.trim().length > 0 ? annotation.header : "Untitled Annotation"} -
  • + annotationAttributes={annotation} + onDrag={this._handleDragStart} + onDragEnd={this._handleDragEnd} + doChange={this._handleChange} + turnOnEditable={() => this._handleMakeEditable()} + turnOffEditable={() => this._handleDisableEditable()} + index={index} + isEditTools={this.props.isEditMode} + setEditIndex={(num) => this._setEditIndex(num)} + getEditIndex={this.state.editIndex} + turnOffEditTools={() => this._handleRemoveEditTools()} + doRemove={this.props.remove} + isEditor={this.props.isEditor} + /> + }, this ) }
- { this.props.isEditor ? - : null }
); } - _handleClick(e) { - this.props.show(parseInt(e.target.dataset.id)); + _setEditIndex(num){ + this.setState({ editIndex: num }); + } - if (this.props.isEditor) { - this.props.hideEditTools(); - }; + _handleMakeEditable(){ + this.props.enableNodeSelectable(); + } + + _handleDisableEditable(){ + this.props.disableNodeSelectable(); } + _handleRemoveEditTools(){ + this.props.toggle(); + } + + _handleChange(index) { + this.props.show(parseInt(index)); + } + + _handleDragStart(e) { + if (e.currentTarget.className != "annotationParent active"){ + e.currentTarget.className = "annotationParent"; + } this._startY = e.clientY; this._dragged = e.currentTarget; - this._placeholder.innerHTML = e.currentTarget.innerHTML; + this._placeholder.innerHTML = "
" + (this._dragged.children[2].innerHTML) + "
"; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/html", e.currentTarget); } _handleDragEnd(e) { + if (this.props.currentIndex == this._dragged.id.split("annotationIndex")[1]){ + e.currentTarget.className = "annotationParent active"; + } this._dragged.style.display = "block"; - this._dragged.parentNode.removeChild(this._placeholder); - - // update store - let from = Number(this._dragged.dataset.id); - let to = Number(this._over.dataset.id); - + this._placeholder.parentNode.removeChild(this._placeholder); + let from = Number(this._dragged.id.split("annotationIndex")[1]); + let to = Number(this._over.id.split("annotationIndex")[1]); this.props.move(from, to); - this._startY = undefined; } _handleDragOver(e) { e.preventDefault(); + var theTarg; + + if (e.target.className == "placeholder" || e.target.dataset.reactid == undefined){ + return; + } + + //ensure this isn't a child element of the li + if (e.target.className.split("annotationParent").length > 1){ + theTarg = e.target; + } else { + theTarg = e.target.parentNode; + } + let thisHeight = this._dragged.offsetHeight; this._dragged.style.display = "none"; - if (e.target.className == "placeholder") return; - this._over = e.target; + this._over = theTarg; let relY = e.clientY - this._startY; let height = (this._over.offsetHeight || thisHeight) / 2; - let parent = e.target.parentNode; + let parent = theTarg.parentNode; if (relY > height) { - parent.insertBefore(this._placeholder, e.target.nextElementSibling); + parent.insertBefore(this._placeholder, theTarg.nextElementSibling); } else if (relY < height) { - parent.insertBefore(this._placeholder, e.target); + parent.insertBefore(this._placeholder, theTarg); } } } \ No newline at end of file diff --git a/app/components/GraphAnnotationListItem.jsx b/app/components/GraphAnnotationListItem.jsx index 3f308f3..7e3b31d 100644 --- a/app/components/GraphAnnotationListItem.jsx +++ b/app/components/GraphAnnotationListItem.jsx @@ -1,36 +1,188 @@ import React, { Component, PropTypes } from 'react'; import BaseComponent from './BaseComponent'; +import Editor from 'react-medium-editor'; + export default class GraphAnnotationListItem extends BaseComponent { constructor(props) { super(props); - this.bindAll('_handleDragStart', '_handleDragEnd'); + this.bindAll('componentDidMount', 'componentWillReceiveProps', 'componentDidUpdate', '_handleShowClick', '_handleEditClick', '_handleRemove', '_handleHeaderChange', '_handleTextChange' ); + if (this.props.annotationAttributes.header == "Untitled Annotation"){ + this.state = { + isNew: true, + editable: true, + header: this.props.annotationAttributes.header, + text: this.props.annotationAttributes.text + }; + } else { + this.state = { + isNew: false, + editable: false, + header: this.props.annotationAttributes.header, + text: this.props.annotationAttributes.text + }; + } + } + + componentDidUpdate(){ + if (this.state.editable){ + if (this.props.getEditIndex != this.props.index){ + this.setState({editable: false}); + } + if (this.props.isEditTools){ + this.setState({editable: false}); + this.props.setEditIndex(null); + } + } + } + + componentDidMount(){ + this.setState({ isNew: false }); + if (this.props.getEditIndex == null && this.state.editable && this.state.isNew == false){ + this.props.setEditIndex(null); + this.props.turnOffEditable(); + this.setState({ editable: false }); + } + if (this.state.editable){ + this.refs.editorHeader.medium.subscribe("blur", () => { + this.saveText(); + }); + + this.refs.editorBody.medium.subscribe("blur", () => { + this.saveText(); + }); + } + } + + componentWillReceiveProps(){ + if (this.state.editable && this.props.getEditIndex == null){ + this.setState({ editable: false }); + } + } + + + _handleShowClick(e){ + this.props.doChange(this.props.index); + if (!this.state.editable){ + this.props.turnOffEditable(); + this.props.setEditIndex(null); + } + } + + _handleEditClick(e){ + if (this.props.isEditTools){ + this.props.turnOffEditTools(); + } + this.props.doChange(this.props.index); + this.setState({ editable: !this.state.editable }); + if (this.state.editable){ + this.props.setEditIndex(null); + this.props.turnOffEditable(); + } else { + this.props.setEditIndex(this.props.index); + this.props.turnOnEditable(); + } + + } + + _handleHeaderChange(value, medium) { + this.setState({ header: value }); + } + + _handleTextChange(value, medium) { + this.setState({ text: value }); + } + + _handleRemove() { + if (confirm("Are you sure you want to delete this annotation?")) { + this.props.doRemove(this.props.index); + } + } + + saveText() { + this.props.update(this.props.currentIndex, this.state); } + render() { - let active = this.props.currentIndex == this.props.index; + var theClass; + if (this.props.sendClass){ + theClass = "annotationParent " + this.props.sendClass; + if (this.state.editable && !this.props.isEditTools){ + theClass = theClass + " editableAnnotation" + } + } else { + theClass = "annotationParent"; + } + + let editorHeaderOptions = { + toolbar: { buttons: [ + 'bold', 'italic', 'underline' + ] }, + targetBlank: true, + placeholder: { text: "Annotation title" } + } + + let editorBodyOptions = { + toolbar: { buttons: [ + 'bold', 'italic', 'underline', 'h3', 'h4', 'quote', 'unorderedlist', 'orderedlist' + ] }, + targetBlank: true, + placeholder: { text: "Type your annotation here..." } + } return (
  • - {this.props.annotation.header} + id={"annotationIndex" + this.props.index} + > + { this.props.isEditor == true ? +
    this._handleEditClick(event)} > +
    : null + } + { this.props.isEditor == true ? +
    this._handleRemove(event)} > +
    : null + } + { this.state.editable == true ? + this._handleShowClick(event)} + className="annotationHeaderWrapper" + text={this.state.header} + options={editorHeaderOptions} + onChange={this._handleHeaderChange} > + : +
    this._handleShowClick(event)} + className = {"annotationHeaderWrapper"} + dangerouslySetInnerHTML={{ __html: this.state.header }}> +
    } + { this.state.editable == true ? + this._handleShowClick(event)} + className="annotationBodyWrapper" + text={this.state.text} + options={editorBodyOptions} + onChange={this._handleTextChange} > + : +
    this._handleShowClick(event)} + className = {"annotationBodyWrapper"} + dangerouslySetInnerHTML={{ __html: this.state.text }}> +
    }
  • ); } - _handleDragStart() { - - } - _handleDragEnd() { - - } - - _handleClick() { - this.props.show(this.props.index); - } } \ No newline at end of file diff --git a/app/components/GraphAnnotations.jsx b/app/components/GraphAnnotations.jsx index f935711..b4cf131 100644 --- a/app/components/GraphAnnotations.jsx +++ b/app/components/GraphAnnotations.jsx @@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react'; import GraphNavButtons from './GraphNavButtons'; import GraphAnnotationList from './GraphAnnotationList'; import GraphAnnotation from './GraphAnnotation'; -import GraphAnnotationForm from './GraphAnnotationForm'; +import ShowHideAnnotations from './ShowHideAnnotations'; require('../styles/oligrapher.annotations.css'); export default class GraphAnnotations extends Component { @@ -11,16 +11,12 @@ export default class GraphAnnotations extends Component { let { prevClick, nextClick, isEditor, editForm, navList, swapAnnotations, annotation, currentIndex, update, remove, swapEditForm, annotations, show, - create, move, canClickPrev, canClickNext } = this.props; + create, move, canClickPrev, canClickNext, allowEditNodes } = this.props; let navComponent = ( ); - let formComponent = ( - - ); - let annotationComponent = ( ); @@ -29,11 +25,15 @@ export default class GraphAnnotations extends Component { ); + let showHideAnnotation = ( + + ); + return (
    + { showHideAnnotation } + { navListComponent } { (annotation || isEditor) && navComponent } - { isEditor && navList && navListComponent } - { annotation && (isEditor ? formComponent : annotationComponent) }
    ); } diff --git a/app/components/GraphNavButtons.jsx b/app/components/GraphNavButtons.jsx index 26c819b..90ca509 100644 --- a/app/components/GraphNavButtons.jsx +++ b/app/components/GraphNavButtons.jsx @@ -1,6 +1,12 @@ import React, { Component, PropTypes } from 'react'; +import BaseComponent from './BaseComponent'; -export default class GraphNavButtons extends Component { + +export default class GraphNavButtons extends BaseComponent { + constructor(props) { + super(props); + this.bindAll('_handleNewAnnotation'); + } render() { return ( @@ -13,15 +19,22 @@ export default class GraphNavButtons extends Component { className="clickplz btn btn-lg btn-default" onClick={this.props.nextClick} disabled={!this.props.canClickNext}>Next -
    - -
    + { this.props.isEditor ? + : null }
    + ); } + + _handleNewAnnotation() { + this.props.create(); + } + + } \ No newline at end of file diff --git a/app/components/Node.jsx b/app/components/Node.jsx index e56d0ec..e61329d 100644 --- a/app/components/Node.jsx +++ b/app/components/Node.jsx @@ -21,7 +21,6 @@ export default class Node extends BaseComponent { const { x, y, name } = this.state; const groupId = `node-${n.id}`; const transform = `translate(${x}, ${y})`; - return ( - + { this.state.name ? : null } diff --git a/app/components/NodeCircle.jsx b/app/components/NodeCircle.jsx index d89adfa..5f8495e 100644 --- a/app/components/NodeCircle.jsx +++ b/app/components/NodeCircle.jsx @@ -7,7 +7,7 @@ export default class NodeCircle extends BaseComponent { render() { return ( - { this.props.selected ? this._selectionCirlce() : null } + { this.props.selected ? this._selectionCircle() : null } {this._bgCircle()} {this._circle()} @@ -23,13 +23,15 @@ export default class NodeCircle extends BaseComponent { } } - _selectionCirlce() { - const { scale } = this.props.node.display; - const r = ds.circleRadius * scale; - const bgColor = ds.selectColor; - const bgOpacity = 0.5; - const bgRadius = r + (ds.selectionRadiusDiff * scale); - return ; + _selectionCircle() { + if (this.props.showEditTools){ + const { scale } = this.props.node.display; + const r = ds.circleRadius * scale; + const bgColor = ds.selectColor; + const bgOpacity = 0.5; + const bgRadius = r + (ds.selectionRadiusDiff * scale); + return ; + } } _bgCircle() { diff --git a/app/components/NodeLabel.jsx b/app/components/NodeLabel.jsx index 5dfc280..37a9a49 100644 --- a/app/components/NodeLabel.jsx +++ b/app/components/NodeLabel.jsx @@ -9,37 +9,26 @@ export default class NodeLabel extends Component { let r = ds.circleRadius * scale; let textOffsetY = ds.textMarginTop + r; let textLines = this._textLines(name); - let linkAttributes = `xlink:href="${url}" target="_blank"`; - - let tspans = url ? - ` + - textLines.map((line, i) => { - let dy = (i == 0 ? textOffsetY : ds.lineHeight); - return `${line}`; - }).join("") + ``) - } } /> - : - ( - { textLines.map( - (line, i) => { - let dy = (i == 0 ? textOffsetY : ds.lineHeight); - return {line}; - }) } - ); + + let tspans = textLines.map((line, i) => + + {line} + + ); - let rects = textLines.map( - (line, i) => { + let rects = textLines.map((line, i) => { let width = line.length * 8; let height = ds.lineHeight; let y = r + 4 + (i * ds.lineHeight); - return (); + y={y} />; }); return ( {rects} - {tspans} + + + { tspans } + + ); } diff --git a/app/components/Root.jsx b/app/components/Root.jsx index 704f9d3..8b7446b 100644 --- a/app/components/Root.jsx +++ b/app/components/Root.jsx @@ -18,7 +18,7 @@ import { loadGraph, showGraph, loadAnnotations, showAnnotation, createAnnotation, toggleAnnotations, updateAnnotation, deleteAnnotation, moveAnnotation, - toggleHelpScreen, setSettings, toggleSettings } from '../actions'; + toggleHelpScreen, setSettings, toggleSettings, toggleNodeSelectable, allowEditNodes } from '../actions'; import Graph from './Graph'; import Editor from './Editor'; import GraphHeader from './GraphHeader'; @@ -46,13 +46,13 @@ class Root extends Component { render() { let { dispatch, graph, selection, isEditor, isLocked, title, - showEditTools, showSaveButton, showHelpScreen, + showEditTools, showSaveButton, showHelpScreen, allowEditNodes, hasSettings, graphSettings, showSettings, onSave, currentIndex, annotation, annotations, visibleAnnotations } = this.props; let that = this; // apply annotation highlights to graph if available - let annotatedGraph = graph && annotation ? GraphModel.setHighlights(graph, annotation, !isEditor) : graph; + let annotatedGraph = graph && annotation ? GraphModel.setHighlights(graph, annotation, !isEditor, showEditTools, allowEditNodes) : graph; const keyMap = { 'undo': 'ctrl+,', @@ -95,19 +95,31 @@ class Root extends Component { }; let clickNode = (nodeId) => { - isEditor && showEditTools ? - dispatch(swapNodeSelection(nodeId, !that.state.shiftKey)) : - (isLocked ? null : dispatch(swapNodeHighlight(nodeId))) + if (isEditor && showEditTools){ + dispatch(swapNodeSelection(nodeId, !that.state.shiftKey)); + } else { + if (allowEditNodes){ + isLocked ? null : dispatch(swapNodeHighlight(nodeId)); + } + } } let clickEdge = (edgeId) => { - isEditor && showEditTools ? - dispatch(swapEdgeSelection(edgeId, !that.state.shiftKey)) : - (isLocked ? null : dispatch(swapEdgeHighlight(edgeId))) + if (isEditor && showEditTools){ + dispatch(swapEdgeSelection(edgeId, !that.state.shiftKey)); + } else { + if (allowEditNodes){ + isLocked ? null : dispatch(swapEdgeHighlight(edgeId)); + } + } } let clickCaption = (captionId) => { - isEditor && showEditTools ? - dispatch(swapCaptionSelection(captionId, !that.state.shiftKey)) : - (isLocked ? null : dispatch(swapCaptionHighlight(captionId))) + if (isEditor && showEditTools && allowEditNodes){ + dispatch(swapCaptionSelection(captionId, !that.state.shiftKey)); + } else { + if (allowEditNodes){ + isLocked ? null : dispatch(swapCaptionHighlight(captionId)); + } + } } // annotations stuff @@ -122,7 +134,8 @@ class Root extends Component { let update = (index, data) => dispatch(updateAnnotation(index, data)); let remove = (index) => dispatch(deleteAnnotation(index)); let show = (index) => dispatch(showAnnotation(index)); - let create = () => { + let create = () => { + dispatch(createAnnotation(this.props.annotations.length)); }; let move = (from, to) => dispatch(moveAnnotation(from, to)); @@ -140,8 +153,8 @@ class Root extends Component { return (
    - -
    + +
    { (isEditor || title) && dispatch(moveNode(graphId, nodeId, x, y))} moveEdge={(graphId, edgeId, cx, cy) => dispatch(moveEdge(graphId, edgeId, cx, cy))} - moveCaption={(graphId, captionId, x, y) => dispatch(moveCaption(graphId, captionId, x, y))} /> + moveCaption={(graphId, captionId, x, y) => dispatch(moveCaption(graphId, captionId, x, y))} + allowEditNodes={allowEditNodes} /> } { graph && @@ -188,12 +202,13 @@ class Root extends Component { { isEditor && dispatch(toggleHelpScreen())} /> }
    - + { showSaveButton && isEditor && onSave && this.handleSave()} /> } { showSettings && hasSettings && }
    { showAnnotations && - dispatch(toggleEditTools(false))} /> - } + toggle={() => this.toggleEditTools(false)} + enableNodeSelectable={() => dispatch(toggleNodeSelectable(true))} + disableNodeSelectable={() => dispatch(toggleNodeSelectable(false))} + isEditMode={showEditTools} + /> + }
    { !showAnnotations && this.enableAnnotations() &&
    -
    } - { showSaveButton && isEditor && onSave && this.handleSave()} /> } { showHelpScreen && dispatch(toggleHelpScreen(false))} /> }
    @@ -235,6 +253,7 @@ class Root extends Component { if (isEditor && (!data || !data.graph)) { // show edit tools if isEditor and there's no initial graph this.toggleEditTools(true); + this.toggleNodeSelectable(false); } if (settings) { @@ -253,7 +272,6 @@ class Root extends Component { } if (JSON.stringify(prevProps.graph) !== JSON.stringify(this.props.graph)) { - // this.updateAnnotationHighlights(); // fire update callback if graph changed if (this.props.onUpdate) { @@ -296,9 +314,14 @@ class Root extends Component { } toggleEditTools(value) { + this.props.dispatch(toggleNodeSelectable(false)); this.props.dispatch(toggleEditTools(value)); }; + toggleNodeSelectable(value){ + this.props.dispatch(toggleNodeSelectable(value)); + } + prevIndex() { let { currentIndex, numAnnotations } = this.props; @@ -370,7 +393,9 @@ function select(state) { graphSettings: state.settings, hasSettings: Object.keys(state.settings).length > 0, showHelpScreen: state.showHelpScreen, - showSettings: state.showSettings + showSettings: state.showSettings, + allowEditNodes: state.allowEditNodes + }; } diff --git a/app/components/ShowHideAnnotations.js b/app/components/ShowHideAnnotations.js new file mode 100644 index 0000000..54ca257 --- /dev/null +++ b/app/components/ShowHideAnnotations.js @@ -0,0 +1,21 @@ +import React, { Component, PropTypes } from 'react'; + +export default class ShowHideAnnotations extends Component { + + render() { + return ( +
    +
    + +
    +
    + ) + } + +} \ No newline at end of file diff --git a/app/components/UpdateNodeForm.jsx b/app/components/UpdateNodeForm.jsx index a56ed35..fc90acb 100644 --- a/app/components/UpdateNodeForm.jsx +++ b/app/components/UpdateNodeForm.jsx @@ -29,8 +29,6 @@ export default class UpdateNodeForm extends BaseComponent { [3, "3x"] ]; - console.log(display.color); - return (
    @@ -86,7 +84,7 @@ export default class UpdateNodeForm extends BaseComponent { if (this.props.data) { let name = this.refs.name.value; let image = this.refs.image.value.trim(); - let color = newColor || null; + let color = newColor || this.refs.color.state.color; let scale = parseFloat(this.refs.scale.value); let url = this.refs.url.value.trim(); this.props.updateNode(this.props.data.id, { display: { name, image, color, scale, url } }); diff --git a/app/components/__tests__/Caption-test.jsx b/app/components/__tests__/Caption-test.jsx index 61d528d..32aeae2 100644 --- a/app/components/__tests__/Caption-test.jsx +++ b/app/components/__tests__/Caption-test.jsx @@ -1,45 +1,42 @@ -jest.dontMock('../BaseComponent'); -jest.dontMock('../Caption'); -jest.dontMock('react-draggable'); +jest.unmock('../BaseComponent'); +jest.unmock('../Caption'); +jest.unmock('react-draggable'); import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -const Caption = require('../Caption'); +import { shallow } from "enzyme"; +import Caption from '../Caption'; describe("Caption Component", () => { const data = { id: 1, display: { text: "Here's an interesting fact!" } }; it("should have an svg transform", () => { - let caption = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(caption); + let element = wrapper.find("g.caption"); let { x, y } = data.display; - expect(element.getAttribute("transform")).toBe(`translate(${x}, ${y})`); + expect(element.props().transform).toBe(`translate(${x}, ${y})`); }); it("should display text", () => { - let caption = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(caption); - let text = element.querySelector("text"); + let text = wrapper.find("text"); - expect(text.textContent).toBe(data.display.text); + expect(text.text()).toBe(data.display.text); }); it("should call click callback if clicked", () => { let clickCaption = jest.genMockFunction(); - let caption = TestUtils.renderIntoDocument( - + let wrapper = shallow( + ); - let element = ReactDOM.findDOMNode(caption); + let element = wrapper.find("g.caption"); + element.simulate("click"); - TestUtils.Simulate.click(element); - expect(clickCaption.mock.calls[0][0]).toBe("someid"); - expect(clickCaption.mock.calls[0][1]).toBe(data.id); + expect(clickCaption.mock.calls[0][0]).toBe(data.id); }); }); \ No newline at end of file diff --git a/app/components/__tests__/ChangeColorInput-test.jsx b/app/components/__tests__/ChangeColorInput-test.jsx new file mode 100644 index 0000000..ccc4be2 --- /dev/null +++ b/app/components/__tests__/ChangeColorInput-test.jsx @@ -0,0 +1,70 @@ +jest.disableAutomock(); + +import React from "react"; +import { shallow } from "enzyme"; + +import ChangeColorInput from "../ChangeColorInput"; +import ds from "../../NodeDisplaySettings"; +import { CompactPicker } from "react-color"; + +describe("ChangeColorInput", () => { + let wrapper; + let onChange; + + beforeEach(() => { + onChange = jest.genMockFunction(); + wrapper = shallow( + + ); + }); + + it("shows a swatch with the currently selected color", () => { + let swatch = wrapper.find(".nodeColorInputSwatch"); + expect(swatch.props().style).toEqual({ background: "#abc" }); + }); + + it("shows a clear button", () => { + let clearer = wrapper.find(".nodeColorInputClearer"); + let glyph = wrapper.find(".glyphicon-remove-sign"); + expect(clearer.length).toBe(1); + expect(glyph.length).toBe(1); + }); + + it("clears color and hides picker when clear button is clicked", () => { + wrapper.setState({ displayColorPicker: true }); + let clearer = wrapper.find(".nodeColorInputClearer"); + clearer.simulate("click"); + expect(wrapper.state().color).toBe(ds.circleColor["highlighted"]); + }); + + it ("shows and closes color picker when swatch is clicked", () => { + let swatch = wrapper.find(".nodeColorInputSwatch"); + swatch.simulate("click"); + + let picker = wrapper.find(CompactPicker); + expect(picker.length).toBe(1); + expect(picker.props().color).toBe("#abc"); + + swatch.simulate("click"); + picker = wrapper.find(CompactPicker); + expect(picker.length).toBe(0); + }); + + it("updates color when receiving new props", () => { + wrapper.setProps({ value: "#def" }); + expect(wrapper.state().color).toBe("#def"); + }); + + it("calls onChange when CompactPicker changes color", () => { + wrapper.setState({ displayColorPicker: true }); + let picker = wrapper.find(CompactPicker); + let pickerOnChange = picker.props().onChange; + pickerOnChange({ hex: "def" }); + + expect(onChange.mock.calls.length).toBe(1); + expect(onChange.mock.calls[0][0]).toBe("#def"); + }); +}); diff --git a/app/components/__tests__/Edge-test.jsx b/app/components/__tests__/Edge-test.jsx index 1632aaa..8ff6768 100644 --- a/app/components/__tests__/Edge-test.jsx +++ b/app/components/__tests__/Edge-test.jsx @@ -1,10 +1,10 @@ -jest.dontMock('../BaseComponent'); -jest.dontMock('../Edge'); +jest.unmock('../BaseComponent'); +jest.unmock('../Edge'); import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; -const Edge = require('../Edge'); +import Edge from '../Edge'; describe("Edge Component", () => { @@ -70,7 +70,6 @@ describe("Edge Component", () => { let select = element.querySelector(".edgeSelect"); TestUtils.Simulate.click(select); - expect(clickEdge.mock.calls[0][0]).toBe("someid"); - expect(clickEdge.mock.calls[0][1]).toBe(data.id); + expect(clickEdge.mock.calls[0][0]).toBe(data.id); }); }); \ No newline at end of file diff --git a/app/components/__tests__/Graph-test.jsx b/app/components/__tests__/Graph-test.jsx index dc0b576..ec9e07f 100644 --- a/app/components/__tests__/Graph-test.jsx +++ b/app/components/__tests__/Graph-test.jsx @@ -1,44 +1,43 @@ -jest.dontMock('../Graph'); -jest.dontMock('../Node'); -jest.dontMock('../Edge'); -jest.dontMock('../Caption'); -jest.dontMock('classnames'); +jest.unmock('../Graph'); +jest.unmock('../Node'); +jest.unmock('../Edge'); +jest.unmock('../Caption'); +jest.unmock('classnames'); import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -const Graph = require('../Graph'); +import { shallow } from "enzyme"; +import Graph from '../Graph'; +import Node from "../Node"; +import Edge from "../Edge"; +import Caption from "../Caption"; describe("Graph Component", () => { const data = { id: "someid", nodes: { 1: { id: 1, display: { name: "Node 1", scale: 1, status: "normal", x: 260.81190983749696, y: 78.09522392060452 } }, 2: { id: 2, display: { name: "Node 2", scale: 1, status: "normal", x: 1.3981085859366804, y: -5.974907363126558 } }, 3: { id: 3, display: { name: "Node 3", scale: 1, status: "normal", x: -258.01571204120563, y: -90.04497885992393 } } }, edges: { 1: { id: 1, node1_id: 1, node2_id: 2, display: { label: "Edge 1", cx: null, cy: null, scale: 1, arrow: false, status: "normal", x1: 260.81190983749696, y1: 78.09522392060452, x2: 1.3981085859366804, y2: -5.974907363126558, s1: 1, s2: 1 } }, 2: { id: 2, node1_id: 2, node2_id: 3, display: { label: "Edge 1", cx: null, cy: null, scale: 1, arrow: false, status: "normal", x1: 1.3981085859366804, y1: -5.974907363126558, x2: -258.01571204120563, y2: -90.04497885992393, s1: 1, s2: 1 } } }, captions: { 1: { id: 1, display: { text: "Caption 1", x: -458.01571204120563, y: -90.04497885992393 } } } }; it("sould render nodes", () => { - let graph = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(graph); - let nodes = element.querySelectorAll(".node"); + let nodes = wrapper.find(Node); expect(nodes.length).toBe(Object.keys(data.nodes).length); }); it("sould render edges", () => { - let graph = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(graph); - let edges = element.querySelectorAll(".edge"); + let edges = wrapper.find(Edge); expect(edges.length).toBe(Object.keys(data.edges).length); }); it("sould render captions", () => { - let graph = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(graph); - let captions = element.querySelectorAll(".caption"); + let captions = wrapper.find(Caption); expect(captions.length).toBe(Object.keys(data.captions).length); }); diff --git a/app/components/__tests__/Node-test.jsx b/app/components/__tests__/Node-test.jsx index 25ec9f1..3d3f84e 100644 --- a/app/components/__tests__/Node-test.jsx +++ b/app/components/__tests__/Node-test.jsx @@ -1,13 +1,14 @@ -jest.dontMock('../BaseComponent'); -jest.dontMock('../Node'); -jest.dontMock('../NodeLabel'); -jest.dontMock('../NodeCircle'); -jest.dontMock('react-draggable'); +jest.unmock('../BaseComponent'); +jest.unmock('../Node'); +jest.unmock('../NodeLabel'); +jest.unmock('../NodeCircle'); +jest.unmock('react-draggable'); import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -const Node = require('../Node'); +import { shallow } from "enzyme"; +import Node from '../Node'; +import NodeCircle from "../NodeCircle"; +import NodeLabel from "../NodeLabel"; describe("Node Component", () => { @@ -25,58 +26,56 @@ describe("Node Component", () => { }; it("should have an svg transform", () => { - let node = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(node); + let element = wrapper.find("g.node"); let { x, y } = data.display; - expect(element.getAttribute("transform")).toBe(`translate(${x}, ${y})`); + expect(element.props().transform).toBe(`translate(${x}, ${y})`); }); - it("should display a name", () => { - let node = TestUtils.renderIntoDocument( - + it("should display a NodeCircle", () => { + let wrapper = shallow( + ); - let element = ReactDOM.findDOMNode(node); - let text = element.querySelector("text"); + let circle = wrapper.find(NodeCircle); - expect(text.textContent).toBe(data.display.name); + expect(circle.props().node).toBe(data); + expect(circle.props().selected).toBe(true); }); - it("should display an image if provided an image url", () => { - let node = TestUtils.renderIntoDocument( - + it("should display a NodeLabel", () => { + let wrapper = shallow( + ); - let element = ReactDOM.findDOMNode(node); - let image = element.querySelector("img"); // for some reason we have to select "img" instead of "image" + let label = wrapper.find(NodeLabel); - expect(image.getAttribute("xlink:href")).toBe(data.display.image); + expect(label.props().node).toBe(data); }); it("should call click callback if clicked", () => { let clickNode = jest.genMockFunction(); - let node = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(node); + let element = wrapper.find("g.node"); + element.simulate("click"); - TestUtils.Simulate.click(element); - expect(clickNode.mock.calls[0][0]).toBe("someid"); - expect(clickNode.mock.calls[0][1]).toBe(data.id); + expect(clickNode.mock.calls[0][0]).toBe(data.id); }); - // NOT WORKING: EVENT HANDLERS ARENT TRIGGERED FOR SOME REASON + // NOT WORKING: TO FIND .handle WE NEED FULL RENDER, WHICH ISN'T WORKING FOR SVG xit("can be dragged to a new position", () => { let moveNode = jest.genMockFunction(); - let node = TestUtils.renderIntoDocument( + let wrapper = shallow( ); - let element = ReactDOM.findDOMNode(node); - let handle = element.querySelector(".handle"); + let handle = wrapper.find(".handle"); - TestUtils.Simulate.mouseDown(handle); - TestUtils.Simulate.mouseUp(handle); + handle.simulate("dragStart"); + handle.simulate("drag"); + handle.simulate("dragEnd"); expect(moveNode.mock.calls.length).toBe(1); }); diff --git a/app/components/__tests__/UpdateNodeForm-test.jsx b/app/components/__tests__/UpdateNodeForm-test.jsx new file mode 100644 index 0000000..0828637 --- /dev/null +++ b/app/components/__tests__/UpdateNodeForm-test.jsx @@ -0,0 +1,47 @@ +jest.disableAutomock(); + +import React from "react"; +import { mount } from "enzyme"; + +import UpdateNodeForm from "../UpdateNodeForm"; +import ChangeColorInput from "../ChangeColorInput"; + +describe("UpdateNodeForm", () => { + let wrapper; + let updateNode; + let data = { + id: 1, + display: { + name: "Node", + color: "#abc", + status: "highlighted" + } + }; + + beforeEach(() => { + updateNode = jest.genMockFunction(); + wrapper = mount( + + ); + }); + + it("shows color picker with current node color", () => { + let picker = wrapper.find(ChangeColorInput); + + expect(picker.length).toBe(1); + expect(picker.props().value).toBe(data.display.color); + expect(picker.props().status).toBe(data.display.status); + }); + + it("passes new color to updateNode", () => { + let picker = wrapper.find(ChangeColorInput); + let pickerOnChange = picker.props().onChange; + pickerOnChange("#def"); + + expect(updateNode.mock.calls.length).toBe(1); + expect(updateNode.mock.calls[0][1].display.color).toBe("#def"); + }); +}); \ No newline at end of file diff --git a/app/main.jsx b/app/main.jsx index bd40316..6edce8d 100644 --- a/app/main.jsx +++ b/app/main.jsx @@ -27,9 +27,12 @@ class Oligrapher { config = merge({ isEditor: false, - isLocked: true, + isLocked: true, + annotationNodeEditable: false, logActions: false, - viewOnlyHighlighted: true + viewOnlyHighlighted: true, + allowEditNodes: false + }, config); config.height = config.graphHeight || config.root.offsetHeight; @@ -77,11 +80,6 @@ class Oligrapher { this.root.dispatchProps.dispatch(toggleEditTools(value)); } - toggleEditor(value) { - value = typeof value === "undefined" ? !this._currentProps().isEditor : value; - this.update({ isEditor: value }); - } - toggleLocked(value) { value = typeof value === "undefined" ? !this._currentProps().isLocked : value; this.update({ isLocked: value }); @@ -184,7 +182,7 @@ class Oligrapher { } setHighlights(highlights, otherwiseFaded = false) { - this.root.dispatchProps.dispatch(setHighlights(highlights, otherwiseFaded)); + this.root.dispatchProps.dispatch(setHighlights(highlights, otherwiseFaded, showEditTools, allowEditNodes)); return this.root.getWrappedInstance().props.graph; } diff --git a/app/models/Annotation.js b/app/models/Annotation.js index be8b61b..ff50191 100644 --- a/app/models/Annotation.js +++ b/app/models/Annotation.js @@ -16,4 +16,5 @@ export default class Annotation { static setDefaults(annotation) { return merge({}, this.defaults(), annotation); } + } \ No newline at end of file diff --git a/app/models/Graph.js b/app/models/Graph.js index 6417760..1ef72a4 100644 --- a/app/models/Graph.js +++ b/app/models/Graph.js @@ -14,6 +14,7 @@ import compact from 'lodash/array/compact'; import size from 'lodash/collection/size'; import includes from 'lodash/collection/includes'; import isNumber from 'lodash/lang/isNumber'; +import Springy from "springy"; class Graph { static defaults() { @@ -140,22 +141,20 @@ class Graph { } static buildForceLayout(graph) { - return graph; + let gr = new Springy.Graph(); - // let gr = new Springy.Graph(); + let nodeIds = Object.keys(graph.nodes); + let edges = values(graph.edges).map(e => [e.node1_id, e.node2_id]); - // let nodeIds = Object.keys(graph.nodes); - // let edges = values(graph.edges).map(e => [e.node1_id, e.node2_id]); + gr.addNodes(...nodeIds); + gr.addEdges(...edges); - // gr.addNodes(...nodeIds); - // gr.addEdges(...edges); + let stiffness = 200.0; + let repulsion = 300.0; + let damping = 0.5; + let minEnergyThreshold = 0.1; - // let stiffness = 200.0; - // let repulsion = 300.0; - // let damping = 0.5; - // let minEnergyThreshold = 0.1; - - // return new Springy.Layout.ForceDirected(gr, stiffness, repulsion, damping, minEnergyThreshold); + return new Springy.Layout.ForceDirected(gr, stiffness, repulsion, damping, minEnergyThreshold); } static updateEdgePosition(edge, graph) { @@ -470,11 +469,10 @@ class Graph { // HIGHLIGHTING - static setHighlights(graph, highlights, otherwiseFaded = false) { + static setHighlights(graph, highlights, otherwiseFaded = false, showEditTools, allowEditNodes) { if (highlights.nodeIds.length + highlights.edgeIds.length + highlights.captionIds.length == 0) { otherwiseFaded = false; } - let { nodeIds, edgeIds, captionIds } = highlights; let otherwise = otherwiseFaded ? "faded" : "normal"; let newGraph = cloneDeep(graph); @@ -485,15 +483,27 @@ class Graph { captionIds = captionIds.map(id => String(id)); values(newGraph.nodes).forEach(node => { - newGraph.nodes[node.id].display.status = includes(nodeIds, String(node.id)) ? "highlighted" : otherwise; + if (!allowEditNodes){ + newGraph.nodes[node.id].display.status = includes(nodeIds, String(node.id)) ? "highlighted" : otherwise; + } else { + newGraph.nodes[node.id].display.status = includes(nodeIds, String(node.id)) ? "editable" : otherwise; + } }); values(newGraph.edges).forEach(edge => { - newGraph.edges[edge.id].display.status = includes(edgeIds, String(edge.id)) ? "highlighted" : otherwise; + if (!allowEditNodes){ + newGraph.edges[edge.id].display.status = includes(edgeIds, String(edge.id)) ? "highlighted" : otherwise; + } else { + newGraph.edges[edge.id].display.status = includes(edgeIds, String(edge.id)) ? "editable" : otherwise; + } }); values(newGraph.captions).forEach(caption => { - newGraph.captions[caption.id].display.status = includes(captionIds, String(caption.id)) ? "highlighted" : otherwise; + if (!allowEditNodes){ + newGraph.captions[caption.id].display.status = includes(captionIds, String(caption.id)) ? "highlighted" : otherwise; + } else { + newGraph.captions[caption.id].display.status = includes(captionIds, String(caption.id)) ? "editable" : otherwise; + } }); return newGraph; diff --git a/app/models/__tests__/Edge-test.js b/app/models/__tests__/Edge-test.js index 12a2fdf..483969e 100644 --- a/app/models/__tests__/Edge-test.js +++ b/app/models/__tests__/Edge-test.js @@ -1,6 +1,6 @@ -jest.dontMock('../Edge'); +jest.unmock('../Edge'); -const Edge = require('../Edge'); +import Edge from '../Edge'; describe("Edge", () => { diff --git a/app/models/__tests__/Graph-test.js b/app/models/__tests__/Graph-test.js index 6812b5a..68fcb35 100644 --- a/app/models/__tests__/Graph-test.js +++ b/app/models/__tests__/Graph-test.js @@ -1,18 +1,18 @@ -jest.dontMock('../Graph'); -jest.dontMock('../Node'); -jest.dontMock('../Edge'); -jest.dontMock('../Caption'); -jest.dontMock('../Helpers'); -jest.dontMock('springy'); - -const Graph = require('../Graph'); -const Edge = require('../Edge'); -const Caption = require('../Caption'); -const merge = require('lodash/object/merge'); -const values = require('lodash/object/values'); -const uniq = require('lodash/array/uniq'); -const range = require('lodash/utility/range'); -const pick = require('lodash/object/pick'); +jest.unmock('../Graph'); +jest.unmock('../Node'); +jest.unmock('../Edge'); +jest.unmock('../Caption'); +jest.unmock('../Helpers'); +jest.unmock('springy'); + +import Graph from '../Graph'; +import Edge from '../Edge'; +import Caption from '../Caption'; +import merge from 'lodash/object/merge'; +import values from 'lodash/object/values'; +import uniq from 'lodash/array/uniq'; +import range from 'lodash/utility/range'; +import pick from 'lodash/object/pick'; describe("Graph", () => { @@ -150,11 +150,17 @@ describe("Graph", () => { expect(ys).toBeArrayOfNumbers; }); - it("doesn't alter already-positioned cpations", () => { - let graph2 = merge({}, basicGraph, { captions: { 1: { display: { x: -100, y: -100 } } } }); - let graph3 = Graph.prepareCaptions(Graph.prepareLayout(graph2, 'circleLayout')); + it("doesn't alter already-positioned captions", () => { + let graph2 = merge({}, basicGraph, { captions: { 1: { display: { + scale: 2, + status: "highlighted", + x: -100, + y: -100 + } } } }); + let graph3 = Graph.prepareLayout(graph2, 'circleLayout'); + let graph4 = Graph.prepareCaptions(graph3); - expect(graph2.captions[1]).toEqual(graph3.captions[1]); + expect(graph3.captions[1]).toEqual(graph4.captions[1]); }); }); diff --git a/app/models/__tests__/Helpers-test.js b/app/models/__tests__/Helpers-test.js index f95159b..0fdbb7f 100644 --- a/app/models/__tests__/Helpers-test.js +++ b/app/models/__tests__/Helpers-test.js @@ -1,6 +1,6 @@ -jest.dontMock('../Helpers'); +jest.unmock('../Helpers'); -const Helpers = require('../Helpers'); +import Helpers from '../Helpers'; describe("Helpers", () => { diff --git a/app/reducers.js b/app/reducers.js index e2f1d38..3d86557 100644 --- a/app/reducers.js +++ b/app/reducers.js @@ -1,5 +1,5 @@ import { combineReducers } from 'redux'; -import graph from './reducers/graph'; +import graph from './reducers/undoable-graph'; import selection from './reducers/selection'; import zoom from './reducers/zoom'; import editTools from './reducers/editTools'; @@ -8,6 +8,7 @@ import annotations from './reducers/annotations'; import settings from './reducers/settings'; import showHelpScreen from './reducers/showHelpScreen'; import showSettings from './reducers/showSettings'; +import allowEditNodes from './reducers/allowEditNodes'; export default combineReducers({ graph, @@ -18,5 +19,6 @@ export default combineReducers({ annotations, settings, showHelpScreen, - showSettings + showSettings, + allowEditNodes }); \ No newline at end of file diff --git a/app/reducers/__tests__/graphs-test.js b/app/reducers/__tests__/graph-test.js similarity index 66% rename from app/reducers/__tests__/graphs-test.js rename to app/reducers/__tests__/graph-test.js index b501d4d..4fc1699 100644 --- a/app/reducers/__tests__/graphs-test.js +++ b/app/reducers/__tests__/graph-test.js @@ -1,23 +1,23 @@ -jest.dontMock('../graphs'); -jest.dontMock('../../models/Graph'); -jest.dontMock('../../models/Node'); -jest.dontMock('../../models/Edge'); -jest.dontMock('../../models/Helpers'); - -const reducer = require('../graphs'); -const Graph = require('../../models/Graph'); -const Node = require('../../models/Node'); -const Edge = require('../../models/Edge'); -const Helpers = require('../../models/Helpers'); -const merge = require('lodash/object/merge'); -const assign = require('lodash/object/assign'); -const values = require('lodash/object/values'); -const uniq = require('lodash/array/uniq'); -const keys = require('lodash/object/keys'); +jest.unmock("../graph"); +jest.unmock("../../models/Graph"); +jest.unmock("../../models/Node"); +jest.unmock("../../models/Edge"); +jest.unmock("../../models/Helpers"); + +import reducer from "../graph"; +import Graph from '../../models/Graph'; +import Node from '../../models/Node'; +import Edge from '../../models/Edge'; +import Helpers from '../../models/Helpers'; +import merge from 'lodash/object/merge'; +import assign from 'lodash/object/assign'; +import values from 'lodash/object/values'; +import uniq from 'lodash/array/uniq'; +import keys from 'lodash/object/keys'; describe("graph reducer", () => { - const basicGraph = { + const graph = { id: "someid", nodes: { 1: { id: 1, display: { name: "Node 1" } }, @@ -33,30 +33,26 @@ describe("graph reducer", () => { } }; - const graphs = { [basicGraph.id]: basicGraph }; - it("should return the initial state", () => { - expect(reducer(undefined, {})).toEqual({}); + expect(reducer(undefined, {})).toEqual(null); }); describe("ADD_NODE", () => { it("should add a node with only a name", () => { - let newGraph = reducer(graphs, { + let newGraph = reducer(graph, { type: 'ADD_NODE', - graphId: basicGraph.id, node: { display: { name: "Dick Cheney" } } - })[basicGraph.id]; + }); - expect(values(newGraph.nodes).length).toBe(values(basicGraph.nodes).length + 1); + expect(values(newGraph.nodes).length).toBe(values(graph.nodes).length + 1); }); it("should preserve the id of the added node", () => { - let newGraph = reducer(graphs, { + let newGraph = reducer(graph, { type: 'ADD_NODE', - graphId: basicGraph.id, node: { id: "angler", display: { name: "Dick Cheney" } } - })[basicGraph.id]; + }); let newNode = newGraph.nodes["angler"]; expect(newNode.display.name).toBe("Dick Cheney"); @@ -64,11 +60,10 @@ describe("graph reducer", () => { }); it("should set position coordinates for the new node if not provided", () => { - let newGraph = reducer(graphs, { + let newGraph = reducer(graph, { type: 'ADD_NODE', - graphId: basicGraph.id, node: { id: "angler", display: { name: "Dick Cheney" } } - })[basicGraph.id]; + }); let newNode = newGraph.nodes["angler"]; expect(newNode.display.x).toEqual(jasmine.any(Number)); @@ -79,33 +74,30 @@ describe("graph reducer", () => { describe("ADD_EDGE", () => { it("should add an edge with only two node ids and a label", () => { - let newGraph = reducer(graphs, { + let newGraph = reducer(graph, { type: 'ADD_EDGE', - graphId: basicGraph.id, edge: { node1_id: 1, node2_id: 2, display: { label: "best friend" } } - })[basicGraph.id]; + }); - expect(values(newGraph.edges).length).toBe(values(basicGraph.edges).length + 1); + expect(values(newGraph.edges).length).toBe(values(graph.edges).length + 1); }); - it("should preserve the id of the added node", () => { - let newGraph = reducer(graphs, { + it("should preserve the id of the added edge", () => { + let newGraph = reducer(graph, { type: 'ADD_EDGE', - graphId: basicGraph.id, edge: { id: "someid", node1_id: 1, node2_id: 2, display: { label: "best friend" } } - })[basicGraph.id]; + }); let newEdge = newGraph.edges["someid"]; expect(newEdge.display.label).toBe("best friend"); }); - it("should give endpoint coordinates to the added node", () => { - let graph = Graph.prepare(basicGraph); - let newGraph = reducer(graphs, { + it("should give endpoint coordinates to the added edge", () => { + let preparedGraph = Graph.prepare(graph); + let newGraph = reducer(preparedGraph, { type: 'ADD_EDGE', - graphId: basicGraph.id, edge: { id: "someid", node1_id: 1, node2_id: 2, display: { label: "best friend" } } - })[basicGraph.id]; + }); let node1 = newGraph.nodes[1]; let node2 = newGraph.nodes[2]; @@ -123,20 +115,19 @@ describe("graph reducer", () => { const nodeId = 2; const action = { type: 'DELETE_NODE', - graphId: basicGraph.id, nodeId: nodeId }; it("should remove the node from the graph", () => { - let newGraph = reducer(graphs, action)[basicGraph.id]; + let newGraph = reducer(graph, action); - expect(values(newGraph.nodes).length).toBe(values(basicGraph.nodes).length - 1); + expect(values(newGraph.nodes).length).toBe(values(graph.nodes).length - 1); expect(newGraph.nodes[nodeId]).toBeUndefined(); }); it("should remove all of the node's edges", () => { - let newGraph = reducer(graphs, action)[basicGraph.id]; - let edges = Graph.edgesConnectedToNode(basicGraph, nodeId); + let newGraph = reducer(graph, action); + let edges = Graph.edgesConnectedToNode(graph, nodeId); let newEdges = edges.map(edge => newGraph.edges[edge.id]); expect(uniq(newEdges)).toEqual([undefined]); @@ -148,21 +139,20 @@ describe("graph reducer", () => { const edgeId = 2; const action = { type: 'DELETE_EDGE', - graphId: basicGraph.id, edgeId: edgeId }; it("should remove the edge from the graph", () => { - let newGraph = reducer(graphs, action)[basicGraph.id]; + let newGraph = reducer(graph, action); - expect(values(newGraph.edges).length).toBe(values(basicGraph.edges).length - 1); + expect(values(newGraph.edges).length).toBe(values(graph.edges).length - 1); expect(newGraph.edges[edgeId]).toBeUndefined(); }); it("should not remove any nodes from the graph", () => { - let newGraph = reducer(graphs, action)[basicGraph.id]; + let newGraph = reducer(graph, action); - expect(values(newGraph.nodes).length).toBe(values(basicGraph.nodes).length); + expect(values(newGraph.nodes).length).toBe(values(graph.nodes).length); }); }); @@ -175,12 +165,11 @@ describe("graph reducer", () => { }; const action = { type: 'DELETE_SELECTION', - graphId: basicGraph.id, selection: selection }; it("should delete the selection and any edges connected to it", () => { - let newGraph = reducer(graphs, action)[basicGraph.id]; + let newGraph = reducer(graph, action); expect(keys(newGraph.nodes)).toEqual(['2', '3']); expect(keys(newGraph.edges)).toEqual([]); @@ -201,14 +190,13 @@ describe("graph reducer", () => { }; const action = { type: 'UPDATE_NODE', - graphId: basicGraph.id, nodeId: nodeId, data: update }; let newGraph; beforeEach(() => { - newGraph = reducer(graphs, action)[basicGraph.id]; + newGraph = reducer(graph, action); }); it("should update the node's display and data objects with new data", () => { @@ -224,8 +212,7 @@ describe("graph reducer", () => { it("should remove data when provided null values", () => { let update2 = { display: { image: null } }; let action2 = assign({}, action, { data: update2 }); - let graphs2 = assign({}, graphs, { [basicGraph.id]: newGraph }); - let newGraph2 = reducer(graphs2, action2)[basicGraph.id]; + let newGraph2 = reducer(newGraph, action2); expect(newGraph2.nodes[nodeId].display.image).toBeUndefined(); }) @@ -233,8 +220,7 @@ describe("graph reducer", () => { it("should revert to default values when required fields are removed", () => { let update2 = { display: { scale: null } }; let action2 = assign({}, action, { data: update2 }); - let graphs2 = assign({}, graphs, { [basicGraph.id]: newGraph }); - let newGraph2 = reducer(graphs2, action2)[basicGraph.id]; + let newGraph2 = reducer(newGraph, action2); let defaults = Node.defaults(); expect(newGraph2.nodes[nodeId].display.scale).toBe(defaults.display.scale); @@ -255,14 +241,13 @@ describe("graph reducer", () => { }; const action = { type: 'UPDATE_EDGE', - graphId: basicGraph.id, edgeId: edgeId, data: update }; let newGraph; beforeEach(() => { - newGraph = reducer(graphs, action)[basicGraph.id]; + newGraph = reducer(graph, action); }); it("should update the edge's display and data objects with new data", () => { @@ -279,8 +264,7 @@ describe("graph reducer", () => { it("should remove data when provided null values", () => { let update2 = { display: { url: null } }; let action2 = assign({}, action, { data: update2 }); - let graphs2 = assign({}, graphs, { [basicGraph.id]: newGraph }); - let newGraph2 = reducer(graphs2, action2)[basicGraph.id]; + let newGraph2 = reducer(newGraph, action2); expect(newGraph2.edges[edgeId].display.url).toBeUndefined(); }) @@ -288,8 +272,7 @@ describe("graph reducer", () => { it("should revert to default values when required fields are removed", () => { let update2 = { display: { scale: null } }; let action2 = assign({}, action, { data: update2 }); - let graphs2 = assign({}, graphs, { [basicGraph.id]: newGraph }); - let newGraph2 = reducer(graphs2, action2)[basicGraph.id]; + let newGraph2 = reducer(newGraph, action2); let defaults = Edge.defaults(); expect(newGraph2.edges[edgeId].display.scale).toBe(defaults.display.scale); @@ -300,21 +283,18 @@ describe("graph reducer", () => { const deleteAction = { type: 'DELETE_EDGE', - graphId: basicGraph.id, edgeId: 1 }; const pruneAction = { type: 'PRUNE_GRAPH', - graphId: basicGraph.id }; - let graphs2, graphs3, graph3; + let graph2, graph3; beforeEach(() => { - graphs2 = reducer(graphs, deleteAction); - graphs3 = reducer(graphs2, pruneAction); - graph3 = graphs3[basicGraph.id]; + graph2 = reducer(graph, deleteAction); + graph3 = reducer(graph2, pruneAction); }); it("should remove all unconnected nodes", () => { @@ -322,8 +302,4 @@ describe("graph reducer", () => { expect(graph3.nodes[2]).not.toBeUndefined(); }); }); - - describe("LAYOUT_CIRCLE", () => { - - }); }); diff --git a/app/reducers/allowEditNodes.js b/app/reducers/allowEditNodes.js new file mode 100644 index 0000000..415380b --- /dev/null +++ b/app/reducers/allowEditNodes.js @@ -0,0 +1,12 @@ +import { TOGGLE_NODE_SELECTABLE } from '../actions'; + +export default function allowEditNodes(state = false, action) { + switch (action.type) { + + case TOGGLE_NODE_SELECTABLE: + return typeof action.value == "undefined" ? !state : action.value; + + default: + return state; + } +}; \ No newline at end of file diff --git a/app/reducers/graph.js b/app/reducers/graph.js index c9ec21c..3b7d59e 100644 --- a/app/reducers/graph.js +++ b/app/reducers/graph.js @@ -9,9 +9,8 @@ import { LOAD_GRAPH, SHOW_GRAPH, NEW_GRAPH, SET_HIGHLIGHTS, TOGGLE_EDIT_TOOLS } from '../actions'; import Graph from '../models/Graph'; import Edge from '../models/Edge'; -import undoable, { excludeAction, distinctState } from 'redux-undo'; -function graph(state = null, action) { +export default function graph(state = null, action) { let newState, graph; switch (action.type) { @@ -97,16 +96,9 @@ function graph(state = null, action) { return Graph.prepareEdges(Graph.circleLayout(state, true)); case SET_HIGHLIGHTS: - return Graph.setHighlights(state, action.highlights, action.otherwiseFaded); + return Graph.setHighlights(state, action.highlights, action.otherwiseFaded, showEditTools, allowEditNodes); default: return state; } -} - -// export default undoable(graphs, { filter: excludeAction([LOAD_GRAPH, SHOW_GRAPH, TOGGLE_EDIT_TOOLS]) }); -// export default undoable(graphs, { filter: distinctState() }); -export default undoable(graph, { filter: function filterState(action, currentState, previousState) { - // only add to history if not initializing graph and state changed - return ([LOAD_GRAPH, SHOW_GRAPH].indexOf(action.type) === -1) && (currentState !== previousState); -} }); \ No newline at end of file +} \ No newline at end of file diff --git a/app/reducers/showSettings.js b/app/reducers/showSettings.js index 5b4ad7b..b536a0b 100644 --- a/app/reducers/showSettings.js +++ b/app/reducers/showSettings.js @@ -4,6 +4,7 @@ export default function showSettings(state = false, action) { switch (action.type) { case TOGGLE_SETTINGS: + console.log(action, state); return typeof action.value == "undefined" ? !state : action.value; default: diff --git a/app/reducers/undoable-graph.js b/app/reducers/undoable-graph.js new file mode 100644 index 0000000..41ab2fd --- /dev/null +++ b/app/reducers/undoable-graph.js @@ -0,0 +1,8 @@ +import graph from "./graph"; +import undoable, { excludeAction, distinctState } from 'redux-undo'; +import { LOAD_GRAPH, SHOW_GRAPH } from "../actions"; + +export default undoable(graph, { filter: function filterState(action, currentState, previousState) { + // only add to history if not initializing graph and state changed + return ([LOAD_GRAPH, SHOW_GRAPH].indexOf(action.type) === -1) && (currentState !== previousState); +} }); \ No newline at end of file diff --git a/app/styles/oligrapher.annotations.css b/app/styles/oligrapher.annotations.css index baffafc..03c5907 100644 --- a/app/styles/oligrapher.annotations.css +++ b/app/styles/oligrapher.annotations.css @@ -2,13 +2,6 @@ position: relative; } -#oligrapherGraphContainer { - position: relative; -} - -#oligrapherHeader { - margin-bottom: 10px; -} #oligrapherTitle { font-size: 50px; @@ -76,16 +69,21 @@ margin-bottom: 10px; } +#oligrapherEditorContainer{ + position: absolute; + top: 0px; +} + #oligrapherEditButton.editContentMode { background-color: rgba(0, 255, 0, 0.5); } #oligrapherEditButton.editAnnotationsMode { - background-color: rgba(255, 255, 0, 0.5); + background-color: #fff; } #oligrapherSaveButton { - position: fixed; + position: absolute; bottom: 20px; left: 20px; background-color: #f8f8f8; @@ -93,12 +91,12 @@ #oligrapherShowAnnotations { position: absolute; - top: 5px; + top: 11px; right: 10px; } #oligrapherShowAnnotations button { - font-size: 24px; + font-size: 18px; font-weight: 200; } @@ -108,19 +106,21 @@ #oligrapherNavButtons { margin-top: 5px; - margin-bottom: 25px; + margin-bottom: 5px; + flex: 0 1 10%; +} + +#oligrapherNavButtons button:first-child{ + margin-right:10px; } #oligrapherNavButtons button { - margin-right: 10px; font-size: 24px; font-weight: 200; - border: 0 solid #eee; - background-color: #eee; + &:disabled { color: #ccc; - background-color: #f8f8f8; opacity: 1; } } @@ -132,6 +132,18 @@ background-color: #fff; } +#oligrapherHideAnnotationsButton{ + margin-top: -6px; + font-size: 18px; + font-weight: 200; +} + +.annotationTitle{ + font-weight: 400; + padding-left: 1px; +} + + .clickplz:enabled:not(:focus) { animation: clickplz 3s; -webkit-animation: clickplz 3s; @@ -140,15 +152,27 @@ } @keyframes clickplz { - 0% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); } - 50% { color: #666; background-color:#ffd; text-shadow: 0 0 9px rgba(255,255,128,0.75); } - 100% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); } + 0% { color: #666; background-color:#fff; text-shadow: 0 0 9px rgba(255,255,255,0); } + 50% { color: #666; background-color:#ff9; text-shadow: 0 0 9px rgba(255,255,128,0.75); } + 100% { color: #666; background-color:#fff; text-shadow: 0 0 9px rgba(255,255,255,0); } } @-webkit-keyframes clickplz { - 0% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); } - 50% { color: #666; background-color:#ffd; text-shadow: 0 0 9px rgba(255,255,128,0.75); } - 100% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); } + 0% { color: #666; background-color:#fff; text-shadow: 0 0 9px rgba(255,255,255,0); } + 50% { color: #666; background-color:#ff9; text-shadow: 0 0 9px rgba(255,255,128,0.75); } + 100% { color: #666; background-color:#fff; text-shadow: 0 0 9px rgba(255,255,255,0); } +} + +#oligrapherGraphAnnotations{ + height:100%; + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + flex-direction: column; + background-color: #f6f6f6; + padding-top: 11px; } #oligrapherAnnotationListItems { @@ -165,54 +189,124 @@ } #oligrapherAnnotationListItems li { - font-size: 18px; - line-height: 34px; - padding-left: 10px; - padding-right: 10px; - margin-left: -10px; - border-bottom: 1px solid #eee; + font-size: 17px; + border-bottom: 1px solid #ccc; cursor: pointer; width: 100%; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; border-radius: 3px; + overflow: hidden; + position: relative; +} + +#oligrapherToggleAnnotationButton{ + padding: 10px 0px; +} + +#oligrapherAnnotationListItems li.active .annotationHeaderWrapper{ + overflow: visible; + white-space: initial; } #oligrapherAnnotationListItems li.active { - background-color: #eee; + padding-bottom: 10px; } -#oligrapherAnnotationList button { - margin-top: 0px; - margin-bottom: 5px; + +#oligrapherAnnotationListItems li div.glyphicon{ + top: 7px; + width: 13px; + text-align: center; + position: absolute; + right: 0px; + font-size: 14px; + right: 5px; } -#oligrapherGraphAnnotationEditButton { - margin-top: 10px; +#oligrapherAnnotationListItems li div.glyphicon-edit { + right: 23px; } -#oligrapherGraphAnnotationForm { - margin-top: 10px; +#oligrapherAnnotationListItems li.editableAnnotation div.glyphicon-edit{ + color: #FF9800; } -#oligrapherGraphAnnotationForm textarea { - margin-bottom: 10px; +#oligrapherAnnotationListItems li div.glyphicon:hover{ + color: #FF9800; } -#oligrapherGraphAnnotationFormText, #oligrapherGraphAnnotationFormHeader { - /* height: 400px; */ - padding: 10px; - padding-left: 16px; - padding-right: 16px; - border: 1px solid #eee; - border-radius: 6px; +#oligrapherAnnotationListItems .editableAnnotation div:not(.glyphicon){ + background-color: #fff; + border: 1px solid #bbb; + cursor: text; +} + +#oligrapherAnnotationListItems li .annotationHeaderWrapper{ + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + margin: 7px 35px 5px 8px; + font-weight: 500; + line-height: initial; +} + +#oligrapherAnnotationListItems:not(.annotationListPresentationMode) li .annotationHeaderWrapper{ + width: calc(100% - 50px); +} + +#oligrapherAnnotationListItems.annotationListPresentationMode li .annotationHeaderWrapper{ + width: calc(100% - 15px); +} + +#oligrapherAnnotationListItems li:not(.active) .annotationBodyWrapper{ + height: 0; +} + +.annotationBodyWrapper{ + width: calc(100% - 15px); + white-space: initial; + line-height: initial; + font-size: 0.9em; + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; + margin: 0 0px 0px 8px; +} + +.newAnnotationText{ + padding-left: 5px; + float: right; +} + +.newAnnotationText::after{ + content: "New Annotation"; +} + +#oligrapherAnnotationListItems li.active { + background-color: #DCDCDC; +} + +#oligrapherAnnotationList { + -webkit-box-flex: 1 1 auto; + -moz-box-flex: 1 1 auto; + flex: 1 1 auto; + overflow-y: scroll; +} + +#oligrapherAnnotationList button { + margin-top: 0px; + margin-bottom: 5px; +} + +#oligrapherCreateGraphAnnotationButton{ + float: right; } -#oligrapherGraphAnnotationFormText { - margin-bottom: 20px; +#oligrapherGraphAnnotationEditButton { + margin-top: 10px; } + #oligrapherSettingsForm { position: absolute; display: inline-block; @@ -532,4 +626,10 @@ color: #fff; } .medium-editor-placeholder:after { - color: #b3b3b1; } \ No newline at end of file + color: #b3b3b1; } + +@media (max-width: 1350px) { + .newAnnotationText::after{ + content: ""; + } +} \ No newline at end of file diff --git a/app/styles/oligrapher.css b/app/styles/oligrapher.css index 0b1ddc5..3fd8a7c 100644 --- a/app/styles/oligrapher.css +++ b/app/styles/oligrapher.css @@ -1,7 +1,18 @@ +html, body{ + height:100%; + padding: 0px; +} + #oligrapherContainer:focus, #oligrapherContainer :focus { outline: none; } +#oligrapher{ + position: relative; + width: 100%; + height:100%; +} + a.nodeLabel { cursor: pointer; } @@ -39,15 +50,24 @@ a.nodeLabel text tspan { #oligrapherContainer { position: relative; + height: 100%; + position: relative; } #oligrapherGraphContainer { - position: relative; border: 1px solid #f8f8f8; + height: calc(100% - 80px); + position: relative; +} + +#oligrapherGraphCol{ + height: calc(100% - 15px); + padding-left: 15px; + padding-top: 15px; } #oligrapherHeader { - margin-bottom: 10px; + height: 80px; } #oligrapherTitle { @@ -80,6 +100,16 @@ a.nodeLabel text tspan { color: #888; } +#oligrapherWrapper{ + height: 100%; + width: 100%; + margin:0px; +} + +#hotKeyWrapper{ + height: 100%; +} + #oligrapherGraphLinks { margin-left: 2px; } diff --git a/app/styles/oligrapher.editor.css b/app/styles/oligrapher.editor.css index 018e7f8..6d2ee0b 100755 --- a/app/styles/oligrapher.editor.css +++ b/app/styles/oligrapher.editor.css @@ -6,10 +6,15 @@ background-color: #f8f8f8; } -#oligrapherEditorContainer button:not(#nodeColorInputClearer) { +#oligrapherEditorContainer button:not(.nodeColorInputClearer) { background-color: #f8f8f8; } +#oligrapherEditorContainer{ + pointer-events: none; + width: 100%; +} + #zoomButtons { position: absolute; top: 15px; @@ -160,27 +165,29 @@ button#toggleEditTools { float: right; } -#nodeColorInput { - width: 50px; - padding: 1px 4px; - cursor: pointer; +.nodeColorInputWrapper { + position: relative; + display: inline-block; } -#nodeColorPickerWrapper{ - position: absolute; - right: 0px; +.nodeColorInputWrapper .nodeColorInput { + width: 50px; + padding: 4px 8px; + cursor: pointer; + position: relative; + display: inline-block; } -#color{ - width: 22px; - height: 22px; - border-radius: 22px; - margin-top: -1px; - margin-left: -5px; - cursor: pointer; +.nodeColorInputSwatch { + width: 22px; + height: 22px; + border-radius: 22px; + margin-top: -1px; + margin-left: -5px; + cursor: pointer; } -#nodeColorInputClearer{ +.nodeColorInputClearer { background: none; border: none; position: absolute; @@ -189,19 +196,11 @@ button#toggleEditTools { top: 0px; } -#nodeColorInputWrapper{ - position: relative; - display: inline-block; +.nodeColorPickerWrapper { + position: absolute; + right: 0px; } #edgeUrlInput { width: 337px; -} - -#swatch{ - width: 50px; - position: relative; - display: inline-block; -} - - +} \ No newline at end of file diff --git a/build/dev.html b/build/dev.html index bff98e4..0ed13d7 100644 --- a/build/dev.html +++ b/build/dev.html @@ -8,23 +8,18 @@ -
    - +