diff --git a/packages/joint-core/src/dia/ToolView.mjs b/packages/joint-core/src/dia/ToolView.mjs index e42b8b5e0..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,31 +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() { - let isVisible; - if (this.isVisible()) { - const { visibility } = this.options; - if (typeof visibility === 'function') { - isVisible = !!visibility.call(this, this.relatedView, this); - } else { - isVisible = true; - } - } else { - isVisible = false; - } + 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._visible = true; + this._visibleExplicit = true; this.updateVisibility(); }, hide: function() { - 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 31a952e57..32036c72f 100644 --- a/packages/joint-core/src/dia/ToolsView.mjs +++ b/packages/joint-core/src/dia/ToolsView.mjs @@ -49,13 +49,14 @@ 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(); } - tool.updateVisibility(); } if (!this.isMounted()) { this.mount(); @@ -88,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 498401ebf..14dd7b99c 100644 --- a/packages/joint-core/test/jointjs/dia/linkTools.js +++ b/packages/joint-core/test/jointjs/dia/linkTools.js @@ -85,58 +85,99 @@ QUnit.module('linkTools', function(hooks) { assert.notEqual(getComputedStyle(remove.el).display, 'none'); }); - QUnit.test('option: visibility', function(assert) { - let isVisible = true; - const visibilitySpy = sinon.spy(() => isVisible); - const removeButton = new joint.linkTools.Remove({ - visibility: visibilitySpy + 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()); }); - 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.ok(removeButton.isVisible()); - 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()); - toolsView.blurTool(otherButton); - assert.equal(getComputedStyle(removeButton.el).display, 'none'); - assert.ok(removeButton.isVisible()); - - isVisible = true; - toolsView.update(); - toolsView.focusTool(otherButton); - assert.equal(getComputedStyle(removeButton.el).display, 'none'); - assert.notOk(removeButton.isVisible()); - toolsView.blurTool(otherButton); - assert.notEqual(getComputedStyle(removeButton.el).display, 'none'); - assert.ok(removeButton.isVisible()); + 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(); + }); }); }); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 2bdf7fdd0..ddba4a6f5 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -1899,6 +1899,12 @@ export namespace dia { isVisible(): boolean; + isExplicitlyVisible(): boolean; + + updateVisibility(): void; + + protected computeVisibility(): boolean; + focus(): void; blur(): void;