').appendTo('body');
13 | });
14 | afterEach(function () {
15 | stage.remove();
16 | second.remove();
17 | });
18 | it('applies width and height by adding subtracting offset from data width', function () {
19 | stage.data({width: 200, height: 100, offsetX: 50, offsetY: 10}).updateStage();
20 | expect(stage.css('width')).toBe('150px');
21 | expect(stage.css('min-width')).toBe('150px');
22 | expect(stage.css('height')).toBe('90px');
23 | expect(stage.css('min-height')).toBe('90px');
24 | });
25 | it('translates by offsetX, offsetY if scale is 1', function () {
26 | /* different browsers report transformations differently so we transform an element and compare css */
27 | stage.data({width: 200, height: 100, offsetX: 50, offsetY: 10, scale: 1}).updateStage();
28 | second.css({'width': '100px', 'height': '200px', 'transform': 'translate(50px,10px)'});
29 | expect(stage.css('transform')).toEqual(second.css('transform'));
30 | second.remove();
31 | });
32 | it('scales then transforms', function () {
33 | stage.data({width: 200, height: 100, offsetX: 50, offsetY: 10, scale: 2}).updateStage();
34 | second.css({'transform-origin': 'top left', 'width': '100px', 'height': '200px', 'transform': 'scale(2) translate(50px,10px)'});
35 | expect(stage.css('transform')).toEqual(second.css('transform'));
36 | expect(stage.css('transform-origin')).toEqual(second.css('transform-origin'));
37 | });
38 | it('rounds coordinates for performance', function () {
39 | stage.data({width: 137.33, height: 100.34, offsetX: 50.21, offsetY: 10.93, scale: 1}).updateStage();
40 | second.css({'width': '137px', 'height': '100px', 'transform': 'translate(50px,11px)'});
41 | expect(stage.css('transform')).toEqual(second.css('transform'));
42 | expect(stage.css('width')).toEqual('87px');
43 | expect(stage.css('min-width')).toEqual('87px');
44 | expect(stage.css('height')).toEqual('89px');
45 | expect(stage.css('min-height')).toEqual('89px');
46 |
47 | });
48 | it('updates the svg container if present', function () {
49 | const svgContainer = createSVG()
50 | .css({
51 | position: 'absolute',
52 | top: 0,
53 | left: 0
54 | })
55 | .attr({
56 | 'data-mapjs-role': 'svg-container',
57 | 'class': 'mapjs-draw-container',
58 | 'width': '100%',
59 | 'height': '100%'
60 | })
61 | .appendTo(stage);
62 |
63 | stage.data({width: 137.33, height: 100.34, offsetX: 50.21, offsetY: 10.93, scale: 1}).updateStage();
64 | expect(svgContainer[0].getAttribute('viewBox')).toEqual('-50 -11 137 100');
65 | expect(svgContainer[0].style.top).toEqual('-11px');
66 | expect(svgContainer[0].style.left).toEqual('-50px');
67 | expect(svgContainer[0].style.width).toEqual('137px');
68 | expect(svgContainer[0].style.height).toEqual('100px');
69 | });
70 | });
71 |
72 |
--------------------------------------------------------------------------------
/specs/core/content/calc-idea-level-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it , expect, beforeEach*/
2 |
3 | const content = require('../../../src/core/content/content'),
4 | underTest = require('../../../src/core/content/calc-idea-level');
5 |
6 | describe('calcIdeaLevel', () => {
7 | 'use strict';
8 | let activeContent;
9 | beforeEach(() => {
10 | activeContent = content({
11 | id: 1,
12 | ideas: {
13 | 1: {
14 | id: 11,
15 | ideas: {
16 | 1: {
17 | id: 111
18 | }
19 | }
20 | }
21 | }
22 | });
23 | });
24 | it('should throw invalid-args when missing activeContent', () => {
25 | expect(() => underTest(undefined, 1)).toThrow('invalid-args');
26 | });
27 | it('should return level 0 for idea root', () => {
28 | expect(underTest(activeContent, 'root')).toEqual(0);
29 | });
30 |
31 | it('should return level 1 for root nodes', () => {
32 | expect(underTest(activeContent, 1)).toEqual(1);
33 | });
34 | it('should return level 1 when nodId is falsy', () => {
35 | expect(underTest(activeContent)).toBeUndefined();
36 | });
37 | it('should return levels for nodes down the tree', () => {
38 | expect(underTest(activeContent, 11)).toEqual(2);
39 | expect(underTest(activeContent, 111)).toEqual(3);
40 | });
41 | it('should return undefined for non existent node ids', () => {
42 | expect(underTest(activeContent, 2)).toBeUndefined();
43 | });
44 | it('should return 1 for non existent songle node', () => {
45 | activeContent = content({
46 | id: 1
47 | });
48 | expect(underTest(activeContent, 1)).toEqual(1);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/specs/core/content/content-upgrade-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, expect, it, require*/
2 | const contentUpgrade = require('../../../src/core/content/content-upgrade');
3 | describe('content upgrade', function () {
4 | 'use strict';
5 | describe('upgrade to v3', function () {
6 | it ('should do nothing if already v3', function () {
7 | const content = {
8 | formatVersion: 3,
9 | id: 'original',
10 | attr: {
11 | style: 'red'
12 | }
13 | };
14 | contentUpgrade(content);
15 | expect(content).toEqual({
16 | formatVersion: 3,
17 | id: 'original',
18 | attr: {
19 | style: 'red'
20 | }
21 | });
22 | });
23 | it('should upgrade version number', function () {
24 | const content = {};
25 | contentUpgrade(content);
26 | expect(content.formatVersion).toEqual(3);
27 | });
28 | it('should add an parent idea above the root idea', function () {
29 | const content = {id: 1, title: 'hello'};
30 | contentUpgrade(content);
31 | expect(content.ideas).toEqual({
32 | 1: {id: 1, title: 'hello', attr: {}}
33 | });
34 | });
35 | it('should change the root node to have id of "root"', function () {
36 | const content = {id: 1, title: 'hello'};
37 | contentUpgrade(content);
38 | expect(content.id).toEqual('root');
39 | });
40 | it('should remove the title from the idea root', function () {
41 | const content = {id: 1, title: 'hello'};
42 | contentUpgrade(content);
43 | expect(content.title).toBeFalsy();
44 | });
45 | it('should preserve root attributes on root', function () {
46 | const content = {id: 1, title: 'hello', attr: {theme: 'foo', themeOverrides: 'bar', 'measurements-config': 'bar', storyboards: 'foobar', someother: 'foo'}};
47 | contentUpgrade(content);
48 | expect(content.attr).toEqual({theme: 'foo', themeOverrides: 'bar', 'measurements-config': 'bar', storyboards: 'foobar'});
49 | });
50 | it('should move non root attributes to new sub idea', function () {
51 | const content = {id: 1, title: 'hello', attr: {theme: 'foo', 'measurements-config': 'bar', storyboards: 'foobar', someother: 'foo'}};
52 | contentUpgrade(content);
53 | expect(content.ideas[1].attr).toEqual({someother: 'foo'});
54 | });
55 | it('should move the root idea subnodes, preserving rank', function () {
56 | const content = {
57 | id: 1,
58 | title: 'hello',
59 | ideas: {
60 | '-1': {id: 2, title: 'sub1'},
61 | 2: {id: 3, title: 'sub2'}
62 | }
63 | };
64 | contentUpgrade(content);
65 | expect(content.ideas[1].ideas).toEqual({
66 | '-1': {id: 2, title: 'sub1'},
67 | 2: {id: 3, title: 'sub2'}
68 | });
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/specs/core/content/format-note-to-html-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require */
2 | const underTest = require('../../../src/core/content/format-note-to-html');
3 |
4 | describe('formatNoteToHtml', () => {
5 | 'use strict';
6 | it('returns a blank string for falsy content', () => {
7 | expect(underTest()).toBe('');
8 | expect(underTest('')).toBe('');
9 | expect(underTest(undefined)).toBe('');
10 | expect(underTest(false)).toBe('');
11 | });
12 | it('throws an error', () => {
13 | expect(() => underTest({a: 1})).toThrow('invalid-args');
14 | });
15 | it('escapes HTML', () => {
16 | expect(underTest('abc ')).toEqual('abc <script>xyz</script>');
17 | });
18 | it('formats links as HTML after escaping', () => {
19 | expect(underTest('abc https://www.google.com ')).toEqual('abc
https://www.google.com <script>xyz</script>');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/specs/core/content/formatted-node-title-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it, expect*/
2 | const underTest = require('../../../src/core/content/formatted-node-title');
3 | describe('formattedNodeTitle', function () {
4 | 'use strict';
5 | [
6 | ['a text title with no link', 'hello', 'hello'],
7 | ['an empty string if nothing provided', undefined, ''],
8 | ['a title without link if title contains text followed by link', 'hello www.google.com', 'hello'],
9 | ['a title without link if title contains link followed by text', 'www.google.com hello', 'hello'],
10 | ['a title without link if title contains link surrounded by text', 'hello www.google.com bye', 'hello bye'],
11 | ['a title with second link if title contains multiple links with text', 'hello www.google.com www.google.com', 'hello www.google.com'],
12 | ['a title with second link if title contains multiple links', 'www.google.com www.google.com', 'www.google.com'],
13 | ['a link if title is link only', 'www.google.com', 'www.google.com']
14 | ].forEach(function (args) {
15 | it('should return ' + args[0], function () {
16 | expect(underTest(args[1])).toEqual(args[2]);
17 | });
18 | });
19 | it('truncates link-only titles if maxlength is provided', function () {
20 | expect(underTest('http://google.com/search?q=onlylink', 25)).toEqual('http://google.com/search?...');
21 | expect(underTest('http://google.com/search?q=onlylink', 100)).toEqual('http://google.com/search?q=onlylink');
22 | });
23 | it('does not truncate links if maxlength is not provided', function () {
24 | expect(underTest('http://google.com/search?q=onlylink')).toEqual('http://google.com/search?q=onlylink');
25 | });
26 | it('does not truncate text even if maxlength is provided', function () {
27 | expect(underTest('http google.com search?q=onlylink', 25)).toEqual('http google.com search?q=onlylink');
28 | });
29 | it('replaces multiple spaces with a single space', function () {
30 | expect(underTest('something else\t\t again', 100)).toEqual('something else again');
31 | });
32 | it('removes non printable characters', () => {
33 | expect(underTest('abc\bdef', 100)).toEqual('abcdef');
34 | expect(underTest('abc\u0007def\u001Fghi\u0080jkl\u009Fx')).toEqual('abcdefghijklx');
35 | });
36 | it('trims lines but keeps new lines when replacing spaces', function () {
37 | expect(underTest(' something \n\nelse\t \n\t again ', 100)).toEqual('something\n\nelse\nagain');
38 | });
39 | it('transforms windows line endings into linux line endings', function () {
40 | expect(underTest('something \r\n\r\nelse\t\r\n\tagain', 100)).toEqual('something\n\nelse\nagain');
41 | });
42 |
43 |
44 | });
45 |
--------------------------------------------------------------------------------
/specs/core/content/is-empty-group-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require*/
2 | const isEmptyGroup = require('../../../src/core/content/is-empty-group');
3 | describe('isEmptyGroup', function () {
4 | 'use strict';
5 | it('returns true only if group attr present and no subideas', function () {
6 | expect(isEmptyGroup({})).toBeFalsy();
7 | expect(isEmptyGroup({ attr: {x: 1} })).toBeFalsy();
8 | expect(isEmptyGroup({ attr: {group: 'standard', x: 1} })).toBeTruthy();
9 | expect(isEmptyGroup({ attr: {group: 'standard'} })).toBeTruthy();
10 | expect(isEmptyGroup({ ideas: {}, attr: {group: 'standard'} })).toBeTruthy();
11 | expect(isEmptyGroup({ ideas: { 1: {} }, attr: {group: 'standard'} })).toBeFalsy();
12 | expect(isEmptyGroup({ ideas: { 1: {} }})).toBeFalsy();
13 | expect(isEmptyGroup({ ideas: {}})).toBeFalsy();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/specs/core/content/sorted-sub-ideas-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require */
2 | const sortedSubIdeas = require('../../../src/core/content/sorted-sub-ideas');
3 | describe('sortedSubIdeas', function () {
4 | 'use strict';
5 | it('sorts children by key, positive first then negative, by absolute value', function () {
6 | const content = {id: 1, title: 'root', ideas: {'-100': {title: '-100'}, '-1': {title: '-1'}, '1': {title: '1'}, '100': {title: '100'}}},
7 | result = sortedSubIdeas(content).map(function (subidea) {
8 | return subidea.title;
9 | });
10 | expect(result).toEqual(['1', '100', '-1', '-100']);
11 | });
12 | });
13 |
14 |
--------------------------------------------------------------------------------
/specs/core/content/traverse-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require*/
2 | const traverse = require('../../../src/core/content/traverse');
3 | describe('traverse', function () {
4 | 'use strict';
5 | describe('when version is not specified (so non root nodes or v2)', function () {
6 | it('applies a depth-first, pre-order traversal', function () {
7 | const content = { id: 1, ideas: { '11': {id: 11, ideas: { 1: { id: 111}, 2: {id: 112} } }, '-12': {id: 12, ideas: { 1: {id: 121} } }, '-13': {id: 13} } },
8 | result = [],
9 | levels = [];
10 | traverse(content, function (idea, level) {
11 | result.push(idea.id);
12 | levels.push(level);
13 | });
14 | expect(result).toEqual([1, 11, 111, 112, 12, 121, 13]);
15 | expect(levels).toEqual([1, 2, 3, 3, 2, 3, 2]);
16 | });
17 | it('does a post-order traversal if second argument is true', function () {
18 | const content = { id: 1, ideas: { '11': {id: 11, ideas: { 1: { id: 111}, 2: {id: 112} } }, '-12': {id: 12, ideas: { 1: {id: 121} } }, '-13': {id: 13} } },
19 | result = [],
20 | levels = [];
21 | traverse(content, function (idea, level) {
22 | result.push(idea.id);
23 | levels.push(level);
24 | }, true);
25 | expect(result).toEqual([111, 112, 11, 121, 12, 13, 1]);
26 | expect(levels).toEqual([3, 3, 2, 3, 2, 2, 1]);
27 | });
28 | });
29 | describe('v3', function () {
30 | it('skips root node in preorder traversal', function () {
31 | const content = { formatVersion: 3, id: 1, ideas: { '11': {id: 11, ideas: { 1: { id: 111}, 2: {id: 112} } }, '-12': {id: 12, ideas: { 1: {id: 121} } }, '-13': {id: 13} } },
32 | result = [],
33 | levels = [];
34 | traverse(content, function (idea, level) {
35 | result.push(idea.id);
36 | levels.push(level);
37 | });
38 | expect(result).toEqual([11, 111, 112, 12, 121, 13]);
39 | expect(levels).toEqual([1, 2, 2, 1, 2, 1]);
40 | });
41 | it('skips root node in postoder traversal', function () {
42 | const content = { id: 1, formatVersion: 3, ideas: { '11': {id: 11, ideas: { 1: { id: 111}, 2: {id: 112} } }, '-12': {id: 12, ideas: { 1: {id: 121} } }, '-13': {id: 13} } },
43 | result = [],
44 | levels = [];
45 | traverse(content, function (idea, level) {
46 | result.push(idea.id);
47 | levels.push(level);
48 | }, true);
49 | expect(result).toEqual([111, 112, 11, 121, 12, 13]);
50 | expect(levels).toEqual([2, 2, 1, 2, 1, 1]);
51 | });
52 | });
53 | });
54 |
55 |
--------------------------------------------------------------------------------
/specs/core/deep-assign-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it expect*/
2 |
3 | const underTest = require('../../src/core/deep-assign');
4 |
5 | describe('deepAssign', () => {
6 | 'use strict';
7 | describe('should throw invalid-args', () => {
8 | it('when called without arguments', () => {
9 | expect(() => underTest()).toThrowError('invalid-args');
10 | });
11 | it('when called with non object arguments', () => {
12 | expect(() => underTest(1)).toThrowError('invalid-args');
13 | });
14 | });
15 | it('returns the object unchanged when called with one argument', () => {
16 | expect(underTest({})).toEqual({});
17 | });
18 | it('returns the first object with primitive assignments', () => {
19 | expect(underTest({a: 1, b: 1}, {a: 2}, {c: 3})).toEqual({a: 2, b: 1, c: 3});
20 | });
21 | it('mutates the first object with primitive assignments', () => {
22 | const assignee = {a: 1, b: 1};
23 | underTest(assignee, {a: 2}, {c: 3});
24 | expect(assignee).toEqual({a: 2, b: 1, c: 3});
25 | });
26 | it('creates copies of new assigned object attributes', () => {
27 | const assignee = {},
28 | assigner1 = {b: {c: '2'}};
29 | underTest(assignee, assigner1);
30 | assigner1.b.c = '2after';
31 | assigner1.c = 'newafter';
32 | expect(assignee).toEqual({b: {c: '2'}});
33 |
34 | });
35 | it('does not mutate the other objects with primitive assignments', () => {
36 | const assignee = {a: 1, b: 1},
37 | assigner1 = {b: {c: '2'}},
38 | assigner2 = {b: {d: '3'}};
39 | underTest(assignee, assigner1, assigner2);
40 | expect(assignee).toEqual({a: 1, b: {c: '2', d: '3'}});
41 | expect(assigner1).toEqual({b: {c: '2'}});
42 | expect(assigner2).toEqual({b: {d: '3'}});
43 | });
44 | it('replaces primitives with objects for the same key', () => {
45 | expect(underTest({a: 1, b: 1}, {a: {c: '2'}})).toEqual({a: {c: '2'}, b: 1});
46 | });
47 | it('replaces objects with primitive for the same key', () => {
48 | expect(underTest({a: {c: 2}, b: 1}, {a: 3})).toEqual({a: 3, b: 1});
49 | });
50 | it('recursively merges object for the same key', () => {
51 | const assignee = {
52 | a: 'a from assignee',
53 | b: {
54 | c: 'b.c from assignee',
55 | d: 'b.d from assignee',
56 | e: 'b.e from assignee'
57 | }
58 | },
59 | assigner1 = {
60 | b: {
61 | c: 'b.c from assigner1',
62 | e: 'b.e from assigner1'
63 | },
64 | c: 'c from assigner1',
65 | d: {
66 | a: 'd.a from assigner1'
67 | },
68 | e: 'e from assigner1'
69 | },
70 | assigner2 = {
71 | b: {
72 | e: 'b.e from assigner2',
73 | f: 'b.f from assigner2'
74 | },
75 | c: 'c from assigner2',
76 | d: {
77 | b: 'd.b from assigner2'
78 | },
79 | f: 'f from assigner2'
80 |
81 | };
82 | underTest(assignee, assigner1, assigner2);
83 | expect(assignee).toEqual({
84 | a: 'a from assignee',
85 | b: {
86 | c: 'b.c from assigner1',
87 | d: 'b.d from assignee',
88 | e: 'b.e from assigner2',
89 | f: 'b.f from assigner2'
90 | },
91 | c: 'c from assigner2',
92 | d: {
93 | a: 'd.a from assigner1',
94 | b: 'd.b from assigner2'
95 | },
96 | e: 'e from assigner1',
97 | f: 'f from assigner2'
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/specs/core/is-object-object-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it, expect*/
2 |
3 | const underTest = require('../../src/core/is-object-object');
4 |
5 | describe('isObjectObject', () => {
6 | 'use strict';
7 | describe('returns truthy for', () => {
8 | [
9 | ['empty map', {}],
10 | ['non empty map', {a: 1, b: 'c'}]
11 | ].forEach(args => {
12 | it(args[0], () => {
13 | expect(underTest(args[1])).toBeTruthy();
14 | });
15 | });
16 | });
17 | describe('returns falsy for', () => {
18 | [
19 | ['undefined', undefined],
20 | ['false', false],
21 | ['true', true],
22 | ['integer', 1],
23 | ['float', 1],
24 | ['Date', new Date()],
25 | ['string', 'hello'],
26 | ['String', new String('hello')],
27 | ['array', ['hello']]
28 | ].forEach(args => {
29 | it(args[0], () => {
30 | expect(underTest(args[1])).toBeFalsy();
31 | });
32 | });
33 | });
34 |
35 | });
36 |
--------------------------------------------------------------------------------
/specs/core/layout/extract-connectors-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, require, expect, beforeEach*/
2 | const _ = require('underscore'),
3 | extractConnectors = require('../../../src/core/layout/extract-connectors');
4 | describe('extractConnectors', function () {
5 | 'use strict';
6 | const makeConnector = (obj) => _.extend({type: 'connector'}, obj);
7 | let visibleNodes, idea;
8 | beforeEach(function () {
9 | idea = {
10 | id: 'root',
11 | ideas: {
12 | 1: {
13 | title: 'parent',
14 | id: 1,
15 | ideas: {
16 | 5: {
17 | title: 'second child',
18 | id: 12,
19 | ideas: { 1: { id: 112, title: 'XYZ' } }
20 | },
21 | 4: {
22 | title: 'child',
23 | id: 11,
24 | ideas: { 1: { id: 111, title: 'XYZ' } }
25 | }
26 | }
27 | }
28 | }
29 | };
30 |
31 | visibleNodes = {
32 | 1: {},
33 | 12: {},
34 | 112: {},
35 | 11: {},
36 | 111: {}
37 | };
38 | });
39 | it('creates an object indexed by child ID with from-to connector information', function () {
40 | const result = extractConnectors(idea, visibleNodes);
41 | expect(result).toEqual({
42 | 11: makeConnector({ from: 1, to: 11 }),
43 | 12: makeConnector({ from: 1, to: 12 }),
44 | 112: makeConnector({ from: 12, to: 112 }),
45 | 111: makeConnector({ from: 11, to: 111 })
46 | });
47 | });
48 | it('should not include connector when child node is not visible', function () {
49 | delete visibleNodes[12];
50 | delete visibleNodes[111];
51 | expect(extractConnectors(idea, visibleNodes)).toEqual({
52 | 11: makeConnector({ from: 1, to: 11 })
53 | });
54 | });
55 | describe('parentConnector handling', function () {
56 | beforeEach(function () {
57 | visibleNodes[12].attr = {parentConnector: {great: true}};
58 | });
59 |
60 | it('adds parentConnector attribute properties to the connector attributes if the theme is not set', function () {
61 | const result = extractConnectors(idea, visibleNodes);
62 | expect(result[12]).toEqual(makeConnector({
63 | from: 1,
64 | to: 12,
65 | attr: {great: true}
66 | }));
67 | });
68 | it('adds parentConnector if the theme is set and does not have a connectorEditingContext', function () {
69 | const result = extractConnectors(idea, visibleNodes, {connectorEditingContext: false});
70 | expect(result[12]).toEqual(makeConnector({
71 | from: 1,
72 | to: 12,
73 | attr: {great: true}
74 | }));
75 | });
76 | it('adds parentConnector if the theme is set and has connectorEditingContext with allowed values', function () {
77 | const result = extractConnectors(idea, visibleNodes, {connectorEditingContext: {allowed: ['great']}});
78 | expect(result[12]).toEqual(makeConnector({
79 | connectorEditingContext: {
80 | allowed: ['great']
81 | },
82 | from: 1,
83 | to: 12,
84 | attr: {great: true}
85 | }));
86 | });
87 | it('ignores parentConnector properties when the theme blocks overrides', function () {
88 | const result = extractConnectors(idea, visibleNodes, {connectorEditingContext: {allowed: []}});
89 | expect(result[12]).toEqual(makeConnector({
90 | from: 1,
91 | to: 12
92 | }));
93 | });
94 | it('clones the parent connnector so changes to node can be detected', function () {
95 | const result = extractConnectors(idea, visibleNodes);
96 | visibleNodes[12].attr.parentConnector.great = false;
97 | expect(result[12].attr.great).toEqual(true);
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/specs/core/layout/extract-links-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, expect, it, beforeEach, require*/
2 | const extractLinks = require('../../../src/core/layout/extract-links');
3 |
4 | describe('extractLinks', function () {
5 | 'use strict';
6 | let contentAggregate, visibleNodes;
7 | beforeEach(function () {
8 | contentAggregate = {
9 | links: [
10 | {ideaIdFrom: 2, ideaIdTo: 3},
11 | {ideaIdFrom: 2, ideaIdTo: 4, attr: {color: 'blue'}}
12 | ]
13 | };
14 | visibleNodes = {
15 | 2: true,
16 | 3: true,
17 | 4: true
18 | };
19 | });
20 | it('should not include links when node from is not visible', function () {
21 | delete visibleNodes[3];
22 | delete visibleNodes[4];
23 | expect(extractLinks(contentAggregate, visibleNodes)).toEqual({});
24 | });
25 | it('should not include links when node to is not visible', function () {
26 | delete visibleNodes[2];
27 | expect(extractLinks(contentAggregate, visibleNodes)).toEqual({});
28 | });
29 | it('should not include links when from and to nodes are visible', function () {
30 | expect(extractLinks(contentAggregate, visibleNodes)).toEqual({
31 | '2_3': {
32 | type: 'link',
33 | ideaIdFrom: 2,
34 | ideaIdTo: 3,
35 | attr: undefined
36 | },
37 | '2_4': {
38 | type: 'link',
39 | ideaIdFrom: 2,
40 | ideaIdTo: 4,
41 | attr: {color: 'blue'}
42 | }
43 | });
44 | });
45 | it('should clone the attribute', function () {
46 | const result = extractLinks(contentAggregate, visibleNodes);
47 | result['2_4'].attr.color = 'red';
48 | expect(contentAggregate.links[1].attr.color).toEqual('blue');
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/specs/core/layout/node-to-box-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require*/
2 | const nodeToBox = require('../../../src/core/layout/node-to-box');
3 | describe('nodeToBox', function () {
4 | 'use strict';
5 | it('should convert node to a box', function () {
6 | expect(nodeToBox({x: 10, styles: ['blue'], y: 20, width: 30, height: 40, level: 2})).toEqual({left: 10, styles: ['blue'], top: 20, width: 30, height: 40, level: 2});
7 | });
8 | it('should append default styles if not provided', function () {
9 | expect(nodeToBox({x: 10, y: 20, width: 30, height: 40, level: 2})).toEqual({left: 10, styles: ['default'], top: 20, width: 30, height: 40, level: 2});
10 | });
11 | it('should return falsy for undefined', function () {
12 | expect(nodeToBox()).toBeFalsy();
13 | });
14 | it('should return falsy for falsy', function () {
15 | expect(nodeToBox(false)).toBeFalsy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/specs/core/layout/top-down/compacted-group-width-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require */
2 | const compactedGroupWidth = require('../../../../src/core/layout/top-down/compacted-group-width');
3 | describe('compactedGroupWidth', function () {
4 | 'use strict';
5 | it('does not add margins if the group contains a single node', () => {
6 | expect(compactedGroupWidth([{width: 20}], 10)).toEqual(20);
7 | });
8 | it('adds margins between nodes to calculate full width', () => {
9 | expect(compactedGroupWidth([{width: 15}, {width: 30}], 10)).toEqual(55);
10 | });
11 | it('returns 0 for empty groups', () => {
12 | expect(compactedGroupWidth([], 10)).toEqual(0);
13 | expect(compactedGroupWidth(false, 10)).toEqual(0);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/specs/core/layout/top-down/sort-nodes-by-left-position-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, beforeEach, require */
2 | const underTest = require('../../../../src/core/layout/top-down/sort-nodes-by-left-position');
3 | describe('sortNodesByLeftPosition', () => {
4 | 'use strict';
5 | let first, second, third;
6 | beforeEach(() => {
7 | third = {x: 10, width: 5};
8 | first = {x: -2, width: 3};
9 | second = {x: 0, width: 5};
10 | });
11 | it('does not do anything in case of an empty array', () => {
12 | expect(underTest()).toBeFalsy();
13 | expect(underTest([])).toEqual([]);
14 | });
15 | it('sorts an array of nodes by left position', () => {
16 | expect(underTest([third, first, second])).toEqual([first, second, third]);
17 | expect(underTest([first, third, second])).toEqual([first, second, third]);
18 | expect(underTest([first, second, third])).toEqual([first, second, third]);
19 | });
20 | it('does not change a single-element array', () => {
21 | expect(underTest([first])).toEqual([first]);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/specs/core/theme/calc-child-position-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it, expect, beforeEach*/
2 |
3 | const underTest = require('../../../src/core/theme/calc-child-position');
4 |
5 | describe('calcChildPosition', () => {
6 | 'use strict';
7 | let parent, child;
8 | beforeEach(() => {
9 | parent = {top: 100, height: 100};
10 | child = {top: 100, height: 100};
11 | });
12 | 'use strict';
13 | it('should return above when child mid point is above the parent top with tolerance', () => {
14 | child.top = 39;
15 | expect(underTest(parent, child, 10)).toEqual('above');
16 | });
17 | it('should return below when child mid point is below the parent bottom with tolerance', () => {
18 | child.top = 161;
19 | expect(underTest(parent, child, 10)).toEqual('below');
20 | });
21 | describe('should return horizontal', () => {
22 | it('when child mid point is not above the parent top with tolerance', () => {
23 | child.top = 40;
24 | expect(underTest(parent, child, 10)).toEqual('horizontal');
25 | });
26 | it('when child mid point is not below the parent top with tolerance', () => {
27 | child.top = 160;
28 | expect(underTest(parent, child, 10)).toEqual('horizontal');
29 | });
30 | it('when child mid point is within then parent top and bottom', () => {
31 | expect(underTest(parent, child, 10)).toEqual('horizontal');
32 | });
33 |
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/specs/core/theme/color-to-rgb-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, require, expect, it*/
2 |
3 | const underTest = require('../../../src/core/theme/color-to-rgb');
4 |
5 | describe('convertToRGB', function () {
6 | 'use strict';
7 |
8 | describe('hex colors', function () {
9 | [
10 | ['#000000', [0, 0, 0]],
11 | ['#ffffff', [255, 255, 255]],
12 | ['#FFFFFF', [255, 255, 255]]
13 | ].forEach(function (args) {
14 | it('should convert ' + args[0] + ' to rgb:' + args[1].join(','), function () {
15 | expect(underTest(args[0])).toEqual(args[1]);
16 | });
17 | });
18 |
19 | });
20 | describe('rgb css colors', function () {
21 | [
22 | ['rgb(0,0,0)', [0, 0, 0]],
23 | ['rgb(255, 255, 255)', [255, 255, 255]],
24 | ['rgb(255, 254, 253)', [255, 254, 253]],
25 | ['rgb(255,254,253)', [255, 254, 253]]
26 | ].forEach(function (args) {
27 | it('should convert ' + args[0] + ' to rgb:' + args[1].join(','), function () {
28 | expect(underTest(args[0])).toEqual(args[1]);
29 | });
30 | });
31 | });
32 | describe('rgba css colors', function () {
33 | [
34 | ['rgba(0,0,0,1)', [0, 0, 0]],
35 | ['rgba(255, 255, 255, 0.8)', [255, 255, 255]],
36 | ['rgba(255, 254, 253, 0)', [255, 254, 253]],
37 | ['rgba(255,254,253,0.9)', [255, 254, 253]]
38 | ].forEach(function (args) {
39 | it('should convert ' + args[0] + ' to rgb:' + args[1].join(','), function () {
40 | expect(underTest(args[0])).toEqual(args[1]);
41 | });
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/specs/core/theme/foreground-style-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, expect, it, require*/
2 | const foregroundStyle = require('../../../src/core/theme/foreground-style');
3 | describe('foregroundStyle', function () {
4 | 'use strict';
5 | [
6 | ['#FFFFFF', 'darkColor'],
7 | ['rgba(255,255,255,1)', 'darkColor'],
8 | ['#000000', 'lightColor'],
9 | ['#EEEEEE', 'color'],
10 | ['#22AAE0', 'color'],
11 | ['#0000FF', 'lightColor'],
12 | ['#4F4F4F', 'lightColor'],
13 | ['#E0E0E0', 'color']
14 | ].forEach(function (args) {
15 | it('calculates the text class of nodes with background color ' + args[0] + ' to ' + args[1], function () {
16 | expect(foregroundStyle(args[0])).toEqual(args[1]);
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/specs/core/theme/line-styles-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it, expect*/
2 | const lineStyles = require('../../../src/core/theme/line-styles');
3 |
4 | describe('lineStyles', () => {
5 | 'use strict';
6 | describe('strokes', () => {
7 | describe('should return empty string', () => {
8 | it('when name is falsy', () => expect(lineStyles.strokes(undefined, 2)).toEqual(''));
9 | it('when name is solid', () => expect(lineStyles.strokes('solid', 2)).toEqual(''));
10 | });
11 | it('width can be a minimum of 1', () => {
12 | expect(lineStyles.strokes('dashed', -1)).toEqual('4, 4');
13 | expect(lineStyles.strokes('dashed')).toEqual('4, 4');
14 | });
15 | it('should return multiple of width when dashed', () => {
16 | expect(lineStyles.strokes('dashed', 2)).toEqual('8, 8');
17 | expect(lineStyles.strokes('dashed', 1)).toEqual('4, 4');
18 | expect(lineStyles.strokes('dashed', 10)).toEqual('40, 40');
19 | });
20 | it('should return multiple of width when dotted', () => {
21 | expect(lineStyles.strokes('dotted', 2)).toEqual('1, 8');
22 | expect(lineStyles.strokes('dotted', 1)).toEqual('1, 4');
23 | expect(lineStyles.strokes('dotted', 10)).toEqual('1, 40');
24 | });
25 | });
26 | describe('linecap', () => {
27 | it('should return a square cap for undefined style', () => expect(lineStyles.linecap()).toEqual('square'));
28 | it('should return a square cap for solid', () => expect(lineStyles.linecap('solid')).toEqual('square'));
29 | it('should return a round cap for dotted', () => expect(lineStyles.linecap('dotted')).toEqual('round'));
30 | it('should return an empty cap as default', () => expect(lineStyles.linecap('idunno')).toEqual(''));
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/specs/core/theme/line-types-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, beforeEach, it, expect, require*/
2 | const underTest = require('../../../src/core/theme/line-types');
3 | describe('lineTypes', function () {
4 | 'use strict';
5 |
6 | let calculatedConnector, position, parent, child;
7 | beforeEach(function () {
8 | calculatedConnector = {
9 | from: {
10 | x: 0,
11 | y: 20
12 | },
13 | to: {
14 | x: 100,
15 | y: 30
16 | },
17 | connectorTheme: {
18 | controlPoint: {
19 | height: 1.25
20 | }
21 | }
22 | };
23 | position = {
24 | top: 10,
25 | left: 10
26 | };
27 | parent = {
28 | height: 40
29 | };
30 | child = {
31 | height: 30
32 | };
33 | });
34 | describe('quadratic', function () {
35 | it('should return quadratic path', function () {
36 | expect(underTest.quadratic(calculatedConnector, position, parent, child)).toEqual({
37 | d: 'M-10,10Q-10,33 90,20',
38 | position: position
39 | });
40 | });
41 | });
42 | describe('vertical-quadratic-s-curve', function () {
43 | beforeEach(function () {
44 | calculatedConnector = {
45 | from: {
46 | x: 0,
47 | y: 20
48 | },
49 | to: {
50 | x: 30,
51 | y: 100
52 | }
53 | };
54 | });
55 | it('should return quadratic s curve path', function () {
56 | expect(underTest['vertical-quadratic-s-curve'](calculatedConnector, position, parent, child)).toEqual({
57 | d: 'M-10,10q0,20 15,40q15,20 15,40',
58 | initialRadius: 10,
59 | position: position
60 | });
61 | });
62 | describe('should return a straight line if connector to is below the connector from with tolerance', function () {
63 | it('to the left', function () {
64 | calculatedConnector.to.x = -19;
65 | expect(underTest['vertical-quadratic-s-curve'](calculatedConnector, position, parent, child)).toEqual({
66 | d: 'M-10,10l-19,80',
67 | initialRadius: 10,
68 | position: position
69 | });
70 | });
71 | it('to the right', function () {
72 | calculatedConnector.to.x = 19;
73 | expect(underTest['vertical-quadratic-s-curve'](calculatedConnector, position, parent, child)).toEqual({
74 | d: 'M-10,10l19,80',
75 | initialRadius: 10,
76 | position: position
77 | });
78 | });
79 | });
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/specs/core/theme/link-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, expect, it, beforeEach, require*/
2 | const defaultTheme = require('../../../src/core/theme/default-theme'),
3 | link = require('../../../src/core/theme/link'),
4 | Theme = require('../../../src/core/theme/theme');
5 | describe('Connectors', function () {
6 | 'use strict';
7 | let parent, child;
8 | beforeEach(function () {
9 | parent = { top: 100, left: 200, width: 100, height: 40, styles: ['default']};
10 | child = { top: 220, left: 330, width: 12, height: 44, styles: ['default']};
11 | });
12 | describe('linkPath', function () {
13 | it('draws a straight line between the borders of two nodes', function () {
14 | const path = link(parent, child);
15 | expect(path.d).toEqual('M100,20L136,120');
16 | expect(path.position).toEqual({ left: 200, top: 100, width: 142, height: 164 });
17 |
18 | });
19 | it('includes the label in result if attribute set', function () {
20 | const path = link(parent, child, {label: 'is here'});
21 | expect(path.label).toEqual('is here');
22 | });
23 | it('calculates the arrow if link attributes require it', function () {
24 | const path = link(parent, child, {arrow: true});
25 | expect(path.arrows).toEqual(['M136,106L136,120L127,109Z']);
26 | });
27 | it('does not calculate the arrow if link arrow attribute is "false"', function () {
28 | const path = link(parent, child, {arrow: 'false'});
29 | expect(path.arrows).toBeFalsy();
30 | });
31 | it('returns the default link theme if no theme is provided', function () {
32 | const path = link(parent, child);
33 | expect(path.theme).toEqual(defaultTheme.link.default);
34 | });
35 | it('returns the link theme from the provided theme object', function () {
36 | const path = link(parent, child, {}, new Theme({
37 | link: {
38 | default: {
39 | line: 'lll',
40 | label: 'xxx'
41 | }
42 | }
43 | }));
44 | expect(path.theme).toEqual({label: 'xxx', line: 'lll'});
45 | });
46 | it('requests the theme from link attributes', function () {
47 | const path = link(parent, child, {type: 'curly'}, new Theme({
48 | link: {
49 | curly: {
50 | line: 'clll',
51 | label: 'cxxx'
52 | },
53 | default: {
54 | line: 'lll',
55 | label: 'xxx'
56 | }
57 | }
58 | }));
59 | expect(path.theme).toEqual({label: 'cxxx', line: 'clll'});
60 |
61 | });
62 | it('merges link attributes with the theme to create line properties', function () {
63 | const theme = new Theme({
64 | link: {
65 | default: {
66 | line: {
67 | lineStyle: 'dashed',
68 | width: 5,
69 | color: 'green'
70 | }
71 | }
72 | }
73 | });
74 | expect(link(parent, child, {}, theme).lineProps).toEqual({
75 | strokes: '20, 20',
76 | linecap: '',
77 | width: 5,
78 | color: 'green'
79 | });
80 | expect(link(parent, child, {color: 'blue'}, theme).lineProps).toEqual({
81 | strokes: '20, 20',
82 | linecap: '',
83 | width: 5,
84 | color: 'blue'
85 | });
86 | expect(link(parent, child, {lineStyle: 'dotted'}, theme).lineProps).toEqual({
87 | strokes: '1, 20',
88 | linecap: 'round',
89 | width: 5,
90 | color: 'green'
91 | });
92 |
93 | expect(link(parent, child, {lineStyle: 'solid'}, theme).lineProps).toEqual({
94 | strokes: '',
95 | linecap: 'square',
96 | width: 5,
97 | color: 'green'
98 | });
99 | expect(link(parent, child, {width: 9}, theme).lineProps).toEqual({
100 | strokes: '36, 36',
101 | linecap: '',
102 | width: 9,
103 | color: 'green'
104 | });
105 | });
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/specs/core/theme/theme-fallback-values-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe it, expect*/
2 | const underTest = require('../../../src/core/theme/theme-fallback-values');
3 |
4 | describe('theme-fallback-values', () => {
5 | 'use strict';
6 | it('should include a node theme', () => {
7 | expect(underTest.nodeTheme).toEqual({
8 | margin: 5,
9 | font: {
10 | lineSpacing: 2.5,
11 | size: 9,
12 | weight: 'bold'
13 | },
14 | maxWidth: 146,
15 | backgroundColor: '#E0E0E0',
16 | borderType: 'surround',
17 | cornerRadius: 10,
18 | lineColor: '#707070',
19 | lineWidth: 1,
20 | lineStyle: 'solid',
21 | text: {
22 | color: '#4F4F4F',
23 | lightColor: '#EEEEEE',
24 | darkColor: '#000000'
25 | }
26 | });
27 | });
28 | it('should include a connector control point', () => {
29 | expect(underTest.connectorControlPoint).toEqual({
30 | horizontal: 1,
31 | default: 1.75
32 | });
33 | });
34 | it('should include a connector theme', () => {
35 | expect(underTest.connectorTheme).toEqual({
36 | type: 'quadratic',
37 | label: {
38 | position: {
39 | ratio: 0.5
40 | },
41 | backgroundColor: 'transparent',
42 | borderColor: 'transparent',
43 | text: {
44 | color: '#4F4F4F',
45 | font: {
46 | size: 9,
47 | sizePx: 12,
48 | weight: 'normal'
49 | }
50 | }
51 | },
52 | line: {
53 | color: '#707070',
54 | width: 1
55 | }
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/specs/core/theme/theme-to-dictionary-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it, expect*/
2 |
3 | const underTest = require('../../../src/core/theme/theme-to-dictionary'),
4 | defaultTheme = require('../../../src/core/theme/default-theme');
5 |
6 | describe('themeToDictionary', () => {
7 | 'use strict';
8 | ['name', 'connector', 'link'].forEach(attr => {
9 | it(`should leave ${attr} attribute unchanged`, () => {
10 | expect(underTest(defaultTheme)[attr]).toEqual(defaultTheme[attr]);
11 | });
12 | });
13 | it('should convert the node array into a dictionary', () => {
14 | const expectedNodes = {
15 | default: defaultTheme.node[0],
16 | level_1: defaultTheme.node[1],
17 | activated: defaultTheme.node[2],
18 | selected: defaultTheme.node[3],
19 | collapsed: defaultTheme.node[4],
20 | 'collapsed.selected': defaultTheme.node[5]
21 | };
22 | expect(underTest(defaultTheme).node).toEqual(expectedNodes);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/specs/core/util/calc-max-width-spec.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, expect, require, beforeEach */
2 | const calcMaxWidth = require('../../../src/core/util/calc-max-width');
3 | describe('calcMaxWidth', () => {
4 | 'use strict';
5 | let theme;
6 | beforeEach(() => {
7 | theme = {
8 | text: {
9 | maxWidth: 150,
10 | margin: 10
11 | }
12 | };
13 | });
14 | it('uses node width when it is specified instead of theme default width', () => {
15 | expect(calcMaxWidth({style: {width: 300}}, theme)).toEqual(300);
16 | });
17 | it('uses the theme default width if the node style does not override it', () => {
18 | expect(calcMaxWidth({}, theme)).toEqual(150);
19 | });
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/specs/core/util/observable-spec.js:
--------------------------------------------------------------------------------
1 | /*global require, describe, it, jasmine, beforeEach, expect, spyOn, console */
2 | const observable = require('../../../src/core/util/observable');
3 | describe('Observable', function () {
4 | 'use strict';
5 | let obs, listener;
6 | beforeEach(function () {
7 | obs = observable({});
8 | listener = jasmine.createSpy('Listener');
9 | });
10 | it('allows subscribers to observe an event', function () {
11 | obs.addEventListener('TestEvt', listener);
12 | obs.dispatchEvent('TestEvt', 'some', 'args');
13 | expect(listener).toHaveBeenCalledWith('some', 'args');
14 | });
15 | it('allows multiple subscribers to observe the same event', function () {
16 | obs.addEventListener('TestEvt', function () {});
17 | obs.addEventListener('TestEvt', listener);
18 | obs.dispatchEvent('TestEvt', 'some', 'args');
19 | expect(listener).toHaveBeenCalledWith('some', 'args');
20 | });
21 | it('allows same subscriber to observe multiple events', function () {
22 | obs.addEventListener('TestEvt', listener);
23 | obs.addEventListener('TestEvt2', listener);
24 | obs.dispatchEvent('TestEvt', 'some', 'args');
25 | obs.dispatchEvent('TestEvt2', 'more', 'params');
26 | expect(listener).toHaveBeenCalledWith('some', 'args');
27 | expect(listener).toHaveBeenCalledWith('more', 'params');
28 | });
29 | it('allows same subscriber to observe multiple events with a single subscription', function () {
30 | obs.addEventListener('TestEvt TestEvt2', listener);
31 | obs.dispatchEvent('TestEvt', 'some', 'args');
32 | obs.dispatchEvent('TestEvt2', 'more', 'params');
33 | expect(listener).toHaveBeenCalledWith('some', 'args');
34 | expect(listener).toHaveBeenCalledWith('more', 'params');
35 | });
36 | it('stops propagation if an event listener returns false', function () {
37 | obs.addEventListener('TestEvt', function () {
38 | return false;
39 | });
40 | obs.addEventListener('TestEvt', listener);
41 | obs.dispatchEvent('TestEvt', 'some', 'args');
42 | expect(listener).not.toHaveBeenCalledWith();
43 | });
44 | it('continnues if a listener barfs', function () {
45 | const barf = new Error('barf');
46 | obs.addEventListener('TestEvt', function () {
47 | throw barf;
48 | }, 1);
49 | obs.addEventListener('TestEvt', listener);
50 | spyOn(console, 'trace');
51 | try {
52 | obs.dispatchEvent('TestEvt', 'some', 'args');
53 | } catch (e) {
54 |
55 | }
56 |
57 | expect(listener).toHaveBeenCalledWith('some', 'args');
58 | expect(console.trace).toHaveBeenCalledWith('dispatchEvent failed', barf, jasmine.any(Object));
59 | });
60 | it('does not dispatch events to unsubscribed listeners', function () {
61 | obs.addEventListener('TestEvt', listener);
62 | obs.removeEventListener('TestEvt', listener);
63 | obs.dispatchEvent('TestEvt', 'some', 'args');
64 | expect(listener).not.toHaveBeenCalled();
65 | });
66 | it('does not dispatch events to subscribers of unrelated events', function () {
67 | obs.addEventListener('TestEvt', listener);
68 | obs.dispatchEvent('UnrelatedEvt', 'some', 'args');
69 | expect(listener).not.toHaveBeenCalled();
70 | });
71 | it('supports listener priorities', function () {
72 | let result = '';
73 | obs.addEventListener('TestEvt', function () {
74 | result += 'first';
75 | }, 1);
76 | obs.addEventListener('TestEvt', function () {
77 | result += 'second';
78 | }, 3);
79 | obs.addEventListener('TestEvt', function () {
80 | result += 'third';
81 | }, 2);
82 | obs.dispatchEvent('TestEvt');
83 |
84 | expect(result).toBe('secondthirdfirst');
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/specs/helpers/jquery-extension-matchers.js:
--------------------------------------------------------------------------------
1 | /*global beforeEach, jasmine, require */
2 | const _ = require('underscore');
3 | beforeEach(function () {
4 | 'use strict';
5 | jasmine.addMatchers({
6 | toHaveBeenCalledOnJQueryObject: function () {
7 | return {
8 | compare: function (actual, expected) {
9 | return {
10 | pass: actual.calls && actual.calls.mostRecent() && actual.calls.mostRecent().object[0] === expected[0]
11 | };
12 | }
13 | };
14 | },
15 | toHaveOwnStyle: function () {
16 | const checkStyle = function (element, style) {
17 | if (element.attr('style')) {
18 | if (_.isArray(style)) {
19 | return _.find(style, function (aStyle) {
20 | return checkStyle(element, aStyle);
21 | });
22 | } else {
23 | return element.attr('style').indexOf(style) >= 0;
24 | }
25 | }
26 | return false;
27 | };
28 | return {
29 | compare: function (element, styleName) {
30 | const result = {
31 | pass: checkStyle(element, styleName)
32 | };
33 | if (result.pass) {
34 | result.message = element.attr('style') + ' has own style ' + styleName;
35 | } else {
36 | result.message = element[0] + ' does not have own style ' + styleName + ' (' + element.attr('style') + ')';
37 | }
38 | return result;
39 | }
40 | };
41 | }
42 | });
43 | });
44 |
45 |
--------------------------------------------------------------------------------
/specs/support/jasmine-runner.js:
--------------------------------------------------------------------------------
1 | /*global jasmine, require, process*/
2 | const Jasmine = require('jasmine'),
3 | SpecReporter = require('jasmine-spec-reporter').SpecReporter,
4 | jrunner = new Jasmine(),
5 | runJasmine = function () {
6 | 'use strict';
7 | let filter;
8 | process.argv.slice(2).forEach(option => {
9 | if (option === 'full') {
10 | jasmine.getEnv().clearReporters();
11 | jasmine.getEnv().addReporter(new SpecReporter({
12 | displayStacktrace: 'all'
13 | }));
14 | }
15 | if (option.match('^filter=')) {
16 | filter = option.match('^filter=(.*)')[1];
17 | }
18 | });
19 | jrunner.loadConfig({
20 | 'spec_dir': 'specs',
21 | 'spec_files': [
22 | 'core/**/*[sS]pec.js'
23 | ],
24 | 'helpers': [
25 | 'helpers/**/*.js'
26 | ]
27 | });
28 | jrunner.execute(undefined, filter);
29 | };
30 |
31 | runJasmine();
32 |
--------------------------------------------------------------------------------
/src/browser/build-connection.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const themeConnector = require('../core/theme/connector');
3 | require('./get-data-box');
4 | module.exports = function buildConnection(element, optional) {
5 | 'use strict';
6 | const applyInnerRect = (shape, box) => {
7 | const innerRect = shape.data().innerRect;
8 | if (innerRect) {
9 | box.left += innerRect.dx;
10 | box.top += innerRect.dy;
11 | box.width = innerRect.width;
12 | box.height = innerRect.height;
13 | }
14 | },
15 | connectorBuilder = optional && optional.connectorBuilder || themeConnector,
16 | shapeFrom = element.data('nodeFrom'),
17 | shapeTo = element.data('nodeTo'),
18 | theme = optional && optional.theme,
19 | connectorAttr = element.data('attr'),
20 | fromBox = shapeFrom && shapeFrom.getDataBox(),
21 | toBox = shapeTo && shapeTo.getDataBox();
22 | if (!shapeFrom || !shapeTo || shapeFrom.length === 0 || shapeTo.length === 0) {
23 | return;
24 | }
25 |
26 | applyInnerRect(shapeFrom, fromBox);
27 | applyInnerRect(shapeTo, toBox);
28 | fromBox.styles = shapeFrom.data('styles');
29 | toBox.styles = shapeTo.data('styles');
30 |
31 | return Object.assign(connectorBuilder(fromBox, toBox, theme), connectorAttr);
32 |
33 | };
34 |
--------------------------------------------------------------------------------
/src/browser/calc-label-center-point.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const defaultTheme = require('../core/theme/default-theme'),
3 | createSVG = require('./create-svg'),
4 | pathElement = createSVG('path');
5 | module.exports = function calcLabelCenterPoint(connectionPosition, fromBox, toBox, d, labelTheme) {
6 | 'use strict';
7 | labelTheme = labelTheme || defaultTheme.connector.default.label;
8 | const labelPosition = labelTheme.position || {};
9 |
10 | pathElement.attr('d', d);
11 | if (labelPosition.aboveEnd) {
12 | const middleToBox = toBox.left + (toBox.width / 2) - connectionPosition.left,
13 | middleFromBox = fromBox.left + (fromBox.width / 2) - connectionPosition.left,
14 | multiplier = labelPosition.ratio || 1;
15 | return {
16 | x: Math.round(middleFromBox + multiplier * (middleToBox - middleFromBox)),
17 | y: toBox.top - connectionPosition.top - labelPosition.aboveEnd
18 | };
19 | } else if (labelPosition.ratio) {
20 | return pathElement[0].getPointAtLength(pathElement[0].getTotalLength() * labelTheme.position.ratio);
21 | }
22 |
23 | return pathElement[0].getPointAtLength(pathElement[0].getTotalLength() * 0.5);
24 |
25 | };
26 |
27 |
--------------------------------------------------------------------------------
/src/browser/create-connector.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery'),
3 | createSVG = require('./create-svg'),
4 | connectorKey = require('../core/util/connector-key'),
5 | buildConnection = require('../browser/build-connection'),
6 | convertPositionToTransform = require('../core/util/convert-position-to-transform');
7 |
8 | jQuery.fn.createConnector = function (connector, optional) {
9 | 'use strict';
10 | const stage = this.parent('[data-mapjs-role=stage]'),
11 | element = createSVG('g').data({'nodeFrom': stage.nodeWithId(connector.from), 'nodeTo': stage.nodeWithId(connector.to), attr: connector.attr}).attr({'id': connectorKey(connector), 'data-mapjs-role': 'connector'}),
12 | connection = buildConnection(element, optional);
13 | return element.css(Object.assign(convertPositionToTransform(connection.position), {stroke: connection.color}))
14 | .appendTo(this);
15 | };
16 |
17 |
--------------------------------------------------------------------------------
/src/browser/create-link.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery'),
3 | createSVG = require('./create-svg'),
4 | linkKey = require('../core/util/link-key'),
5 | themeLink = require('../core/theme/link'),
6 | convertPositionToTransform = require('../core/util/convert-position-to-transform');
7 |
8 | require('./get-data-box');
9 | jQuery.fn.createLink = function (l, optional) {
10 | 'use strict';
11 | const stage = this.parent('[data-mapjs-role=stage]'),
12 | theme = (optional && optional.theme),
13 | linkBuilder = (optional && optional.linkBuilder) || themeLink,
14 | elementData = {
15 | 'nodeFrom': stage.nodeWithId(l.ideaIdFrom),
16 | 'nodeTo': stage.nodeWithId(l.ideaIdTo),
17 | attr: (l.attr && l.attr.style) || {}
18 | },
19 | element = createSVG('g')
20 | .attr({
21 | 'id': linkKey(l),
22 | 'data-mapjs-role': 'link'
23 | })
24 | .data(elementData),
25 | connection = linkBuilder(elementData.nodeFrom.getDataBox(), elementData.nodeTo.getDataBox(), elementData.attrs, theme);
26 | element.css(Object.assign(convertPositionToTransform(connection.position), {stroke: connection.lineProps.color}));
27 | element.appendTo(this);
28 | return element;
29 | };
30 |
31 |
--------------------------------------------------------------------------------
/src/browser/create-node.js:
--------------------------------------------------------------------------------
1 | /*global require*/
2 | const jQuery = require('jquery'),
3 | nodeKey = require('../core/util/node-key');
4 | jQuery.fn.createNode = function (node) {
5 | 'use strict';
6 | return jQuery('
')
7 | .attr({'id': nodeKey(node.id), 'tabindex': 0, 'data-mapjs-role': 'node' })
8 | .css({
9 | display: 'block',
10 | opacity: 0,
11 | position: 'absolute',
12 | top: Math.round(node.y || 0) + 'px',
13 | left: Math.round(node.x || 0) + 'px'
14 | })
15 | .addClass('mapjs-node')
16 | .appendTo(this);
17 | };
18 |
--------------------------------------------------------------------------------
/src/browser/create-reorder-bounds.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | jQuery.fn.createReorderBounds = function () {
4 | 'use strict';
5 | const result = jQuery('
').attr({
6 | 'data-mapjs-role': 'reorder-bounds',
7 | 'class': 'mapjs-reorder-bounds'
8 | }).hide().css('position', 'absolute').appendTo(this);
9 | return result;
10 | };
11 |
12 |
--------------------------------------------------------------------------------
/src/browser/create-svg.js:
--------------------------------------------------------------------------------
1 | /*global module, require, document */
2 | const jQuery = require('jquery');
3 | module.exports = function createSVG(tag) {
4 | 'use strict';
5 | return jQuery(document.createElementNS('http://www.w3.org/2000/svg', tag || 'svg'));
6 | };
7 |
--------------------------------------------------------------------------------
/src/browser/edit-node.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 |
4 | require('./inner-text');
5 | require('./place-caret-at-end');
6 | require('./select-all');
7 | require('./hammer-draggable');
8 |
9 |
10 | jQuery.fn.editNode = function (shouldSelectAll) {
11 | 'use strict';
12 | const node = this,
13 | textBox = this.find('[data-mapjs-role=title]'),
14 | unformattedText = this.data('title'),
15 | originalText = textBox.text();
16 |
17 | if (unformattedText !== originalText) { /* links or some other potential formatting issues */
18 | textBox.css('word-break', 'break-all');
19 | }
20 | textBox.text(unformattedText).attr('contenteditable', true).focus();
21 | if (shouldSelectAll) {
22 | textBox.selectAll();
23 | } else if (unformattedText) {
24 | textBox.placeCaretAtEnd();
25 | }
26 | node.shadowDraggable({disable: true});
27 |
28 | return new Promise((resolve, reject) => {
29 | const clear = function () {
30 | detachListeners(); //eslint-disable-line no-use-before-define
31 | textBox.css('word-break', '');
32 | textBox.removeAttr('contenteditable');
33 | node.shadowDraggable();
34 | },
35 | finishEditing = function () {
36 | const content = textBox.innerText();
37 | if (content === unformattedText) {
38 | return cancelEditing(); //eslint-disable-line no-use-before-define
39 | }
40 | clear();
41 | resolve(content);
42 | },
43 | cancelEditing = function () {
44 | clear();
45 | textBox.text(originalText);
46 | reject();
47 | },
48 | keyboardEvents = function (e) {
49 | const ENTER_KEY_CODE = 13,
50 | ESC_KEY_CODE = 27,
51 | TAB_KEY_CODE = 9,
52 | S_KEY_CODE = 83,
53 | Z_KEY_CODE = 90;
54 | if (e.which === ENTER_KEY_CODE && !e.shiftKey) { // allow shift+enter to break lines
55 | finishEditing();
56 | e.stopPropagation();
57 | } else if (e.which === ESC_KEY_CODE) {
58 | cancelEditing();
59 | e.preventDefault();
60 | e.stopPropagation();
61 | } else if (e.which === TAB_KEY_CODE || (e.which === S_KEY_CODE && (e.metaKey || e.ctrlKey) && !e.altKey)) {
62 | finishEditing();
63 | e.preventDefault(); /* stop focus on another object */
64 | } else if (!e.shiftKey && e.which === Z_KEY_CODE && (e.metaKey || e.ctrlKey) && !e.altKey) { /* undo node edit on ctrl+z if text was not changed */
65 | if (textBox.text() === unformattedText) {
66 | cancelEditing();
67 | }
68 | e.stopPropagation();
69 | }
70 | textBox.trigger('keydown-complete');
71 | },
72 | attachListeners = function () {
73 | textBox.on('blur', finishEditing).on('keydown', keyboardEvents);
74 | },
75 | detachListeners = function () {
76 | textBox.off('blur', finishEditing).off('keydown', keyboardEvents);
77 | };
78 | attachListeners();
79 | });
80 | };
81 |
82 |
--------------------------------------------------------------------------------
/src/browser/find-line.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery'),
3 | connectorKey = require('../core/util/connector-key'),
4 | linkKey = require('../core/util/link-key');
5 | jQuery.fn.findLine = function (line) {
6 | 'use strict';
7 | if (line && line.type === 'connector') {
8 | return this.find('#' + connectorKey(line));
9 | } else if (line && line.type === 'link') {
10 | return this.find('#' + linkKey(line));
11 | }
12 | console.log('invalid.line', line); //eslint-disable-line
13 | throw 'invalid-args';
14 | };
15 |
16 |
--------------------------------------------------------------------------------
/src/browser/get-box.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | jQuery.fn.getBox = function () {
4 | 'use strict';
5 | const domShape = this && this[0];
6 | if (!domShape) {
7 | return false;
8 | }
9 | return {
10 | top: domShape.offsetTop,
11 | left: domShape.offsetLeft,
12 | width: domShape.offsetWidth,
13 | height: domShape.offsetHeight
14 | };
15 | };
16 |
17 |
--------------------------------------------------------------------------------
/src/browser/get-data-box.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | require('./get-box');
4 | jQuery.fn.getDataBox = function () {
5 | 'use strict';
6 | const domShapeData = this.data();
7 | if (domShapeData && domShapeData.width && domShapeData.height) {
8 | return {
9 | top: domShapeData.y,
10 | left: domShapeData.x,
11 | width: domShapeData.width,
12 | height: domShapeData.height
13 | };
14 | }
15 | return this.getBox();
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/src/browser/inner-text.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | jQuery.fn.innerText = function () {
4 | 'use strict';
5 | const htmlContent = this.html(),
6 | containsBr = /
/.test(htmlContent),
7 | containsDiv = /
/.test(htmlContent);
8 | if (containsDiv && this[0].innerText) { /* broken safari jquery text */
9 | return this[0].innerText.trim();
10 | } else if (containsBr) { /*broken firefox innerText */
11 | return htmlContent.replace(/
/gi, '\n').replace(/(<([^>]+)>)/gi, '');
12 | }
13 | return this.text();
14 | };
15 |
16 |
--------------------------------------------------------------------------------
/src/browser/link-edit-widget.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | jQuery.fn.linkEditWidget = function (mapModel) {
4 | 'use strict';
5 | return this.each(function () {
6 | const element = jQuery(this),
7 | colorElement = element.find('.color'),
8 | lineStyleElement = element.find('.lineStyle'),
9 | arrowElement = element.find('.arrow');
10 | let currentLink, width, height;
11 | mapModel.addEventListener('linkSelected', function (link, selectionPoint, linkStyle) {
12 | currentLink = link;
13 | element.show();
14 | width = width || element.width();
15 | height = height || element.height();
16 | element.css({
17 | top: (selectionPoint.y - 0.5 * height - 15) + 'px',
18 | left: (selectionPoint.x - 0.5 * width - 15) + 'px'
19 | });
20 | colorElement.val(linkStyle.color).change();
21 | lineStyleElement.val(linkStyle.lineStyle);
22 | arrowElement[linkStyle.arrow ? 'addClass' : 'removeClass']('active');
23 | });
24 | mapModel.addEventListener('mapMoveRequested', function () {
25 | element.hide();
26 | });
27 | element.find('.delete').click(function () {
28 | mapModel.removeLink('mouse', currentLink.ideaIdFrom, currentLink.ideaIdTo);
29 | element.hide();
30 | });
31 | colorElement.change(function () {
32 | mapModel.updateLinkStyle('mouse', currentLink.ideaIdFrom, currentLink.ideaIdTo, 'color', jQuery(this).val());
33 | });
34 | lineStyleElement.find('a').click(function () {
35 | mapModel.updateLinkStyle('mouse', currentLink.ideaIdFrom, currentLink.ideaIdTo, 'lineStyle', jQuery(this).text());
36 | });
37 | arrowElement.click(function () {
38 | mapModel.updateLinkStyle('mouse', currentLink.ideaIdFrom, currentLink.ideaIdTo, 'arrow', !arrowElement.hasClass('active'));
39 | });
40 | element.mouseleave(element.hide.bind(element));
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/src/browser/node-cache-mark.js:
--------------------------------------------------------------------------------
1 | /*global require, module */
2 | const _ = require('underscore');
3 | module.exports = function nodeCacheMark(idea, optional) {
4 | 'use strict';
5 | const levelOverride = optional && optional.level,
6 | theme = (optional && optional.theme),
7 | isGroup = idea.attr && idea.attr.group;
8 | return {
9 | title: !isGroup && idea.title,
10 | width: idea.attr && idea.attr.style && idea.attr.style.width,
11 | theme: theme && theme.name,
12 | icon: idea.attr && idea.attr.icon && _.pick(idea.attr.icon, 'width', 'height', 'position'),
13 | collapsed: idea.attr && idea.attr.collapsed,
14 | note: !!(idea.attr && idea.attr.note),
15 | fontMultiplier: idea.attr && idea.attr.style && idea.attr.style.fontMultiplier,
16 | styles: theme && theme.nodeStyles(idea.level || levelOverride, idea.attr),
17 | level: idea.level || levelOverride
18 | };
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/src/browser/node-resize-widget.js:
--------------------------------------------------------------------------------
1 | /*global require*/
2 | const jQuery = require('jquery');
3 | require('./hammer-draggable');
4 | jQuery.fn.nodeResizeWidget = function (nodeId, mapModel, stagePositionForPointEvent) {
5 | 'use strict';
6 | return this.each(function () {
7 | let initialPosition,
8 | initialWidth,
9 | initialStyle;
10 | const element = jQuery(this),
11 | minAllowedWidth = 50,
12 | nodeTextElement = element.find('span[data-mapjs-role=title]'),
13 | nodeTextDOM = nodeTextElement[0],
14 | stopEvent = function (evt) {
15 | if (evt) {
16 | evt.stopPropagation();
17 | }
18 | if (evt && evt.gesture) {
19 | evt.gesture.stopPropagation();
20 | }
21 | },
22 | calcDragWidth = function (evt) {
23 | const pos = stagePositionForPointEvent(evt),
24 | dx = pos && initialPosition && (pos.x - initialPosition.x),
25 | dragWidth = dx && Math.max(minAllowedWidth, (initialWidth + dx));
26 | return dragWidth;
27 | },
28 | dragHandle = jQuery('
').addClass('resize-node').shadowDraggable().on('mm:start-dragging mm:start-dragging-shadow', function (evt) {
29 | if (!mapModel.isEditingEnabled()) {
30 | return stopEvent(evt);
31 | }
32 | mapModel.selectNode(nodeId);
33 | initialPosition = stagePositionForPointEvent(evt);
34 | initialWidth = nodeTextElement.innerWidth();
35 | initialStyle = {
36 | 'node.min-width': element.css('min-width'),
37 | 'span.min-width': nodeTextElement.css('min-width'),
38 | 'span.max-width': nodeTextElement.css('max-width')
39 | };
40 | }).on('mm:stop-dragging mm:cancel-dragging', function (evt) {
41 | if (!mapModel.isEditingEnabled()) {
42 | return stopEvent(evt);
43 | }
44 | const dragWidth = nodeTextElement.outerWidth();
45 | nodeTextElement.css({'max-width': initialStyle['span.max-width'], 'min-width': initialStyle['span.min-width']});
46 | element.css('min-width', initialStyle['node.min-width']);
47 | if (evt) {
48 | evt.stopPropagation();
49 | }
50 | if (evt && evt.gesture) {
51 | evt.gesture.stopPropagation();
52 | }
53 | element.trigger(jQuery.Event('mm:resize', {nodeWidth: dragWidth}));
54 | }).on('mm:drag', function (evt) {
55 | if (!mapModel.isEditingEnabled()) {
56 | return stopEvent(evt);
57 | }
58 | let dragWidth = calcDragWidth(evt);
59 | if (dragWidth) {
60 | nodeTextElement.css({'max-width': dragWidth, 'min-width': dragWidth});
61 | element.css('min-width', nodeTextElement.outerWidth());
62 | if (nodeTextDOM.scrollWidth > nodeTextDOM.offsetWidth) {
63 | dragWidth = nodeTextDOM.scrollWidth;
64 | nodeTextElement.css({'max-width': dragWidth, 'min-width': dragWidth});
65 | element.css('min-width', nodeTextElement.outerWidth());
66 | }
67 | }
68 | stopEvent(evt);
69 | });
70 | dragHandle.appendTo(element);
71 | });
72 | };
73 |
--------------------------------------------------------------------------------
/src/browser/node-with-id.js:
--------------------------------------------------------------------------------
1 | /*global require*/
2 | const jQuery = require('jquery'),
3 | nodeKey = require('../core/util/node-key');
4 |
5 | jQuery.fn.nodeWithId = function (id) {
6 | 'use strict';
7 | return this.find('#' + nodeKey(id));
8 | };
9 |
10 |
--------------------------------------------------------------------------------
/src/browser/place-caret-at-end.js:
--------------------------------------------------------------------------------
1 | /*global require, window, document */
2 | const jQuery = require('jquery');
3 | jQuery.fn.placeCaretAtEnd = function () {
4 | 'use strict';
5 |
6 | if (!window.getSelection || !document.createRange) {
7 | return;
8 | }
9 | const el = this[0],
10 | range = document.createRange(),
11 | sel = window.getSelection();
12 | range.selectNodeContents(el);
13 | range.collapse(false);
14 | sel.removeAllRanges();
15 | sel.addRange(range);
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/src/browser/queue-fade-out.js:
--------------------------------------------------------------------------------
1 | /*global require, setTimeout */
2 | const jQuery = require('jquery');
3 | jQuery.fn.queueFadeOut = function (theme) {
4 | 'use strict';
5 | const element = this,
6 | removeElement = () => {
7 | if (element.is(':focus')) {
8 | element.parents('[tabindex]').focus();
9 | }
10 | return element.remove();
11 | };
12 | if (!theme || theme.noAnimations()) {
13 | return removeElement();
14 | }
15 | return element
16 | .on('transitionend', removeElement)
17 | .css('opacity', 0);
18 | setTimeout(removeElement, 500);
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/src/browser/select-all.js:
--------------------------------------------------------------------------------
1 | /*global require, window, document */
2 | const jQuery = require('jquery');
3 | jQuery.fn.selectAll = function () {
4 | 'use strict';
5 | const el = this[0];
6 | let range, sel, textRange;
7 | if (window.getSelection && document.createRange) {
8 | range = document.createRange();
9 | range.selectNodeContents(el);
10 | sel = window.getSelection();
11 | sel.removeAllRanges();
12 | sel.addRange(range);
13 | } else if (document.body.createTextRange) {
14 | textRange = document.body.createTextRange();
15 | textRange.moveToElementText(el);
16 | textRange.select();
17 | }
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/src/browser/set-theme-class-list.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery'),
3 | _ = require('underscore');
4 | jQuery.fn.setThemeClassList = function (classList) {
5 | 'use strict';
6 | const domElement = this[0],
7 | filterClasses = function (classes) {
8 | return _.filter(classes, function (c) {
9 | return /^level_.+/.test(c) || /^attr_.+/.test(c);
10 | });
11 | },
12 | toRemove = filterClasses(domElement.classList),
13 | toAdd = classList && classList.length && filterClasses(classList);
14 | domElement.classList.remove.apply(domElement.classList, toRemove);
15 | if (toAdd && toAdd.length) {
16 | domElement.classList.add.apply(domElement.classList, toAdd);
17 | }
18 | return this;
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/src/browser/update-connector-text.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const createSVG = require('./create-svg'),
3 | getTextElement = function (parentElement, labelText, elementType, centrePoint) {
4 | 'use strict';
5 | elementType = elementType || 'text';
6 | let textElement = parentElement.find(elementType + '.mapjs-connector-text');
7 | if (!labelText) {
8 | textElement.remove();
9 | return false;
10 | } else {
11 | if (textElement.length === 0) {
12 | textElement = createSVG(elementType).attr('class', 'mapjs-connector-text');
13 | if (centrePoint) {
14 | textElement[0].style.transform = `translate(${centrePoint.x}px, ${centrePoint.y}px)`;
15 | }
16 | textElement.appendTo(parentElement);
17 | }
18 | return textElement;
19 | }
20 | },
21 | updateConnectorText = function (parentElement, centrePoint, labelText, labelTheme) {
22 | 'use strict';
23 | const g = getTextElement(parentElement, labelText, 'g', centrePoint),
24 | rectElement = g && getTextElement(g, labelText, 'rect'),
25 | textElement = g && getTextElement(g, labelText),
26 | textDOM = textElement && textElement[0],
27 | rectDOM = rectElement && rectElement[0],
28 | translate = {};
29 |
30 | let dimensions = false;
31 | if (!textDOM) {
32 | return false;
33 | }
34 | textDOM.style.stroke = 'none';
35 | textDOM.style.fill = labelTheme.text.color;
36 | textDOM.style.fontSize = labelTheme.text.font.sizePx + 'px';
37 | textDOM.style.fontWeight = labelTheme.text.font.weight;
38 | textDOM.style.dominantBaseline = 'hanging';
39 | textElement.text(labelText.trim());
40 | dimensions = textDOM.getClientRects()[0];
41 | translate.x = Math.round(centrePoint.x - dimensions.width / 2);
42 | translate.y = Math.round(centrePoint.y - dimensions.height - 2);
43 | // textDOM.style.left = Math.round(centrePoint.x - dimensions.width / 2);
44 | // textDOM.style.top = Math.round(centrePoint.y - dimensions.height);
45 | g[0].style.transform = `translate(${translate.x}px, ${translate.y}px)`;
46 | textDOM.setAttribute('x', 0); //Math.round(centrePoint.x - dimensions.width / 2));
47 | textDOM.setAttribute('y', 2); //Math.round(centrePoint.y - dimensions.height));
48 |
49 | // rectDOM.style.left = Math.round(centrePoint.x - dimensions.width / 2);
50 | // rectDOM.style.top = Math.round(centrePoint.y - dimensions.height - 2);
51 | rectDOM.setAttribute('x', 0); //Math.round(centrePoint.x - dimensions.width / 2));
52 | rectDOM.setAttribute('y', 0); //Math.round(centrePoint.y - dimensions.height - 2));
53 | rectDOM.setAttribute('height', Math.round(dimensions.height));
54 | rectDOM.setAttribute('width', Math.round(dimensions.width));
55 | rectDOM.style.fill = labelTheme.backgroundColor;
56 | rectDOM.style.stroke = labelTheme.borderColor;
57 | return textElement;
58 | };
59 |
60 | module.exports = updateConnectorText;
61 |
--------------------------------------------------------------------------------
/src/browser/update-connector.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 |
3 | const jQuery = require('jquery'),
4 | createSVG = require('./create-svg'),
5 | defaultTheme = require('../core/theme/default-theme'),
6 | lineStrokes = require('../core/theme/line-strokes'),
7 | convertPositionToTransform = require('../core/util/convert-position-to-transform'),
8 | updateConnectorText = require('./update-connector-text'),
9 | calcLabelCenterPont = require('./calc-label-center-point'),
10 | buildConnection = require('../browser/build-connection'),
11 | connectionIsUpdated = (element, connection, theme) => {
12 | 'use strict';
13 | const connectionPropCheck = JSON.stringify(connection) + (theme && theme.name);
14 | if (!connection || connectionPropCheck === element.data('changeCheck')) {
15 | return false;
16 | }
17 | element.data('changeCheck', connectionPropCheck);
18 | return connection;
19 | };
20 | require('./get-data-box');
21 |
22 | jQuery.fn.updateConnector = function (optional) {
23 | 'use strict';
24 | const theme = optional && optional.theme;
25 | return jQuery.each(this, function () {
26 | let pathElement, hitElement;
27 | const element = jQuery(this),
28 | connectorAttr = element.data('attr'),
29 | allowParentConnectorOverride = !theme || !(theme.connectorEditingContext || theme.blockParentConnectorOverride) || (theme.connectorEditingContext && theme.connectorEditingContext.allowed && theme.connectorEditingContext.allowed.length), //TODO: rempve blockParentConnectorOverride once site has been live for a while
30 | connection = buildConnection(element, optional),
31 | applyLabel = function () {
32 | const labelText = (connectorAttr && connectorAttr.label) || '',
33 | shapeTo = labelText && element.data('nodeTo'),
34 | shapeFrom = labelText && element.data('nodeFrom'),
35 | labelTheme = (connection.theme && connection.theme.label) || defaultTheme.connector.default.label,
36 | labelCenterPoint = labelText && calcLabelCenterPont(connection.position, shapeFrom.getDataBox(), shapeTo.getDataBox(), connection.d, labelTheme);
37 | updateConnectorText(
38 | element,
39 | labelCenterPoint,
40 | labelText,
41 | labelTheme
42 | );
43 | };
44 |
45 | if (!connection) {
46 | element.remove();
47 | return;
48 | }
49 |
50 | if (!connectionIsUpdated(element, connection, theme)) {
51 | return;
52 | }
53 | element.data('theme', connection.theme);
54 | element.data('position', Object.assign({}, connection.position));
55 | pathElement = element.find('path.mapjs-connector');
56 | hitElement = element.find('path.mapjs-link-hit');
57 | element.css(Object.assign(convertPositionToTransform(connection.position), {stroke: connection.color}));
58 | if (pathElement.length === 0) {
59 | pathElement = createSVG('path').attr('class', 'mapjs-connector').appendTo(element);
60 | }
61 | //TODO: if the map was translated (so only the relative position changed), do not re-update the curve!!!!
62 | pathElement.attr({
63 | 'd': connection.d,
64 | 'stroke-width': connection.width,
65 | 'stroke-dasharray': lineStrokes[connection.lineStyle || 'solid'],
66 | fill: 'transparent'
67 | });
68 | if (allowParentConnectorOverride) {
69 | if (hitElement.length === 0) {
70 | hitElement = createSVG('path').attr('class', 'mapjs-link-hit noTransition').appendTo(element);
71 | }
72 | hitElement.attr({
73 | 'd': connection.d,
74 | 'stroke-width': connection.width + 12
75 | });
76 | } else {
77 | if (hitElement.length > 0) {
78 | hitElement.remove();
79 | }
80 | }
81 | applyLabel();
82 | });
83 | };
84 |
85 |
--------------------------------------------------------------------------------
/src/browser/update-link.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery'),
3 | createSVG = require('./create-svg'),
4 | convertPositionToTransform = require('../core/util/convert-position-to-transform'),
5 | updateConnectorText = require('./update-connector-text'),
6 | themeLink = require('../core/theme/link'),
7 | calcLabelCenterPont = require('./calc-label-center-point'),
8 | showArrows = function (connection, element) {
9 | 'use strict';
10 | const arrowElements = element.find('path.mapjs-arrow');
11 | if (connection.arrows && connection.arrows.length) {
12 | //connection.arrow can be true, 'to', 'from', 'both'
13 | connection.arrows.forEach((arrow, index) => {
14 | let arrowElement = arrowElements.eq(index);
15 | if (arrowElement.length === 0) {
16 | arrowElement = createSVG('path').attr('class', 'mapjs-arrow').appendTo(element);
17 | }
18 | arrowElement
19 | .attr({
20 | d: arrow,
21 | fill: connection.lineProps.color,
22 | 'stroke-width': connection.lineProps.width
23 | })
24 | .show();
25 | });
26 | arrowElements.slice(connection.arrows.length).hide();
27 | } else {
28 | arrowElements.hide();
29 | }
30 | };
31 |
32 | require('./get-data-box');
33 |
34 | jQuery.fn.updateLink = function (optional) {
35 | 'use strict';
36 | const linkBuilder = (optional && optional.linkBuilder) || themeLink,
37 | theme = (optional && optional.theme);
38 | return jQuery.each(this, function () {
39 | const element = jQuery(this),
40 | shapeFrom = element.data('nodeFrom'),
41 | shapeTo = element.data('nodeTo'),
42 | attrs = element.data('attr') || {},
43 | applyLabel = function (connection, fromBox, toBox) {
44 | const labelText = attrs.label || '',
45 | labelTheme = connection.theme.label,
46 | labelCenterPoint = labelText && calcLabelCenterPont(connection.position, fromBox, toBox, connection.d, labelTheme);
47 | updateConnectorText(
48 | element,
49 | labelCenterPoint,
50 | labelText,
51 | labelTheme
52 | );
53 | };
54 | let connection = false,
55 | pathElement = element.find('path.mapjs-link'),
56 | hitElement = element.find('path.mapjs-link-hit'),
57 | fromBox = false, toBox = false, changeCheck = false;
58 | if (!shapeFrom || !shapeTo || shapeFrom.length === 0 || shapeTo.length === 0) {
59 | element.hide();
60 | return;
61 | }
62 | fromBox = shapeFrom.getDataBox();
63 | toBox = shapeTo.getDataBox();
64 |
65 | connection = linkBuilder(fromBox, toBox, attrs, theme);
66 | changeCheck = JSON.stringify(connection) + (theme && theme.name);
67 | if (changeCheck === element.data('changeCheck')) {
68 | return;
69 | }
70 | element.data('changeCheck', changeCheck);
71 |
72 |
73 | element.data('theme', connection.theme);
74 | element.data('position', Object.assign({}, connection.position));
75 | element.css(Object.assign(convertPositionToTransform(connection.position), {stroke: connection.lineProps.color}));
76 |
77 | if (pathElement.length === 0) {
78 | pathElement = createSVG('path').attr('class', 'mapjs-link').appendTo(element);
79 | }
80 | pathElement.attr({
81 | 'd': connection.d,
82 | 'stroke-width': connection.lineProps.width,
83 | 'stroke-dasharray': connection.lineProps.strokes,
84 | 'stroke-linecap': connection.lineProps.linecap,
85 | fill: 'transparent'
86 | });
87 |
88 | if (hitElement.length === 0) {
89 | hitElement = createSVG('path').attr('class', 'mapjs-link-hit noTransition').appendTo(element);
90 | }
91 | hitElement.attr({
92 | 'd': connection.d,
93 | 'stroke-width': connection.lineProps.width + 12
94 | });
95 | showArrows(connection, element);
96 | applyLabel(connection, fromBox, toBox);
97 | });
98 | };
99 |
100 |
--------------------------------------------------------------------------------
/src/browser/update-reorder-bounds.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | jQuery.fn.updateReorderBounds = function (border, box, dropCoords) {
4 | 'use strict';
5 | const element = this;
6 | if (!border) {
7 | element.hide();
8 | return;
9 | }
10 | element.show();
11 | element.attr('mapjs-edge', border.edge);
12 | if (border.edge === 'top') {
13 | element.css({
14 | top: border.minY,
15 | left: Math.round(dropCoords.x - element.width() / 2)
16 | });
17 | } else {
18 | element.css({
19 | top: Math.round(dropCoords.y - element.height() / 2),
20 | left: border.x - (border.edge === 'left' ? element.width() : 0)
21 | });
22 | }
23 |
24 | };
25 |
--------------------------------------------------------------------------------
/src/browser/update-stage.js:
--------------------------------------------------------------------------------
1 | /*global require */
2 | const jQuery = require('jquery');
3 | jQuery.fn.updateStage = function () {
4 | 'use strict';
5 | const data = this.data(),
6 | size = {
7 | 'min-width': Math.round(data.width - data.offsetX),
8 | 'min-height': Math.round(data.height - data.offsetY),
9 | 'width': Math.round(data.width - data.offsetX),
10 | 'height': Math.round(data.height - data.offsetY),
11 | 'transform-origin': 'top left',
12 | 'transform': 'translate3d(' + Math.round(data.offsetX) + 'px, ' + Math.round(data.offsetY) + 'px, 0)'
13 | },
14 | svgContainer = this.find('[data-mapjs-role=svg-container]')[0];
15 | if (data.scale && data.scale !== 1) {
16 | size.transform = 'scale(' + data.scale + ') translate(' + Math.round(data.offsetX) + 'px, ' + Math.round(data.offsetY) + 'px)';
17 | }
18 | this.css(size);
19 | if (svgContainer) {
20 | svgContainer.setAttribute('viewBox',
21 | '' + Math.round(-1 * data.offsetX) + ' ' + Math.round(-1 * data.offsetY) + ' ' + Math.round(data.width) + ' ' + Math.round(data.height)
22 | );
23 | svgContainer.setAttribute('style',
24 | 'top:' + Math.round(-1 * data.offsetY) + 'px; ' +
25 | 'left:' + Math.round(-1 * data.offsetX) + 'px; ' +
26 | 'width:' + Math.round(data.width) + 'px; ' +
27 | 'height:' + Math.round(data.height) + 'px;'
28 | );
29 | }
30 | return this;
31 | };
32 |
33 |
--------------------------------------------------------------------------------
/src/core/content/apply-idea-attributes-to-node-theme.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 |
3 | const foregroundStyle = require('../theme/foreground-style');
4 | module.exports = function applyIdeaAttributesToNodeTheme(idea, nodeTheme) {
5 | 'use strict';
6 | if (!nodeTheme || !idea || !idea.attr || !idea.attr.style) {
7 | return nodeTheme;
8 | }
9 | const isColorSetByUser = () => {
10 | const setByUser = idea.attr && idea.attr.style && idea.attr.style.background;
11 | if (setByUser === 'false' || setByUser === 'transparent') {
12 | return false;
13 | }
14 | return setByUser;
15 |
16 | },
17 | fontMultiplier = idea.attr.style.fontMultiplier,
18 | textAlign = idea.attr.style.textAlign,
19 | colorSetByUser = isColorSetByUser(),
20 | colorText = nodeTheme.borderType !== 'surround';
21 |
22 | if (colorSetByUser) {
23 | if (colorText) {
24 | nodeTheme.text.color = colorSetByUser;
25 | } else {
26 | nodeTheme.text.color = nodeTheme.text[foregroundStyle(colorSetByUser)];
27 | nodeTheme.backgroundColor = colorSetByUser;
28 | }
29 | }
30 |
31 | if (textAlign) {
32 | nodeTheme.text = Object.assign({}, nodeTheme.text, {alignment: textAlign});
33 | }
34 |
35 | if ((nodeTheme && nodeTheme.hasFontMultiplier)) {
36 | return nodeTheme;
37 | }
38 |
39 | if (!nodeTheme.font || !fontMultiplier || Math.abs(fontMultiplier) <= 0.01 || Math.abs(fontMultiplier - 1) <= 0.01) {
40 | return nodeTheme;
41 | }
42 | if (nodeTheme.font.size) {
43 | nodeTheme.font.size = nodeTheme.font.size * fontMultiplier;
44 | }
45 |
46 | if (nodeTheme.font.lineSpacing) {
47 | nodeTheme.font.lineSpacing = nodeTheme.font.lineSpacing * fontMultiplier;
48 | }
49 |
50 | if (nodeTheme.font.sizePx) {
51 | nodeTheme.font.sizePx = nodeTheme.font.sizePx * fontMultiplier;
52 | }
53 | if (nodeTheme.font.lineSpacingPx) {
54 | nodeTheme.font.lineSpacingPx = nodeTheme.font.lineSpacingPx * fontMultiplier;
55 | }
56 | nodeTheme.hasFontMultiplier = true;
57 |
58 |
59 | return nodeTheme;
60 | };
61 |
--------------------------------------------------------------------------------
/src/core/content/calc-idea-level.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const _ = require('underscore');
3 |
4 | module.exports = function calcIdeaLevel(contentIdea, nodeId, currentLevel) {
5 | 'use strict';
6 | if (!contentIdea) {
7 | throw 'invalid-args';
8 | }
9 | if (contentIdea.id == nodeId) { //eslint-disable-line eqeqeq
10 | return currentLevel || 0;
11 | }
12 | if (!nodeId) {
13 | return;
14 | }
15 | currentLevel = currentLevel || 1;
16 |
17 | const directChild = _.find(contentIdea.ideas, function (idea) {
18 | return idea.id == nodeId; //eslint-disable-line eqeqeq
19 | });
20 | if (directChild) {
21 | return currentLevel;
22 | }
23 |
24 | return _.reduce(contentIdea.ideas, function (result, idea) {
25 | return result || calcIdeaLevel(idea, nodeId, currentLevel + 1);
26 | }, undefined);
27 | };
28 |
--------------------------------------------------------------------------------
/src/core/content/content-upgrade.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore');
3 | module.exports = function contentUpgrade(content) {
4 | 'use strict';
5 | const upgradeV2 = function () {
6 | const doUpgrade = function (idea) {
7 | let collapsed;
8 | if (idea.style) {
9 | idea.attr = {};
10 | collapsed = idea.style.collapsed;
11 | delete idea.style.collapsed;
12 | idea.attr.style = idea.style;
13 | if (collapsed) {
14 | idea.attr.collapsed = collapsed;
15 | }
16 | delete idea.style;
17 | }
18 | if (idea.ideas) {
19 | _.each(idea.ideas, doUpgrade);
20 | }
21 | };
22 | if (content.formatVersion && content.formatVersion >= 2) {
23 | return;
24 | }
25 | doUpgrade(content);
26 | content.formatVersion = 2;
27 | },
28 | upgradeV3 = function () {
29 | const doUpgrade = function () {
30 | const rootAttrKeys = ['theme', 'themeOverrides', 'measurements-config', 'storyboards', 'progress-statuses'],
31 | oldRootAttr = (content && content.attr) || {},
32 | newRootAttr = _.pick(oldRootAttr, rootAttrKeys),
33 | newRootNodeAttr = _.omit(oldRootAttr, rootAttrKeys),
34 | firstLevel = (content && content.ideas),
35 | newRoot = {
36 | id: content.id,
37 | title: content.title,
38 | attr: newRootNodeAttr
39 | };
40 | if (firstLevel) {
41 | newRoot.ideas = firstLevel;
42 | }
43 | content.id = 'root';
44 | content.ideas = {
45 | 1: newRoot
46 | };
47 | delete content.title;
48 | content.attr = newRootAttr;
49 | };
50 | if (content.formatVersion && content.formatVersion >= 3) {
51 | return;
52 | }
53 | doUpgrade();
54 | content.formatVersion = 3;
55 | };
56 |
57 | upgradeV2();
58 | upgradeV3();
59 | return content;
60 | };
61 |
--------------------------------------------------------------------------------
/src/core/content/format-note-to-html.js:
--------------------------------------------------------------------------------
1 | /* global module, require */
2 | const URLHelper = require('../util/url-helper'),
3 | _ = require('underscore');
4 | module.exports = function formatNoteToHtml(noteText) {
5 | 'use strict';
6 | if (!noteText) {
7 | return '';
8 | }
9 | if (typeof noteText !== 'string') {
10 | throw 'invalid-args';
11 | }
12 | const safeString = _.escape(noteText);
13 | return URLHelper.formatLinks(safeString);
14 | };
15 |
--------------------------------------------------------------------------------
/src/core/content/formatted-node-title.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const urlHelper = require('../util/url-helper'),
3 | removeLinks = function (nodeTitle, maxUrlLength) {
4 | 'use strict';
5 | const strippedTitle = nodeTitle && urlHelper.stripLink(nodeTitle);
6 | if (strippedTitle.trim() === '') {
7 | return (!maxUrlLength || (nodeTitle.length < maxUrlLength) ? nodeTitle : (nodeTitle.substring(0, maxUrlLength) + '...'));
8 | } else {
9 | return strippedTitle;
10 | }
11 | },
12 | removeExtraSpaces = function (nodeTitle) {
13 | 'use strict';
14 | return nodeTitle.replace(/[ \t]+/g, ' ');
15 | },
16 | cleanNonPrintable = function (nodeTitle) {
17 | 'use strict';
18 | return nodeTitle.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F\u0080-\u009F]+/gu, '');
19 | },
20 | trimLines = function (nodeTitle) {
21 | 'use strict';
22 | return nodeTitle.replace(/\r/g, '').split('\n').map(line => line.trim()).join('\n');
23 | };
24 | module.exports = function (nodeTitle, maxUrlLength) {
25 | 'use strict';
26 | if (!nodeTitle || !nodeTitle.trim()) {
27 | return '';
28 | }
29 | const sanitizedTitle = cleanNonPrintable(nodeTitle),
30 | withoutLinks = removeLinks(sanitizedTitle, maxUrlLength),
31 | withConsolidatedSpaces = removeExtraSpaces(withoutLinks);
32 | return trimLines(withConsolidatedSpaces);
33 | };
34 |
35 |
--------------------------------------------------------------------------------
/src/core/content/is-empty-group.js:
--------------------------------------------------------------------------------
1 | /* global require, module */
2 | const _ = require('underscore');
3 | module.exports = function isEmptyGroup(contentIdea) {
4 | 'use strict';
5 | return contentIdea.attr && contentIdea.attr.group && _.isEmpty(contentIdea.ideas);
6 | };
7 |
--------------------------------------------------------------------------------
/src/core/content/sorted-sub-ideas.js:
--------------------------------------------------------------------------------
1 | /*global module */
2 | const positive = function positive(key) {
3 | 'use strict';
4 | return key >= 0;
5 | },
6 | negative = function negative(key) {
7 | 'use strict';
8 | return !positive(key);
9 | },
10 | absCompare = function (a, b) {
11 | 'use strict';
12 | return Math.abs(a) - Math.abs(b);
13 | },
14 | safeSort = function (contentIdea) {
15 | 'use strict';
16 | const childKeys = Object.keys(contentIdea.ideas).map(parseFloat),
17 | sortedChildKeys = childKeys.filter(positive).sort(absCompare).concat(childKeys.filter(negative).sort(absCompare));
18 | return sortedChildKeys.map(function (key) {
19 | return contentIdea.ideas[key];
20 | });
21 | };
22 | module.exports = function sortedSubIdeas(contentIdea) {
23 | 'use strict';
24 |
25 | if (!contentIdea.ideas) {
26 | return [];
27 | }
28 | return safeSort(contentIdea);
29 | };
30 |
31 |
--------------------------------------------------------------------------------
/src/core/content/traverse.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const sortedSubIdeas = require('./sorted-sub-ideas');
3 | module.exports = function traverse(contentIdea, iterator, postOrder, level) {
4 | 'use strict';
5 | const isSingleRootMap = !level && (!contentIdea.formatVersion || contentIdea.formatVersion < 3);
6 | level = level || (isSingleRootMap ? 1 : 0);
7 | if (!postOrder && (isSingleRootMap || level)) {
8 | iterator(contentIdea, level);
9 | }
10 | sortedSubIdeas(contentIdea).forEach(function (subIdea) {
11 | traverse(subIdea, iterator, postOrder, level + 1);
12 | });
13 | if (postOrder && (isSingleRootMap || level)) {
14 | iterator(contentIdea, level);
15 | }
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/src/core/deep-assign.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const isObjectObject = require('./is-object-object'),
3 | isNotRecursableObject = value => {
4 | 'use strict';
5 | return !isObjectObject(value);
6 | };
7 | module.exports = function deepAssign() {
8 | 'use strict';
9 | const args = Array.prototype.slice.call(arguments, 0),
10 | assignee = (args && args[0]),
11 | assigners = (args && args.length > 1 && args.slice(1)) || [];
12 | if (!assignee || args.find(isNotRecursableObject)) {
13 | throw new Error('invalid-args');
14 | }
15 | assigners.forEach(assigner => {
16 | Object.keys(assigner)
17 | .forEach(key => {
18 | if (isObjectObject(assigner[key]) && isObjectObject(assignee[key])) {
19 | assignee[key] = deepAssign({}, assignee[key], assigner[key]);
20 | } else if (isObjectObject(assigner[key])) {
21 | assignee[key] = deepAssign({}, assigner[key]);
22 | } else {
23 | assignee[key] = assigner[key];
24 | }
25 |
26 | });
27 | });
28 | return assignee;
29 | };
30 |
--------------------------------------------------------------------------------
/src/core/is-object-object.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | module.exports = function isObjectObject(value) {
3 | 'use strict';
4 | if (!value) {
5 | return false;
6 | }
7 | const type = typeof value;
8 | if (type === 'object') {
9 | return Object.prototype.toString.call(value) === '[object Object]';
10 | }
11 | return false;
12 | };
13 |
--------------------------------------------------------------------------------
/src/core/layout/calculate-layout.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const contentUpgrade = require('../content/content-upgrade'),
3 | Theme = require('../theme/theme'),
4 | extractConnectors = require('./extract-connectors'),
5 | extractLinks = require('./extract-links'),
6 | MultiRootLayout = require('./multi-root-layout'),
7 | nodeAttributeUtils = require('./node-attribute-utils'),
8 | defaultLayouts = {
9 | 'standard': require('./standard/calculate-standard-layout'),
10 | 'top-down': require('./top-down/calculate-top-down-layout')
11 | },
12 | formatResult = function (result, idea, theme, orientation) {
13 | 'use strict';
14 | nodeAttributeUtils.setThemeAttributes(result, theme);
15 | return {
16 | orientation: orientation,
17 | nodes: result,
18 | connectors: extractConnectors(idea, result, theme),
19 | links: extractLinks(idea, result),
20 | theme: idea.attr && idea.attr.theme,
21 | themeOverrides: Object.assign({}, idea.attr && idea.attr.themeOverrides)
22 | };
23 | };
24 |
25 | module.exports = function calculateLayout(idea, dimensionProvider, optional) {
26 | 'use strict';
27 | const layouts = (optional && optional.layouts) || defaultLayouts,
28 | theme = (optional && optional.theme) || new Theme({}),
29 | multiRootLayout = new MultiRootLayout(),
30 | margin = theme.attributeValue(['layout'], [], ['spacing'], {h: 20, v: 20}),
31 | orientation = theme.attributeValue(['layout'], [], ['orientation'], 'standard'),
32 | calculator = layouts[orientation] || layouts.standard;
33 |
34 | idea = contentUpgrade(idea);
35 |
36 | Object.keys(idea.ideas).forEach(function (rank) {
37 | const rootIdea = idea.ideas[rank],
38 | rootResult = calculator(rootIdea, dimensionProvider, {h: (margin.h || margin), v: (margin.v || margin)});
39 | multiRootLayout.appendRootNodeLayout(rootResult, rootIdea);
40 | });
41 |
42 | return formatResult (multiRootLayout.getCombinedLayout(10, optional), idea, theme, orientation);
43 | // result = calculator(idea, dimensionProvider, {h: (margin.h || margin), v: (margin.v || margin)});
44 |
45 | };
46 |
47 |
--------------------------------------------------------------------------------
/src/core/layout/extract-connectors.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore');
3 | module.exports = function extractConnectors(aggregate, visibleNodes, theme) {
4 | 'use strict';
5 | const result = {},
6 | allowParentConnectorOverride = !(theme && (theme.connectorEditingContext || theme.blockParentConnectorOverride)), //TODO: rempve blockParentConnectorOverride once site has been live for a while
7 | traverse = function (idea, parentId, isChildNode) {
8 | if (isChildNode) {
9 | const visibleNode = visibleNodes[idea.id];
10 | if (!visibleNode) {
11 | return;
12 | }
13 | if (parentId !== aggregate.id) {
14 | result[idea.id] = {
15 | type: 'connector',
16 | from: parentId,
17 | to: idea.id
18 | };
19 | if (visibleNode.attr && visibleNode.attr.parentConnector) {
20 | if (allowParentConnectorOverride && visibleNode.attr && visibleNode.attr.parentConnector) {
21 | result[idea.id].attr = _.clone(visibleNode.attr.parentConnector);
22 | } else if (theme && theme.connectorEditingContext && theme.connectorEditingContext.allowed && theme.connectorEditingContext.allowed.length) {
23 | result[idea.id].connectorEditingContext = theme.connectorEditingContext;
24 | result[idea.id].attr = _.pick(visibleNode.attr.parentConnector, theme.connectorEditingContext.allowed);
25 | }
26 | }
27 | }
28 | }
29 | if (idea.ideas) {
30 | Object.keys(idea.ideas).forEach(function (subNodeRank) {
31 | traverse(idea.ideas[subNodeRank], idea.id, true);
32 | });
33 | }
34 | };
35 | traverse(aggregate);
36 | return result;
37 | };
38 |
--------------------------------------------------------------------------------
/src/core/layout/extract-links.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const _ = require('underscore');
3 | module.exports = function extractLinks(idea, visibleNodes) {
4 | 'use strict';
5 | const result = {};
6 | _.each(idea.links, function (link) {
7 | if (visibleNodes[link.ideaIdFrom] && visibleNodes[link.ideaIdTo]) {
8 | result[link.ideaIdFrom + '_' + link.ideaIdTo] = {
9 | type: 'link',
10 | ideaIdFrom: link.ideaIdFrom,
11 | ideaIdTo: link.ideaIdTo,
12 | attr: _.clone(link.attr)
13 | };
14 | }
15 | });
16 | return result;
17 | };
18 |
19 |
--------------------------------------------------------------------------------
/src/core/layout/node-attribute-utils.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const objectUtils = require('../util/object-utils'),
3 | _ = require('underscore'),
4 | INHERIT_MARKER = 'theme_inherit',
5 | inheritAttributeKeysFromParentNode = (parentNode, node, keysToInherit) => {
6 | 'use strict';
7 | let remainingToInherit = [];
8 | if (parentNode.attr) {
9 | keysToInherit.forEach((keyToInherit) => {
10 | const parentValue = objectUtils.getValue(parentNode.attr, keyToInherit);
11 | if (parentValue && parentValue !== INHERIT_MARKER) {
12 |
13 | objectUtils.setValue(node.attr, keyToInherit, parentValue);
14 | } else {
15 | remainingToInherit.push(keyToInherit);
16 | }
17 | });
18 | } else {
19 | remainingToInherit = keysToInherit;
20 | }
21 | return remainingToInherit;
22 | },
23 | inheritAttributeKeys = (nodesMap, node, keysToInherit) => {
24 | 'use strict';
25 | if (!node || !node.parentId) {
26 | return;
27 | }
28 | const parentNode = nodesMap[node.parentId],
29 | remainingToInherit = (parentNode && inheritAttributeKeysFromParentNode(parentNode, node, keysToInherit)) || [];
30 | if (!remainingToInherit.length || !parentNode || !parentNode.parentId) {
31 | return;
32 | }
33 | inheritAttributeKeys(nodesMap, parentNode, remainingToInherit);
34 | inheritAttributeKeysFromParentNode(parentNode, node, remainingToInherit);
35 | },
36 | inheritAttributes = (nodesMap, node) => {
37 | 'use strict';
38 | if (!node || !node.parentId || !node.attr) {
39 | return;
40 | }
41 | const keysToInherit = objectUtils.keyComponentsWithValue(node.attr, INHERIT_MARKER);
42 | if (!keysToInherit || !keysToInherit.length) {
43 | return;
44 | }
45 | inheritAttributeKeys(nodesMap, node, keysToInherit);
46 | },
47 | setThemeAttributes = function (nodes, theme) {
48 | 'use strict';
49 | if (!nodes || !theme) {
50 | throw 'invalid-args';
51 | }
52 | Object.keys(nodes).forEach(function (nodeKey) {
53 | const node = nodes[nodeKey];
54 | node.styles = theme.nodeStyles(node.level, node.attr);
55 | node.attr = _.extend({}, theme.getLayoutConnectorAttributes(node.styles), node.attr);
56 | });
57 | Object.keys(nodes).forEach(function (nodeKey) {
58 | const node = nodes[nodeKey];
59 | inheritAttributes(nodes, node);
60 | });
61 | };
62 |
63 | module.exports = {
64 | INHERIT_MARKER: INHERIT_MARKER,
65 | inheritAttributes: inheritAttributes,
66 | inheritAttributeKeys: inheritAttributeKeys,
67 | inheritAttributeKeysFromParentNode: inheritAttributeKeysFromParentNode,
68 | setThemeAttributes: setThemeAttributes
69 | };
70 |
--------------------------------------------------------------------------------
/src/core/layout/node-to-box.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | module.exports = function nodeToBox(node) {
3 | 'use strict';
4 | if (!node) {
5 | return false;
6 | }
7 | return {
8 | left: node.x,
9 | top: node.y,
10 | width: node.width,
11 | height: node.height,
12 | level: node.level,
13 | styles: node.styles || ['default']
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/src/core/layout/standard/calculate-standard-layout.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore'),
3 | treeUtils = require('./tree');
4 | module.exports = function calculateStandardLayout(idea, dimensionProvider, margin) {
5 | 'use strict';
6 | const positive = function (rank, parentId) {
7 | return parentId !== idea.id || rank > 0;
8 | },
9 | negative = function (rank, parentId) {
10 | return parentId !== idea.id || rank < 0;
11 | },
12 | positiveTree = treeUtils.calculateTree(idea, dimensionProvider, margin, positive),
13 | negativeTree = treeUtils.calculateTree(idea, dimensionProvider, margin, negative),
14 | layout = positiveTree.toLayout(),
15 | negativeLayout = negativeTree.toLayout();
16 | _.each(negativeLayout.nodes, function (n) {
17 | n.x = -1 * n.x - n.width;
18 | });
19 | return _.extend(negativeLayout.nodes, layout.nodes);
20 | };
21 |
22 |
--------------------------------------------------------------------------------
/src/core/layout/top-down/align-group.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore'),
3 | compactedGroupWidth = require('./compacted-group-width'),
4 | sortNodesByLeftPosition = require('./sort-nodes-by-left-position');
5 | module.exports = function alignGroup(result, rootIdea, margin) {
6 | 'use strict';
7 | if (!margin) {
8 | throw 'invalid-args';
9 | }
10 | const nodes = result.nodes,
11 | rootNode = nodes[rootIdea.id],
12 | childIds = _.values(rootIdea.ideas).map(function (idea) {
13 | return idea.id;
14 | }),
15 | childNodes = childIds.map(function (id) {
16 | return nodes[id];
17 | }).filter(function (node) {
18 | return node;
19 | }),
20 | sortedChildNodes = sortNodesByLeftPosition(childNodes),
21 | getChildNodeBoundaries = function () {
22 | const rightMost = sortedChildNodes[sortedChildNodes.length - 1];
23 | return {
24 | left: sortedChildNodes[0].x,
25 | right: rightMost.x + rightMost.width
26 | };
27 | },
28 | setGroupWidth = function () {
29 | if (!childNodes.length) {
30 | return;
31 | }
32 | const levelBoundaries = getChildNodeBoundaries();
33 | rootNode.x = levelBoundaries.left;
34 | rootNode.width = levelBoundaries.right - levelBoundaries.left;
35 | },
36 | compactChildNodes = function () {
37 | if (!childNodes.length) {
38 | return;
39 | }
40 | const levelBoundaries = getChildNodeBoundaries(),
41 | levelCenter = levelBoundaries.left + (levelBoundaries.right - levelBoundaries.left) / 2,
42 | requiredWidth = compactedGroupWidth(childNodes, margin);
43 | let position = levelCenter - requiredWidth / 2;
44 | sortedChildNodes.forEach(node => {
45 | node.x = position;
46 | position = position + node.width + margin;
47 | });
48 | },
49 | sameLevelNodes = _.values(nodes).filter(function (node) {
50 | return node.level === rootNode.level && node.id !== rootNode.id;
51 | });
52 |
53 | compactChildNodes();
54 | setGroupWidth();
55 |
56 | sameLevelNodes.forEach(function (node) {
57 | node.verticalOffset = (node.verticalOffset || 0) + rootNode.height;
58 | });
59 | };
60 |
--------------------------------------------------------------------------------
/src/core/layout/top-down/calculate-top-down-layout.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const _ = require('underscore'),
3 | isEmptyGroup = require('../../content/is-empty-group'),
4 | alignGroup = require('./align-group'),
5 | combineVerticalSubtrees = require('./combine-vertical-subtrees');
6 | module.exports = function calculateTopDownLayout(aggregate, dimensionProvider, margin) {
7 | 'use strict';
8 | const isGroup = function (node) {
9 | return node.attr && node.attr.group;
10 | },
11 | toNode = function (idea, level, parentId) {
12 | const dimensions = dimensionProvider(idea, level),
13 | node = _.extend({level: level, verticalOffset: 0, title: isGroup(idea) ? '' : idea.title}, dimensions, _.pick(idea, ['id', 'attr']));
14 | if (parentId) {
15 | node.parentId = parentId;
16 | }
17 | return node;
18 | },
19 | //TODO: adds some complexity to the standard traverse function - includes parent id, omits post order, skips groups
20 | traverse = function (idea, predicate, level, parentId) {
21 | const childResults = {},
22 | shouldIncludeSubIdeas = !(_.isEmpty(idea.ideas) || (idea.attr && idea.attr.collapsed));
23 |
24 | level = level || 1;
25 | if (shouldIncludeSubIdeas) {
26 | Object.keys(idea.ideas).forEach(function (subNodeRank) {
27 | const newLevel = isGroup(idea) ? level : level + 1,
28 | result = traverse(idea.ideas[subNodeRank], predicate, newLevel, idea.id);
29 | if (result) {
30 | childResults[subNodeRank] = result;
31 | }
32 | });
33 | }
34 | return predicate(idea, childResults, level, parentId);
35 | },
36 | traversalLayout = function (idea, childLayouts, level, parentId) {
37 | const node = toNode(idea, level, parentId);
38 | let result;
39 |
40 | if (isGroup(node) && !_.isEmpty(idea.ideas)) {
41 | result = combineVerticalSubtrees(node, childLayouts, margin.h, true);
42 | alignGroup(result, idea, margin.h);
43 | } else {
44 | result = combineVerticalSubtrees(node, childLayouts, margin.h);
45 | }
46 | return result;
47 | },
48 | traversalLayoutWithoutEmptyGroups = function (idea, childLayouts, level, parentId) {
49 | return (idea === aggregate || !isEmptyGroup(idea)) && traversalLayout(idea, childLayouts, level, parentId);
50 | },
51 | setLevelHeights = function (nodes, levelHeights) {
52 | _.each(nodes, function (node) {
53 | node.y = levelHeights[node.level - 1] + node.verticalOffset;
54 | delete node.verticalOffset;
55 | });
56 | },
57 | getLevelHeights = function (nodes) {
58 | const maxHeights = [],
59 | heights = [];
60 | let level,
61 | totalHeight = 0;
62 |
63 | _.each(nodes, function (node) {
64 | maxHeights[node.level - 1] = Math.max(maxHeights[node.level - 1] || 0, node.height + node.verticalOffset);
65 | });
66 | totalHeight = maxHeights.reduce(function (memo, item) {
67 | return memo + item;
68 | }, 0) + (margin.v * (maxHeights.length - 1));
69 |
70 | heights[0] = Math.round(-0.5 * totalHeight);
71 |
72 | for (level = 1; level < maxHeights.length; level++) {
73 | heights [level] = heights [level - 1] + margin.v + maxHeights[level - 1];
74 | }
75 | return heights;
76 | },
77 | tree = traverse(aggregate, traversalLayoutWithoutEmptyGroups);
78 |
79 | setLevelHeights(tree.nodes, getLevelHeights(tree.nodes));
80 |
81 | return tree.nodes;
82 | };
83 |
84 |
--------------------------------------------------------------------------------
/src/core/layout/top-down/combine-vertical-subtrees.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore'),
3 | VerticalSubtreeCollection = require('./vertical-subtree-collection');
4 | module.exports = function combineVerticalSubtrees(node, childLayouts, margin, sameLevel) {
5 | 'use strict';
6 | const result = {
7 | nodes: { }
8 | },
9 | shift = function (nodes, xOffset) {
10 | _.each(nodes, function (node) {
11 | node.x += xOffset;
12 | });
13 | return nodes;
14 | },
15 | verticalSubtreeCollection = new VerticalSubtreeCollection(childLayouts, margin);
16 | let treeOffset;
17 |
18 | if (Array.isArray(childLayouts)) {
19 | throw 'child layouts are an array!';
20 | }
21 |
22 | result.nodes[node.id] = node;
23 | node.x = Math.round(-0.5 * node.width);
24 | result.levels = [{width: node.width, xOffset: node.x}];
25 |
26 | if (!verticalSubtreeCollection.isEmpty()) {
27 | if (sameLevel) {
28 | result.levels = verticalSubtreeCollection.getMergedLevels();
29 | treeOffset = result.levels[0].xOffset;
30 | } else {
31 | result.levels = result.levels.concat(verticalSubtreeCollection.getMergedLevels());
32 | treeOffset = result.levels[1].xOffset;
33 | }
34 | Object.keys(childLayouts).forEach(function (subtreeRank) {
35 | _.extend(result.nodes, shift(childLayouts[subtreeRank].nodes, treeOffset + verticalSubtreeCollection.getExpectedTranslation(subtreeRank)));
36 | });
37 | }
38 | return result;
39 | };
40 |
--------------------------------------------------------------------------------
/src/core/layout/top-down/compacted-group-width.js:
--------------------------------------------------------------------------------
1 | /*global module */
2 | module.exports = function compactedGroupWidth(nodeGroup, margin) {
3 | 'use strict';
4 | if (!nodeGroup || !nodeGroup.length) {
5 | return 0;
6 | }
7 | const totalWidth = nodeGroup.reduce((total, current) => total + current.width, 0),
8 | requiredMargins = (nodeGroup.length - 1) * margin;
9 | return totalWidth + requiredMargins;
10 | };
11 |
--------------------------------------------------------------------------------
/src/core/layout/top-down/sort-nodes-by-left-position.js:
--------------------------------------------------------------------------------
1 | /*global module */
2 | module.exports = function sortNodesByLeftPosition(nodes) {
3 | 'use strict';
4 | if (!nodes || !nodes.length) {
5 | return nodes;
6 | }
7 | return [].concat(nodes).sort((a, b) => a.x - b.x);
8 | };
9 |
--------------------------------------------------------------------------------
/src/core/layout/top-down/vertical-subtree-collection.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore');
3 | module.exports = function VerticalSubtreeCollection(subtreeMap, marginArg) {
4 | 'use strict';
5 | const self = this,
6 | sortedRanks = function () {
7 | if (!subtreeMap) {
8 | return [];
9 | }
10 | return _.sortBy(Object.keys(subtreeMap), parseFloat);
11 | },
12 | margin = marginArg || 0,
13 | calculateExpectedTranslations = function () {
14 | const ranks = sortedRanks(),
15 | translations = {},
16 |
17 | sortByRank = function () {
18 | /* todo: cache */
19 | if (_.isEmpty(subtreeMap)) {
20 | return [];
21 | }
22 | return sortedRanks().map(function (key) {
23 | return subtreeMap[key];
24 | });
25 | };
26 | let currentWidthByLevel;
27 |
28 | sortByRank().forEach(function (childLayout, rankIndex) {
29 | const currentRank = ranks[rankIndex];
30 | if (currentWidthByLevel === undefined) {
31 | translations[currentRank] = 0 - childLayout.levels[0].xOffset;
32 | currentWidthByLevel = childLayout.levels.map(function (level) {
33 | return level.width + translations[currentRank] + level.xOffset;
34 | });
35 | } else {
36 | childLayout.levels.forEach(function (level, levelIndex) {
37 | const currentLevelWidth = currentWidthByLevel[levelIndex];
38 | if (currentLevelWidth !== undefined) {
39 | if (translations[currentRank] === undefined) {
40 | translations[currentRank] = currentLevelWidth + margin - level.xOffset;
41 | } else {
42 | translations[currentRank] = Math.max(translations[currentRank], currentLevelWidth + margin - level.xOffset);
43 | }
44 | }
45 | });
46 |
47 | childLayout.levels.forEach(function (level, levelIndex) {
48 | currentWidthByLevel[levelIndex] = translations[currentRank] + level.xOffset + level.width;
49 | });
50 | }
51 | });
52 | return translations;
53 | },
54 | translationsByRank = calculateExpectedTranslations();
55 |
56 | self.getLevelWidth = function (level) {
57 | const candidateRanks = sortedRanks().filter(function (rank) {
58 | return self.existsOnLevel(rank, level);
59 | }),
60 | referenceLeft = candidateRanks[0], /* won't work if the first child layout does not exist on the widest level */
61 | referenceRight = candidateRanks[candidateRanks.length - 1],
62 | leftLayout = subtreeMap[referenceLeft],
63 | rightLayout = subtreeMap[referenceRight],
64 | leftx = leftLayout.levels[level].xOffset + self.getExpectedTranslation(referenceLeft),
65 | rightx = rightLayout.levels[level].xOffset + self.getExpectedTranslation(referenceRight);
66 | return rightx + rightLayout.levels[level].width - leftx;
67 | };
68 | self.getLevelWidths = function () {
69 | /* todo: cache */
70 | const result = [],
71 | maxLevel = _.max(_.map(subtreeMap, function (childLayout) {
72 | return childLayout.levels.length;
73 | }));
74 | for (let levelIdx = 0; levelIdx < maxLevel; levelIdx++) {
75 | result.push(self.getLevelWidth(levelIdx));
76 | }
77 | return result;
78 | };
79 | self.isEmpty = function () {
80 | return _.isEmpty(subtreeMap);
81 | };
82 |
83 | self.getExpectedTranslation = function (rank) {
84 | return translationsByRank[rank];
85 | };
86 | self.existsOnLevel = function (rank, level) {
87 | return subtreeMap[rank].levels.length > level;
88 | };
89 | self.getMergedLevels = function () {
90 | const targetCombinedLeftOffset = Math.round(self.getLevelWidth(0) * -0.5);
91 | return self.getLevelWidths().map(function (levelWidth, index) {
92 | const candidateRanks = sortedRanks().filter(function (rank) {
93 | return self.existsOnLevel(rank, index);
94 | }),
95 | referenceLeft = candidateRanks[0], /* won't work if the first child layout does not exist on the widest level */
96 | leftLayout = subtreeMap[referenceLeft];
97 | return {
98 | width: levelWidth,
99 | xOffset: leftLayout.levels[index].xOffset + self.getExpectedTranslation(referenceLeft) + targetCombinedLeftOffset
100 | };
101 | });
102 |
103 | };
104 |
105 | };
106 |
--------------------------------------------------------------------------------
/src/core/npm-core.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | module.exports = {
3 | MapModel: require('./map-model'),
4 | content: require('./content/content'),
5 | observable: require('./util/observable'),
6 | ThemeProcessor: require('./theme/theme-processor')
7 | };
8 |
--------------------------------------------------------------------------------
/src/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mindmup/mapjs-core",
3 | "license": "MIT",
4 | "private": true,
5 | "version": "4.0.0",
6 | "main": "npm-core.js",
7 | "dependencies": {
8 | "underscore": "^1.8.3",
9 | "monotone-convex-hull-2d": "^1.0.1",
10 | "polybooljs": "^1.1.1"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/core/theme/calc-child-position.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 |
3 | module.exports = function calcChildPosition(parent, child, tolerance) {
4 | 'use strict';
5 | const childMid = child.top + child.height * 0.5;
6 | if (childMid < parent.top - tolerance) {
7 | return 'above';
8 | }
9 | if (childMid > parent.top + parent.height + tolerance) {
10 | return 'below';
11 | }
12 | return 'horizontal';
13 | };
14 |
--------------------------------------------------------------------------------
/src/core/theme/color-parser.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const convertToRGB = require('./color-to-rgb');
3 | module.exports = function colorParser(colorObj) {
4 | 'use strict';
5 | if (!colorObj.color || colorObj.opacity === 0) {
6 | return 'transparent';
7 | }
8 | if (colorObj.opacity) {
9 | return 'rgba(' + convertToRGB(colorObj.color).join(',') + ',' + colorObj.opacity + ')';
10 | } else {
11 | return colorObj.color;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/core/theme/color-to-rgb.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const _ = require('underscore'),
3 | regCSSRGB = new RegExp(/^rgba?\(([^,\s]+)[,\s]*([^,\s]+)[,\s]*([^,\s\()]+).*$/),
4 | fromCSSRGB = function (colorString) {
5 | 'use strict';
6 | let matched;
7 | if (regCSSRGB.test(colorString)) {
8 |
9 | matched = colorString.match(regCSSRGB);
10 | if (matched.length === 4) {
11 | return _.map(matched.slice(1), function (i) {
12 | return parseInt(i);
13 | });
14 | }
15 | }
16 | },
17 | fromHexString = function (colorString) {
18 | 'use strict';
19 | const match = colorString.toString(16).match(/[a-f0-9]{6}/i);
20 | let integer, r, g, b;
21 | if (match) {
22 | integer = parseInt(match[0], 16);
23 | r = (integer >> 16) & 0xFF;
24 | g = (integer >> 8) & 0xFF;
25 | b = integer & 0xFF;
26 |
27 | return [r, g, b];
28 | }
29 | };
30 | module.exports = function convertToRGB(colorString) {
31 | 'use strict';
32 | return fromCSSRGB(colorString) || fromHexString(colorString) || [0, 0, 0];
33 | };
34 |
--------------------------------------------------------------------------------
/src/core/theme/foreground-style.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const convertToRGB = require('./color-to-rgb');
3 |
4 | module.exports = function foregroundStyle(backgroundColor) {
5 | 'use strict';
6 |
7 | /*jslint newcap:true*/
8 | // var luminosity = Color(backgroundColor).mix(Color('#EEEEEE')).luminosity();
9 | const mix = function (color1, color2) {
10 | return [
11 | Math.round(0.5 * (color1[0] + color2[0])),
12 | Math.round(0.5 * (color1[1] + color2[1])),
13 | Math.round(0.5 * (color1[2] + color2[2]))
14 | ];
15 | },
16 | calcLuminosity = function () {
17 | // http://www.w3.org/TR/WCAG20/#relativeluminancedef
18 | const rgb = mix(convertToRGB(backgroundColor), convertToRGB('#EEEEEE')),
19 | lum = [];
20 | let chan;
21 | for (let i = 0; i < rgb.length; i++) {
22 | chan = rgb[i] / 255;
23 | lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);
24 | }
25 | return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
26 | },
27 | luminosity = calcLuminosity();
28 | if (luminosity < 0.5) {
29 | return 'lightColor';
30 | } else if (luminosity < 0.9) {
31 | return 'color';
32 | }
33 | return 'darkColor';
34 | };
35 |
--------------------------------------------------------------------------------
/src/core/theme/line-strokes.js:
--------------------------------------------------------------------------------
1 | /*global module */
2 | module.exports = {
3 | dashed: '8, 8',
4 | solid: ''
5 | };
6 |
--------------------------------------------------------------------------------
/src/core/theme/line-styles.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 |
3 | module.exports = {
4 | strokes: (name, width) => {
5 | 'use strict';
6 | if (!name || name === 'solid') {
7 | return '';
8 | }
9 | const multipleWidth = Math.max(width || 1, 1) * 4;
10 | if (name === 'dashed') {
11 | return [multipleWidth, multipleWidth].join(', ');
12 | } else {
13 | return [1, multipleWidth].join(', ');
14 | }
15 | },
16 | linecap: (name) => {
17 | 'use strict';
18 | if (!name || name === 'solid') {
19 | return 'square';
20 | }
21 | if (name === 'dotted') {
22 | return 'round';
23 | }
24 | return '';
25 | }
26 |
27 | };
28 |
--------------------------------------------------------------------------------
/src/core/theme/merge-themes.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const deepAssign = require('../deep-assign'),
3 | isObjectObject = require('../is-object-object');
4 | module.exports = function mergeThemes(theme, themeOverride) {
5 | 'use strict';
6 | if (!isObjectObject(theme) || !isObjectObject(themeOverride)) {
7 | throw new Error('invalid-args');
8 | }
9 | if (theme.blockThemeOverrides) {
10 | return theme;
11 | }
12 | const themeNode = theme.node || [],
13 | themeOverrideNodes = themeOverride.node,
14 | mergedTheme = deepAssign({}, theme, themeOverride);
15 | if (themeOverrideNodes && themeOverrideNodes.length) {
16 | mergedTheme.node = [];
17 | themeNode.forEach(node => {
18 | const toMerge = themeOverrideNodes.find(overrride => overrride.name === node.name) || {};
19 | mergedTheme.node.push(deepAssign({}, node, toMerge));
20 | });
21 | themeOverrideNodes.forEach(overrride => {
22 | if (!mergedTheme.node.find(node => node.name === overrride.name)) {
23 | const toAdd = deepAssign({}, overrride);
24 | mergedTheme.node.push(toAdd);
25 | }
26 | });
27 | }
28 | return mergedTheme;
29 | };
30 |
--------------------------------------------------------------------------------
/src/core/theme/node-connection-point-x.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | const nearestInset = function (node, relatedNode, inset) {
3 | 'use strict';
4 | if (node.left + node.width < relatedNode.left) {
5 | return node.left + node.width - inset;
6 | }
7 | return node.left + inset;
8 | };
9 |
10 | module.exports = {
11 | 'center': function (node) {
12 | 'use strict';
13 | return Math.round(node.left + node.width * 0.5);
14 | },
15 | 'center-separated': function (node, relatedNode, horizontalInset, verticalInsetRatio) {
16 | 'use strict';
17 | const insetY = node.height * (verticalInsetRatio || 0.2),
18 | insetX = horizontalInset || 10,
19 | halfWidth = node.width / 2,
20 | nodeMidX = node.left + halfWidth,
21 | relatedNodeMidX = relatedNode.left + (relatedNode.width / 2),
22 | relatedNodeRight = (relatedNode.left + relatedNode.width),
23 | dy = relatedNode.top - node.top + node.height - insetY,
24 | calcDx = function () {
25 | if (relatedNode.left > node.left + node.width) {
26 | return relatedNode.left - nodeMidX;
27 | } else if (relatedNodeRight < node.left) {
28 | return relatedNodeRight - nodeMidX;
29 | } else if (relatedNode.left < nodeMidX) {
30 | return relatedNodeMidX - nodeMidX;
31 | } else {
32 | return relatedNodeMidX - nodeMidX;
33 | }
34 | },
35 | dx = calcDx(),
36 | requestedOffset = (dx / Math.abs(dy)) * insetY,
37 | cappedOffset = Math.max(requestedOffset, (halfWidth * -1) + insetX),
38 | offsetX = Math.min(cappedOffset, halfWidth - insetX);
39 | return Math.round(node.left + (node.width * 0.5) + offsetX);
40 | },
41 | 'nearest': function (node, relatedNode) {
42 | 'use strict';
43 | return nearestInset(node, relatedNode, 0);
44 | },
45 | 'nearest-inset': nearestInset
46 | };
47 |
--------------------------------------------------------------------------------
/src/core/theme/theme-attribute-utils.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const deepAssign = require('../deep-assign'),
3 | colorParser = require('./color-parser'),
4 | isObjectObject = require('../is-object-object'),
5 | themeFallbackValues = require('./theme-fallback-values'),
6 | attributeForPath = function (object, pathArray, fallback) {
7 | 'use strict';
8 | if (!object || !pathArray || !pathArray.length) {
9 | return (object === undefined && fallback) || object;
10 | }
11 | if (pathArray.length === 1) {
12 | return (object[pathArray[0]] === undefined && fallback) || object[pathArray[0]];
13 | }
14 | let remaining = pathArray.slice(0),
15 | current = object;
16 |
17 | while (remaining.length > 0) {
18 | current = current[remaining[0]];
19 | if (current === undefined) {
20 | return fallback;
21 | }
22 | remaining = remaining.slice(1);
23 | }
24 | return current;
25 | },
26 | themeAttributeValue = (themeDictionary, prefixes, styles, postfixes, fallback) => {
27 | 'use strict';
28 | const rootElement = attributeForPath(themeDictionary, prefixes);
29 | let toAssign = [{}];
30 | if (!rootElement) {
31 | return fallback;
32 | }
33 | if (styles && styles.length) {
34 | toAssign = toAssign.concat(styles.slice(0).reverse().map(style => rootElement[style]).filter(item => !!item));
35 | } else if (isObjectObject(rootElement)) {
36 | toAssign.push(rootElement);
37 | } else if (!postfixes || !postfixes.length) {
38 | return rootElement;
39 | } else {
40 | return fallback;
41 | }
42 | return attributeForPath(deepAssign.apply(deepAssign, toAssign), postfixes, fallback);
43 | },
44 | nodeAttributeToNodeTheme = (nodeAttribute) => {
45 | 'use strict';
46 | const getBackgroundColor = function () {
47 | const colorObj = attributeForPath(nodeAttribute, ['background']);
48 | if (colorObj) {
49 | return colorParser(colorObj);
50 | }
51 | return attributeForPath(nodeAttribute, ['backgroundColor']);
52 | },
53 | result = deepAssign({}, themeFallbackValues.nodeTheme);
54 | if (nodeAttribute) {
55 | result.margin = attributeForPath(nodeAttribute, ['text', 'margin'], result.margin);
56 | result.font = deepAssign({}, result.font, attributeForPath(nodeAttribute, ['text', 'font'], result.font));
57 | result.text = deepAssign({}, result.text, attributeForPath(nodeAttribute, ['text'], result.text));
58 | result.borderType = attributeForPath(nodeAttribute, ['border', 'type'], result.borderType);
59 | result.backgroundColor = getBackgroundColor() || result.backgroundColor;
60 | result.cornerRadius = attributeForPath(nodeAttribute, ['cornerRadius'], result.cornerRadius);
61 | result.lineColor = attributeForPath(nodeAttribute, ['border', 'line', 'color'], result.lineColor);
62 | result.lineWidth = attributeForPath(nodeAttribute, ['border', 'line', 'width'], result.lineWidth);
63 | result.lineStyle = attributeForPath(nodeAttribute, ['border', 'line', 'style'], result.lineStyle);
64 | }
65 | return result;
66 |
67 | },
68 | connectorControlPoint = (themeDictionary, childPosition, connectorStyle) => {
69 | 'use strict';
70 | const controlPointOffset = childPosition === 'horizontal' ? themeFallbackValues.connectorControlPoint.horizontal : themeFallbackValues.connectorControlPoint.default,
71 | defaultControlPoint = {'width': 0, 'height': controlPointOffset},
72 | configuredControlPoint = connectorStyle && attributeForPath(themeDictionary, ['connector', connectorStyle, 'controlPoint', childPosition]);
73 |
74 | return (configuredControlPoint && Object.assign({}, configuredControlPoint)) || defaultControlPoint;
75 | };
76 |
77 | module.exports = {
78 | attributeForPath: attributeForPath,
79 | themeAttributeValue: themeAttributeValue,
80 | nodeAttributeToNodeTheme: nodeAttributeToNodeTheme,
81 | connectorControlPoint: connectorControlPoint
82 | };
83 |
--------------------------------------------------------------------------------
/src/core/theme/theme-fallback-values.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const defaultTheme = require('./default-theme'),
3 | deepFreeze = require('../util/deep-freeze'),
4 | firstNode = defaultTheme.node[0],
5 | defaultConnector = defaultTheme.connector.default;
6 |
7 | module.exports = deepFreeze({
8 | nodeTheme: {
9 | margin: firstNode.text.margin,
10 | font: firstNode.text.font,
11 | maxWidth: firstNode.text.maxWidth,
12 | backgroundColor: firstNode.backgroundColor,
13 | borderType: firstNode.border.type,
14 | cornerRadius: firstNode.cornerRadius,
15 | lineColor: firstNode.border.line.color,
16 | lineWidth: firstNode.border.line.width,
17 | lineStyle: firstNode.border.line.style,
18 | text: {
19 | color: firstNode.text.color,
20 | lightColor: firstNode.text.lightColor,
21 | darkColor: firstNode.text.darkColor
22 | }
23 | },
24 | connectorControlPoint: {
25 | horizontal: defaultConnector.controlPoint.horizontal.height,
26 | default: defaultConnector.controlPoint.above.height
27 | },
28 | connectorTheme: {
29 | type: defaultConnector.type,
30 | label: defaultConnector.label,
31 | line: defaultConnector.line
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/core/theme/theme-to-dictionary.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 |
3 | module.exports = function themeToDictionary(themeJson) {
4 | 'use strict';
5 | const themeDictionary = Object.assign({}, themeJson),
6 | nodeArray = themeDictionary.node;
7 | if (themeDictionary && Array.isArray(themeDictionary.node)) {
8 | themeDictionary.node = {};
9 | nodeArray.forEach(function (nodeStyle) {
10 | themeDictionary.node[nodeStyle.name] = nodeStyle;
11 | });
12 | }
13 | return themeDictionary;
14 | };
15 |
--------------------------------------------------------------------------------
/src/core/util/calc-max-width.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 |
3 | module.exports = function calcMaxWidth(attr, nodeTheme/*, options*/) {
4 | 'use strict';
5 | return (attr && attr.style && attr.style.width) || (nodeTheme && nodeTheme.text && nodeTheme.text.maxWidth);
6 | };
7 |
--------------------------------------------------------------------------------
/src/core/util/clean-dom-id.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | module.exports = function cleanDOMId(s) {
3 | 'use strict';
4 | return s.replace(/[^A-Za-z0-9_-]/g, '_');
5 | };
6 |
--------------------------------------------------------------------------------
/src/core/util/connector-key.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const cleanDOMId = require('./clean-dom-id');
3 | module.exports = function connectorKey(connectorObj) {
4 | 'use strict';
5 | return cleanDOMId('connector_' + connectorObj.from + '_' + connectorObj.to);
6 | };
7 |
8 |
--------------------------------------------------------------------------------
/src/core/util/convert-position-to-transform.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const _ = require('underscore');
3 | module.exports = function convertPositionToTransform(cssPosition) {
4 | 'use strict';
5 | const position = _.omit(cssPosition, 'left', 'top');
6 | position.transform = 'translate(' + cssPosition.left + 'px,' + cssPosition.top + 'px)';
7 | return position;
8 | };
9 |
10 |
--------------------------------------------------------------------------------
/src/core/util/deep-freeze.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 |
3 | const requiresRecursion = (toFreeze, prop) => {
4 | 'use strict';
5 | return (typeof toFreeze[prop] === 'object' || typeof toFreeze[prop] === 'function') && !Object.isFrozen(toFreeze[prop]);
6 | },
7 | deepFreeze = function (toFreeze) {
8 | 'use strict';
9 | Object.freeze(toFreeze);
10 |
11 | Object.getOwnPropertyNames(toFreeze).forEach((prop) => {
12 | if (toFreeze.hasOwnProperty(prop) && toFreeze[prop] !== null && requiresRecursion(toFreeze, prop)) {
13 | deepFreeze(toFreeze[prop]);
14 | }
15 | });
16 |
17 | return toFreeze;
18 | };
19 | module.exports = deepFreeze;
20 |
--------------------------------------------------------------------------------
/src/core/util/link-key.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 | const cleanDOMId = require('./clean-dom-id');
3 | module.exports = function linkKey(linkObj) {
4 | 'use strict';
5 | return cleanDOMId('link_' + linkObj.ideaIdFrom + '_' + linkObj.ideaIdTo);
6 | };
7 |
--------------------------------------------------------------------------------
/src/core/util/node-key.js:
--------------------------------------------------------------------------------
1 | /*global module, require*/
2 | const cleanDOMId = require('./clean-dom-id');
3 | module.exports = function (id) {
4 | 'use strict';
5 | return cleanDOMId('node_' + id);
6 | };
7 |
--------------------------------------------------------------------------------
/src/core/util/object-utils.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | const getValue = (hashmap, attributeNameComponents) => {
3 | 'use strict';
4 | if (!hashmap || !attributeNameComponents || !attributeNameComponents.length || typeof hashmap !== 'object' || !Array.isArray(attributeNameComponents)) {
5 | return false;
6 | }
7 | const val = hashmap[attributeNameComponents[0]],
8 | remaining = attributeNameComponents.slice(1);
9 | if (remaining.length) {
10 | return getValue(val, remaining);
11 | }
12 | return val;
13 | },
14 | setValue = (hashmap, attributeNameComponents, value) => {
15 | 'use strict';
16 | if (!hashmap || !attributeNameComponents || !attributeNameComponents.length || typeof hashmap !== 'object' || !Array.isArray(attributeNameComponents)) {
17 | return false;
18 | }
19 | const remaining = attributeNameComponents.slice(1),
20 | currentKey = attributeNameComponents[0];
21 |
22 | if (remaining.length) {
23 | if (!hashmap[currentKey]) {
24 | if (!value) {
25 | return;
26 | }
27 | hashmap[currentKey] = {};
28 | }
29 | setValue(hashmap[currentKey], remaining, value);
30 | return;
31 | }
32 | if (!value) {
33 | delete hashmap[currentKey];
34 | } else {
35 | hashmap[currentKey] = value;
36 | }
37 | },
38 | keyComponentsWithValue = (hashmap, searchingFor) => {
39 | 'use strict';
40 | if (typeof searchingFor === 'object' || Array.isArray(searchingFor)) {
41 | throw 'search-type-not-supported';
42 | }
43 | const result = [];
44 | if (!hashmap || typeof hashmap !== 'object') {
45 | return [];
46 | }
47 | Object.keys(hashmap).forEach((key) => {
48 | const val = hashmap[key];
49 | if (val === searchingFor) {
50 | result.push([key]);
51 | }
52 | if (typeof val === 'object') {
53 | keyComponentsWithValue(val, searchingFor).forEach((subKey) => {
54 | if (!subKey || !subKey.length) {
55 | return;
56 | }
57 | const newComps = [key].concat(subKey);
58 | result.push(newComps);
59 | });
60 | }
61 | });
62 | return result;
63 | };
64 |
65 | module.exports = {
66 | getValue: getValue,
67 | setValue: setValue,
68 | keyComponentsWithValue: keyComponentsWithValue
69 | };
70 |
--------------------------------------------------------------------------------
/src/core/util/observable.js:
--------------------------------------------------------------------------------
1 | /*global module, console*/
2 | module.exports = function observable(base) {
3 | 'use strict';
4 | let listeners = [];
5 | base.addEventListener = function (types, listener, priority) {
6 | types.split(' ').forEach(function (type) {
7 | if (type) {
8 | listeners.push({
9 | type: type,
10 | listener: listener,
11 | priority: priority || 0
12 | });
13 | }
14 | });
15 | };
16 | base.listeners = function (type) {
17 | return listeners.filter(function (listenerDetails) {
18 | return listenerDetails.type === type;
19 | }).map(function (listenerDetails) {
20 | return listenerDetails.listener;
21 | });
22 | };
23 | base.removeEventListener = function (type, listener) {
24 | listeners = listeners.filter(function (details) {
25 | return details.listener !== listener;
26 | });
27 | };
28 | base.dispatchEvent = function (type) {
29 | const args = Array.prototype.slice.call(arguments, 1);
30 | listeners
31 | .filter(function (listenerDetails) {
32 | return listenerDetails.type === type;
33 | })
34 | .sort(function (firstListenerDetails, secondListenerDetails) {
35 | return secondListenerDetails.priority - firstListenerDetails.priority;
36 | })
37 | .some(function (listenerDetails) {
38 | try {
39 | return listenerDetails.listener.apply(undefined, args) === false;
40 | } catch (e) {
41 | console.trace('dispatchEvent failed', e, listenerDetails);
42 | }
43 |
44 | });
45 | };
46 | return base;
47 | };
48 |
--------------------------------------------------------------------------------
/src/core/util/url-helper.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | const URLHelper = function () {
3 | 'use strict';
4 | const self = this,
5 | urlPattern = /(https?:\/\/|www\.)[\w-]+(\.[\w-]+)+([\w\(\)\u0080-\u00FF.,!@?^=%&:\/~+#-]*[\w\(\)\u0080-\u00FF!@?^=%&\/~+#-])?/i,
6 | hrefUrl = function (url) {
7 | if (!url) {
8 | return '';
9 | }
10 | if (url[0] === '/') {
11 | return url;
12 | }
13 | if (/^[a-z]+:\/\//i.test(url)) {
14 | return url;
15 | }
16 | return 'http://' + url;
17 | },
18 | getGlobalPattern = function () {
19 | return new RegExp(urlPattern, 'gi');
20 | };
21 |
22 |
23 | self.containsLink = function (text) {
24 | return urlPattern.test(text);
25 | };
26 | self.getLink = function (text) {
27 | const url = text && text.match(urlPattern);
28 | if (url && url[0]) {
29 | return hrefUrl(url[0]);
30 | }
31 | return url;
32 | };
33 |
34 | self.stripLink = function (text) {
35 | if (!text) {
36 | return '';
37 | }
38 | return text.replace(urlPattern, '').trim();
39 | };
40 | self.formatLinks = function (text) {
41 | if (!text) {
42 | return '';
43 | }
44 | return text.replace(self.getPattern(), url => `
${url}`);
45 | };
46 | self.getPattern = getGlobalPattern;
47 | self.hrefUrl = hrefUrl;
48 | };
49 |
50 | module.exports = new URLHelper();
51 |
--------------------------------------------------------------------------------
/src/npm-main.js:
--------------------------------------------------------------------------------
1 | /*global module, require */
2 |
3 | require('./browser/dom-map-widget');
4 | require('./browser/link-edit-widget');
5 |
6 | module.exports = {
7 | MapModel: require('./core/map-model'),
8 | content: require('./core/content/content'),
9 | observable: require('./core/util/observable'),
10 | DomMapController: require('./browser/dom-map-controller'),
11 | ThemeProcessor: require('./core/theme/theme-processor'),
12 | Theme: require('./core/theme/theme'),
13 | defaultTheme: require('./core/theme/default-theme'),
14 | formatNoteToHtml: require('./core/content/format-note-to-html'),
15 | version: 4
16 | };
17 |
--------------------------------------------------------------------------------
/test/chevron-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/test/chevron-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/test/icon-link-active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
51 |
52 |
--------------------------------------------------------------------------------
/test/icon-link-inactive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
51 |
52 |
--------------------------------------------------------------------------------
/test/icon-paperclip-active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/test/icon-paperclip-inactive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
25 |
65 |
66 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/test/mapjs-default-styles.css:
--------------------------------------------------------------------------------
1 | .mapjs-node {
2 | margin: 0;
3 | padding: 7px;
4 | z-index: 2;
5 | user-select: none;
6 | -moz-user-select: none;
7 | -webkit-user-select: none;
8 | -ms-user-select: none;
9 | color: #4F4F4F;
10 | cursor: hand;
11 | }
12 | .mapjs-node .resize-node {
13 | position: absolute;
14 | height: 100%;
15 | width: 20px;
16 | right: -10px;
17 | top: 0;
18 | border-radius: 10px;
19 | background-color: transparent;
20 | cursor: ew-resize;
21 | }
22 | .mapjs-connector-text {
23 | font-family: NotoSans, "Helvetica Neue", Roboto, Helvetica, Arial, sans-serif;
24 | }
25 | .resize-node:hover {
26 | background-color: black;
27 | opacity: 0.3;
28 | }
29 | .mapjs-node:focus {
30 | outline: none;
31 | }
32 | .mapjs-node.droppable {
33 | border: 1px solid red;
34 | }
35 | .mapjs-add-link {
36 | cursor: crosshair;
37 | }
38 | .mapjs-add-link .mapjs-node {
39 | cursor: alias;
40 | }
41 | .mapjs-node span[contenteditable=true] {
42 | user-select: text;
43 | -moz-user-select: text;
44 | -webkit-user-select: text;
45 | -ms-user-select: text;
46 | }
47 | .mapjs-node {
48 | font-family: -apple-system, "Helvetica Neue", Roboto, Helvetica, Arial, sans-serif;
49 | font-weight: 500;
50 | font-size: 12px;
51 | line-height: 15px;
52 | }
53 | .mapjs-node span {
54 | white-space: pre-wrap;
55 | display: block;
56 | max-width: 146px;
57 | min-height: 1em;
58 | min-width: 1em;
59 | outline: none;
60 | }
61 | .mapjs-node.attr_group span {
62 | min-height: 1.5em;
63 | }
64 | .mapjs-node.dragging {
65 | opacity: 0.4;
66 | }
67 | .mapjs-node[mapjs-level="1"] {
68 | background-color:#22AAE0;
69 | }
70 | .mapjs-decorations {
71 | position: absolute;
72 | display: block;
73 | white-space: nowrap;
74 | }
75 | .mapjs-label {
76 | background: black;
77 | opacity: 0.5;
78 | color: white;
79 | display: inline-block;
80 | }
81 | .mapjs-hyperlink {
82 | background-image: url(icon-link-inactive.svg);
83 | width: 32px;
84 | height: 32px;
85 | background-size: 32px 32px;
86 | background-repeat: no-repeat no-repeat;
87 | display: inline-block;
88 | }
89 | .mapjs-hyperlink:hover {
90 | background-image:url(icon-link-active.svg);
91 | }
92 | .mapjs-attachment {
93 | background-image: url(icon-paperclip-inactive.svg);
94 | width: 16px;
95 | height: 32px;
96 | background-size: 16px 32px;
97 | background-repeat: no-repeat no-repeat;
98 | display: inline-block;
99 | }
100 | .mapjs-attachment:hover{
101 | background-image:url(icon-paperclip-active.svg);
102 |
103 | }
104 | .mapjs-draw-container {
105 | position: absolute;
106 | margin: 0px;
107 | padding: 0px;
108 | z-index: 1;
109 | }
110 | .mapjs-link-hit:hover {
111 | opacity: .1;
112 | }
113 | .mapjs-link-hit {
114 | opacity: 0;
115 | fill: transparent;
116 | cursor: crosshair;
117 | transition: opacity .2s;
118 | }
119 |
120 | .drag-shadow {
121 | opacity: 0.5;
122 | }
123 | .mapjs-reorder-bounds {
124 | stroke-width: 5px;
125 | fill: none;
126 | stroke: #000;
127 | }
128 | .mapjs-reorder-bounds {
129 | z-index: 999;
130 | background-image:url(chevron-left.svg);
131 | width: 11px;
132 | background-height: 100%;
133 | background-width: 100%;
134 | height: 20px;
135 | background-repeat: no-repeat;
136 | }
137 | .mapjs-reorder-bounds[mapjs-edge="left"] {
138 | background-image:url(chevron-right.svg);
139 | }
140 | .mapjs-reorder-bounds[mapjs-edge="top"] {
141 | transform: rotate(-90deg);
142 | }
143 |
--------------------------------------------------------------------------------
/test/start.js:
--------------------------------------------------------------------------------
1 | /*global require, document, window, console */
2 | const MAPJS = require('../src/npm-main'),
3 | jQuery = require('jquery'),
4 | themeProvider = require('./theme'),
5 | testMap = require('./example-map'),
6 | content = MAPJS.content,
7 | init = function () {
8 | 'use strict';
9 | let domMapController = false;
10 | const container = jQuery('#container'),
11 | idea = content(testMap),
12 | touchEnabled = false,
13 | mapModel = new MAPJS.MapModel([]),
14 | layoutThemeStyle = function (themeJson) {
15 | const themeCSS = themeJson && new MAPJS.ThemeProcessor().process(themeJson).css;
16 | if (!themeCSS) {
17 | return false;
18 | }
19 |
20 | if (!window.themeCSS) {
21 | jQuery('').appendTo('head').text(themeCSS);
22 | }
23 | return true;
24 | },
25 | themeJson = themeProvider.default || MAPJS.defaultTheme,
26 | theme = new MAPJS.Theme(themeJson),
27 | getTheme = () => theme;
28 |
29 | jQuery.fn.attachmentEditorWidget = function (mapModel) {
30 | return this.each(function () {
31 | mapModel.addEventListener('attachmentOpened', function (nodeId, attachment) {
32 | mapModel.setAttachment(
33 | 'attachmentEditorWidget',
34 | nodeId, {
35 | contentType: 'text/html',
36 | content: window.prompt('attachment', attachment && attachment.content)
37 | });
38 | });
39 | });
40 | };
41 | window.onerror = window.alert;
42 | window.jQuery = jQuery;
43 |
44 | container.domMapWidget(console, mapModel, touchEnabled);
45 |
46 | domMapController = new MAPJS.DomMapController(
47 | mapModel,
48 | container.find('[data-mapjs-role=stage]'),
49 | touchEnabled,
50 | undefined, // resourceTranslator
51 | getTheme
52 | );
53 | //jQuery('#themecss').themeCssWidget(themeProvider, new MAPJS.ThemeProcessor(), mapModel, domMapController);
54 | // activityLog, mapModel, touchEnabled, imageInsertController, dragContainer, centerSelectedNodeOnOrientationChange
55 |
56 | jQuery('body').attachmentEditorWidget(mapModel);
57 | layoutThemeStyle(themeJson);
58 | mapModel.setIdea(idea);
59 |
60 |
61 | jQuery('#linkEditWidget').linkEditWidget(mapModel);
62 | window.mapModel = mapModel;
63 | jQuery('.arrow').click(function () {
64 | jQuery(this).toggleClass('active');
65 | });
66 |
67 | container.on('drop', function (e) {
68 | const dataTransfer = e.originalEvent.dataTransfer;
69 | e.stopPropagation();
70 | e.preventDefault();
71 | if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
72 | const fileInfo = dataTransfer.files[0];
73 | if (/\.mup$/.test(fileInfo.name)) {
74 | const oFReader = new window.FileReader();
75 | oFReader.onload = function (oFREvent) {
76 | mapModel.setIdea(content(JSON.parse(oFREvent.target.result)));
77 | };
78 | oFReader.readAsText(fileInfo, 'UTF-8');
79 | }
80 | }
81 | });
82 | };
83 | document.addEventListener('DOMContentLoaded', init);
84 |
--------------------------------------------------------------------------------
/test/themes/top-down-simple.js:
--------------------------------------------------------------------------------
1 | var MAPJS = MAPJS || {};
2 | MAPJS.Themes = MAPJS.Themes || {};
3 | MAPJS.Themes.topdown = {
4 | 'name': 'MindMup Top Down Straight Lines',
5 | 'layout': {
6 | 'orientation': 'top-down',
7 | 'spacing': {
8 | 'h': 20,
9 | 'v': 100
10 | }
11 | },
12 | 'node': [
13 | {
14 | 'name': 'default',
15 | 'cornerRadius': 5.0,
16 | 'background': {
17 | 'color': 'transparent',
18 | 'opacity': 0.0
19 | },
20 | 'border': {
21 | 'type': 'overline'
22 | },
23 | 'shadow': [
24 | {
25 | 'color': 'transparent'
26 | }
27 | ],
28 | 'text': {
29 | 'margin': 5.0,
30 | 'alignment': 'left',
31 | 'color': '#4F4F4F',
32 | 'lightColor': '#EEEEEE',
33 | 'darkColor': '#000000',
34 | 'font': {
35 | 'lineSpacing': 2,
36 | 'size': 10,
37 | 'weight': 'light'
38 | }
39 | },
40 | 'connections': {
41 | 'default': {
42 | 'h': 'center',
43 | 'v': 'base'
44 | },
45 | 'from': {
46 | 'horizontal': {
47 | 'h': 'center',
48 | 'v': 'base'
49 | }
50 | },
51 | 'to': {
52 | 'h': 'center',
53 | 'v': 'top'
54 | }
55 | },
56 | 'decorations': {
57 | 'height': 32,
58 | 'edge': 'right',
59 | 'overlap': true,
60 | 'position': 'center'
61 | }
62 | },
63 | {
64 | 'name': 'activated',
65 | 'border': {
66 | 'type': 'surround',
67 | 'line': {
68 | 'color': '#22AAE0',
69 | 'width': 3.0,
70 | 'style': 'dotted'
71 | }
72 | }
73 | },
74 | {
75 | 'name': 'selected',
76 | 'shadow': [
77 | {
78 | 'color': '#000000',
79 | 'opacity': 0.9,
80 | 'offset': {
81 | 'width': 2,
82 | 'height': 2
83 | },
84 | 'radius': 2
85 | }
86 | ]
87 | },
88 | {
89 | 'name': 'collapsed',
90 | 'shadow': [
91 | {
92 | 'color': '#888888',
93 | 'offset': {
94 | 'width': 0,
95 | 'height': 1
96 | },
97 | 'radius': 0
98 | },
99 | {
100 | 'color': '#FFFFFF',
101 | 'offset': {
102 | 'width': 0,
103 | 'height': 3
104 | },
105 | 'radius': 0
106 | },
107 | {
108 | 'color': '#888888',
109 | 'offset': {
110 | 'width': 0,
111 | 'height': 4
112 | },
113 | 'radius': 0
114 | },
115 | {
116 | 'color': '#FFFFFF',
117 | 'offset': {
118 | 'width': 0,
119 | 'height': 6
120 | },
121 | 'radius': 0
122 | },
123 | {
124 | 'color': '#888888',
125 | 'offset': {
126 | 'width': 0,
127 | 'height': 7
128 | },
129 | 'radius': 0
130 | }
131 | ]
132 | },
133 | {
134 | 'name': 'collapsed.selected',
135 | 'shadow': [
136 | {
137 | 'color': '#FFFFFF',
138 | 'offset': {
139 | 'width': 0,
140 | 'height': 1
141 | },
142 | 'radius': 0
143 | },
144 | {
145 | 'color': '#888888',
146 | 'offset': {
147 | 'width': 0,
148 | 'height': 3
149 | },
150 | 'radius': 0
151 | },
152 | {
153 | 'color': '#FFFFFF',
154 | 'offset': {
155 | 'width': 0,
156 | 'height': 6
157 | },
158 | 'radius': 0
159 | },
160 | {
161 | 'color': '#555555',
162 | 'offset': {
163 | 'width': 0,
164 | 'height': 7
165 | },
166 | 'radius': 0
167 | },
168 | {
169 | 'color': '#FFFFFF',
170 | 'offset': {
171 | 'width': 0,
172 | 'height': 10
173 | },
174 | 'radius': 0
175 | },
176 | {
177 | 'color': '#333333',
178 | 'offset': {
179 | 'width': 0,
180 | 'height': 11
181 | },
182 | 'radius': 0
183 | }
184 | ]
185 | }
186 | ],
187 | 'connector': {
188 | 'default': {
189 | 'type': 'top-down-s-curve',
190 | 'line': {
191 | 'color': '#707070',
192 | 'width': 2.0
193 | }
194 | }
195 | }
196 | };
197 |
198 |
--------------------------------------------------------------------------------
/testem.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_page": "./testem/jasmine2runner.mustache",
3 | "serve_files": [
4 | "./testem/compiled/**/*.js"
5 | ],
6 | "src_files": [
7 | "src/**/*.js",
8 | "specs/**/*.js"
9 | ],
10 | "before_tests": "webpack --config webpack.testem.config.js",
11 | "launch_in_dev": ["Chrome"],
12 | "launch_in_ci": ["Chrome"],
13 | "browser_args": {
14 | "Chrome": [
15 | "--auto-open-devtools-for-tabs"
16 | ]
17 | },
18 | "routes": {
19 | "/jasmine": "node_modules/jasmine-core/lib/jasmine-core"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/testem/jasmine2runner.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Test'em
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{#serve_files}}
12 |
13 | {{/serve_files}}
14 |
15 | {{#css_files}}
16 |
17 | {{/css_files}}
18 |
19 |
20 |
21 | {{#styles}}
{{/styles}}
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/v4-restructure-todo.md:
--------------------------------------------------------------------------------
1 | # Restructure for v4
2 |
3 | - [x] break source into core and widgets
4 | - [x] move specs for core into plain jasmine
5 | - [ ] review dependencies and minimise where possible
6 | - [ ] underscore
7 | - [ ] jquery (move away from widgets for sharing code)
8 | - [x] include layout and model as part of core
9 | - [ ] figure out how to publish a separate core module
10 | - [ ] figure out how to deal with dependencies only for core (eg convex-hull)
11 | - [x] remove DOMRender
12 | - [ ] break down dom-map-view into separate files
13 | - [ ] break down dom-map-view-spec into separate files
14 | - [ ] remove editing widgets and move to @mindmup
15 | - [ ] move image drop widget and image insert controller to @mindmup
16 | - [ ] move dom-map-widget
17 | - [ ] move mapmodel editing methods
18 | - [ ] delete map-toolbar-widget and move to model actions in @mindmup
19 | - [ ] check if node-resize-widget can move to @mindmup
20 | - [ ] move link-edit-widget
21 | - [ ] check theme css widget
22 | - [ ] review all files and break into individual function files (eg hammer-draggable)
23 | - [ ] investigate if canUseData for connectors/links can be replaced with just theme changed? (is that the only case?)
24 | - [ ] move theme updating directly to domMapController listening on mapModel, instead of the widget intermediating
25 |
26 | # discuss with dave
27 |
28 | - [+] layout-geometry:257 console log -> throw?
29 | - `npm run sourcemap testem/compiled/browser/dom-map-view-spec.js.js:20811:44`
30 | - mapModel.layoutCalculator dependency
31 |
32 | # write specs for files without currently
33 |
34 | - [ ] core/theme/color-parser
35 | - [ ] browser/place-caret-at-end
36 | - [ ] browser/queue-fade-in
37 | - [ ] browser/queue-fade-out
38 | - [ ] browser/select-all
39 | - [ ] core/util/connector-key
40 | - [ ] core/util/link-key
41 | - [ ] browser/create-connector
42 | - [ ] browser/create-link
43 | - [ ] browser/find-line
44 | - [ ] browser/create-reorder-margin
45 |
46 |
47 | # propagate to mindmup
48 |
49 | - theme css widget changes
50 | - dommapcontroller init
51 | - mapmodel init
52 | - dom map widget init
53 |
--------------------------------------------------------------------------------
/webpack.appraise.config.js:
--------------------------------------------------------------------------------
1 | /*global require, module, __dirname */
2 | const path = require('path');
3 | module.exports = {
4 | entry: './examples/assets/webpack-main',
5 | output: {
6 | filename: 'webpack-bundle.js',
7 | path: path.resolve(__dirname, 'examples', 'assets')
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*global require, module, __dirname */
2 | const path = require('path');
3 | module.exports = {
4 | entry: './test/start',
5 | devtool: 'source-map',
6 | output: {
7 | filename: 'bundle.js',
8 | path: path.resolve(__dirname, 'test/')
9 | },
10 | devServer: {
11 | contentBase: path.join(__dirname, 'test'),
12 | port: 9000
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/webpack.testem.config.js:
--------------------------------------------------------------------------------
1 | /*global require, module, __dirname, process, console */
2 | const path = require('path'),
3 | recursiveLs = require('fs-readdir-recursive'),
4 | entries = {},
5 | testFilter = process.env.npm_package_config_test_filter,
6 | buildEntries = function (dir) {
7 | 'use strict';
8 | recursiveLs(dir).filter(name => /.+-spec\.js/.test(name)).forEach(function (f) {
9 | if (!testFilter || f.indexOf(testFilter) >= 0) {
10 | entries[f] = path.join(dir, f);
11 | }
12 | });
13 |
14 | };
15 | console.log('testFilter', testFilter);
16 | //buildEntries('core');
17 | buildEntries(path.resolve(__dirname, 'specs'));
18 | console.log('entries', entries);
19 | module.exports = {
20 | entry: entries,
21 | devtool: 'source-map',
22 | output: {
23 | path: path.resolve(__dirname, 'testem', 'compiled'),
24 | filename: '[name]'
25 | }
26 | };
27 |
--------------------------------------------------------------------------------