mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 14:52:55 +01:00
Finished node canvas UI tweaks. Failed to add connection highlighting
This commit is contained in:
@@ -74,69 +74,69 @@
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
NODE TYPE COLORS
|
||||
Subtle variations to distinguish node types on canvas
|
||||
Using desaturated colors so they don't compete with the red accent
|
||||
Modern, vibrant colors to distinguish node types on canvas
|
||||
Brightened and more saturated for better visual distinction
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
/* Node-Pink - For Custom/User nodes */
|
||||
--base-color-node-pink-100: #fdf2f8;
|
||||
--base-color-node-pink-200: #f5d0e5;
|
||||
--base-color-node-pink-300: #e8a8ca;
|
||||
--base-color-node-pink-400: #d87caa;
|
||||
--base-color-node-pink-500: #c2578a;
|
||||
--base-color-node-pink-600: #a63d6f;
|
||||
--base-color-node-pink-700: #862d56;
|
||||
--base-color-node-pink-800: #6b2445;
|
||||
--base-color-node-pink-900: #521c35;
|
||||
--base-color-node-pink-1000: #2d0e1c;
|
||||
/* Node-Pink - For Custom/User nodes - Vibrant Pink */
|
||||
--base-color-node-pink-100: #fef2f8;
|
||||
--base-color-node-pink-200: #fce0ed;
|
||||
--base-color-node-pink-300: #f9b5d8;
|
||||
--base-color-node-pink-400: #f58ac3;
|
||||
--base-color-node-pink-500: #ec5ca8;
|
||||
--base-color-node-pink-600: #d63a8e;
|
||||
--base-color-node-pink-700: #b02872;
|
||||
--base-color-node-pink-800: #8a1f5b;
|
||||
--base-color-node-pink-900: #641746;
|
||||
--base-color-node-pink-1000: #3a0d28;
|
||||
|
||||
/* Node-Purple - For Component nodes */
|
||||
--base-color-node-purple-100: #f8f5fa;
|
||||
--base-color-node-purple-200: #e8dff0;
|
||||
--base-color-node-purple-300: #d4c4e3;
|
||||
--base-color-node-purple-400: #b8a0cf;
|
||||
--base-color-node-purple-500: #9a7bb8;
|
||||
--base-color-node-purple-600: #7d5a9e;
|
||||
--base-color-node-purple-700: #624382;
|
||||
--base-color-node-purple-800: #4b3366;
|
||||
--base-color-node-purple-900: #37264b;
|
||||
--base-color-node-purple-1000: #1e1429;
|
||||
/* Node-Purple - For Component nodes - Vibrant Purple */
|
||||
--base-color-node-purple-100: #faf7fc;
|
||||
--base-color-node-purple-200: #f0e8f8;
|
||||
--base-color-node-purple-300: #ddc5ef;
|
||||
--base-color-node-purple-400: #c99ee5;
|
||||
--base-color-node-purple-500: #b176db;
|
||||
--base-color-node-purple-600: #9854c6;
|
||||
--base-color-node-purple-700: #7d3da5;
|
||||
--base-color-node-purple-800: #612e82;
|
||||
--base-color-node-purple-900: #46215e;
|
||||
--base-color-node-purple-1000: #291436;
|
||||
|
||||
/* Node-Green - For Data nodes */
|
||||
--base-color-node-green-100: #f4f7f4;
|
||||
--base-color-node-green-200: #d8e5d8;
|
||||
--base-color-node-green-300: #b5cfb5;
|
||||
--base-color-node-green-400: #8eb58e;
|
||||
--base-color-node-green-500: #6a996a;
|
||||
--base-color-node-green-600: #4d7d4d;
|
||||
--base-color-node-green-700: #3a613a;
|
||||
--base-color-node-green-800: #2c4a2c;
|
||||
--base-color-node-green-900: #203520;
|
||||
--base-color-node-green-1000: #111c11;
|
||||
/* Node-Green - For Data nodes - Vibrant Green */
|
||||
--base-color-node-green-100: #f2fdf2;
|
||||
--base-color-node-green-200: #ddf5dd;
|
||||
--base-color-node-green-300: #b5e8b5;
|
||||
--base-color-node-green-400: #8ddb8d;
|
||||
--base-color-node-green-500: #5fcb5f;
|
||||
--base-color-node-green-600: #3db83d;
|
||||
--base-color-node-green-700: #2d9a2d;
|
||||
--base-color-node-green-800: #227822;
|
||||
--base-color-node-green-900: #185618;
|
||||
--base-color-node-green-1000: #0d2f0d;
|
||||
|
||||
/* Node-Gray - For Logic nodes */
|
||||
--base-color-node-grey-100: #f5f5f5;
|
||||
--base-color-node-grey-200: #e0e0e0;
|
||||
--base-color-node-grey-300: #c2c2c2;
|
||||
--base-color-node-grey-400: #9e9e9e;
|
||||
--base-color-node-grey-500: #757575;
|
||||
--base-color-node-grey-600: #5c5c5c;
|
||||
--base-color-node-grey-700: #454545;
|
||||
--base-color-node-grey-800: #333333;
|
||||
--base-color-node-grey-900: #212121;
|
||||
--base-color-node-grey-1000: #0d0d0d;
|
||||
/* Node-Gray - For Logic nodes - Slightly warmer gray */
|
||||
--base-color-node-grey-100: #f8f8f8;
|
||||
--base-color-node-grey-200: #e8e8e8;
|
||||
--base-color-node-grey-300: #cccccc;
|
||||
--base-color-node-grey-400: #acacac;
|
||||
--base-color-node-grey-500: #888888;
|
||||
--base-color-node-grey-600: #6a6a6a;
|
||||
--base-color-node-grey-700: #525252;
|
||||
--base-color-node-grey-800: #3a3a3a;
|
||||
--base-color-node-grey-900: #262626;
|
||||
--base-color-node-grey-1000: #141414;
|
||||
|
||||
/* Node-Blue - For Visual nodes */
|
||||
--base-color-node-blue-100: #f4f6f8;
|
||||
--base-color-node-blue-200: #dce3eb;
|
||||
--base-color-node-blue-300: #bccad9;
|
||||
--base-color-node-blue-400: #96adc2;
|
||||
--base-color-node-blue-500: #7090a9;
|
||||
--base-color-node-blue-600: #53758f;
|
||||
--base-color-node-blue-700: #3e5a72;
|
||||
--base-color-node-blue-800: #2f4557;
|
||||
--base-color-node-blue-900: #22323f;
|
||||
--base-color-node-blue-1000: #121b22;
|
||||
/* Node-Blue - For Visual nodes - Vibrant Blue */
|
||||
--base-color-node-blue-100: #f3f8fc;
|
||||
--base-color-node-blue-200: #deeaf7;
|
||||
--base-color-node-blue-300: #b8d7ee;
|
||||
--base-color-node-blue-400: #91c3e5;
|
||||
--base-color-node-blue-500: #62aed9;
|
||||
--base-color-node-blue-600: #3d96ca;
|
||||
--base-color-node-blue-700: #2c7aac;
|
||||
--base-color-node-blue-800: #21608a;
|
||||
--base-color-node-blue-900: #174563;
|
||||
--base-color-node-blue-1000: #0d2638;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
LEGACY ALIASES - For backwards compatibility
|
||||
|
||||
@@ -605,6 +605,45 @@ export class NodeGraphNode extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the comment text for this node
|
||||
getComment(): string | undefined {
|
||||
return this.metadata?.comment;
|
||||
}
|
||||
|
||||
// Check if this node has a comment
|
||||
hasComment(): boolean {
|
||||
return !!this.metadata?.comment?.trim();
|
||||
}
|
||||
|
||||
// Set or clear the comment for this node, supports undo
|
||||
setComment(comment: string | undefined, args?: { undo?: any; label?: any }) {
|
||||
const _this = this;
|
||||
const oldComment = this.getComment();
|
||||
|
||||
if (!this.metadata) this.metadata = {};
|
||||
|
||||
// Store trimmed comment or undefined if empty
|
||||
this.metadata.comment = comment?.trim() || undefined;
|
||||
|
||||
// Notify listeners of the change
|
||||
this.notifyListeners('commentChanged', { comment: this.metadata.comment });
|
||||
|
||||
// Undo support
|
||||
if (args && args.undo) {
|
||||
const undo = typeof args.undo === 'object' ? args.undo : UndoQueue.instance;
|
||||
|
||||
undo.push({
|
||||
label: args.label || 'Edit node comment',
|
||||
do: function () {
|
||||
_this.setComment(comment);
|
||||
},
|
||||
undo: function () {
|
||||
_this.setComment(oldComment);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set a parameter for the node instance
|
||||
setParameter(name: string, value, args?) {
|
||||
const _this = this;
|
||||
|
||||
@@ -536,3 +536,122 @@
|
||||
.confirm-modal .cancel-button:hover {
|
||||
background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
/* String Input Popup */
|
||||
.string-input-popup {
|
||||
padding: 16px;
|
||||
width: 500px;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.string-input-popup-label {
|
||||
display: block;
|
||||
color: var(--theme-color-fg-default);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.string-input-popup-editor-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background-color: var(--theme-color-bg-1);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
/* Fixed height to prevent modal from growing */
|
||||
height: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.string-input-popup-line-numbers {
|
||||
flex-shrink: 0;
|
||||
/* Match textarea padding exactly for alignment */
|
||||
padding: 12px 8px 12px 12px;
|
||||
background-color: var(--theme-color-bg-2);
|
||||
border-right: 1px solid var(--theme-color-border-default);
|
||||
color: var(--theme-color-fg-muted);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 20.8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
min-width: 36px;
|
||||
/* Allow scroll sync - hide scrollbar but allow scrollTop changes */
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for line numbers in WebKit browsers */
|
||||
.string-input-popup-line-numbers::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.string-input-popup-textarea {
|
||||
flex: 1;
|
||||
/* Fill the fixed-height wrapper */
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: var(--theme-color-fg-default);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 20.8px;
|
||||
resize: none;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.string-input-popup-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--theme-color-primary);
|
||||
box-shadow: 0 0 0 2px var(--theme-color-primary-transparent);
|
||||
}
|
||||
|
||||
.string-input-popup-textarea::placeholder {
|
||||
color: var(--theme-color-fg-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.string-input-popup-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.string-input-popup-button-ok,
|
||||
.string-input-popup-button-cancel {
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.string-input-popup-button-ok {
|
||||
background-color: var(--theme-color-primary);
|
||||
color: var(--theme-color-bg-2);
|
||||
}
|
||||
|
||||
.string-input-popup-button-ok:hover {
|
||||
background-color: var(--theme-color-primary-hover);
|
||||
}
|
||||
|
||||
.string-input-popup-button-cancel {
|
||||
background-color: var(--theme-color-bg-4);
|
||||
color: var(--theme-color-fg-default);
|
||||
}
|
||||
|
||||
.string-input-popup-button-cancel:hover {
|
||||
background-color: var(--theme-color-bg-5);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<!-- prettier-ignore -->
|
||||
<!-- IMPORTANT: This is a LEGACY HTML TEMPLATE, not JSX! Use 'class' not 'className' -->
|
||||
<div class="string-input-popup">
|
||||
<label class="string-input-popup-label" data-text="label"></label>
|
||||
<input class="string-input-popup-input sidebar-input"></input>
|
||||
<div class="string-input-popup-editor-wrapper">
|
||||
<div class="string-input-popup-line-numbers" aria-hidden="true"></div>
|
||||
<textarea class="string-input-popup-input string-input-popup-textarea sidebar-input" rows="8" placeholder="// Add your comment here..." spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="string-input-popup-buttons">
|
||||
<button class="string-input-popup-button-ok" data-click="onOkClicked" data-text="okLabel"></button>
|
||||
<button class="string-input-popup-button-cancel" data-click="onCancelClicked" data-text="cancelLabel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ProjectModel } from '../../models/projectmodel';
|
||||
import { ViewerConnection } from '../../ViewerConnection';
|
||||
import { NodeGraphEditor } from '../nodegrapheditor';
|
||||
import PopupLayer from '../popuplayer';
|
||||
import { fillRoundRect, roundRect, strokeRoundRect, truncateText } from './canvasHelpers';
|
||||
import { NodeGraphEditorConnection } from './NodeGraphEditorConnection';
|
||||
|
||||
function _getColorForAnnotation(annotation) {
|
||||
@@ -288,6 +289,7 @@ export class NodeGraphEditorNode {
|
||||
public static readonly attachedThreshold = 20;
|
||||
public static readonly propertyConnectionHeight = 20;
|
||||
public static readonly verticalSpacing = 8;
|
||||
public static readonly cornerRadius = 6;
|
||||
|
||||
model: NodeGraphNode;
|
||||
x: number;
|
||||
@@ -315,6 +317,8 @@ export class NodeGraphEditorNode {
|
||||
iconSize: number;
|
||||
iconRotation: number;
|
||||
|
||||
commentIconBounds: { x: number; y: number; width: number; height: number } | undefined;
|
||||
|
||||
constructor(model) {
|
||||
this.model = model;
|
||||
this.x = model.x || 0;
|
||||
@@ -519,6 +523,16 @@ export class NodeGraphEditorNode {
|
||||
PopupLayer.instance.hideTooltip();
|
||||
|
||||
if (this.owner.highlighted === this) {
|
||||
// Check if clicking on comment icon
|
||||
const inCommentIcon = this.commentIconBounds && this.isPointInCommentIcon(pos);
|
||||
|
||||
if (inCommentIcon) {
|
||||
// Show comment edit prompt
|
||||
this.showCommentEditPopup();
|
||||
evt.stopPropagation && evt.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.borderHighlighted || this.connectionDragAreaHighlighted) {
|
||||
// User starts dragging from the border or connection area with circle icon
|
||||
this.owner.startDraggingConnection(this);
|
||||
@@ -615,19 +629,17 @@ export class NodeGraphEditorNode {
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.save();
|
||||
|
||||
// Clip
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height);
|
||||
// Clip to rounded rectangle
|
||||
roundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius);
|
||||
ctx.clip();
|
||||
|
||||
// Bg
|
||||
// Bg - Use rounded rectangle for modern appearance
|
||||
ctx.fillStyle = nc.header;
|
||||
ctx.fillRect(x, y, this.nodeSize.width, this.nodeSize.height);
|
||||
fillRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius);
|
||||
|
||||
const titlebarHeight = this.titlebarHeight();
|
||||
|
||||
// Darken plate
|
||||
//ctx.globalAlpha = 0.07;
|
||||
// Darken plate (body area below title)
|
||||
ctx.fillStyle = nc.base;
|
||||
ctx.fillRect(x, y + titlebarHeight, this.nodeSize.width, this.nodeSize.height - titlebarHeight);
|
||||
|
||||
@@ -637,7 +649,7 @@ export class NodeGraphEditorNode {
|
||||
ctx.globalCompositeOperation = 'hard-light'; // additive blending looks better
|
||||
ctx.globalAlpha = 0.19;
|
||||
ctx.fillStyle = nc.text;
|
||||
ctx.fillRect(x, y, this.nodeSize.width, this.nodeSize.height);
|
||||
fillRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius);
|
||||
ctx.globalCompositeOperation = prevCompOperation;
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
@@ -694,7 +706,7 @@ export class NodeGraphEditorNode {
|
||||
// Title
|
||||
ctx.fillStyle = nc.text;
|
||||
|
||||
ctx.font = '12px Inter-Regular';
|
||||
ctx.font = '12px Inter-Medium';
|
||||
ctx.textBaseline = 'top';
|
||||
textWordWrap(
|
||||
ctx,
|
||||
@@ -711,7 +723,7 @@ export class NodeGraphEditorNode {
|
||||
ctx.save();
|
||||
ctx.fillStyle = nc.text;
|
||||
ctx.globalAlpha = 0.65;
|
||||
ctx.font = '12px Inter-Regular';
|
||||
ctx.font = '12px Inter-Medium';
|
||||
ctx.textBaseline = 'top';
|
||||
textWordWrap(
|
||||
ctx,
|
||||
@@ -726,6 +738,85 @@ export class NodeGraphEditorNode {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Draw comment icon (if node has comment OR is highlighted)
|
||||
// Position on right side, before the node icon area if present
|
||||
const hasComment = this.model.hasComment();
|
||||
if (hasComment || isHighligthed) {
|
||||
const commentIconSize = 14;
|
||||
// Adjust offset based on whether node icon is present
|
||||
// If icon exists, offset more to avoid overlap; if not, position closer to edge
|
||||
const commentIconRightOffset = this.icon ? 30 : 10;
|
||||
const commentIconX =
|
||||
x + this.nodeSize.width - connectionDragAreaWidth - commentIconSize - commentIconRightOffset;
|
||||
const commentIconY = y + titlebarHeight / 2 - commentIconSize / 2;
|
||||
|
||||
// Store bounds for click detection
|
||||
this.commentIconBounds = {
|
||||
x: commentIconX,
|
||||
y: commentIconY,
|
||||
width: commentIconSize,
|
||||
height: commentIconSize
|
||||
};
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Set opacity based on whether comment exists
|
||||
ctx.globalAlpha = hasComment ? 1.0 : 0.4;
|
||||
ctx.fillStyle = nc.text;
|
||||
ctx.strokeStyle = nc.text;
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
// Draw speech bubble (rounded rectangle)
|
||||
const bubbleWidth = commentIconSize;
|
||||
const bubbleHeight = commentIconSize * 0.8;
|
||||
const bubbleRadius = 2;
|
||||
|
||||
// Main bubble body
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(commentIconX + bubbleRadius, commentIconY);
|
||||
ctx.lineTo(commentIconX + bubbleWidth - bubbleRadius, commentIconY);
|
||||
ctx.quadraticCurveTo(
|
||||
commentIconX + bubbleWidth,
|
||||
commentIconY,
|
||||
commentIconX + bubbleWidth,
|
||||
commentIconY + bubbleRadius
|
||||
);
|
||||
ctx.lineTo(commentIconX + bubbleWidth, commentIconY + bubbleHeight - bubbleRadius);
|
||||
ctx.quadraticCurveTo(
|
||||
commentIconX + bubbleWidth,
|
||||
commentIconY + bubbleHeight,
|
||||
commentIconX + bubbleWidth - bubbleRadius,
|
||||
commentIconY + bubbleHeight
|
||||
);
|
||||
|
||||
// Draw tail (small triangle at bottom)
|
||||
const tailWidth = 3;
|
||||
const tailHeight = 3;
|
||||
const tailX = commentIconX + bubbleWidth * 0.7;
|
||||
ctx.lineTo(tailX + tailWidth, commentIconY + bubbleHeight);
|
||||
ctx.lineTo(tailX, commentIconY + bubbleHeight + tailHeight);
|
||||
ctx.lineTo(tailX - tailWidth / 2, commentIconY + bubbleHeight);
|
||||
|
||||
// Complete the bubble
|
||||
ctx.lineTo(commentIconX + bubbleRadius, commentIconY + bubbleHeight);
|
||||
ctx.quadraticCurveTo(
|
||||
commentIconX,
|
||||
commentIconY + bubbleHeight,
|
||||
commentIconX,
|
||||
commentIconY + bubbleHeight - bubbleRadius
|
||||
);
|
||||
ctx.lineTo(commentIconX, commentIconY + bubbleRadius);
|
||||
ctx.quadraticCurveTo(commentIconX, commentIconY, commentIconX + bubbleRadius, commentIconY);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
} else {
|
||||
// Clear bounds when not visible
|
||||
this.commentIconBounds = undefined;
|
||||
}
|
||||
|
||||
ctx.restore(); // Restore clip so we can draw border
|
||||
|
||||
if (isHighligthed) {
|
||||
@@ -745,16 +836,21 @@ export class NodeGraphEditorNode {
|
||||
// );
|
||||
}
|
||||
|
||||
// Border
|
||||
// Border - Use rounded rectangles for modern appearance
|
||||
const health = this.model.getHealth();
|
||||
if (!health.healthy) {
|
||||
ctx.setLineDash([5]);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = '#F57569';
|
||||
ctx.globalAlpha = 0.7;
|
||||
ctx.beginPath();
|
||||
ctx.rect(x - 1, y - 1, this.nodeSize.width + 2, this.nodeSize.height + 2);
|
||||
ctx.stroke();
|
||||
strokeRoundRect(
|
||||
ctx,
|
||||
x - 1,
|
||||
y - 1,
|
||||
this.nodeSize.width + 2,
|
||||
this.nodeSize.height + 2,
|
||||
NodeGraphEditorNode.cornerRadius + 1
|
||||
);
|
||||
ctx.setLineDash([]); // Restore line dash
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
@@ -762,9 +858,7 @@ export class NodeGraphEditorNode {
|
||||
if (this.selected || this.borderHighlighted || this.connectionDragAreaHighlighted) {
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height);
|
||||
ctx.stroke();
|
||||
strokeRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius);
|
||||
}
|
||||
|
||||
if (this.model.annotation) {
|
||||
@@ -773,9 +867,7 @@ export class NodeGraphEditorNode {
|
||||
else if (this.model.annotation === 'Created') ctx.strokeStyle = '#5BF59E';
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height);
|
||||
ctx.stroke();
|
||||
strokeRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius);
|
||||
}
|
||||
|
||||
// Paint plugs
|
||||
@@ -793,35 +885,61 @@ export class NodeGraphEditorNode {
|
||||
}
|
||||
|
||||
function dot(side, color) {
|
||||
const cx = x + (side === 'left' ? 0 : _this.nodeSize.width);
|
||||
const radius = 6; // Back to normal size
|
||||
|
||||
// Draw main port indicator
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + (side === 'left' ? 0 : _this.nodeSize.width), ty, 4, 0, 2 * Math.PI, false);
|
||||
ctx.arc(cx, ty, radius, 0, 2 * Math.PI, false);
|
||||
ctx.fill();
|
||||
|
||||
// Add subtle inner highlight for depth
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx - 0.5, ty - 0.5, radius * 0.4, 0, 2 * Math.PI, false);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawPlugs(plugs, offset) {
|
||||
ctx.font = '11px Inter-Regular';
|
||||
ctx.font = '11px Inter-Medium';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
for (const i in plugs) {
|
||||
const p = plugs[i];
|
||||
|
||||
// Label
|
||||
// Calculate Y position for this port
|
||||
ty = p.index * NodeGraphEditorNode.propertyConnectionHeight + offset;
|
||||
|
||||
if (p.loc === 'left' || p.loc === 'middle') tx = x + horizontalSpacing;
|
||||
else if (p.loc === 'right') tx = x + _this.nodeSize.width - horizontalSpacing;
|
||||
else tx = x + _this.nodeSize.width / 2;
|
||||
// Draw labels at normal positions
|
||||
if (p.loc === 'left' || p.loc === 'middle') {
|
||||
// Left-aligned labels
|
||||
tx = x + horizontalSpacing;
|
||||
} else if (p.loc === 'right') {
|
||||
// Right-aligned labels
|
||||
tx = x + _this.nodeSize.width - horizontalSpacing;
|
||||
} else {
|
||||
tx = x + _this.nodeSize.width / 2;
|
||||
}
|
||||
|
||||
ctx.fillStyle = nc.text;
|
||||
ctx.textAlign = p.loc === 'right' ? 'right' : 'left';
|
||||
|
||||
ctx.fillText(p.displayName ? p.displayName : p.property, tx, ty);
|
||||
// Truncate port labels to prevent overflow
|
||||
const label = p.displayName ? p.displayName : p.property;
|
||||
const portAreaWidth =
|
||||
p.loc === 'middle'
|
||||
? _this.nodeSize.width - 2 * horizontalSpacing
|
||||
: _this.nodeSize.width - horizontalSpacing - 8;
|
||||
const truncatedLabel = truncateText(ctx, label, portAreaWidth);
|
||||
|
||||
// Plug
|
||||
if (p.leftCons.length) {
|
||||
ctx.fillText(truncatedLabel, tx, ty);
|
||||
|
||||
// Plug - Left side
|
||||
if (p.leftCons.length || p.leftIcon) {
|
||||
var connectionColors = NodeLibrary.instance.colorSchemeForConnectionType(
|
||||
NodeLibrary.nameForPortType(p.leftCons[0].fromPort ? p.leftCons[0].fromPort.type : undefined)
|
||||
NodeLibrary.nameForPortType(p.leftCons[0]?.fromPort ? p.leftCons[0].fromPort.type : undefined)
|
||||
);
|
||||
var color = _.find(p.leftCons, function (p) {
|
||||
return p.isHighlighted();
|
||||
@@ -834,7 +952,7 @@ export class NodeGraphEditorNode {
|
||||
return p.isHighlighted();
|
||||
}) || p.leftCons[p.leftCons.length - 1];
|
||||
|
||||
if (topConnection.model.annotation) {
|
||||
if (topConnection && topConnection.model.annotation) {
|
||||
color = _getColorForAnnotation(topConnection.model.annotation);
|
||||
}
|
||||
|
||||
@@ -845,9 +963,10 @@ export class NodeGraphEditorNode {
|
||||
}
|
||||
}
|
||||
|
||||
if (p.rightCons.length) {
|
||||
// Plug - Right side
|
||||
if (p.rightCons.length || p.rightIcon) {
|
||||
connectionColors = NodeLibrary.instance.colorSchemeForConnectionType(
|
||||
NodeLibrary.nameForPortType(p.rightCons[0].fromPort ? p.rightCons[0].fromPort.type : undefined)
|
||||
NodeLibrary.nameForPortType(p.rightCons[0]?.fromPort ? p.rightCons[0].fromPort.type : undefined)
|
||||
);
|
||||
color = _.find(p.rightCons, function (p) {
|
||||
return p.isHighlighted();
|
||||
@@ -860,7 +979,7 @@ export class NodeGraphEditorNode {
|
||||
return p.isHighlighted();
|
||||
}) || p.rightCons[p.rightCons.length - 1];
|
||||
|
||||
if (topConnection.model.annotation) {
|
||||
if (topConnection && topConnection.model.annotation) {
|
||||
color = _getColorForAnnotation(topConnection.model.annotation);
|
||||
}
|
||||
|
||||
@@ -1088,4 +1207,75 @@ export class NodeGraphEditorNode {
|
||||
this.parent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a point (in local node coordinates) is within the comment icon bounds
|
||||
*/
|
||||
isPointInCommentIcon(pos: { x: number; y: number }): boolean {
|
||||
if (!this.commentIconBounds) return false;
|
||||
|
||||
// Convert local pos to global for comparison with commentIconBounds (which are in global coords)
|
||||
const globalX = pos.x + this.global.x;
|
||||
const globalY = pos.y + this.global.y;
|
||||
|
||||
const bounds = this.commentIconBounds;
|
||||
const padding = 4; // Extra hit area padding for easier clicking
|
||||
|
||||
return (
|
||||
globalX >= bounds.x - padding &&
|
||||
globalX <= bounds.x + bounds.width + padding &&
|
||||
globalY >= bounds.y - padding &&
|
||||
globalY <= bounds.y + bounds.height + padding
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a popup for editing the node comment
|
||||
*/
|
||||
showCommentEditPopup() {
|
||||
const currentComment = this.model.getComment() || '';
|
||||
const nodeLabel = this.model.label || 'Node';
|
||||
const model = this.model;
|
||||
const owner = this.owner;
|
||||
|
||||
// Use PopupLayer.StringInputPopup for Electron compatibility
|
||||
const popup = new PopupLayer.StringInputPopup({
|
||||
label: `Comment for "${nodeLabel}"`,
|
||||
okLabel: 'Save',
|
||||
cancelLabel: 'Cancel',
|
||||
onOk: (newComment: string) => {
|
||||
// Set comment with undo support
|
||||
model.setComment(newComment || undefined, {
|
||||
undo: true,
|
||||
label: newComment ? 'Edit node comment' : 'Remove node comment'
|
||||
});
|
||||
|
||||
// Repaint to update the icon appearance
|
||||
owner.repaint();
|
||||
PopupLayer.instance.hidePopup();
|
||||
},
|
||||
onCancel: () => {
|
||||
PopupLayer.instance.hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
// Render popup BEFORE showing it
|
||||
popup.render();
|
||||
|
||||
// Set initial value after render
|
||||
popup.$('.string-input-popup-input').val(currentComment);
|
||||
|
||||
// Use requestAnimationFrame + setTimeout to ensure we're past both the current
|
||||
// event cycle AND any pending DOM updates. This prevents the PopupLayer's body
|
||||
// click handler from immediately closing the popup.
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
PopupLayer.instance.showPopup({
|
||||
content: popup,
|
||||
position: 'screen-center',
|
||||
isBackgroundDimmed: true
|
||||
});
|
||||
}, 100); // 100ms delay to be extra safe
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Canvas Helper Utilities for Node Graph Rendering
|
||||
*
|
||||
* Provides utility functions for drawing rounded rectangles and text truncation
|
||||
* on HTML5 Canvas. Used primarily for modernizing node appearance in the graph editor.
|
||||
*
|
||||
* @module canvasHelpers
|
||||
* @since TASK-000I-A
|
||||
*/
|
||||
|
||||
/**
|
||||
* Corner radius configuration for rounded rectangles
|
||||
* Can be a single number for all corners, or an object specifying each corner
|
||||
*/
|
||||
export type CornerRadius =
|
||||
| number
|
||||
| {
|
||||
tl: number; // top-left
|
||||
tr: number; // top-right
|
||||
br: number; // bottom-right
|
||||
bl: number; // bottom-left
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw a rounded rectangle path (does not fill or stroke)
|
||||
*
|
||||
* Uses arcTo() for drawing rounded corners. This method only creates the path;
|
||||
* you must call ctx.fill() or ctx.stroke() afterwards.
|
||||
*
|
||||
* @param ctx - Canvas rendering context
|
||||
* @param x - X coordinate of top-left corner
|
||||
* @param y - Y coordinate of top-left corner
|
||||
* @param width - Width of rectangle
|
||||
* @param height - Height of rectangle
|
||||
* @param radius - Corner radius (number for all corners, or object for individual corners)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* roundRect(ctx, 10, 10, 100, 50, 6);
|
||||
* ctx.fill();
|
||||
* ```
|
||||
*/
|
||||
export function roundRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: CornerRadius
|
||||
): void {
|
||||
// Normalize radius to object format
|
||||
const r = typeof radius === 'number' ? { tl: radius, tr: radius, br: radius, bl: radius } : radius;
|
||||
|
||||
// Clamp radius to reasonable values (can't be larger than half the smallest dimension)
|
||||
const maxRadius = Math.min(width, height) / 2;
|
||||
const tl = Math.min(r.tl, maxRadius);
|
||||
const tr = Math.min(r.tr, maxRadius);
|
||||
const br = Math.min(r.br, maxRadius);
|
||||
const bl = Math.min(r.bl, maxRadius);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + tl, y);
|
||||
|
||||
// Top edge and top-right corner
|
||||
ctx.lineTo(x + width - tr, y);
|
||||
ctx.arcTo(x + width, y, x + width, y + tr, tr);
|
||||
|
||||
// Right edge and bottom-right corner
|
||||
ctx.lineTo(x + width, y + height - br);
|
||||
ctx.arcTo(x + width, y + height, x + width - br, y + height, br);
|
||||
|
||||
// Bottom edge and bottom-left corner
|
||||
ctx.lineTo(x + bl, y + height);
|
||||
ctx.arcTo(x, y + height, x, y + height - bl, bl);
|
||||
|
||||
// Left edge and top-left corner
|
||||
ctx.lineTo(x, y + tl);
|
||||
ctx.arcTo(x, y, x + tl, y, tl);
|
||||
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a rounded rectangle
|
||||
*
|
||||
* Convenience wrapper that creates a rounded rectangle path and fills it.
|
||||
*
|
||||
* @param ctx - Canvas rendering context
|
||||
* @param x - X coordinate of top-left corner
|
||||
* @param y - Y coordinate of top-left corner
|
||||
* @param width - Width of rectangle
|
||||
* @param height - Height of rectangle
|
||||
* @param radius - Corner radius
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ctx.fillStyle = '#333';
|
||||
* fillRoundRect(ctx, 10, 10, 100, 50, 6);
|
||||
* ```
|
||||
*/
|
||||
export function fillRoundRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: CornerRadius
|
||||
): void {
|
||||
roundRect(ctx, x, y, width, height, radius);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stroke a rounded rectangle
|
||||
*
|
||||
* Convenience wrapper that creates a rounded rectangle path and strokes it.
|
||||
*
|
||||
* @param ctx - Canvas rendering context
|
||||
* @param x - X coordinate of top-left corner
|
||||
* @param y - Y coordinate of top-left corner
|
||||
* @param width - Width of rectangle
|
||||
* @param height - Height of rectangle
|
||||
* @param radius - Corner radius
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ctx.strokeStyle = '#fff';
|
||||
* ctx.lineWidth = 2;
|
||||
* strokeRoundRect(ctx, 10, 10, 100, 50, 6);
|
||||
* ```
|
||||
*/
|
||||
export function strokeRoundRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: CornerRadius
|
||||
): void {
|
||||
roundRect(ctx, x, y, width, height, radius);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text to fit within a maximum width, adding ellipsis if needed
|
||||
*
|
||||
* Efficiently truncates text by measuring progressively shorter strings
|
||||
* until one fits within the specified width. Uses the context's current font settings.
|
||||
*
|
||||
* @param ctx - Canvas rendering context (with font already set)
|
||||
* @param text - Text to truncate
|
||||
* @param maxWidth - Maximum width in pixels
|
||||
* @returns Truncated text with '…' appended if truncation occurred
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ctx.font = '12px Inter-Regular';
|
||||
* const displayText = truncateText(ctx, 'Very Long Port Name', 80);
|
||||
* // Returns "Very Long Po…" if it doesn't fit
|
||||
* ctx.fillText(displayText, x, y);
|
||||
* ```
|
||||
*/
|
||||
export function truncateText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string {
|
||||
// If the text already fits, return it as-is
|
||||
if (ctx.measureText(text).width <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const ellipsis = '…';
|
||||
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
||||
|
||||
// If even the ellipsis doesn't fit, just return it
|
||||
if (ellipsisWidth > maxWidth) {
|
||||
return ellipsis;
|
||||
}
|
||||
|
||||
// Binary search for the optimal truncation point
|
||||
let left = 0;
|
||||
let right = text.length;
|
||||
let result = '';
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const truncated = text.slice(0, mid);
|
||||
const width = ctx.measureText(truncated + ellipsis).width;
|
||||
|
||||
if (width <= maxWidth) {
|
||||
result = truncated;
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result + ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Port Type Icons for Node Graph Editor
|
||||
*
|
||||
* Provides simple, minimal icon indicators for port data types.
|
||||
* Uses Unicode characters for reliability and clarity at small sizes.
|
||||
*
|
||||
* @module portIcons
|
||||
* @since TASK-000I-C2
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported port types in the Noodl system
|
||||
*/
|
||||
export type PortType =
|
||||
| 'signal'
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'object'
|
||||
| 'array'
|
||||
| 'color'
|
||||
| 'any'
|
||||
| 'component'
|
||||
| 'enum';
|
||||
|
||||
/**
|
||||
* Icon representation for a port type
|
||||
*/
|
||||
export interface PortIcon {
|
||||
/** Unicode character to display */
|
||||
char: string;
|
||||
/** Optional description for debugging */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon definitions for each port type
|
||||
* Using simple, clear Unicode characters that render well at small sizes
|
||||
*/
|
||||
export const PORT_ICONS: Record<PortType, PortIcon> = {
|
||||
signal: {
|
||||
char: '⚡',
|
||||
description: 'Signal/Event trigger'
|
||||
},
|
||||
string: {
|
||||
char: 'T',
|
||||
description: 'Text/String data'
|
||||
},
|
||||
number: {
|
||||
char: '#',
|
||||
description: 'Numeric data'
|
||||
},
|
||||
boolean: {
|
||||
char: '◐',
|
||||
description: 'True/False value'
|
||||
},
|
||||
object: {
|
||||
char: '{ }',
|
||||
description: 'Object/Record'
|
||||
},
|
||||
array: {
|
||||
char: '[ ]',
|
||||
description: 'Array/List'
|
||||
},
|
||||
color: {
|
||||
char: '●',
|
||||
description: 'Color value'
|
||||
},
|
||||
any: {
|
||||
char: '◇',
|
||||
description: 'Any type'
|
||||
},
|
||||
component: {
|
||||
char: '◈',
|
||||
description: 'Component reference'
|
||||
},
|
||||
enum: {
|
||||
char: '≡',
|
||||
description: 'Enumeration/List'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Visual constants for port icon rendering
|
||||
*/
|
||||
export const PORT_ICON_SIZE = 10; // Font size in pixels
|
||||
export const PORT_ICON_PADDING = 4; // Space between icon and label
|
||||
|
||||
/**
|
||||
* Map Noodl internal type names to icon types
|
||||
*
|
||||
* Noodl uses various type names internally - this function normalizes them
|
||||
* to our standard PortType set for consistent icon display.
|
||||
*
|
||||
* @param type - The internal Noodl type name (may be undefined)
|
||||
* @returns The corresponding PortType for icon selection
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* getPortIconType('*') // returns 'signal'
|
||||
* getPortIconType('string') // returns 'string'
|
||||
* getPortIconType(undefined) // returns 'any'
|
||||
* ```
|
||||
*/
|
||||
export function getPortIconType(type: string | undefined): PortType {
|
||||
// Handle undefined or non-string types (runtime safety)
|
||||
if (!type || typeof type !== 'string') return 'any';
|
||||
|
||||
// Normalize to lowercase for case-insensitive matching
|
||||
const normalizedType = type.toLowerCase();
|
||||
|
||||
// Direct type mappings
|
||||
const typeMap: Record<string, PortType> = {
|
||||
// Signal types
|
||||
signal: 'signal',
|
||||
'*': 'signal',
|
||||
|
||||
// Primitive types
|
||||
string: 'string',
|
||||
number: 'number',
|
||||
boolean: 'boolean',
|
||||
|
||||
// Complex types
|
||||
object: 'object',
|
||||
array: 'array',
|
||||
color: 'color',
|
||||
|
||||
// Special types
|
||||
component: 'component',
|
||||
enum: 'enum',
|
||||
|
||||
// Aliases
|
||||
text: 'string',
|
||||
bool: 'boolean',
|
||||
list: 'array',
|
||||
json: 'object'
|
||||
};
|
||||
|
||||
return typeMap[normalizedType] || 'any';
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a port type icon on canvas
|
||||
*
|
||||
* Renders a small icon character indicating the port's data type.
|
||||
* The icon is drawn with the specified color and at the given position.
|
||||
*
|
||||
* @param ctx - Canvas rendering context
|
||||
* @param type - The port type to render an icon for
|
||||
* @param x - X coordinate (center of icon)
|
||||
* @param y - Y coordinate (center of icon)
|
||||
* @param color - Color to render the icon (CSS color string)
|
||||
* @param alpha - Optional opacity override (0-1)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* drawPortIcon(ctx, 'signal', 100, 50, '#ff0000', 0.8);
|
||||
* drawPortIcon(ctx, 'number', 150, 50, 'rgba(255, 255, 255, 0.6)');
|
||||
* ```
|
||||
*/
|
||||
export function drawPortIcon(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
type: PortType,
|
||||
x: number,
|
||||
y: number,
|
||||
color: string,
|
||||
alpha: number = 1
|
||||
): void {
|
||||
const icon = PORT_ICONS[type];
|
||||
if (!icon) {
|
||||
console.warn(`Unknown port type: ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Set rendering properties
|
||||
ctx.fillStyle = color;
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.font = `${PORT_ICON_SIZE}px Inter-Regular`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// Draw the icon character
|
||||
ctx.fillText(icon.char, x, y);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visual width of an icon (for layout calculations)
|
||||
*
|
||||
* Measures the actual rendered width of a port icon character.
|
||||
* Useful for positioning labels correctly after icons.
|
||||
*
|
||||
* @param ctx - Canvas rendering context (with font already set)
|
||||
* @param type - The port type
|
||||
* @returns Width in pixels
|
||||
*/
|
||||
export function getPortIconWidth(ctx: CanvasRenderingContext2D, type: PortType): number {
|
||||
const icon = PORT_ICONS[type];
|
||||
if (!icon) return 0;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = `${PORT_ICON_SIZE}px Inter-Regular`;
|
||||
const width = ctx.measureText(icon.char).width;
|
||||
ctx.restore();
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -281,8 +281,14 @@ PopupLayer.prototype.showPopup = function (args) {
|
||||
args.content.owner = this;
|
||||
|
||||
this.$('.popup-layer-popup-content').append(content);
|
||||
var contentWidth = content.outerWidth(true);
|
||||
var contentHeight = content.outerHeight(true);
|
||||
|
||||
// Force a reflow to ensure the element is measurable
|
||||
void this.$('.popup-layer-popup-content')[0].offsetHeight;
|
||||
|
||||
// Query the actual appended element to measure dimensions
|
||||
var popupContent = this.$('.popup-layer-popup-content');
|
||||
var contentWidth = popupContent.children().first().outerWidth(true);
|
||||
var contentHeight = popupContent.children().first().outerHeight(true);
|
||||
|
||||
if (args.position === 'screen-center') {
|
||||
if (args.isBackgroundDimmed) {
|
||||
@@ -921,13 +927,17 @@ PopupLayer.StringInputPopup.prototype = Object.create(View.prototype);
|
||||
PopupLayer.StringInputPopup.prototype.render = function () {
|
||||
this.el = this.bindView($(StringInputPopupTemplate), this);
|
||||
|
||||
this.$('.string-input-popup-input')
|
||||
.off('keypress')
|
||||
.on('keypress', (e) => {
|
||||
// Only close on Enter for single-line inputs, not textareas
|
||||
const input = this.$('.string-input-popup-input');
|
||||
const isTextarea = input.is('textarea');
|
||||
|
||||
if (!isTextarea) {
|
||||
input.off('keypress').on('keypress', (e) => {
|
||||
if (e.which == 13) {
|
||||
this.onOkClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.el;
|
||||
};
|
||||
@@ -949,8 +959,49 @@ PopupLayer.StringInputPopup.prototype.onCancelClicked = function () {
|
||||
this.owner.hidePopup();
|
||||
};
|
||||
|
||||
PopupLayer.StringInputPopup.prototype.updateLineNumbers = function () {
|
||||
const textarea = this.$('.string-input-popup-input')[0];
|
||||
const lineNumbersEl = this.$('.string-input-popup-line-numbers')[0];
|
||||
|
||||
if (!textarea || !lineNumbersEl) return;
|
||||
|
||||
// Count lines based on textarea value
|
||||
const text = textarea.value;
|
||||
const lines = text ? text.split('\n').length : 1;
|
||||
|
||||
// Always show at least 8 lines (matching rows="8")
|
||||
const displayLines = Math.max(8, lines);
|
||||
|
||||
// Generate line numbers
|
||||
let lineNumbersHTML = '';
|
||||
for (let i = 1; i <= displayLines; i++) {
|
||||
lineNumbersHTML += i + '\n';
|
||||
}
|
||||
|
||||
lineNumbersEl.textContent = lineNumbersHTML;
|
||||
|
||||
// Sync scroll
|
||||
lineNumbersEl.scrollTop = textarea.scrollTop;
|
||||
};
|
||||
|
||||
PopupLayer.StringInputPopup.prototype.onOpen = function () {
|
||||
this.$('.string-input-popup-input').focus();
|
||||
const textarea = this.$('.string-input-popup-input');
|
||||
|
||||
// Initial line numbers
|
||||
this.updateLineNumbers();
|
||||
|
||||
// Update line numbers on input
|
||||
textarea.on('input', () => this.updateLineNumbers());
|
||||
|
||||
// Sync scroll between textarea and line numbers
|
||||
textarea.on('scroll', () => {
|
||||
const lineNumbersEl = this.$('.string-input-popup-line-numbers')[0];
|
||||
if (lineNumbersEl) {
|
||||
lineNumbersEl.scrollTop = textarea[0].scrollTop;
|
||||
}
|
||||
});
|
||||
|
||||
textarea.focus();
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -161,49 +161,49 @@ function generateNodeLibrary(nodeRegister) {
|
||||
colors: {
|
||||
nodes: {
|
||||
component: {
|
||||
base: '#643D8B',
|
||||
baseHighlighted: '#79559b',
|
||||
header: '#4E2877',
|
||||
headerHighlighted: '#643d8b',
|
||||
outline: '#4E2877',
|
||||
base: '#8B4DAB',
|
||||
baseHighlighted: '#A167C5',
|
||||
header: '#6B2D8B',
|
||||
headerHighlighted: '#8B4DAB',
|
||||
outline: '#6B2D8B',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#dbd0e4'
|
||||
text: '#FFFFFF'
|
||||
},
|
||||
visual: {
|
||||
base: '#315272',
|
||||
baseHighlighted: '#4d6784',
|
||||
header: '#173E5D',
|
||||
headerHighlighted: '#315272',
|
||||
outline: '#173E5D',
|
||||
base: '#4A7CA8',
|
||||
baseHighlighted: '#6496C2',
|
||||
header: '#2A5C88',
|
||||
headerHighlighted: '#4A7CA8',
|
||||
outline: '#2A5C88',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#cfd5de'
|
||||
text: '#FFFFFF'
|
||||
},
|
||||
data: {
|
||||
base: '#465524',
|
||||
baseHighlighted: '#5b6a37',
|
||||
header: '#314110',
|
||||
headerHighlighted: '#465524',
|
||||
outline: '#314110',
|
||||
base: '#6B8F3C',
|
||||
baseHighlighted: '#85A956',
|
||||
header: '#4B6F1C',
|
||||
headerHighlighted: '#6B8F3C',
|
||||
outline: '#4B6F1C',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#d2d6c5'
|
||||
text: '#FFFFFF'
|
||||
},
|
||||
javascript: {
|
||||
base: '#7E3660',
|
||||
baseHighlighted: '#944e74',
|
||||
header: '#67214B',
|
||||
headerHighlighted: '#7e3660',
|
||||
outline: '#67214B',
|
||||
base: '#B84D7C',
|
||||
baseHighlighted: '#CE6796',
|
||||
header: '#982D5C',
|
||||
headerHighlighted: '#B84D7C',
|
||||
outline: '#982D5C',
|
||||
outlineHighlighted: '#d57bab',
|
||||
text: '#e4cfd9'
|
||||
text: '#FFFFFF'
|
||||
},
|
||||
default: {
|
||||
base: '#4C4F59',
|
||||
baseHighlighted: '#62656e',
|
||||
header: '#373B45',
|
||||
headerHighlighted: '#4c4f59',
|
||||
outline: '#373B45',
|
||||
base: '#6C6F79',
|
||||
baseHighlighted: '#868993',
|
||||
header: '#4C4F59',
|
||||
headerHighlighted: '#6C6F79',
|
||||
outline: '#4C4F59',
|
||||
outlineHighlighted: '#b58900',
|
||||
text: '#d3d4d6'
|
||||
text: '#FFFFFF'
|
||||
}
|
||||
},
|
||||
connections: {
|
||||
|
||||
Reference in New Issue
Block a user