Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨Add Component's Description Tooltip #151

Merged
merged 11 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
"react-sanity-pagination": "^2.0.2",
"react-switch": "^6.0.0",
"react-textarea-autosize": "^8.3.3",
"react-toggle": "^4.1.2"
"react-toggle": "^4.1.2",
"react-tooltip": "^4.2.21"
},
"devDependencies": {
"@babel/core": "^7.12.10",
Expand Down
8 changes: 5 additions & 3 deletions src/components/CustomNodeFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import { CustomNodeModel } from './CustomNodeModel';
import {AbstractReactFactory, GenerateModelEvent, GenerateWidgetEvent} from '@projectstorm/react-canvas-core';
import { DiagramEngine } from '@projectstorm/react-diagrams-core';
import {CustomNodeWidget} from "./CustomNodeWidget";
import { JupyterFrontEnd } from '@jupyterlab/application';
import { ILabShell, JupyterFrontEnd } from '@jupyterlab/application';

export class CustomNodeFactory extends AbstractReactFactory<CustomNodeModel, DiagramEngine> {
app : JupyterFrontEnd
constructor(app) {
shell : ILabShell
constructor(app, shell) {
super('custom-node');
this.app = app;
this.shell = shell;
}

generateModel(initialConfig: GenerateModelEvent) {
return new CustomNodeModel();
}

generateReactWidget(event: GenerateWidgetEvent<any>): JSX.Element {
return <CustomNodeWidget engine={this.engine as DiagramEngine} node={event.model} app={this.app}/>;
return <CustomNodeWidget engine={this.engine as DiagramEngine} node={event.model} app={this.app} shell={this.shell}/>;
}
}
227 changes: 173 additions & 54 deletions src/components/CustomNodeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import ToolTip from 'react-portal-tooltip';
import { Pagination } from "krc-pagination";
import 'krc-pagination/styles.css';
import Toggle from 'react-toggle'
import { JupyterFrontEnd } from '@jupyterlab/application';
import { ILabShell, JupyterFrontEnd } from '@jupyterlab/application';
import { commandIDs } from './xircuitBodyWidget';
import { CustomPortLabel } from './port/CustomPortLabel';
import TextareaAutosize from 'react-textarea-autosize';
import { Dialog } from '@jupyterlab/apputils';
import { formDialogWidget } from '../dialog/formDialogwidget';
import { showFormDialog } from '../dialog/FormDialog';
import { CommentDialog } from '../dialog/CommentDialog';
import ReactTooltip from 'react-tooltip';

var S;
(function (S) {
Expand Down Expand Up @@ -53,6 +54,14 @@ var S;
font-size: 12px;
border: solid 2px ${(p) => p.selected ? 'rgb(0,192,255)':'black'};
padding: 5px;
`;

S.DescriptionName = styled.div<{ color:string }>`
color: ${(p) => p.color ?? 'rgb(0, 0, 0)'};
text-align: justify;
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 13px;
`;

S.Ports = styled.div`
Expand Down Expand Up @@ -83,6 +92,7 @@ export interface DefaultNodeProps {
node: DefaultNodeModel;
engine: DiagramEngine;
app: JupyterFrontEnd;
shell : ILabShell;
}

/**
Expand All @@ -100,6 +110,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
isTooltipActive: false,
nodeDeletable: false,
commentInput: this.props.node['extras']['commentInput'],
showDescription: false,

imageGalleryItems:[
{
Expand Down Expand Up @@ -188,36 +199,169 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
this.handleOnChangeCanvas();
}

render() {
/**
* Show/Hide Component's Description Tooltip
*/
async handleDescription() {
await this.setState({ showDescription: !this.state.showDescription });
ReactTooltip.show(this.element as Element)
}

if(this.props.node.getOptions().extras["tip"]!=undefined&&this.props.node.getOptions().extras["tip"]!=""){
// Hide Error Tooltip
hideErrorTooltip(){
delete this.props.node.getOptions().extras["tip"];
this.props.node.getOptions().extras["borderColor"]="rgb(0,192,255)";
}

render() {
if (this.props.node.getOptions()["name"] !== 'Start' && this.props.node.getOptions()["name"] !== 'Finish') {
return (
<S.Node
onMouseEnter={this.showTooltip.bind(this)}
onMouseLeave={this.hideTooltip.bind(this)}
ref={(element) => { this.element = element }}
borderColor={this.props.node.getOptions().extras["borderColor"]}
data-default-node-name={this.props.node.getOptions().name}
selected={this.props.node.isSelected()}
background={this.props.node.getOptions().color}>
<ToolTip active={this.state.isTooltipActive} position="top" arrow="center" parent={this.element}>
<p>{this.props.node.getOptions().extras["tip"]}</p>
</ToolTip>
<S.Title>
<S.TitleName>{this.props.node.getOptions().name}</S.TitleName>
<label>
<Toggle
className='lock'
checked={this.props.node.isLocked()}
onChange={this.handleDeletableNode.bind(this, 'nodeDeletable')}
/>
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(this.props.node.getInPorts(), this.generatePort)}</S.PortsContainer>
<S.PortsContainer>{_.map(this.props.node.getOutPorts(), this.generatePort)}</S.PortsContainer>
</S.Ports>
</S.Node>
<>
<S.Node
onMouseEnter={this.showTooltip.bind(this)}
onMouseLeave={this.hideTooltip.bind(this)}
ref={(element) => { this.element = element }}
data-tip data-for={this.props.node.getOptions().id} // Data for tooltip
borderColor={this.props.node.getOptions().extras["borderColor"]}
data-default-node-name={this.props.node.getOptions().name}
selected={this.props.node.isSelected()}
background={this.props.node.getOptions().color}
onDoubleClick={this.handleEditLiteral.bind(this)}>
<S.Title>
<S.TitleName>{this.props.node.getOptions().name}</S.TitleName>
<label>
<Toggle
className='lock'
checked={this.props.node.isLocked()}
onChange={this.handleDeletableNode.bind(this, 'nodeDeletable')}
/>
<Toggle
className='description'
name='Description'
checked={this.state.showDescription}
onChange={this.handleDescription.bind(this)}
/>
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(this.props.node.getInPorts(), this.generatePort)}</S.PortsContainer>
<S.PortsContainer>{_.map(this.props.node.getOutPorts(), this.generatePort)}</S.PortsContainer>
</S.Ports>
</S.Node>
{/** Description Tooltip */}
{this.state.showDescription && <ReactTooltip
id={this.props.node.getOptions().id}
className='description-tooltip'
arrowColor='rgb(255, 255, 255)'
clickable
afterShow={() => { this.setState({ showDescription: true }) }}
afterHide={() => { this.setState({ showDescription: false }) }}
delayHide={60000}
delayUpdate={5000}
getContent={() =>
<div data-no-drag style={{ cursor: 'default' }}>
<button
type="button"
className="close"
data-dismiss="modal"
aria-label="Close"
onClick={() => { this.setState({ showDescription: false }); }}>
<span aria-hidden="true">&times;</span>
</button>
<S.DescriptionName color={this.props.node.getOptions().color}>{this.props.node.getOptions()["name"]}</S.DescriptionName>
<p className='description-title'>Description:</p>
<div className='description-container'>
<pre className='description-text'>{this.props.node['extras']['description'] ?? <i>No description provided</i>}</pre>
</div>
</div>}
overridePosition={(
{ left, top },
currentEvent, currentTarget, node, refNode) => {
const currentNode = this.props.node;
const nodeDimension = { x: currentNode.width, y: currentNode.height };
const nodePosition = { x: currentNode.getX(), y: currentNode.getY() };
let newPositionX = nodePosition.x;
let newPositionY = nodePosition.y;
let offset = 0;

if (!this.props.shell.leftCollapsed) {
// Some weird offset happened when left sidebar opened, need to add this
let leftSidebar = document.getElementById('jp-left-stack');
offset = leftSidebar.clientWidth + 2;
}

if (refNode == 'top') {
newPositionX = newPositionX - 208 + offset + (nodeDimension.x / 2);
newPositionY = newPositionY - 220;
}
else if (refNode == 'bottom') {
newPositionX = newPositionX - 208 + offset + (nodeDimension.x / 2);
newPositionY = newPositionY + 85 + nodeDimension.y;
}
else if (refNode == 'right') {
newPositionX = newPositionX + 40 + offset + nodeDimension.x;
newPositionY = newPositionY - 66 + (nodeDimension.y / 2);
}
else if (refNode == 'left') {
newPositionX = newPositionX - 450 + offset;
newPositionY = newPositionY - 66 + (nodeDimension.y / 2);
}
const tooltipPosition = this.props.engine.getRelativePoint(newPositionX, newPositionY);

left = tooltipPosition.x;
top = tooltipPosition.y;
return { top, left }
}}
/>}
{/** Error Tooltip */}
{(this.props.node.getOptions().extras["tip"] != undefined && this.props.node.getOptions().extras["tip"] != "") ?
<ReactTooltip
id={this.props.node.getOptions().id}
clickable
place='bottom'
className='error-tooltip'
arrowColor='rgba(255, 0, 0, .9)'
delayHide={100}
delayUpdate={50}
getContent={() =>
<div data-no-drag className='error-container'>
<p className='error-text'>{this.props.node.getOptions().extras["tip"]}</p>
<button
type="button"
className="close"
data-dismiss="modal"
aria-label="Close"
onClick={this.hideErrorTooltip.bind(this)}>
<span aria-hidden="true">&times;</span>
</button>
</div>
}
overridePosition={({ left, top }) => {
const currentNode = this.props.node;
const nodeDimension = { x: currentNode.width, y: currentNode.height };
const nodePosition = { x: currentNode.getX(), y: currentNode.getY() };
let newPositionX = nodePosition.x;
let newPositionY = nodePosition.y;
let offset = 0;

if (!this.props.shell.leftCollapsed) {
// Some weird offset happened when left sidebar opened, need to add this
let leftSidebar = document.getElementById('jp-left-stack');
offset = leftSidebar.clientWidth + 2;
}

newPositionX = newPositionX - 110 + offset + (nodeDimension.x / 2);
newPositionY = newPositionY + 90 + nodeDimension.y;

const tooltipPosition = this.props.engine.getRelativePoint(newPositionX, newPositionY);

left = tooltipPosition.x;
top = tooltipPosition.y;
return { top, left }
}}
/>
: null}
</>
);
}
else if(this.props.node.getOptions().extras["imageGalleryItems"] != undefined){
Expand Down Expand Up @@ -283,31 +427,6 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
</S.CommentContainer>
);
}
else if(this.props.node.getOptions()["name"] !== 'Start' && this.props.node.getOptions()["name"] !== 'Finish'){
return (
<S.Node
borderColor={this.props.node.getOptions().extras["borderColor"]}
data-default-node-name={this.props.node.getOptions().name}
selected={this.props.node.isSelected()}
background={this.props.node.getOptions().color}
onDoubleClick={this.handleEditLiteral.bind(this)}>
<S.Title>
<S.TitleName>{this.props.node.getOptions().name}</S.TitleName>
<label>
<Toggle
className='lock'
checked={this.props.node.isLocked()}
onChange={this.handleDeletableNode.bind(this, 'nodeDeletable')}
/>
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(this.props.node.getInPorts(), this.generatePort)}</S.PortsContainer>
<S.PortsContainer>{_.map(this.props.node.getOutPorts(), this.generatePort)}</S.PortsContainer>
</S.Ports>
</S.Node>
);
}
return (
<S.Node
borderColor={this.props.node.getOptions().extras["borderColor"]}
Expand Down
6 changes: 3 additions & 3 deletions src/components/XircuitsApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CustomNodeFactory } from "./CustomNodeFactory";
import { CustomNodeModel } from './CustomNodeModel';
import { ZoomCanvasAction } from '@projectstorm/react-canvas-core';
import { CustomActionEvent } from '../commands/CustomActionEvent';
import { JupyterFrontEnd } from '@jupyterlab/application';
import { ILabShell, JupyterFrontEnd } from '@jupyterlab/application';
import { CustomDiagramState } from './state/CustomDiagramState'
import { CustomLinkModel, TriangleLinkModel } from './link/CustomLinkModel';
import { CustomLinkFactory, TriangleLinkFactory } from './link/CustomLinkFactory';
Expand All @@ -14,11 +14,11 @@ export class XircuitsApplication {

protected diagramEngine: SRD.DiagramEngine;

constructor(app: JupyterFrontEnd) {
constructor(app: JupyterFrontEnd, shell: ILabShell) {

this.diagramEngine = SRD.default({ registerDefaultZoomCanvasAction: false, registerDefaultDeleteItemsAction: false });
this.activeModel = new SRD.DiagramModel();
this.diagramEngine.getNodeFactories().registerFactory(new CustomNodeFactory(app));
this.diagramEngine.getNodeFactories().registerFactory(new CustomNodeFactory(app, shell));
this.diagramEngine.getLinkFactories().registerFactory(new CustomLinkFactory());
this.diagramEngine.getLinkFactories().registerFactory(new TriangleLinkFactory());
this.diagramEngine.getActionEventBus().registerAction(new ZoomCanvasAction({ inverseZoom: true }))
Expand Down
5 changes: 3 additions & 2 deletions src/tray_library/AdvanceComponentLib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export function AdvancedComponentLibrary(props: AdvancedComponentLibraryProps) {
color: nodeData.color,
extras: {
"type": nodeData.type,
"path": nodeData.file_path
"path": nodeData.file_path,
"description": nodeData.docstring
}
});
node.addInPortEnhance('▶', 'in-0');
Expand All @@ -42,7 +43,7 @@ export function AdvancedComponentLibrary(props: AdvancedComponentLibraryProps) {
"str": "string"
}

props.model["variables"].forEach(variable => {
nodeData["variables"].forEach(variable => {
let name = variable["name"];
let type = type_name_remappings[variable["type"]] || variable["type"];

Expand Down
6 changes: 4 additions & 2 deletions src/tray_library/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ export default function Sidebar(props: SidebarProps) {
type: componentVal.type,
name: componentVal.task,
color: componentVal.color,
path: componentVal.file_path
path: componentVal.file_path,
docstring: componentVal.docstring
}}
name={componentVal.task}
color={componentVal.color}
Expand Down Expand Up @@ -251,7 +252,8 @@ export default function Sidebar(props: SidebarProps) {
type: val.type,
name: val.task,
color: val.color,
path: val.file_path
path: val.file_path,
docstring: val.docstring
}}
name={val.task}
color={val.color}
Expand Down
Loading