diff --git a/packages/joint-core/src/dia/ToolView.mjs b/packages/joint-core/src/dia/ToolView.mjs index 6bda88230..fe546ff26 100644 --- a/packages/joint-core/src/dia/ToolView.mjs +++ b/packages/joint-core/src/dia/ToolView.mjs @@ -6,6 +6,7 @@ export const ToolView = mvc.View.extend({ className: 'tool', svgElement: true, _visible: true, + _visibleExplicit: true, init: function() { var name = this.name; @@ -30,16 +31,40 @@ export const ToolView = mvc.View.extend({ return this.name; }, + // Evaluate the visibility of the tool and update the `display` CSS property + updateVisibility: function() { + const isVisible = this.computeVisibility(); + this.el.style.display = isVisible ? '' : 'none'; + this._visible = isVisible; + }, + + // Evaluate the visibility of the tool. The method returns `true` if the tool + // should be visible in the DOM. + computeVisibility() { + if (!this.isExplicitlyVisible()) return false; + const { visibility } = this.options; + if (typeof visibility !== 'function') return true; + return !!visibility.call(this, this.relatedView, this); + }, + show: function() { - this.el.style.display = ''; - this._visible = true; + this._visibleExplicit = true; + this.updateVisibility(); }, hide: function() { - this.el.style.display = 'none'; - this._visible = false; + this._visibleExplicit = false; + this.updateVisibility(); + }, + + // The method returns `false` if the `hide()` method was called on the tool. + isExplicitlyVisible: function() { + return !!this._visibleExplicit; }, + // The method returns `false` if the tool is not visible (it has `display: none`). + // This can happen if the `hide()` method was called or the tool is not visible + // because of the `visibility` option was evaluated to `false`. isVisible: function() { return !!this._visible; }, diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs index 6031c3723..32036c72f 100644 --- a/packages/joint-core/src/dia/ToolsView.mjs +++ b/packages/joint-core/src/dia/ToolsView.mjs @@ -49,10 +49,12 @@ export const ToolsView = mvc.View.extend({ var isRendered = this.isRendered; for (var i = 0, n = tools.length; i < n; i++) { var tool = tools[i]; + tool.updateVisibility(); + if (!tool.isVisible()) continue; if (!isRendered) { // First update executes render() tool.render(); - } else if (opt.tool !== tool.cid && tool.isVisible()) { + } else if (opt.tool !== tool.cid) { tool.update(); } } @@ -87,9 +89,12 @@ export const ToolsView = mvc.View.extend({ if (!tools) return this; for (var i = 0, n = tools.length; i < n; i++) { var tool = tools[i]; - if (tool !== blurredTool && !tool.isVisible()) { + if (tool !== blurredTool && !tool.isExplicitlyVisible()) { tool.show(); - tool.update(); + // Check if the tool is conditionally visible too + if (tool.isVisible()) { + tool.update(); + } } } return this; diff --git a/packages/joint-core/test/jointjs/dia/linkTools.js b/packages/joint-core/test/jointjs/dia/linkTools.js index 9d1c1840a..5301baba9 100644 --- a/packages/joint-core/test/jointjs/dia/linkTools.js +++ b/packages/joint-core/test/jointjs/dia/linkTools.js @@ -63,6 +63,124 @@ QUnit.module('linkTools', function(hooks) { }); }); + QUnit.module('Visibility', function() { + + QUnit.test('isVisible()', function(assert) { + const remove = new joint.linkTools.Remove(); + assert.ok(remove.isVisible()); + remove.hide(); + assert.notOk(remove.isVisible()); + remove.show(); + assert.ok(remove.isVisible()); + }); + + QUnit.test('updateVisibility()', function(assert) { + const remove = new joint.linkTools.Remove(); + const toolsView = new joint.dia.ToolsView({ tools: [remove] }); + linkView.addTools(toolsView); + assert.notEqual(getComputedStyle(remove.el).display, 'none'); + remove.hide(); + assert.equal(getComputedStyle(remove.el).display, 'none'); + remove.show(); + assert.notEqual(getComputedStyle(remove.el).display, 'none'); + }); + + QUnit.module('option: visibility', function(assert) { + + QUnit.test('is visible or hidden', function(assert) { + let isVisible = true; + const visibilitySpy = sinon.spy(() => isVisible); + const removeButton = new joint.linkTools.Remove({ + visibility: visibilitySpy + }); + const otherButton = new joint.linkTools.Button(); + const toolsView = new joint.dia.ToolsView({ + tools: [ + removeButton, + otherButton + ] + }); + linkView.addTools(toolsView); + + // Initial state. + assert.notEqual(getComputedStyle(removeButton.el).display, 'none'); + assert.ok(removeButton.isVisible()); + assert.equal(visibilitySpy.callCount, 1); + assert.ok(visibilitySpy.calledWithExactly(linkView, removeButton)); + assert.ok(visibilitySpy.calledOn(removeButton)); + + // Visibility function should be called on update. + isVisible = false; + toolsView.update(); + assert.equal(getComputedStyle(removeButton.el).display, 'none'); + assert.notOk(removeButton.isVisible()); + assert.ok(removeButton.isExplicitlyVisible()); + assert.equal(visibilitySpy.callCount, 2); + assert.ok(visibilitySpy.calledWithExactly(linkView, removeButton)); + assert.ok(visibilitySpy.calledOn(removeButton)); + + // Other button should not be affected by the visibility function. + assert.notEqual(getComputedStyle(otherButton.el).display, 'none'); + assert.ok(otherButton.isVisible()); + + // Focus & blur on other button should not change the visibility of + // the remove button. + toolsView.focusTool(otherButton); + assert.equal(getComputedStyle(removeButton.el).display, 'none'); + assert.notOk(removeButton.isVisible()); + assert.notOk(removeButton.isExplicitlyVisible()); + toolsView.blurTool(otherButton); + assert.equal(getComputedStyle(removeButton.el).display, 'none'); + assert.notOk(removeButton.isVisible()); + assert.ok(removeButton.isExplicitlyVisible()); + + isVisible = true; + toolsView.update(); + toolsView.focusTool(otherButton); + assert.equal(getComputedStyle(removeButton.el).display, 'none'); + assert.notOk(removeButton.isVisible()); + assert.notOk(removeButton.isExplicitlyVisible()); + toolsView.blurTool(otherButton); + assert.notEqual(getComputedStyle(removeButton.el).display, 'none'); + assert.ok(removeButton.isVisible()); + assert.ok(removeButton.isExplicitlyVisible()); + }); + + QUnit.test('it\'s not updated when hidden', function(assert) { + const button1 = new joint.linkTools.Button({ + visibility: () => false + }); + const button2 = new joint.linkTools.Button({ + visibility: () => true + }); + const button1UpdateSpy = sinon.spy(button1, 'update'); + const button2UpdateSpy = sinon.spy(button2, 'update'); + const toolsView = new joint.dia.ToolsView({ + tools: [button1, button2] + }); + linkView.addTools(toolsView); + assert.equal(button1.update.callCount, 0); + assert.equal(button2.update.callCount, 1); + toolsView.update(); + assert.equal(button1.update.callCount, 0); + assert.equal(button2.update.callCount, 2); + button1.show(); + button2.hide(); + toolsView.update(); + assert.equal(button1.update.callCount, 0); + assert.equal(button2.update.callCount, 2); + toolsView.focusTool(null); // hide all + assert.equal(button1.update.callCount, 0); + assert.equal(button2.update.callCount, 2); + toolsView.blurTool(null); // show all + assert.equal(button1.update.callCount, 0); + assert.equal(button2.update.callCount, 3); + button1UpdateSpy.restore(); + button2UpdateSpy.restore(); + }); + }); + }); + QUnit.module('TargetAnchor', function() { [{ resetAnchor: true, diff --git a/packages/joint-core/test/ts/toolsView.test.ts b/packages/joint-core/test/ts/toolsView.test.ts index 1e1b2d446..a5c3c8afc 100644 --- a/packages/joint-core/test/ts/toolsView.test.ts +++ b/packages/joint-core/test/ts/toolsView.test.ts @@ -26,6 +26,14 @@ const toolsView = new dia.ToolsView({ toolsView.configure({ component: false }); +new linkTools.Button({ + visibility: (view) => view.getConnectionLength() > 100, +}); + +new elementTools.Button({ + visibility: (view) => view.model.size().width > 100, +}); + interface RadiusControlOptions extends elementTools.Control.Options { testOption?: number; } diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index f76b01b04..8d4a85702 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -1871,21 +1871,25 @@ export namespace dia { } namespace ToolView { - interface Options extends mvc.ViewOptions { + + type VisibilityCallback = (this: ToolView, view: V, tool: ToolView) => boolean; + + interface Options extends mvc.ViewOptions { focusOpacity?: number; + visibility?: VisibilityCallback; } } - class ToolView extends mvc.View { + class ToolView extends mvc.View { name: string | null; parentView: ToolsView; relatedView: dia.CellView; paper: Paper; - constructor(opt?: ToolView.Options); + constructor(opt?: ToolView.Options); - configure(opt?: ToolView.Options): this; + configure(opt?: ToolView.Options): this; protected simulateRelatedView(el: SVGElement): void; @@ -1895,6 +1899,12 @@ export namespace dia { isVisible(): boolean; + isExplicitlyVisible(): boolean; + + updateVisibility(): void; + + protected computeVisibility(): boolean; + focus(): void; blur(): void; @@ -4141,9 +4151,9 @@ export namespace elementTools { namespace Button { - type ActionCallback = (evt: dia.Event, view: dia.ElementView, tool: dia.ToolView) => void; + type ActionCallback = (evt: dia.Event, view: dia.ElementView, tool: Button) => void; - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { x?: number | string; y?: number | string; offset?: { x?: number, y?: number }; @@ -4155,7 +4165,7 @@ export namespace elementTools { } } - class Button extends dia.ToolView { + class Button extends dia.ToolView { constructor(opt?: Button.Options); @@ -4197,20 +4207,20 @@ export namespace elementTools { } namespace Boundary { - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { padding?: number | dia.Sides; useModelGeometry?: boolean; rotate?: boolean; } } - class Boundary extends dia.ToolView { + class Boundary extends dia.ToolView { constructor(opt?: Boundary.Options); } namespace Control { - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { selector?: string | null; padding?: number; handleAttributes?: Partial; @@ -4218,7 +4228,7 @@ export namespace elementTools { } } - abstract class Control = Control.Options> extends dia.ToolView { + abstract class Control = Control.Options> extends dia.ToolView { options: T; constructor(opt?: T); @@ -4249,7 +4259,7 @@ export namespace elementTools { } } - class HoverConnect extends linkTools.Connect { + class HoverConnect extends Connect { constructor(opt?: HoverConnect.Options); } @@ -4280,7 +4290,7 @@ export namespace linkTools { interactiveLineNode: string; } - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { handleClass?: typeof VertexHandle; snapRadius?: number; redundancyRemoval?: boolean; @@ -4292,7 +4302,7 @@ export namespace linkTools { } } - class Vertices extends dia.ToolView { + class Vertices extends dia.ToolView { constructor(opt?: Vertices.Options); } @@ -4308,7 +4318,7 @@ export namespace linkTools { protected onPointerUp(evt: dia.Event): void; } - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { handleClass?: typeof SegmentHandle; snapRadius?: number; snapHandle?: boolean; @@ -4320,19 +4330,19 @@ export namespace linkTools { } } - class Segments extends dia.ToolView { + class Segments extends dia.ToolView { constructor(opt?: Segments.Options); } namespace Arrowhead { - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { scale?: number; } } - abstract class Arrowhead extends dia.ToolView { + abstract class Arrowhead extends dia.ToolView { ratio: number; arrowheadType: string; @@ -4357,7 +4367,7 @@ export namespace linkTools { } namespace Anchor { - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { snap?: AnchorCallback; anchor?: AnchorCallback; resetAnchor?: boolean | anchors.AnchorJSON; @@ -4371,7 +4381,7 @@ export namespace linkTools { } } - abstract class Anchor extends dia.ToolView { + abstract class Anchor extends dia.ToolView { type: string; @@ -4396,7 +4406,7 @@ export namespace linkTools { type DistanceCallback = (this: Button, view: dia.LinkView, tool: Button) => Distance; - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { distance?: Distance | DistanceCallback; offset?: number; rotate?: boolean; @@ -4406,7 +4416,7 @@ export namespace linkTools { } } - class Button extends dia.ToolView { + class Button extends dia.ToolView { constructor(opt?: Button.Options); @@ -4440,13 +4450,13 @@ export namespace linkTools { } namespace Boundary { - interface Options extends dia.ToolView.Options { + interface Options extends dia.ToolView.Options { padding?: number | dia.Sides; useModelGeometry?: boolean; } } - class Boundary extends dia.ToolView { + class Boundary extends dia.ToolView { constructor(opt?: Boundary.Options); }