From e076c749ffb1e099cab4e768ade7a4aa4db49fd5 Mon Sep 17 00:00:00 2001 From: CTomlyn Date: Tue, 17 Sep 2024 06:49:17 -0700 Subject: [PATCH] =?UTF-8?q?#2146=20Add=20a=20Configuration=20flag=20to=20a?= =?UTF-8?q?llow=20a=20scroll=20bar=20for=20Annotations=20=E2=80=A6=20(#214?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: CTomlyn --- .../src/common-canvas/common-canvas-utils.js | 94 +++++ .../src/common-canvas/svg-canvas-d3.scss | 95 ++--- .../src/common-canvas/svg-canvas-renderer.js | 134 ++++--- .../svg-canvas-utils-drag-new-link.js | 3 + .../svg-canvas-utils-textarea.js | 268 +++++++------ .../src/object-model/layout-dimensions.js | 8 + .../harness/cypress/e2e/canvas/comment.cy.js | 20 +- .../cypress/support/canvas/comments-cmds.js | 35 +- .../support/canvas/verification-cmds.js | 2 +- .../wysiwyg-comments-canvas.jsx | 6 +- .../wysiwyg-comments-flow.json | 356 +++++++++--------- .../src/styles/canvas-customization.scss | 18 +- .../diagrams/markdownCanvas.json | 22 +- 13 files changed, 595 insertions(+), 466 deletions(-) diff --git a/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js b/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js index 814dbae2e8..1941c0a375 100644 --- a/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js +++ b/canvas_modules/common-canvas/src/common-canvas/common-canvas-utils.js @@ -1355,6 +1355,100 @@ export default class CanvasUtils { return (luma < 108); } + // Applies the outlineStyle format to the D3 comment selection passed in, + // if one exists, in the formats array passed in. + static applyOutlineStyle(commentSel, formats) { + if (formats?.length > 0) { + formats.forEach((f) => { + if (f.type === "outlineStyle") { // Only apply outline style to outer
+ const { field, value } = CanvasUtils.convertFormat(f); + commentSel.style(field, value); + } + }); + } + } + + // Applies all formats from the formats array, that are not outlineStyle, to the + // D3 comment selection passed in. + static applyNonOutlineStyle(commentSel, formats) { + if (formats?.length > 0) { + formats.forEach((f) => { + if (f.type !== "outlineStyle") { // Only apply outline style to outer
+ const { field, value } = CanvasUtils.convertFormat(f); + commentSel.style(field, value); + } + }); + } + } + + // Returns an object contaiing the start and end positions + // of any current selection in the domNode passed in. The + // DOM node is expected to contain text which is stored in a + // set of child nodes that are text objects. + static getSelectionPositions(domNode) { + const sel = window.getSelection(); + let anchorPos; + let focusPos; + let runningLen = 0; + domNode.childNodes.forEach((cn) => { + if (cn.nodeValue) { + const textLen = cn.nodeValue.length; + if (cn === sel.anchorNode) { + anchorPos = runningLen + sel.anchorOffset; + } + if (cn === sel.focusNode) { + focusPos = runningLen + sel.focusOffset; + } + runningLen += textLen; + } + }); + return { start: Math.min(anchorPos, focusPos), end: Math.max(anchorPos, focusPos) }; + } + + // Selects the entire contents of the DOM node passed in. + static selectNodeContents(domNode) { + var range = document.createRange(); + range.selectNodeContents(domNode); + + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + + // Selects the range of characters in the text DOM node passed in + // between the start and end positions passed in. The DOM node is + // expected to contain text which is stored in a set of child nodes + // that are text objects. selection is an optional object containing + // the current selection which is provided by the Cypress test cases. + static selectNodeRange(domNode, start, end, selection) { + const range = document.createRange(); + + let startTextNode; + let endTextNode; + let startTextPos; + let endTextPos; + let runningLen = 0; + domNode.childNodes.forEach((cn) => { + const textLen = cn.nodeValue.length; + runningLen += textLen; + if (start <= runningLen && !startTextNode) { + startTextNode = cn; + startTextPos = textLen - (runningLen - start); + } + if (end <= runningLen && !endTextNode) { + endTextNode = cn; + endTextPos = textLen - (runningLen - end); + } + }); + + range.setStart(startTextNode, startTextPos); + range.setEnd(endTextNode, endTextPos); + + const sel = selection ? selection : window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + // Returns an object containing a CSS field and value that // can be applied to a
contining text based on the // format type and action passed in. diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss index 1240006c09..091ce03250 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-d3.scss @@ -75,7 +75,8 @@ $node-port-input-arrow-connected-stroke-color: $background-inverse; $node-port-input-arrow-connected-fill-color: transparent; // Comment colors -$comment-outline-color: $layer-active-02; +$comment-border-color: $layer-active-02; +$comment-border-width: 1px; $comment-outline-hover-color: $background-inverse-hover; $comment-fill-color: $layer-01; $comment-text-color: $text-primary; @@ -109,7 +110,6 @@ $link-highlight-color: $support-info; .d3-foreign-object-ghost-label, .d3-foreign-object-node-label, .d3-foreign-object-dec-label, -.d3-foreign-object-comment-text, .d3-foreign-object-text-entry { // Don't handle events - let objects inside foreign object handle them. pointer-events: none; @@ -117,23 +117,12 @@ $link-highlight-color: $support-info; overflow: visible; } -.d3-foreign-object-comment-text-wysiwyg, -.d3-foreign-object-text-entry-wysiwyg { +.d3-foreign-object-comment-text { // Don't handle events - let objects inside foreign object handle them. pointer-events: none; - // We hide overflow with wysiwyg text editing overflow: hidden; } -.d3-foreign-object-text-entry-wysiwyg:focus { - outline: none; -} - -.d3-foreign-object-text-entry-wysiwyg:focus-within { - outline: none; - box-shadow: 0 0 0 2px $focus; -} - // Declare our own focus highlighting for text entry. @mixin d3-text-entry-focus-mixin { // Supress the default focus highlighting with non-carbon color and round corners. @@ -142,6 +131,15 @@ $link-highlight-color: $support-info; box-shadow: 0 0 0 2px $focus; } +.d3-foreign-object-text-entry:focus, +.d3-foreign-object-comment-text-entry:focus { + outline: none; +} + +.d3-foreign-object-comment-text-entry:focus-within { + @include d3-text-entry-focus-mixin; +} + /* Pull-out region rectangle used for object selection */ .d3-region-selector { @@ -646,30 +644,16 @@ $link-highlight-color: $support-info; /* Comment styles */ .d3-comment-group:hover { - .d3-comment-rect { - stroke: $comment-outline-hover-color + .d3-comment-text-scroll { + border-color: $comment-outline-hover-color; + border-width: 1px; + border-style: solid; } } -/* Style for default comment background rectangle */ -.d3-comment-rect { - fill: $comment-fill-color; - stroke: $comment-outline-color; - stroke-width: 1; - - /* Prevent elements from being selectable */ - /* This stops it being dragged incorrectly. */ - user-select: none; -} - @mixin d3-comment-color-overrides($background-color, $text-color) { - .d3-comment-rect { - fill: $background-color; - } - .d3-comment-text { - color: $text-color; - } - .d3-comment-entry { + .d3-comment-text, + .d3-comment-text-entry { background-color: $background-color; color: $text-color; } @@ -905,25 +889,20 @@ g.bkg-col-cyan-50 { word-break: break-word; } -.d3-comment-text { - @include d3-comment-mixin; - @include d3-comment-markdown; - border-width: $comment-text-display-border; - user-select: none; -} - /* Style for comment text entry when shown in an HTML textarea control. */ -.d3-comment-entry { - @include d3-comment-mixin; - background-color: $field-01; - border-width: $comment-text-entry-border; - - &:focus { - @include d3-text-entry-focus-mixin; - } +.d3-comment-text-scroll, +.d3-comment-text-entry-scroll { + pointer-events: unset; + overflow-x: hidden; + overflow-y: scroll; + border-color: $comment-border-color; + border-width: $comment-border-width; + border-style: solid; + height: 100%; + width: 100%; } -.d3-comment-text-wysiwyg-outer { +.d3-comment-text-outer { display: table; pointer-events: none; overflow: hidden; @@ -931,18 +910,20 @@ g.bkg-col-cyan-50 { height: 100%; } -.d3-comment-text-wysiwyg { +.d3-comment-text { @include d3-comment-mixin; background-color: $comment-fill-color; padding: $comment-text-display-border; - border-color: $comment-outline-color; - border-width: 1px; user-select: none; overflow: hidden; display: table-cell; + + &.markdown { + @include d3-comment-markdown; + } } -.d3-comment-text-entry-wysiwyg-outer { +.d3-comment-text-entry-outer { display: table; pointer-events: none; overflow: hidden; @@ -951,10 +932,10 @@ g.bkg-col-cyan-50 { outline: none; } -.d3-comment-text-entry-wysiwyg { +.d3-comment-text-entry { @include d3-comment-mixin; padding: $comment-text-display-border; - border-width: 1px; + border-width: $comment-border-width; background-color: $field-01; outline: none; user-select: none; @@ -963,8 +944,6 @@ g.bkg-col-cyan-50 { cursor: text; } - - /* Link styles for branch highlighting */ .d3-link-group.d3-branch-highlight { diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js index 43971dafe7..aa7f0c6aae 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-renderer.js @@ -107,7 +107,9 @@ export default class SVGCanvasRenderer { this.removeTempCursorOverlay.bind(this), // Function this.displayComments.bind(this), // Function this.displayLinks.bind(this), // Function - this.getCommentToolbarPos.bind(this) // Function + this.getCommentToolbarPos.bind(this), // Function + this.addCanvasZoomBehavior.bind(this), // Function + this.removeCanvasZoomBehavior.bind(this) // Function ); this.dispUtils.setDisplayState(); @@ -283,7 +285,9 @@ export default class SVGCanvasRenderer { this.removeTempCursorOverlay.bind(this), // Function this.displayComments.bind(this), // Function this.displayLinks.bind(this), // Function - this.getCommentToolbarPos.bind(this) // Function + this.getCommentToolbarPos.bind(this), // Function + this.addCanvasZoomBehavior.bind(this), // Function + this.removeCanvasZoomBehavior.bind(this) // Function ); @@ -325,7 +329,6 @@ export default class SVGCanvasRenderer { this.superRenderers.forEach((renderer) => { renderer.setSelectionInfo(selectionInfo); }); - } // Returns a subset of renderers, from the current set of super renderers, @@ -1293,7 +1296,7 @@ export default class SVGCanvasRenderer { // the populated case we do. resetCanvasSVGBehaviors() { // Remove zoom behaviors from canvasSVG area. - this.canvasSVG.on(".zoom", null); + this.removeCanvasZoomBehavior(); // If there are no nodes or comments we don't apply any zoom behaviors // to the SVG area. We only attach the zoom behaviour to the top most SVG @@ -1301,8 +1304,7 @@ export default class SVGCanvasRenderer { // or a sub-pipeline full page. if (!this.activePipeline.isEmptyOrBindingsOnly() && this.dispUtils.isDisplayingFullPage()) { - this.canvasSVG - .call(this.zoomUtils.getZoomHandler()); + this.canvasSVG.call(this.zoomUtils.getZoomHandler()); } // These behaviors will be applied to SVG areas at the top level and @@ -1343,6 +1345,17 @@ export default class SVGCanvasRenderer { }); } + // When adding back zoom behavior we need to reset all canvas behaviors + // because the ".zoom" events will have been removed from this.canvasSVG + // whenn removeCanvasZoomBehavior was called. + addCanvasZoomBehavior() { + this.resetCanvasSVGBehaviors(); + } + + removeCanvasZoomBehavior() { + this.canvasSVG.on(".zoom", null); + } + // Resets the pointer cursor on the background rectangle in the Canvas SVG area. resetCanvasCursor() { const selector = ".d3-svg-background[data-pipeline-id='" + this.activePipeline.id + "']"; @@ -3742,67 +3755,24 @@ export default class SVGCanvasRenderer { .attr("data-selected", (c) => (this.activePipeline.isSelected(c.id) ? "yes" : "no")) .attr("style", (d) => this.getNodeSelectionOutlineStyle(d, "default")); - // Comment Body - only used for regular/markdown comments - joinedCommentGrps - .selectChildren(".d3-comment-rect") - .data((c) => (c.contentType !== WYSIWYG ? [c] : []), (c) => c.id) - .join( - (enter) => - enter - .append("rect") - .attr("class", "d3-comment-selection-highlight") - ) - .datum((c) => this.activePipeline.getComment(c.id)) - .attr("height", (c) => c.height) - .attr("width", (c) => c.width) - .attr("class", "d3-comment-rect") - .attr("style", (c) => this.getCommentBodyStyle(c, "default")); - - - // Comment Text - for regular/markdown comment + // Comment Text joinedCommentGrps .selectChildren(".d3-foreign-object-comment-text") - .data((c) => (c.contentType !== WYSIWYG ? [c] : []), (c) => c.id) + .data((d) => [d], (d) => d.id) .join( (enter) => { const fo = enter .append("foreignObject") - .attr("class", "d3-foreign-object-comment-text") + .attr("class", (d) => "d3-foreign-object-comment-text") .attr("x", 0) .attr("y", 0); fo .append("xhtml:div") // Provide a namespace when div is inside foreignObject - .attr("class", "d3-comment-text"); - return fo; - } - ) - .datum((c) => this.activePipeline.getComment(c.id)) - .attr("width", (c) => c.width) - .attr("height", (c) => c.height) - .select("div") - .attr("style", (c) => this.getCommentTextStyle(c, "default")) - .html((c) => ( - this.config.enableMarkdownInComments - ? markdownIt.render(c.content) - : escapeText(c.content)) - ); - - // Comment Text - for WYSIWYG comment - joinedCommentGrps - .selectChildren(".d3-foreign-object-comment-text-wysiwyg") - .data((c) => (c.contentType === WYSIWYG ? [c] : []), (c) => c.id) - .join( - (enter) => { - const fo = enter - .append("foreignObject") - .attr("class", "d3-foreign-object-comment-text-wysiwyg") - .attr("x", 0) - .attr("y", 0); - fo + .attr("class", "d3-comment-text-scroll") .append("xhtml:div") // Provide a namespace when div is inside foreignObject - .attr("class", "d3-comment-text-wysiwyg-outer") + .attr("class", "d3-comment-text-outer") .append("xhtml:div") // Provide a namespace when div is inside foreignObject - .attr("class", "d3-comment-text-wysiwyg"); + .attr("class", (d) => "d3-comment-text" + (d.contentType !== WYSIWYG ? " markdown" : "")); return fo; } ) @@ -3810,22 +3780,27 @@ export default class SVGCanvasRenderer { .attr("width", (c) => c.width) .attr("height", (c) => c.height) - .select(".d3-comment-text-wysiwyg-outer") + .select(".d3-comment-text-scroll") + .each((d, i, commentTexts) => { + const commentElement = d3.select(commentTexts[i]); + CanvasUtils.applyOutlineStyle(commentElement, d.formats); // Only apply outlineStyle format here + }) - .select(".d3-comment-text-wysiwyg") - .attr("style", null) // Wipe the in-line styles before applying + .select(".d3-comment-text-outer") + .select(".d3-comment-text") + .attr("style", null) // Wipe the in-line styles before applying formats .each((d, i, commentTexts) => { - if (d.formats?.length > 0) { - d.formats.forEach((f) => { - const { field, value } = CanvasUtils.convertFormat(f); - d3.select(commentTexts[i]).style(field, value); - }); - } + const commentElement = d3.select(commentTexts[i]); + CanvasUtils.applyNonOutlineStyle(commentElement, d.formats); // Apply all formats except outlineStyle + }) - // .attr("style", (c) => this.getCommentTextStyle(c, "default")) - .html((c) => escapeText(c.content)); + .html((d) => + (d.contentType !== WYSIWYG && this.config.enableMarkdownInComments + ? markdownIt.render(d.content) + : escapeText(d.content)) + ); // Add or remove drag object behavior for the comment groups. if (this.config.enableEditingActions) { @@ -3851,6 +3826,9 @@ export default class SVGCanvasRenderer { if (this.config.enableContextToolbar) { this.addContextToolbar(d3Event, d, "comment"); } + if (this.commentHasScrollableText(d3Event.currentTarget)) { + this.removeCanvasZoomBehavior(); // Remove canvas zoom behavior to allow scrolling of comment + } }) .on("mouseleave", (d3Event, d) => { if (this.config.enableContextToolbar) { @@ -3859,6 +3837,8 @@ export default class SVGCanvasRenderer { if (this.config.enableEditingActions) { this.deleteCommentPort(d3Event.currentTarget); } + + this.addCanvasZoomBehavior(); // Add back zoom behavior to reenable canvas zooming }) // Use mouse down instead of click because it gets called before drag start. .on("mousedown", (d3Event, d) => { @@ -3904,6 +3884,23 @@ export default class SVGCanvasRenderer { }); } + // Returns true if the comment has scrollable text or not. That is if the contents + // of the comment's scroll
is bigger that the scroll
can accommodate. + // When a comment is being edited, it will have a foreignObject containing its own + // scroll
over the top of the foreignObject used to display the comment. + commentHasScrollableText(element) { + // Look for entry foreign object first because, if present it will be over the top + // of the display foreign object and it will be handling scrollable text. + let scrollDiv = element.getElementsByClassName("d3-comment-text-entry-scroll"); + if (!scrollDiv[0]) { + scrollDiv = element.getElementsByClassName("d3-comment-text-scroll"); + } + if (scrollDiv[0].clientHeight < scrollDiv[0].scrollHeight) { + return true; + } + return false; + } + attachCommentSizingListeners(commentGrps) { commentGrps .on("mousedown", (d3Event, d) => { @@ -3963,7 +3960,7 @@ export default class SVGCanvasRenderer { setCommentBodyStyles(d, type, comGrp) { const style = this.getCommentBodyStyle(d, type); - comGrp.selectChildren(".d3-comment-rect").attr("style", style); + comGrp.selectChildren(".d3-comment-text").attr("style", style); } setCommentTextStyles(d, type, comGrp) { @@ -3985,6 +3982,7 @@ export default class SVGCanvasRenderer { } displayCommentTextArea(comment, parentDomObj) { + comment.autoSize = this.canvasLayout.commentAutoSize; // TODO - read from comment layout when canvas layout is refactored. this.svgCanvasTextArea.displayCommentTextArea(comment, parentDomObj); } @@ -4468,7 +4466,7 @@ export default class SVGCanvasRenderer { // If the comment has a classname that isn't the default use it! if (d.class_name && d.class_name !== "canvas-comment" && - d.class_name !== "d3-comment-rect") { + d.class_name !== "d3-comment-rect") { // Left in for historical reasons customClass = " " + d.class_name; } diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js index 78f06b82ce..0260fb706f 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-drag-new-link.js @@ -495,6 +495,9 @@ export default class SVGCanvasUtilsDragNewLink { // Draws a 'snap-back' link with a rubber-band effect that // animates the cancellation of a new link's creation. stopDrawingNewLinkForPorts(drawingNewLinkData) { + if (drawingNewLinkData.linkArray?.length === 0) { + return; + } let saveX1 = drawingNewLinkData.linkArray[0].x1; let saveY1 = drawingNewLinkData.linkArray[0].y1; let saveX2 = drawingNewLinkData.linkArray[0].x2; diff --git a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-textarea.js b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-textarea.js index 70fe924664..4ff9bef9f1 100644 --- a/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-textarea.js +++ b/canvas_modules/common-canvas/src/common-canvas/svg-canvas-utils-textarea.js @@ -14,6 +14,7 @@ * limitations under the License. */ /* eslint no-invalid-this: "off" */ +/* eslint no-lonely-if: "off" */ import * as d3Selection from "d3-selection"; const d3 = Object.assign({}, d3Selection); @@ -46,7 +47,8 @@ const RAB_KEY = 190; // Right angle bracket > const SEVEN_KEY = 55; const EIGHT_KEY = 56; -const SCROLL_PADDING = 12; +const SCROLL_PADDING_COMMENT = 2; +const SCROLL_PADDING_LABEL = 12; const WHITE = "#FFFFFF"; const BLACK = "#000000"; @@ -55,7 +57,8 @@ export default class SvgCanvasTextArea { constructor(config, dispUtils, nodeUtils, decUtils, canvasController, canvasDiv, activePipeline, removeTempCursorOverlay, - displayCommentsCallback, displayLinksCallback, getCommentToolbarPosCallback) { + displayCommentsCallback, displayLinksCallback, getCommentToolbarPosCallback, + addCanvasZoomBehavior, removeCanvasZoomBehavior) { this.config = config; this.dispUtils = dispUtils; @@ -68,6 +71,8 @@ export default class SvgCanvasTextArea { this.displayCommentsCallback = displayCommentsCallback; this.displayLinksCallback = displayLinksCallback; this.getCommentToolbarPosCallback = getCommentToolbarPosCallback; + this.addCanvasZoomBehavior = addCanvasZoomBehavior; + this.removeCanvasZoomBehavior = removeCanvasZoomBehavior; this.logger = new Logger("SvgCanvasTextArea"); @@ -102,10 +107,10 @@ export default class SvgCanvasTextArea { yPos: 0, width: d.width, height: d.height, + autoSize: d.autoSize, // TODO - read from comment layout when canvas layout is refactored. contentType: d.contentType, formats: d.formats, newFormats: cloneDeep(d.formats), - className: "d3-comment-entry", parentDomObj: parentDomObj, keyboardInputCallback: d.contentType !== WYSIWYG && this.config.enableMarkdownInComments ? this.commentKeyboardHandler.bind(this) @@ -115,14 +120,7 @@ export default class SvgCanvasTextArea { closeTextAreaCallback: this.closeCommentTextArea.bind(this) }; - - // A WYSIWG comment is edited by making the inner
editable. - // Regular comments are edited with a textarea. - if (d.contentType === WYSIWYG) { - this.displayEditableDiv(this.editingTextData); - } else { - this.displayTextArea(this.editingTextData); - } + this.displayEditableComment(this.editingTextData); if (this.dispUtils.isDisplayingFullPage()) { const pos = this.getCommentToolbarPosCallback(d); @@ -171,10 +169,9 @@ export default class SvgCanvasTextArea { // If there is a relatedTarget and it is set to one of the elements for the // textarea, texttoolbar, etc we ignore the blur event. if (evt.relatedTarget && - (CanvasUtils.getParentElementWithClass(evt.relatedTarget, "d3-comment-entry") || - CanvasUtils.getParentElementWithClass(evt.relatedTarget, "d3-comment-text-entry-wysiwyg") || - CanvasUtils.getParentElementWithClass(evt.relatedTarget, "text-toolbar") || - CanvasUtils.getParentElementWithClass(evt.relatedTarget, "cds--overflow-menu-options__btn"))) { + (CanvasUtils.getParentElementWithClass(evt.relatedTarget, "d3-comment-text-entry") || + CanvasUtils.getParentElementWithClass(evt.relatedTarget, "text-toolbar") || + CanvasUtils.getParentElementWithClass(evt.relatedTarget, "cds--overflow-menu-options__btn"))) { return; } @@ -213,16 +210,19 @@ export default class SvgCanvasTextArea { markdownActionHandler(action, evt) { this.logger.log("markdownActionHandler - action = " + action); - const commentEntry = this.canvasDiv.selectAll(".d3-comment-entry"); - const commentEntryElement = commentEntry.node(); - const start = commentEntryElement.selectionStart; - const end = commentEntryElement.selectionEnd; - const text = commentEntryElement.value; + const textDiv = this.canvasDiv.selectAll(".d3-comment-text-entry") + .node(); + + const pos = CanvasUtils.getSelectionPositions(textDiv); - const mdObj = SvgCanvasMarkdown.processMarkdownAction(action, text, start, end); - if (mdObj) { - evt.preventDefault(); - this.addTextToTextArea(mdObj, commentEntryElement); + const text = textDiv.innerText; + + if (text) { // In Firefox, text can sometimes be null when adding newlines. + const mdObj = SvgCanvasMarkdown.processMarkdownAction(action, text, pos.start, pos.end); + if (mdObj) { + evt.preventDefault(); + this.addTextToTextArea(mdObj, textDiv); + } } } @@ -280,7 +280,6 @@ export default class SvgCanvasTextArea { textColorFormat.value === BLACK) { const isDark = CanvasUtils.isDarkColor(backgroundColor); this.addReplaceFormat("textColor", (isDark ? WHITE : BLACK)); - } } @@ -360,49 +359,71 @@ export default class SvgCanvasTextArea { setWysiwygStyle(field, value) { if (field === "background-color" && value === "transparent") { - d3.select(this.editingTextData.parentDomObj).selectAll(".d3-comment-text-wysiwyg") + d3.select(this.editingTextData.parentDomObj).selectAll(".d3-comment-text") .style("background-color", "transparent"); } - const commentEntry = this.foreignObjectWysiwyg.selectAll(".d3-comment-text-entry-wysiwyg"); + const commentEntry = this.foreignObjectComment.selectAll(".d3-comment-text-entry"); commentEntry.style(field, value); const commentEntryElement = commentEntry.node(); commentEntryElement.focus(); } - // Replaces the text in the currently displayed textarea with the text + // Replaces the text in the currently displayed
with the text // passed in. We use execCommand because this adds the inserted text to the // textarea's undo/redo stack whereas setting the text directly into the // textarea control does not. addTextToTextArea(mdObj, commentEntryElement) { this.addingTextToTextArea = true; - const text = unescapeText(mdObj.newText); + const newText = unescapeText(mdObj.newText); + commentEntryElement.focus(); - commentEntryElement.select(); - document.execCommand("insertText", false, text); - commentEntryElement.setSelectionRange(mdObj.newStart, mdObj.newEnd); + CanvasUtils.selectNodeContents(commentEntryElement); + + document.execCommand("insertText", false, newText); + CanvasUtils.selectNodeRange(commentEntryElement, mdObj.newStart, mdObj.newEnd); + this.addingTextToTextArea = false; } autoSizeComment(textArea, data) { this.logger.log("autoSizeComment - textAreaHt = " + this.textAreaHeight + " scroll ht = " + textArea.scrollHeight); - const pad = data.contentType === WYSIWYG ? 0 : SCROLL_PADDING; - const scrollHeight = textArea.scrollHeight + pad; + if (data.autoSize) { + const pad = this.foreignObjectLabel ? SCROLL_PADDING_LABEL : SCROLL_PADDING_COMMENT; + const scrollHeight = textArea.scrollHeight + pad; - if (this.textAreaHeight < scrollHeight) { - this.textAreaHeight = scrollHeight; - if (this.foreignObject) { - this.foreignObject.style("height", this.textAreaHeight + "px"); + if (this.textAreaHeight < scrollHeight) { + this.textAreaHeight = scrollHeight; + if (this.foreignObjectLabel) { + this.foreignObjectLabel.style("height", this.textAreaHeight + "px"); - } else if (this.foreignObjectWysiwyg) { - this.foreignObjectWysiwyg.style("height", this.textAreaHeight + "px"); + } else if (this.foreignObjectComment) { + this.foreignObjectComment.style("height", this.textAreaHeight + "px"); + } + this.activePipeline.getComment(data.id).height = this.textAreaHeight; + this.displayCommentsCallback(); + this.displayLinksCallback(); + } + } else { + if (this.commentHasScrollableText(data.parentDomObj)) { + this.removeCanvasZoomBehavior(); + } else { + this.addCanvasZoomBehavior(); } - this.activePipeline.getComment(data.id).height = this.textAreaHeight; - this.displayCommentsCallback(); - this.displayLinksCallback(); } } + // Returns true if the scroll
, in the foreignObject used for text entry, + // has contents that are bigger than what the scroll
can accommodate. + commentHasScrollableText(element) { + const scrollDiv = element.getElementsByClassName("d3-comment-text-entry-scroll"); + + if (scrollDiv[0].clientHeight < scrollDiv[0].scrollHeight) { + return true; + } + return false; + } + saveCommentChanges(taData, newText, newHeight) { const comment = this.activePipeline.getComment(taData.id); const data = { @@ -444,18 +465,19 @@ export default class SvgCanvasTextArea { yPos: this.nodeUtils.getNodeLabelTextAreaPosY(node), width: this.nodeUtils.getNodeLabelTextAreaWidth(node), height: this.nodeUtils.getNodeLabelTextAreaHeight(node), + autoSize: true, className: this.nodeUtils.getNodeLabelTextAreaClass(node), parentDomObj: parentDomObj, autoSizeCallback: this.autoSizeMultiLineLabel.bind(this), saveTextChangesCallback: this.saveNodeLabelChanges.bind(this), closeTextAreaCallback: this.closeEntryTextArea.bind(this) }; - this.displayTextArea(this.editingTextData); + this.displayEditableLabel(this.editingTextData); } // Increases the size of the editable multi-line text area for a label based - // on the characters entered, and also ensures the maximum number of - // characters for the label, if one is provided, is not exceeded. + // on the characters entered, and also ensures the maximum number of + // characters for the label, if one is provided, is not exceeded. // This callback works for editable multi-line node labels and also // editable multi-line text decorations for either nodes or links. autoSizeMultiLineLabel(textArea, data) { @@ -469,11 +491,11 @@ export default class SvgCanvasTextArea { // Temporarily set the height to zero so the scrollHeight will get set to // the full height of the text in the textarea. This allows us to close up // the text area when the lines of text reduce. - if (this.foreignObject) { - this.foreignObject.style("height", 0); - const scrollHeight = textArea.scrollHeight + SCROLL_PADDING; + if (this.foreignObjectLabel) { + this.foreignObjectLabel.style("height", 0); + const scrollHeight = textArea.scrollHeight + SCROLL_PADDING_LABEL; this.textAreaHeight = scrollHeight; - this.foreignObject.style("height", this.textAreaHeight + "px"); + this.foreignObjectLabel.style("height", this.textAreaHeight + "px"); } } @@ -495,7 +517,7 @@ export default class SvgCanvasTextArea { this.displayDiv.attr("style", this.displayDivStyle); } - // Displays a text area for an editable text decoration on either a node + // Displays a