diff --git a/packages/joint-core/src/mvc/View.mjs b/packages/joint-core/src/mvc/View.mjs index 72ca0c8cc7..68fbcfd9b2 100644 --- a/packages/joint-core/src/mvc/View.mjs +++ b/packages/joint-core/src/mvc/View.mjs @@ -10,6 +10,7 @@ export const View = ViewBase.extend({ options: {}, theme: null, + classNamePrefix: config.classNamePrefix, themeClassNamePrefix: util.addClassNamePrefix('theme-'), requireSetThemeOverride: false, defaultTheme: config.defaultTheme, @@ -134,7 +135,7 @@ export const View = ViewBase.extend({ _ensureElClassName: function() { var className = util.result(this, 'className'); if (!className) return; - var prefixedClassName = util.addClassNamePrefix(className); + var prefixedClassName = util.addClassNamePrefix(className, this.classNamePrefix); // Note: className removal here kept for backwards compatibility only if (this.svgElement) { this.vel.removeClass(className).addClass(prefixedClassName); diff --git a/packages/joint-core/src/util/util.mjs b/packages/joint-core/src/util/util.mjs index 92a640acf2..707e9d58ff 100644 --- a/packages/joint-core/src/util/util.mjs +++ b/packages/joint-core/src/util/util.mjs @@ -43,14 +43,14 @@ import { merge } from './utilHelpers.mjs'; -export const addClassNamePrefix = function(className) { +export const addClassNamePrefix = function(className, prefix = config.classNamePrefix) { - if (!className) return className; + if (!className || !prefix) return className; return className.toString().split(' ').map(function(_className) { - if (_className.substr(0, config.classNamePrefix.length) !== config.classNamePrefix) { - _className = config.classNamePrefix + _className; + if (_className.substr(0, prefix.length) !== prefix) { + _className = prefix + _className; } return _className; @@ -58,14 +58,14 @@ export const addClassNamePrefix = function(className) { }).join(' '); }; -export const removeClassNamePrefix = function(className) { +export const removeClassNamePrefix = function(className, prefix = config.classNamePrefix) { - if (!className) return className; + if (!className || !prefix) return className; return className.toString().split(' ').map(function(_className) { - if (_className.substr(0, config.classNamePrefix.length) === config.classNamePrefix) { - _className = _className.substr(config.classNamePrefix.length); + if (_className.substr(0, prefix.length) === prefix) { + _className = _className.substr(prefix.length); } return _className; diff --git a/packages/joint-core/test/jointjs/core/util.js b/packages/joint-core/test/jointjs/core/util.js index e5c0ae1aa2..293f55eb1f 100644 --- a/packages/joint-core/test/jointjs/core/util.js +++ b/packages/joint-core/test/jointjs/core/util.js @@ -882,6 +882,31 @@ QUnit.module('util', function(hooks) { assert.equal(joint.util.addClassNamePrefix('some-class some-other-class'), joint.config.classNamePrefix + 'some-class ' + joint.config.classNamePrefix + 'some-other-class'); }); + + QUnit.test('custom prefix provided', function(assert) { + + assert.equal(joint.util.addClassNamePrefix('some-class', 'custom-'), 'custom-some-class'); + }); + + QUnit.test('multiple class names with custom prefix', function(assert) { + + assert.equal(joint.util.addClassNamePrefix('a b', 'x-'), 'x-a x-b'); + }); + + QUnit.test('empty string prefix is a no-op', function(assert) { + + assert.equal(joint.util.addClassNamePrefix('some-class', ''), 'some-class'); + }); + + QUnit.test('null prefix is a no-op', function(assert) { + + assert.equal(joint.util.addClassNamePrefix('some-class', null), 'some-class'); + }); + + QUnit.test('idempotent with custom prefix', function(assert) { + + assert.equal(joint.util.addClassNamePrefix('custom-some-class', 'custom-'), 'custom-some-class'); + }); }); QUnit.module('removeClassNamePrefix', function(hooks) { @@ -919,6 +944,33 @@ QUnit.module('util', function(hooks) { assert.equal(joint.util.removeClassNamePrefix(joint.config.classNamePrefix + 'some-class without-prefix'), 'some-class without-prefix'); }); + + QUnit.test('custom prefix provided', function(assert) { + + assert.equal(joint.util.removeClassNamePrefix('custom-some-class', 'custom-'), 'some-class'); + }); + + QUnit.test('multiple prefixed class names with custom prefix', function(assert) { + + assert.equal(joint.util.removeClassNamePrefix('x-a x-b', 'x-'), 'a b'); + }); + + QUnit.test('empty string prefix is a no-op', function(assert) { + + var input = joint.config.classNamePrefix + 'some-class'; + assert.equal(joint.util.removeClassNamePrefix(input, ''), input); + }); + + QUnit.test('null prefix is a no-op', function(assert) { + + var input = joint.config.classNamePrefix + 'some-class'; + assert.equal(joint.util.removeClassNamePrefix(input, null), input); + }); + + QUnit.test('custom prefix ignores joint- prefixed names', function(assert) { + + assert.equal(joint.util.removeClassNamePrefix(joint.config.classNamePrefix + 'some-class', 'custom-'), joint.config.classNamePrefix + 'some-class'); + }); }); QUnit.module('wrapWith', function(hooks) { diff --git a/packages/joint-core/test/jointjs/mvc.view.js b/packages/joint-core/test/jointjs/mvc.view.js index c925bc8741..f29884a4cb 100644 --- a/packages/joint-core/test/jointjs/mvc.view.js +++ b/packages/joint-core/test/jointjs/mvc.view.js @@ -208,6 +208,97 @@ QUnit.module('joint.mvc.View', function(hooks) { assert.equal(view.$el.attr('class'), joint.util.addClassNamePrefix(className) + ' ' + themeClassName); }); + QUnit.module('classNamePrefix override', function() { + + QUnit.test('default prefix unchanged', function(assert) { + + var SomeView = joint.mvc.View.extend({ + className: 'foo', + defaultTheme: null + }); + var view = new SomeView(); + assert.equal(view.el.className, 'joint-foo'); + view.remove(); + }); + + QUnit.test('custom prefix on prototype', function(assert) { + + var SomeView = joint.mvc.View.extend({ + className: 'foo', + classNamePrefix: 'custom-', + defaultTheme: null + }); + var view = new SomeView(); + assert.equal(view.el.className, 'custom-foo'); + view.remove(); + }); + + QUnit.test('empty prefix opts out of prefixing', function(assert) { + + var SomeView = joint.mvc.View.extend({ + className: 'foo', + classNamePrefix: '', + defaultTheme: null + }); + var view = new SomeView(); + assert.equal(view.el.className, 'foo'); + view.remove(); + }); + + QUnit.test('multiple class names with custom prefix', function(assert) { + + var SomeView = joint.mvc.View.extend({ + className: 'foo bar', + classNamePrefix: 'x-', + defaultTheme: null + }); + var view = new SomeView(); + assert.equal(view.el.className, 'x-foo x-bar'); + view.remove(); + }); + + QUnit.test('idempotent — already-prefixed class names are not double-prefixed', function(assert) { + + var SomeView = joint.mvc.View.extend({ + className: 'x-foo', + classNamePrefix: 'x-', + defaultTheme: null + }); + var view = new SomeView(); + assert.equal(view.el.className, 'x-foo'); + view.remove(); + }); + + QUnit.test('SVG element with custom prefix', function(assert) { + + var SomeView = joint.mvc.View.extend({ + svgElement: true, + tagName: 'g', + className: 'foo', + classNamePrefix: 'custom-', + defaultTheme: null + }); + var view = new SomeView(); + assert.equal(view.el.className.baseVal, 'custom-foo'); + view.remove(); + }); + + QUnit.test('theme class still uses joint- prefix', function(assert) { + + var SomeView = joint.mvc.View.extend({ + className: 'foo', + classNamePrefix: 'custom-' + }); + var view = new SomeView(); + var defaultTheme = joint.mvc.View.prototype.defaultTheme; + var themeClassName = SomeView.prototype.themeClassNamePrefix + defaultTheme; + assert.ok(view.$el.hasClass('custom-foo'), 'custom prefix applied to className'); + assert.ok(view.$el.hasClass(themeClassName), 'theme class still uses joint- prefix'); + assert.equal(view.$el.attr('class'), 'custom-foo ' + themeClassName); + view.remove(); + }); + }); + QUnit.test('mvc.View.extend does not modify prototype or static properties objects', function(assert) { var protoProps = {}; diff --git a/packages/joint-core/types/mvc.d.ts b/packages/joint-core/types/mvc.d.ts index 0cdc8010ef..edd1d5c34a 100644 --- a/packages/joint-core/types/mvc.d.ts +++ b/packages/joint-core/types/mvc.d.ts @@ -494,6 +494,8 @@ export class View; + classNamePrefix: string; + theme: string; themeClassNamePrefix: string; diff --git a/packages/joint-react/src/presets/paper.ts b/packages/joint-react/src/presets/paper.ts index e7469c1c1e..e9be0bfd87 100644 --- a/packages/joint-react/src/presets/paper.ts +++ b/packages/joint-react/src/presets/paper.ts @@ -59,6 +59,8 @@ const linkView = ( }; export const Paper = dia.Paper.extend({ + className: 'jj-paper joint-paper', + classNamePrefix: '', options: { ...dia.Paper.prototype.options, // Required for React integration features: @@ -84,9 +86,4 @@ export const Paper = dia.Paper.extend({ measureNode, linkView, }, - - _ensureElClassName() { - // Note: the `className` property is ignored here. - this.el.classList.add('jj-paper', 'joint-paper'); - }, }) as typeof dia.Paper;