Skip to content

Instantly share code, notes, and snippets.

@plugn
Last active February 14, 2024 09:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save plugn/622702fb15b37ffabea8b777e02e2ade to your computer and use it in GitHub Desktop.
Save plugn/622702fb15b37ffabea8b777e02e2ade to your computer and use it in GitHub Desktop.
JointJS: Comment Mode

JointJS: Comment Mode

Do you want to enable a special mode where users can only post comments? They could only insert or delete comments, but not interfere with the structure of the diagram. Take a look at this code pen.

A Pen by Max Dolgov on CodePen.

License.

<div id="app">
<div id="paper-container"></div>
</div>
<div id="toolbar-container"></div>
<a target="_blank" href="https://www.jointjs.com">
<img id="logo" src="https://assets.codepen.io/7589991/JOINT_JS_LOGO_SYMBOL_RGB-pdf.svg" width="200" height="50"></img>
</a>
const {
dia,
ui,
shapes: defaultShapes,
util,
format,
mvc,
linkTools,
elementTools
} = joint;
const backgroundColor = "#ffffff";
// Shapes
class Comment extends joint.dia.Element {
defaults() {
return {
...super.defaults,
type: "Comment",
size: {
width: 120,
height: 160
},
attrs: {
body: {
fill: "#faf293",
stroke: "#333333",
strokeWidth: 2,
strokeLinejoin: "round",
d: "M 20 0 H calc(w) V calc(h) H 0 L 0 20 L 20 0 20 20 0 20 Z"
},
label: {
text: "",
fontSize: 14,
fontFamily: "sans-serif",
fill: "#333333",
textVerticalAnchor: "middle",
textAnchor: "middle",
textWrap: {
width: "calc(w-10)",
height: "calc(h-20)",
ellipsis: true
},
x: "calc(0.5*w)",
y: "calc(0.5*h)"
}
}
};
}
preinitialize() {
this.markup = [
{
tagName: "path",
selector: "body"
},
{
tagName: "text",
selector: "label"
}
];
}
}
class CommentLink extends joint.shapes.standard.Link {
defaults() {
return {
...super.defaults,
type: "CommentLink",
z: -1,
attrs: {
...super.defaults.attrs,
line: {
...super.defaults.attrs.line,
stroke: "#333333",
strokeWidth: 2,
strokeDasharray: "10,5",
targetMarker: null
}
}
};
}
}
const shapes = { ...defaultShapes, Comment, CommentLink };
// Tools
const ResizeTool = elementTools.Control.extend({
children: [
{
tagName: "g",
selector: "handle",
children: [
{
tagName: "circle",
attributes: {
cursor: "pointer",
"stroke-width": 2,
stroke: backgroundColor,
fill: "#33334F",
r: 8
}
},
{
tagName: "path",
selector: "icon",
attributes: {
d: "M -3 -3 3 3 M 3 -2 3 3 -2 3",
stroke: "#ffffff",
"stroke-width": 2,
fill: "none"
}
}
]
},
{
tagName: "rect",
selector: "extras",
attributes: {
"pointer-events": "none",
fill: "none",
stroke: "#33334F",
"stroke-dasharray": "2,4",
rx: 5,
ry: 5
}
}
],
getPosition: function (view) {
const model = view.model;
const { width, height } = model.size();
return { x: width, y: height };
},
setPosition: function (view, coordinates) {
const model = view.model;
model.resize(Math.max(coordinates.x, 50), Math.max(coordinates.y, 50));
}
});
const RemoveTool = elementTools.Remove.extend({
children: [
{
tagName: "circle",
selector: "button",
attributes: {
r: 8,
fill: "#33334F",
stroke: backgroundColor,
"stroke-width": 2,
cursor: "pointer"
}
},
{
tagName: "path",
selector: "icon",
attributes: {
d: "M -3 -3 3 3 M -3 3 3 -3",
fill: "none",
stroke: "#FFFFFF",
"stroke-width": 2,
"pointer-events": "none"
}
}
]
});
// Paper
let showComments = true;
let textEditing = false;
const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({
model: graph,
cellViewNamespace: shapes,
async: true,
sorting: dia.Paper.sorting.APPROX,
defaultConnectionPoint: { name: "boundary" },
background: { color: backgroundColor },
clickThreshold: 10,
preventDefaultBlankAction: false,
interactive: (cellView) => {
if (cellView.model.get("type") === "Comment") {
return true;
}
return false;
},
viewport: (view) => {
if (!view.model) return true;
const type = view.model.get("type");
if (type === "Comment" || type === "CommentLink") {
return showComments;
}
return true;
}
});
const scroller = new ui.PaperScroller({
paper,
autoResizePaper: true,
baseWidth: 100,
baseHeight: 100,
padding: 100,
contentOptions: {
useModelGeometry: true,
padding: 100,
allowNewOrigin: "any"
}
});
document.getElementById("paper-container").appendChild(scroller.el);
const listener = new mvc.Listener();
listener.listenTo(paper, {
"blank:pointerdown": (evt) => {
scroller.startPanning(evt);
paper.removeTools();
},
"element:pointerdblclick": (elementView) => {
const element = elementView.model;
if (element.get("type") === "Comment") {
editText(element, paper);
return;
} else {
const { x, y } = element.getBBox().topRight();
editText(
addComment(elementView, x, y, `${element.attr("label/text")} Notes`),
paper
);
}
},
"link:pointerdblclick": (linkView, evt, x, y) => {
const link = linkView.model;
if (link.get("type") === "CommentLink") {
return;
}
editText(addComment(linkView, x, y), paper);
},
"cell:mouseenter": (cellView) => {
if (textEditing) return;
const cell = cellView.model;
let commentLink;
let comment;
const type = cell.get("type");
if (type === "CommentLink") {
commentLink = cell;
comment = commentLink.getTargetCell();
} else if (type === "Comment") {
comment = cell;
[commentLink] = graph.getConnectedLinks(comment, { inbound: true });
} else {
return;
}
paper.removeTools();
const commentLinkView = commentLink.findView(paper);
commentLinkView.addTools(
new joint.dia.ToolsView({
tools: [
new linkTools.SourceAnchor({
defaultAnchorAttributes: {
"stroke-width": 2,
stroke: backgroundColor,
fill: "#33334F",
r: 7
},
customAnchorAttributes: {
"stroke-width": 2,
stroke: backgroundColor,
fill: "#33334F",
r: 7
}
})
]
})
);
const commentView = comment.findView(paper);
commentView.addTools(
new joint.dia.ToolsView({
tools: [
new RemoveTool({
x: "calc(w)",
y: 0
}),
new ResizeTool()
]
})
);
}
});
// Example
graph.fromJSON({
cells: [
{
id: "r3",
type: "standard.Rectangle",
position: { x: 200, y: 80 },
size: { width: 100, height: 60 },
attrs: {
body: {
rx: 20,
ry: 20,
fill: "#050d6c"
},
label: {
text: "Start",
fill: "#ffffff"
}
}
},
{
id: "p2",
type: "standard.Path",
position: { x: 200, y: 230 },
size: { width: 100, height: 60 },
attrs: {
body: {
d: "M 20 0 H calc(w) L calc(w-20) calc(h) H 0 Z",
fill: "#ced2df"
},
label: {
text: "Input"
}
}
},
{
id: "p1",
type: "standard.Path",
position: { x: 200, y: 400 },
size: { width: 100, height: 100 },
attrs: {
body: {
d:
"M 0 calc(0.5 * h) calc(0.5 * w) 0 calc(w) calc(0.5 * h) calc(0.5 * w) calc(h) Z",
fill: "#e7e8fe"
},
label: {
text: "Decision"
}
}
},
{
id: "r4",
type: "standard.Rectangle",
position: { x: 200, y: 600 },
size: { width: 100, height: 60 },
attrs: {
body: {
fill: "#717cf9"
},
label: {
text: "Process"
}
}
},
{
id: "e1",
type: "standard.Ellipse",
position: { x: 220, y: 750 },
size: { width: 60, height: 60 },
attrs: {
body: {
fill: "#050d6c"
},
label: {
fill: "#ffffff",
text: "End"
}
}
},
{
id: "l1",
type: "standard.Link",
source: { id: "r3" },
target: { id: "p2" }
},
{
id: "l2",
type: "standard.Link",
source: { id: "p2" },
target: { id: "p1" }
},
{
id: "l3",
type: "standard.Link",
source: { id: "p1" },
target: { id: "r4" },
labels: [{ attrs: { text: { text: "Yes" } } }]
},
{
id: "l4",
type: "standard.Link",
source: { id: "p1" },
target: { id: "p2" },
vertices: [
{ x: 400, y: 450 },
{ x: 400, y: 260 }
],
labels: [{ attrs: { text: { text: "No" } } }]
},
{
id: "l5",
type: "standard.Link",
source: { id: "r4" },
target: { id: "e1" }
}
]
});
scroller.centerContent({ useModelGeometry: true });
// Toolbar
const toolbar = new ui.Toolbar({
theme: "modern",
tools: [
{
type: "button",
text: "Export to PNG",
name: "png"
},
{
type: "button",
name: "comments"
}
]
});
document.getElementById("toolbar-container").appendChild(toolbar.render().el);
// PNG
toolbar.on("png:pointerclick", () => {
paper.hideTools();
format.toPNG(
paper,
(dataUri) => {
const lightbox = new joint.ui.Lightbox({
image: dataUri,
downloadable: true,
fileName: "Rappid"
});
lightbox.open();
if (showComments) {
paper.showTools();
}
},
{
padding: 20,
useComputedStyles: false,
backgroundColor,
size: "2x"
}
);
});
toolbar.on("comments:pointerclick", () => toggleComments());
toggleComments(true);
const startElement = graph.getCell("r3");
addComment(
startElement.findView(paper),
0,
150,
"This is a comment on the Start element."
);
scroller.positionElement(startElement, "top", { padding: 80 });
function toggleComments(showCommentsExplicit = !showComments) {
const commentsButton = toolbar.getWidgetByName("comments");
if (showCommentsExplicit) {
commentsButton.el.textContent = "Hide Comments";
paper.showTools();
showComments = true;
} else {
commentsButton.el.textContent = "Show Comments";
paper.hideTools();
showComments = false;
}
}
function editText(element, paper) {
textEditing = true;
paper.hideTools();
const bbox = element.getBBox();
const textarea = document.createElement("textarea");
// Position & Size
textarea.style.position = "absolute";
textarea.style.boxSizing = "border-box";
textarea.style.width = bbox.width + "px";
textarea.style.height = bbox.height + "px";
textarea.style.transform = V.matrixToTransformString(
paper.matrix().translate(bbox.x, bbox.y)
);
textarea.style.transformOrigin = "0 0";
textarea.placeholder = "Add comment";
// Content
const textPath = "label/text";
textarea.value = element.attr(textPath);
// Styling
textarea.style.fontSize = element.attr("label/fontSize") + "px";
textarea.style.fontFamily = element.attr("label/fontFamily");
textarea.style.color = element.attr("label/fill");
textarea.style.background = element.attr("body/fill");
textarea.style.textAlign = "center";
textarea.style.resize = "none";
textarea.style.padding = "5px 10px";
textarea.style.clipPath =
"polygon(20px 0%, 100% 0%, 100% 100%, 0% 100%, 0% 20px, 20px 0%)";
paper.el.appendChild(textarea);
textarea.focus();
// Select all text
textarea.setSelectionRange(0, textarea.value.length);
textarea.addEventListener("blur", function () {
element.attr(textPath, textarea.value);
textarea.remove();
paper.showTools();
textEditing = false;
});
textarea.addEventListener("keyup", (evt) => {
if (evt.key === "Enter" && !evt.shiftKey) {
const index = textarea.selectionEnd;
textarea.value =
textarea.value.slice(0, index - 1) + textarea.value.slice(index);
textarea.blur();
}
if (evt.key === "Escape") {
textarea.value = element.attr(textPath);
textarea.blur();
}
});
}
function addComment(cellView, x, y, text = "") {
const cell = cellView.model;
const source = { id: cell.id };
if (cell.isLink()) {
const lengthAtPoint = cellView.getClosestPointLength(new g.Point(x, y));
source.anchor = {
name: "connectionLength",
args: {
length: lengthAtPoint
}
};
}
const comment = new Comment({
position: { x: x + 20, y: y - 20 },
attrs: {
label: {
text
}
}
});
const commentLink = new CommentLink({
source,
target: { id: comment.id },
attrs: {
line: {
stroke: "#333333",
strokeWidth: 2,
strokeDasharray: "10,5",
targetMarker: null
}
}
});
graph.addCells([comment, commentLink]);
toggleComments(true);
return comment;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min.js"></script>
<script src="https://resources.jointjs.com/demos/rappid/build/package/rappid.js"></script>
#paper-container {
position: absolute;
right: 0;
top: 0;
left: 0;
bottom: 0;
font-family: sans-serif;
}
#toolbar-container {
position: absolute;
top: 20px;
left: 20px;
.joint-toolbar {
margin: 0;
padding: 4px;
button {
font-family: sans-serif;
font-size: 1.2em;
padding: 10px;
}
}
}
#logo {
position: absolute;
bottom: 20px;
right: 0;
}
<link href="https://resources.jointjs.com/demos/rappid/build/package/rappid.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment