Skip to content

Commit

Permalink
feat(dia.Cell): new toJSON options (#2728)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinKanera authored Aug 12, 2024
1 parent 3bead1c commit bf753a1
Show file tree
Hide file tree
Showing 7 changed files with 428 additions and 35 deletions.
76 changes: 45 additions & 31 deletions packages/joint-core/src/dia/Cell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
defaultsDeep,
has,
sortBy,
defaults
defaults,
objectDifference
} from '../util/util.mjs';
import { Model } from '../mvc/Model.mjs';
import { cloneCells } from '../util/cloneCells.mjs';
Expand All @@ -42,6 +43,22 @@ const attributesMerger = function(a, b) {
}
};

function removeEmptyAttributes(obj) {

// Remove toplevel empty attributes
for (const key in obj) {

const objValue = obj[key];
const isRealObject = isObject(objValue) && !Array.isArray(objValue);

if (!isRealObject) continue;

if (isEmpty(objValue)) {
delete obj[key];
}
}
}

export const Cell = Model.extend({

// This is the same as mvc.Model with the only difference that is uses util.merge
Expand Down Expand Up @@ -75,48 +92,45 @@ export const Cell = Model.extend({
throw new Error('Must define a translate() method.');
},

toJSON: function() {
toJSON: function(opt) {

const { ignoreDefaults, ignoreEmptyAttributes = false } = opt || {};
const defaults = result(this.constructor.prototype, 'defaults');
const defaultAttrs = defaults.attrs || {};
const attrs = this.attributes.attrs;
const finalAttrs = {};

// Loop through all the attributes and
// omit the default attributes as they are implicitly reconstructible by the cell 'type'.
forIn(attrs, function(attr, selector) {

const defaultAttr = defaultAttrs[selector];
if (ignoreDefaults === false) {
// Return all attributes without omitting the defaults
const finalAttributes = cloneDeep(this.attributes);

forIn(attr, function(value, name) {
if (!ignoreEmptyAttributes) return finalAttributes;

// attr is mainly flat though it might have one more level (consider the `style` attribute).
// Check if the `value` is object and if yes, go one level deep.
if (isObject(value) && !Array.isArray(value)) {
removeEmptyAttributes(finalAttributes);

forIn(value, function(value2, name2) {

if (!defaultAttr || !defaultAttr[name] || !isEqual(defaultAttr[name][name2], value2)) {
return finalAttributes;
}

finalAttrs[selector] = finalAttrs[selector] || {};
(finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2;
}
});
let defaultAttributes = {};
let attributes = cloneDeep(this.attributes);

} else if (!defaultAttr || !isEqual(defaultAttr[name], value)) {
// `value` is not an object, default attribute for such a selector does not exist
// or it is different than the attribute value set on the model.
if (ignoreDefaults === true) {
// Compare all attributes with the defaults
defaultAttributes = defaults;
} else {
// Compare only the specified attributes with the defaults, use `attrs` as a default if not specified
const differentiateKeys = Array.isArray(ignoreDefaults) ? ignoreDefaults : ['attrs'];

finalAttrs[selector] = finalAttrs[selector] || {};
finalAttrs[selector][name] = value;
}
differentiateKeys.forEach((key) => {
defaultAttributes[key] = defaults[key] || {};
});
});
}

const attributes = cloneDeep(omit(this.attributes, 'attrs'));
attributes.attrs = finalAttrs;
// Omit `id` and `type` attribute from the defaults since it should be always present
const finalAttributes = objectDifference(attributes, omit(defaultAttributes, 'id', 'type'), { maxDepth: 4 });

if (ignoreEmptyAttributes) {
removeEmptyAttributes(finalAttributes);
}

return attributes;
return finalAttributes;
},

initialize: function(options) {
Expand Down
4 changes: 2 additions & 2 deletions packages/joint-core/src/dia/Graph.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,12 @@ export const Graph = Model.extend({
return (this._in && this._in[node]) || {};
},

toJSON: function() {
toJSON: function(opt = {}) {

// JointJS does not recursively call `toJSON()` on attributes that are themselves models/collections.
// It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitly.
var json = Model.prototype.toJSON.apply(this, arguments);
json.cells = this.get('cells').toJSON();
json.cells = this.get('cells').toJSON(opt.cellAttributes);
return json;
},

Expand Down
39 changes: 39 additions & 0 deletions packages/joint-core/src/util/util.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,45 @@ export const toggleFullScreen = function(el) {
}
};

function findDifference(obj, baseObj, currentDepth, maxDepth) {

if (currentDepth === maxDepth) {
return {};
}

const diff = {};

Object.keys(obj).forEach((key) => {

const objValue = obj[key];
const baseValue = baseObj[key];

if (!Array.isArray(objValue) && !Array.isArray(baseValue) && isObject(objValue) && isObject(baseValue)) {

const nestedDepth = currentDepth + 1;
const nestedDiff = findDifference(objValue, baseValue, nestedDepth, maxDepth);

if (Object.keys(nestedDiff).length > 0) {
diff[key] = nestedDiff;
} else if ((currentDepth === 0 || nestedDepth === maxDepth)) {
diff[key] = {};
}

} else if (!isEqual(objValue, baseValue)) {
diff[key] = objValue;
}
});

return diff;
}

export function objectDifference(object, base, opt) {

const { maxDepth = Number.POSITIVE_INFINITY } = opt || {};

return findDifference(object, base, 0, maxDepth);
}

export {
isBoolean,
isObject,
Expand Down
58 changes: 58 additions & 0 deletions packages/joint-core/test/jointjs/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,64 @@ QUnit.module('cell', function(hooks) {

assert.deepEqual(el.toJSON(), { id: 'el1', attrs: { test2: { prop2: true }}});
});

QUnit.test('should take in account `opt.ignoreDefaults` = false', function(assert) {
const rect = new joint.shapes.standard.Rectangle();

assert.deepEqual(rect.toJSON({ ignoreDefaults: false }), {
id: rect.id,
...joint.shapes.standard.Rectangle.prototype.defaults,
});
});

QUnit.test('should take in account `opt.ignoreDefaults` = false, `opt.ignoreEmptyAttributes` = true', function(assert) {
const El = joint.dia.Element.extend({
defaults: {
type: 'test.Element'
}
});

const el = new El({
foo: {}
});

const expected = joint.util.cloneDeep(el.attributes);
delete expected.foo;

assert.deepEqual(el.toJSON({ ignoreDefaults: false, ignoreEmptyAttributes: true }), expected);
});

QUnit.test('should take in account `opt.ignoreDefaults` = true', function(assert) {
const rect = new joint.shapes.standard.Rectangle();

assert.deepEqual(rect.toJSON({ ignoreDefaults: true }), {
type: joint.shapes.standard.Rectangle.prototype.defaults.type,
id: rect.id,
size: {},
position: {},
attrs: {}
});
});

QUnit.test('should take in account `opt.ignoreDefaults` = true, `opt.ignoreEmptyAttributes` = true', function(assert) {
const rect = new joint.shapes.standard.Rectangle();

assert.deepEqual(rect.toJSON({ ignoreDefaults: true, ignoreEmptyAttributes: true }), {
type: joint.shapes.standard.Rectangle.prototype.defaults.type,
id: rect.id
});
});

QUnit.test('`opt.ignoreDefaults` should accept an array of keys to differentiate', function(assert) {
const rect = new joint.shapes.standard.Rectangle();

assert.deepEqual(rect.toJSON({ ignoreDefaults: ['attrs', 'size'] }), {
id: rect.id,
...joint.shapes.standard.Rectangle.prototype.defaults,
attrs: {},
size: {}
});
});
});

QUnit.module('relative vs absolute points', function() {
Expand Down
Loading

0 comments on commit bf753a1

Please sign in to comment.