├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .tern-project ├── LICENSE ├── README.md ├── examples ├── assets │ ├── .gitignore │ ├── index.html │ ├── page.css │ └── webpack-main.js ├── connectors.md ├── fixtures │ ├── connector.js │ ├── link.js │ ├── mapjs-fixture.js │ ├── theme.js │ └── titles.js ├── images │ ├── basic_connector-f22f968f-ab8f-40ee-948f-d4ebd306e298.png │ ├── basicnodetitle-20461ca9-4bb3-48de-ae24-830fc363ed34.png │ ├── centeralignedtext-4fb01fb2-b9aa-4bc7-a357-eef49e595759.png │ ├── color_connector-478dc90f-ba12-4e0a-9214-819d7ed69f06.png │ ├── color_link-15b5c8f4-6b3f-41a9-9be6-0cd8b8cf0c34.png │ ├── customwidthlongerthantheme-b97e6830-b45d-4be2-945f-97497bbea6f5.png │ ├── customwidthshorterthantheme-45df74d8-4d70-4ea1-8444-2f3c6fc9e0e0.png │ ├── dashed_line_type-1ba06a9f-e265-46d8-ac48-9f98effcefa8.png │ ├── dashedcaps-bb26dc59-dd91-4db5-85c0-48f6f8aada9d.png │ ├── dottedcaps-b2d9f973-c258-4826-a37f-fcad90905529.png │ ├── exportproperties-a669ee17-40a6-4923-b15a-289b110f3f04.png │ ├── largernodetitle-5aeded0f-9a7a-4b84-926d-2c43771d69de.png │ ├── leftalignedtext-bd1a2ace-6736-4de4-8030-63cc010819c6.png │ ├── line_label-5193ddb3-7850-40d7-b6c8-fcaca2b7d230.png │ ├── line_type-b4679d54-b9bc-41b6-9993-ba5f0796875d.png │ ├── linewidth-cc7dafca-51d9-452a-aded-e500bbde8f45.png │ ├── link_arrow.png │ ├── link_combination-6156db52-9714-4d64-88a3-17d4de19eaf9.png │ ├── linkarrowboth-8aa65c31-4557-40df-a426-85c64b01b80e.png │ ├── linkarrowfalse-b9dbb5ed-b53f-4b51-b21b-80154ba5521b.png │ ├── linkarrowfrom-30b4981e-a852-4537-bc39-82c8e690d79e.png │ ├── linkarrowto-2bfeb84d-4bfb-4e18-9f80-7db2ac5a5b23.png │ ├── longnodetitle+width-d2ab95df-716a-4811-a9fd-702c4569740a.png │ ├── longnodetitlewithlinebreak+width-0edf5253-54a6-4568-a8e7-48fbfc96c181.png │ ├── nodetitle+width-d3383f0b-cbf2-4465-8672-3db24d205230.png │ ├── nodetitleswithlinebreaks-8188af02-b936-4fad-896d-cf1229b204d2.png │ ├── nodetitlewrappingwithdefaultwidth-0180b8f3-f2c0-42ca-aa51-482ff20e44a1.png │ ├── nodetitlewrappingwiththemewidth-bb4d9ad9-9e9d-430b-bae2-8d3a903bb838.png │ ├── nodewithlabel-5cdc1e30-bec0-4bd8-85e4-056066f820a9.png │ ├── nodewithmultilinetextandlabel-be4f3571-986a-4397-9965-7b9cf557a827.png │ ├── nodewithshorttextandlabel-e4d20a66-519d-4ab2-9d2b-d3fa3c184ba6.png │ ├── nooptions-c690bfea-63c9-434c-abe9-309dde4be50e.png │ ├── red-5439bdaf-c1df-436a-b8a0-68b108eec81e.png │ ├── rightalignedtext-b8db2855-ece5-4e29-a52d-c419740f8eb4.png │ ├── setastransparent-8a2651de-43a7-4aec-b7fe-daa795e142fa.png │ ├── shortnodetitlewiththemewidth-9ea7889e-24c2-4460-adf6-159b5722899d.png │ ├── smallernodetitle-703907ee-4703-4a9f-ac94-0197a26a9cdb.png │ ├── solid_line_type-0a912392-bd4e-414e-b646-58e90f26c6ea.png │ ├── startalignedtext-bd1a2ace-6736-4de4-8030-63cc010819c6.png │ ├── straightcaps-a63c3d84-25d1-4358-823f-125faf62b820.png │ ├── widthandarrows-7eb0d5c9-6146-401a-887f-13df95335517.png │ ├── widthanddashes-484b082b-022d-4a71-94a2-27a3b7f2d299.png │ └── with_label-c0275bf6-cc53-4d69-8ab8-a8f810e3b38b.png ├── labels.md ├── links.md ├── theme │ ├── images │ │ ├── background-color-6fc2fddf-9f21-4259-82fb-f967edf8294f.png │ │ ├── basicnodetheme-3dccffd3-90f2-4993-ac73-2ef3b095d8b5.png │ │ ├── border-1a1f762b-0a7b-4ef6-aeb3-3feb659c851b.png │ │ ├── corner-radius-fb04b0a8-ab82-4517-a867-467eb96c0ac3.png │ │ └── level-11b42911-d8d2-4c14-a29a-cf2387465688.png │ └── node-themes.md └── titles.md ├── package.json ├── packages └── core-dependencies │ └── package.json ├── performance-todo.md ├── specs ├── browser │ ├── build-connection-spec.js │ ├── calc-label-center-point-spec.js │ ├── create-node-spec.js │ ├── dom-map-controller-spec.js │ ├── edit-node-spec.js │ ├── get-box-spec.js │ ├── get-data-box-spec.js │ ├── inner-text-spec.js │ ├── jquery-extension-matchers-spec.js │ ├── node-cache-mark-spec.js │ ├── node-resize-widget-spec.js │ ├── place-caret-at-end-spec.js │ ├── set-theme-class-list-spec.js │ ├── update-connector-spec.js │ ├── update-link-spec.js │ ├── update-node-content-spec.js │ ├── update-reorder-bounds-spec.js │ └── update-stage-spec.js ├── core │ ├── content │ │ ├── apply-idea-attributes-to-node-theme-spec.js │ │ ├── auto-themed-idea-utils-spec.js │ │ ├── calc-idea-level-spec.js │ │ ├── content-spec.js │ │ ├── content-upgrade-spec.js │ │ ├── format-note-to-html-spec.js │ │ ├── formatted-node-title-spec.js │ │ ├── is-empty-group-spec.js │ │ ├── sorted-sub-ideas-spec.js │ │ └── traverse-spec.js │ ├── deep-assign-spec.js │ ├── is-object-object-spec.js │ ├── layout │ │ ├── calculate-layout-spec.js │ │ ├── extract-connectors-spec.js │ │ ├── extract-links-spec.js │ │ ├── layout-geometry-spec.js │ │ ├── layout-model-spec.js │ │ ├── multi-root-layout-spec.js │ │ ├── node-attribute-utils-spec.js │ │ ├── node-to-box-spec.js │ │ ├── standard │ │ │ ├── calculate-standard-layout-spec.js │ │ │ ├── outline-spec.js │ │ │ └── tree-spec.js │ │ └── top-down │ │ │ ├── align-group-spec.js │ │ │ ├── calculate-top-down-layout-spec.js │ │ │ ├── combine-vertical-subtrees-spec.js │ │ │ ├── compacted-group-width-spec.js │ │ │ ├── sort-nodes-by-left-position-spec.js │ │ │ └── vertical-subtree-collection-spec.js │ ├── map-model-spec.js │ ├── theme │ │ ├── calc-child-position-spec.js │ │ ├── color-to-rgb-spec.js │ │ ├── connector-spec.js │ │ ├── foreground-style-spec.js │ │ ├── line-styles-spec.js │ │ ├── line-types-spec.js │ │ ├── link-spec.js │ │ ├── merge-themes-spec.js │ │ ├── node-connection-point-x-spec.js │ │ ├── theme-attribute-utils-spec.js │ │ ├── theme-dimension-provider-spec.js │ │ ├── theme-fallback-values-spec.js │ │ ├── theme-processor-spec.js │ │ ├── theme-spec.js │ │ └── theme-to-dictionary-spec.js │ └── util │ │ ├── calc-max-width-spec.js │ │ ├── object-utils-spec.js │ │ ├── observable-spec.js │ │ └── url-helper-spec.js ├── helpers │ └── jquery-extension-matchers.js └── support │ └── jasmine-runner.js ├── src ├── browser │ ├── build-connection.js │ ├── calc-label-center-point.js │ ├── create-connector.js │ ├── create-link.js │ ├── create-node.js │ ├── create-reorder-bounds.js │ ├── create-svg.js │ ├── dom-map-controller.js │ ├── dom-map-widget.js │ ├── edit-node.js │ ├── find-line.js │ ├── get-box.js │ ├── get-data-box.js │ ├── hammer-draggable.js │ ├── inner-text.js │ ├── link-edit-widget.js │ ├── node-cache-mark.js │ ├── node-resize-widget.js │ ├── node-with-id.js │ ├── place-caret-at-end.js │ ├── queue-fade-out.js │ ├── select-all.js │ ├── set-theme-class-list.js │ ├── update-connector-text.js │ ├── update-connector.js │ ├── update-link.js │ ├── update-node-content.js │ ├── update-reorder-bounds.js │ └── update-stage.js ├── core │ ├── content │ │ ├── apply-idea-attributes-to-node-theme.js │ │ ├── auto-themed-idea-utils.js │ │ ├── calc-idea-level.js │ │ ├── content-upgrade.js │ │ ├── content.js │ │ ├── format-note-to-html.js │ │ ├── formatted-node-title.js │ │ ├── is-empty-group.js │ │ ├── sorted-sub-ideas.js │ │ └── traverse.js │ ├── deep-assign.js │ ├── is-object-object.js │ ├── layout │ │ ├── calculate-layout.js │ │ ├── extract-connectors.js │ │ ├── extract-links.js │ │ ├── layout-geometry.js │ │ ├── layout-model.js │ │ ├── multi-root-layout.js │ │ ├── node-attribute-utils.js │ │ ├── node-to-box.js │ │ ├── standard │ │ │ ├── calculate-standard-layout.js │ │ │ ├── outline.js │ │ │ └── tree.js │ │ └── top-down │ │ │ ├── align-group.js │ │ │ ├── calculate-top-down-layout.js │ │ │ ├── combine-vertical-subtrees.js │ │ │ ├── compacted-group-width.js │ │ │ ├── sort-nodes-by-left-position.js │ │ │ └── vertical-subtree-collection.js │ ├── map-model.js │ ├── npm-core.js │ ├── package.json │ ├── theme │ │ ├── calc-child-position.js │ │ ├── color-parser.js │ │ ├── color-to-rgb.js │ │ ├── connector.js │ │ ├── default-theme.js │ │ ├── foreground-style.js │ │ ├── line-strokes.js │ │ ├── line-styles.js │ │ ├── line-types.js │ │ ├── link.js │ │ ├── merge-themes.js │ │ ├── node-connection-point-x.js │ │ ├── theme-attribute-utils.js │ │ ├── theme-dimension-provider.js │ │ ├── theme-fallback-values.js │ │ ├── theme-processor.js │ │ ├── theme-to-dictionary.js │ │ └── theme.js │ └── util │ │ ├── calc-max-width.js │ │ ├── clean-dom-id.js │ │ ├── connector-key.js │ │ ├── convert-position-to-transform.js │ │ ├── deep-freeze.js │ │ ├── link-key.js │ │ ├── node-key.js │ │ ├── object-utils.js │ │ ├── observable.js │ │ └── url-helper.js └── npm-main.js ├── test ├── chevron-left.svg ├── chevron-right.svg ├── example-map.json ├── icon-link-active.svg ├── icon-link-inactive.svg ├── icon-paperclip-active.svg ├── icon-paperclip-inactive.svg ├── index.html ├── mapjs-default-styles.css ├── start.js ├── theme.js └── themes │ ├── argument-mapping.js │ ├── compact.js │ ├── top-down-simple.js │ └── v1.js ├── testem.json ├── testem └── jasmine2runner.mustache ├── v4-restructure-todo.md ├── webpack.appraise.config.js ├── webpack.config.js └── webpack.testem.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "crockford", 3 | "env": { 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 6 8 | }, 9 | "rules": { 10 | "strict": ["error", "function"], 11 | "semi": ["error", "always"], 12 | "no-const-assign": "error", 13 | "indent": ["error", "tab", { "SwitchCase": 1, "MemberExpression": "off" } ], 14 | "no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }], 15 | "quotes": ["error", "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 16 | "new-cap": ["error", { "capIsNewExceptions": ["Deferred", "Event"] }], 17 | "no-unused-vars": "error", 18 | "no-const-assign": "error", 19 | "prefer-const": "error", 20 | "no-var": "error" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | results 2 | .DS_Store 3 | testem/compiled 4 | pub 5 | lib 6 | #grunt and jasmine artificats 7 | node_modules/ 8 | SpecRunner.html 9 | .grunt 10 | .sublime* 11 | .tern-port 12 | npm-debug.log 13 | package-lock.json 14 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": ["ecmascript", "browser", "jquery", "underscore"], 3 | "plugins": { 4 | "node": {}, 5 | "jasmine": {}, 6 | "es_modules": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Damjan Vujnovic, David de Florinier, Gojko Adzic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MindMup MapJs 2 | ============= 3 | 4 | MindMup is a zero-friction mind map canvas. Our aim is to create the most productive mind mapping environment out there, removing all the distractions and providing powerful editing shortcuts. 5 | 6 | This project is the JavaScript visualisation portion of MindMup. It provides a canvas for users to create and edit 7 | mind maps in a browser. You can see an example of this live at [mindmup.com](http://www.mindmup.com), or play with the library directly in the browser using `test/index.html` from this project.. 8 | 9 | This project is relatively stand alone and you can use it to create a nice mind map visualisation separate from the 10 | [MindMup Server](https://www.mindmup.com). 11 | 12 | # Using MAPJS in your projects 13 | 14 | MapJS 2 works well with WebPack. Check out the [MAPJS Webpack Example](https://github.com/mindmup/mapjs-webpack-example) project. 15 | 16 | # Testing 17 | 18 | To run the unit tests, execute 19 | 20 | npm test 21 | 22 | To debug and try things out visually, grab the dependencies using: 23 | 24 | npm run pretest 25 | 26 | # Dependencies 27 | 28 | This library depends on the following projects: 29 | 30 | - [JQuery](http://jquery.com/) 31 | - [Underscore.Js](http://underscorejs.org/) 32 | - [JQuery HotKeys](http://jquery.com/) 33 | - [Hammer.JS JQuery Plugin](http://eightmedia.github.com/hammer.js) 34 | - [Color JS](https://github.com/harthur/color) 35 | -------------------------------------------------------------------------------- /examples/assets/.gitignore: -------------------------------------------------------------------------------- 1 | webpack-bundle.* 2 | -------------------------------------------------------------------------------- /examples/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/assets/webpack-main.js: -------------------------------------------------------------------------------- 1 | /*global require, document, window, console */ 2 | const MAPJS = require('../../src/npm-main'), 3 | jQuery = require('jquery'), 4 | content = MAPJS.content, 5 | init = function () { 6 | 'use strict'; 7 | let theme; 8 | const container = jQuery('#container'), 9 | touchEnabled = false, 10 | mapModel = new MAPJS.MapModel([]), 11 | 12 | updateTheme = function (themeJson) { 13 | const themeCSS = themeJson && new MAPJS.ThemeProcessor().process(themeJson).css; 14 | if (!themeCSS) { 15 | return false; 16 | } 17 | 18 | if (window.themeCSS) { 19 | window.themeCSS.remove(); 20 | } 21 | 22 | jQuery('').appendTo('head').text(themeCSS); 23 | 24 | theme = new MAPJS.Theme(themeJson); 25 | return true; 26 | }, 27 | getTheme = () => theme; 28 | 29 | mapModel.setThemeSource(getTheme); 30 | container.domMapWidget(console, mapModel, touchEnabled); 31 | 32 | new MAPJS.DomMapController( 33 | mapModel, 34 | container.find('[data-mapjs-role=stage]'), 35 | touchEnabled, 36 | undefined, // resourceTranslator 37 | getTheme 38 | ); 39 | 40 | updateTheme(MAPJS.defaultTheme); 41 | window.addEventListener('message', function (messageEvent) { 42 | if (messageEvent.data.theme) { 43 | updateTheme(messageEvent.data.theme); 44 | } 45 | if (messageEvent.data.labels) { 46 | mapModel.setLabelGenerator(() => messageEvent.data.labels); 47 | } 48 | if (messageEvent.data.content) { 49 | mapModel.setIdea(content(messageEvent.data.content)); 50 | container.css({overflow: 'visible'}); 51 | } 52 | }); 53 | }; 54 | document.addEventListener('DOMContentLoaded', init); 55 | -------------------------------------------------------------------------------- /examples/connectors.md: -------------------------------------------------------------------------------- 1 | --- 2 | fixture: fixtures/connector.js 3 | initial-width: 1000 4 | initial-height: 600 5 | clip-x: 450 6 | clip-y: 250 7 | clip-width: 500 8 | clip-height: 300 9 | --- 10 | 11 | # Connector properties 12 | 13 | These examples show the effect of the `parentConnector` attribute of a node 14 | 15 | ## basics 16 | 17 | Without any properties, the connector uses the default theme style 18 | 19 | ~~~json example="basic connector" 20 | {} 21 | ~~~ 22 | 23 | ![basic connector](images/basic_connector-f22f968f-ab8f-40ee-948f-d4ebd306e298.png) 24 | 25 | 26 | ## setting the color 27 | 28 | ~~~json example="color connector" 29 | {"color": "#ff0000"} 30 | ~~~ 31 | 32 | ![color connector](images/color_connector-478dc90f-ba12-4e0a-9214-819d7ed69f06.png) 33 | 34 | ## setting the line type 35 | 36 | ~~~json example="line type" 37 | {"lineStyle": "dashed"} 38 | ~~~ 39 | 40 | ![line type](images/line_type-b4679d54-b9bc-41b6-9993-ba5f0796875d.png) 41 | 42 | ## setting the label 43 | 44 | ~~~json example="line label" 45 | {"label": "connecting nodes"} 46 | ~~~ 47 | 48 | ![line label](images/line_label-5193ddb3-7850-40d7-b6c8-fcaca2b7d230.png) 49 | 50 | -------------------------------------------------------------------------------- /examples/fixtures/connector.js: -------------------------------------------------------------------------------- 1 | /*global require, module */ 2 | const mapjsFixture = require('./mapjs-fixture'), 3 | buildMap = function (connectorProps) { 4 | 'use strict'; 5 | return { 6 | formatVersion: 3, 7 | id: 'root', 8 | ideas: { 9 | 1: { 10 | title: 'parent', 11 | id: 1, 12 | attr: { 13 | position: [-185, -182, 1] 14 | }, 15 | ideas: { 16 | 1: { 17 | title: 'child', 18 | id: 2, 19 | attr: { 20 | position: [170, 157, 1], 21 | parentConnector: connectorProps 22 | } 23 | } 24 | } 25 | } 26 | } 27 | }; 28 | }; 29 | 30 | 31 | module.exports = function buildSvgMap(connectorProps) { 32 | 'use strict'; 33 | return mapjsFixture(buildMap(connectorProps)); 34 | }; 35 | -------------------------------------------------------------------------------- /examples/fixtures/link.js: -------------------------------------------------------------------------------- 1 | /*global require, module */ 2 | const mapjsFixture = require('./mapjs-fixture'), 3 | buildMap = function (linkProps) { 4 | 'use strict'; 5 | return { 6 | 'formatVersion': 3, 7 | 'id': 'root', 8 | 'ideas': { 9 | '1': { 10 | 'title': 'from', 11 | 'id': 1, 12 | 'attr': { 13 | 'position': [ 14 | -282, 15 | -75, 16 | 1 17 | ] 18 | } 19 | }, 20 | '2': { 21 | 'title': 'to', 22 | 'id': 2, 23 | 'attr': { 24 | 'position': [ 25 | -29, 26 | 0, 27 | 1 28 | ] 29 | } 30 | } 31 | }, 32 | 'title': '', 33 | 'links': [ 34 | { 35 | 'ideaIdFrom': 1, 36 | 'ideaIdTo': 2, 37 | 'attr': { 38 | 'style': linkProps 39 | } 40 | } 41 | ] 42 | }; 43 | }; 44 | 45 | module.exports = function (linkProps) { 46 | 'use strict'; 47 | return mapjsFixture(buildMap(linkProps)); 48 | }; 49 | -------------------------------------------------------------------------------- /examples/fixtures/mapjs-fixture.js: -------------------------------------------------------------------------------- 1 | /*global require, module, __dirname, window */ 2 | const path = require('path'), 3 | templateDir = path.join(__dirname, '..', 'assets'), 4 | defaultTheme = require('../../src/core/theme/default-theme'), 5 | mergeThemes = require('../../src/core/theme/merge-themes'), 6 | indexFile = path.resolve(templateDir, 'index.html'); 7 | module.exports = function mapjsFixture(mapJson, themeJson, labels) { 8 | 'use strict'; 9 | const baseTheme = themeJson ? mergeThemes(defaultTheme, themeJson) : defaultTheme; 10 | baseTheme.noAnimations = true; 11 | return { 12 | url: `file://${indexFile}`, 13 | beforeScreenshotArgs: [{ 14 | content: mapJson, 15 | theme: baseTheme, 16 | labels: labels 17 | }], 18 | beforeScreenshot: function (data) { 19 | window.postMessage(data, '*'); 20 | } 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/fixtures/theme.js: -------------------------------------------------------------------------------- 1 | /*global require, module */ 2 | const mapjsFixture = require('./mapjs-fixture'), 3 | buildMap = function () { 4 | 'use strict'; 5 | return { 6 | formatVersion: 3, 7 | id: 'root', 8 | ideas: { 9 | 1: { 10 | title: 'First level', 11 | id: 1, 12 | ideas: { 13 | 2: { 14 | id: 2, 15 | title: 'Child 1' 16 | }, 17 | 3: { 18 | id: 3, 19 | title: 'Child 2' 20 | } 21 | } 22 | } 23 | } 24 | }; 25 | }; 26 | 27 | 28 | module.exports = function buildSvgMap(themeProps, contextOptions) { 29 | 'use strict'; 30 | const theme = { 31 | name: 'test theme', 32 | node: [], 33 | connector: {}, 34 | link: {} 35 | }; 36 | theme[contextOptions.params['theme-element']][contextOptions.params['theme-element-property']] = themeProps; 37 | return mapjsFixture(buildMap(), theme); 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /examples/fixtures/titles.js: -------------------------------------------------------------------------------- 1 | /*global require, module */ 2 | const mapjsFixture = require('./mapjs-fixture'), 3 | getTheme = function (titleProps) { 4 | 'use strict'; 5 | if (!titleProps.textTheme) { 6 | return false; 7 | } 8 | return { 9 | node: [{ 10 | name: 'default', 11 | text: titleProps.textTheme 12 | }] 13 | }; 14 | }, 15 | buildMap = function (titleProps) { 16 | 'use strict'; 17 | return { 18 | formatVersion: 3, 19 | id: 'root', 20 | ideas: { 21 | 1: { 22 | title: titleProps.title, 23 | id: 1, 24 | attr: { 25 | style: titleProps.style 26 | } 27 | } 28 | } 29 | }; 30 | }, 31 | getLabels = function (titleProps) { 32 | 'use strict'; 33 | return {1: titleProps.label}; 34 | }; 35 | 36 | module.exports = function (titleProps) { 37 | 'use strict'; 38 | return mapjsFixture(buildMap(titleProps), getTheme(titleProps), getLabels(titleProps)); 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /examples/images/basic_connector-f22f968f-ab8f-40ee-948f-d4ebd306e298.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/basic_connector-f22f968f-ab8f-40ee-948f-d4ebd306e298.png -------------------------------------------------------------------------------- /examples/images/basicnodetitle-20461ca9-4bb3-48de-ae24-830fc363ed34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/basicnodetitle-20461ca9-4bb3-48de-ae24-830fc363ed34.png -------------------------------------------------------------------------------- /examples/images/centeralignedtext-4fb01fb2-b9aa-4bc7-a357-eef49e595759.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/centeralignedtext-4fb01fb2-b9aa-4bc7-a357-eef49e595759.png -------------------------------------------------------------------------------- /examples/images/color_connector-478dc90f-ba12-4e0a-9214-819d7ed69f06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/color_connector-478dc90f-ba12-4e0a-9214-819d7ed69f06.png -------------------------------------------------------------------------------- /examples/images/color_link-15b5c8f4-6b3f-41a9-9be6-0cd8b8cf0c34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/color_link-15b5c8f4-6b3f-41a9-9be6-0cd8b8cf0c34.png -------------------------------------------------------------------------------- /examples/images/customwidthlongerthantheme-b97e6830-b45d-4be2-945f-97497bbea6f5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/customwidthlongerthantheme-b97e6830-b45d-4be2-945f-97497bbea6f5.png -------------------------------------------------------------------------------- /examples/images/customwidthshorterthantheme-45df74d8-4d70-4ea1-8444-2f3c6fc9e0e0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/customwidthshorterthantheme-45df74d8-4d70-4ea1-8444-2f3c6fc9e0e0.png -------------------------------------------------------------------------------- /examples/images/dashed_line_type-1ba06a9f-e265-46d8-ac48-9f98effcefa8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/dashed_line_type-1ba06a9f-e265-46d8-ac48-9f98effcefa8.png -------------------------------------------------------------------------------- /examples/images/dashedcaps-bb26dc59-dd91-4db5-85c0-48f6f8aada9d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/dashedcaps-bb26dc59-dd91-4db5-85c0-48f6f8aada9d.png -------------------------------------------------------------------------------- /examples/images/dottedcaps-b2d9f973-c258-4826-a37f-fcad90905529.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/dottedcaps-b2d9f973-c258-4826-a37f-fcad90905529.png -------------------------------------------------------------------------------- /examples/images/exportproperties-a669ee17-40a6-4923-b15a-289b110f3f04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/exportproperties-a669ee17-40a6-4923-b15a-289b110f3f04.png -------------------------------------------------------------------------------- /examples/images/largernodetitle-5aeded0f-9a7a-4b84-926d-2c43771d69de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/largernodetitle-5aeded0f-9a7a-4b84-926d-2c43771d69de.png -------------------------------------------------------------------------------- /examples/images/leftalignedtext-bd1a2ace-6736-4de4-8030-63cc010819c6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/leftalignedtext-bd1a2ace-6736-4de4-8030-63cc010819c6.png -------------------------------------------------------------------------------- /examples/images/line_label-5193ddb3-7850-40d7-b6c8-fcaca2b7d230.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/line_label-5193ddb3-7850-40d7-b6c8-fcaca2b7d230.png -------------------------------------------------------------------------------- /examples/images/line_type-b4679d54-b9bc-41b6-9993-ba5f0796875d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/line_type-b4679d54-b9bc-41b6-9993-ba5f0796875d.png -------------------------------------------------------------------------------- /examples/images/linewidth-cc7dafca-51d9-452a-aded-e500bbde8f45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/linewidth-cc7dafca-51d9-452a-aded-e500bbde8f45.png -------------------------------------------------------------------------------- /examples/images/link_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/link_arrow.png -------------------------------------------------------------------------------- /examples/images/link_combination-6156db52-9714-4d64-88a3-17d4de19eaf9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/link_combination-6156db52-9714-4d64-88a3-17d4de19eaf9.png -------------------------------------------------------------------------------- /examples/images/linkarrowboth-8aa65c31-4557-40df-a426-85c64b01b80e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/linkarrowboth-8aa65c31-4557-40df-a426-85c64b01b80e.png -------------------------------------------------------------------------------- /examples/images/linkarrowfalse-b9dbb5ed-b53f-4b51-b21b-80154ba5521b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/linkarrowfalse-b9dbb5ed-b53f-4b51-b21b-80154ba5521b.png -------------------------------------------------------------------------------- /examples/images/linkarrowfrom-30b4981e-a852-4537-bc39-82c8e690d79e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/linkarrowfrom-30b4981e-a852-4537-bc39-82c8e690d79e.png -------------------------------------------------------------------------------- /examples/images/linkarrowto-2bfeb84d-4bfb-4e18-9f80-7db2ac5a5b23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/linkarrowto-2bfeb84d-4bfb-4e18-9f80-7db2ac5a5b23.png -------------------------------------------------------------------------------- /examples/images/longnodetitle+width-d2ab95df-716a-4811-a9fd-702c4569740a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/longnodetitle+width-d2ab95df-716a-4811-a9fd-702c4569740a.png -------------------------------------------------------------------------------- /examples/images/longnodetitlewithlinebreak+width-0edf5253-54a6-4568-a8e7-48fbfc96c181.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/longnodetitlewithlinebreak+width-0edf5253-54a6-4568-a8e7-48fbfc96c181.png -------------------------------------------------------------------------------- /examples/images/nodetitle+width-d3383f0b-cbf2-4465-8672-3db24d205230.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodetitle+width-d3383f0b-cbf2-4465-8672-3db24d205230.png -------------------------------------------------------------------------------- /examples/images/nodetitleswithlinebreaks-8188af02-b936-4fad-896d-cf1229b204d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodetitleswithlinebreaks-8188af02-b936-4fad-896d-cf1229b204d2.png -------------------------------------------------------------------------------- /examples/images/nodetitlewrappingwithdefaultwidth-0180b8f3-f2c0-42ca-aa51-482ff20e44a1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodetitlewrappingwithdefaultwidth-0180b8f3-f2c0-42ca-aa51-482ff20e44a1.png -------------------------------------------------------------------------------- /examples/images/nodetitlewrappingwiththemewidth-bb4d9ad9-9e9d-430b-bae2-8d3a903bb838.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodetitlewrappingwiththemewidth-bb4d9ad9-9e9d-430b-bae2-8d3a903bb838.png -------------------------------------------------------------------------------- /examples/images/nodewithlabel-5cdc1e30-bec0-4bd8-85e4-056066f820a9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodewithlabel-5cdc1e30-bec0-4bd8-85e4-056066f820a9.png -------------------------------------------------------------------------------- /examples/images/nodewithmultilinetextandlabel-be4f3571-986a-4397-9965-7b9cf557a827.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodewithmultilinetextandlabel-be4f3571-986a-4397-9965-7b9cf557a827.png -------------------------------------------------------------------------------- /examples/images/nodewithshorttextandlabel-e4d20a66-519d-4ab2-9d2b-d3fa3c184ba6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nodewithshorttextandlabel-e4d20a66-519d-4ab2-9d2b-d3fa3c184ba6.png -------------------------------------------------------------------------------- /examples/images/nooptions-c690bfea-63c9-434c-abe9-309dde4be50e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/nooptions-c690bfea-63c9-434c-abe9-309dde4be50e.png -------------------------------------------------------------------------------- /examples/images/red-5439bdaf-c1df-436a-b8a0-68b108eec81e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/red-5439bdaf-c1df-436a-b8a0-68b108eec81e.png -------------------------------------------------------------------------------- /examples/images/rightalignedtext-b8db2855-ece5-4e29-a52d-c419740f8eb4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/rightalignedtext-b8db2855-ece5-4e29-a52d-c419740f8eb4.png -------------------------------------------------------------------------------- /examples/images/setastransparent-8a2651de-43a7-4aec-b7fe-daa795e142fa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/setastransparent-8a2651de-43a7-4aec-b7fe-daa795e142fa.png -------------------------------------------------------------------------------- /examples/images/shortnodetitlewiththemewidth-9ea7889e-24c2-4460-adf6-159b5722899d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/shortnodetitlewiththemewidth-9ea7889e-24c2-4460-adf6-159b5722899d.png -------------------------------------------------------------------------------- /examples/images/smallernodetitle-703907ee-4703-4a9f-ac94-0197a26a9cdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/smallernodetitle-703907ee-4703-4a9f-ac94-0197a26a9cdb.png -------------------------------------------------------------------------------- /examples/images/solid_line_type-0a912392-bd4e-414e-b646-58e90f26c6ea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/solid_line_type-0a912392-bd4e-414e-b646-58e90f26c6ea.png -------------------------------------------------------------------------------- /examples/images/startalignedtext-bd1a2ace-6736-4de4-8030-63cc010819c6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/startalignedtext-bd1a2ace-6736-4de4-8030-63cc010819c6.png -------------------------------------------------------------------------------- /examples/images/straightcaps-a63c3d84-25d1-4358-823f-125faf62b820.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/straightcaps-a63c3d84-25d1-4358-823f-125faf62b820.png -------------------------------------------------------------------------------- /examples/images/widthandarrows-7eb0d5c9-6146-401a-887f-13df95335517.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/widthandarrows-7eb0d5c9-6146-401a-887f-13df95335517.png -------------------------------------------------------------------------------- /examples/images/widthanddashes-484b082b-022d-4a71-94a2-27a3b7f2d299.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/widthanddashes-484b082b-022d-4a71-94a2-27a3b7f2d299.png -------------------------------------------------------------------------------- /examples/images/with_label-c0275bf6-cc53-4d69-8ab8-a8f810e3b38b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/images/with_label-c0275bf6-cc53-4d69-8ab8-a8f810e3b38b.png -------------------------------------------------------------------------------- /examples/labels.md: -------------------------------------------------------------------------------- 1 | --- 2 | fixture: fixtures/titles.js 3 | --- 4 | 5 | # Node Labels 6 | 7 | Labels show in the right-top corner of a node: 8 | ~~~yaml example="node with label" 9 | title: EEEEE EEEE EEE 10 | label: "1.1" 11 | ~~~ 12 | 13 | ![node with label](images/nodewithlabel-5cdc1e30-bec0-4bd8-85e4-056066f820a9.png) 14 | 15 | 16 | Labels do not get clipped when they are wider than a node 17 | 18 | ~~~yaml example="node with short text and label" 19 | title: I 20 | label: "1.1.1" 21 | ~~~ 22 | 23 | ![node with short text and label](images/nodewithshorttextandlabel-e4d20a66-519d-4ab2-9d2b-d3fa3c184ba6.png) 24 | 25 | Labels are not affected by the height of a node, they always show in the top-right corner 26 | 27 | ~~~yaml example="node with multiline text and label" 28 | title: "EEEEE EEEEE\nEEEE EE\nEEE EEE" 29 | label: "1.1" 30 | ~~~ 31 | 32 | ![node with multiline text and label](images/nodewithmultilinetextandlabel-be4f3571-986a-4397-9965-7b9cf557a827.png) 33 | 34 | -------------------------------------------------------------------------------- /examples/theme/images/background-color-6fc2fddf-9f21-4259-82fb-f967edf8294f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/theme/images/background-color-6fc2fddf-9f21-4259-82fb-f967edf8294f.png -------------------------------------------------------------------------------- /examples/theme/images/basicnodetheme-3dccffd3-90f2-4993-ac73-2ef3b095d8b5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/theme/images/basicnodetheme-3dccffd3-90f2-4993-ac73-2ef3b095d8b5.png -------------------------------------------------------------------------------- /examples/theme/images/border-1a1f762b-0a7b-4ef6-aeb3-3feb659c851b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/theme/images/border-1a1f762b-0a7b-4ef6-aeb3-3feb659c851b.png -------------------------------------------------------------------------------- /examples/theme/images/corner-radius-fb04b0a8-ab82-4517-a867-467eb96c0ac3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/theme/images/corner-radius-fb04b0a8-ab82-4517-a867-467eb96c0ac3.png -------------------------------------------------------------------------------- /examples/theme/images/level-11b42911-d8d2-4c14-a29a-cf2387465688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindmup/mapjs/e30f8d835e028febe2e951e422c313ac304a0431/examples/theme/images/level-11b42911-d8d2-4c14-a29a-cf2387465688.png -------------------------------------------------------------------------------- /examples/theme/node-themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | fixture: fixtures/theme.js 3 | theme-element: node 4 | theme-element-property: 0 5 | --- 6 | 7 | # Node Theming 8 | 9 | By default, borders are solid and rounded, titles are centred with a small padding around the text: 10 | 11 | ~~~yaml example="basic node theme" 12 | name: default 13 | ~~~ 14 | 15 | 16 | ![basic node theme](images/basicnodetheme-3dccffd3-90f2-4993-ac73-2ef3b095d8b5.png) 17 | 18 | A theme can set the background color for a node 19 | ~~~yaml example="background-color" 20 | name: default 21 | backgroundColor: "#FFFFFF" 22 | ~~~ 23 | 24 | ![background-color](images/background-color-6fc2fddf-9f21-4259-82fb-f967edf8294f.png) 25 | 26 | A theme can set the corner radius for a node 27 | 28 | ~~~yaml example="corner-radius" 29 | name: default 30 | cornerRadius: 0 31 | ~~~ 32 | 33 | ![corner-radius](images/corner-radius-fb04b0a8-ab82-4517-a867-467eb96c0ac3.png) 34 | 35 | 36 | A theme can set the properties for a particular level 37 | 38 | ~~~yaml example="level" 39 | name: level_2 40 | backgroundColor: "#FFFFFF" 41 | cornerRadius: 0 42 | ~~~ 43 | 44 | ![level](images/level-11b42911-d8d2-4c14-a29a-cf2387465688.png) 45 | 46 | 47 | A theme can set border color, width and style 48 | 49 | ~~~yaml example="border" 50 | name: default 51 | border: 52 | type: surround 53 | line: 54 | color: "#AA0000" 55 | width: 5 56 | style: dashed 57 | ~~~ 58 | 59 | ![border](images/border-1a1f762b-0a7b-4ef6-aeb3-3feb659c851b.png) 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mindmup/mapjs", 3 | "license": "MIT", 4 | "description": "Mind mapping visualisation library, using SVG, developed for MindMup", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/mindmup/mapjs.git" 8 | }, 9 | "keywords": [ 10 | "mindmup", 11 | "svg" 12 | ], 13 | "files": [ 14 | "src", 15 | "packages" 16 | ], 17 | "bugs": { 18 | "url": "https://github.com/mindmup/mapjs/issues" 19 | }, 20 | "homepage": "https://www.mindmup.com", 21 | "version": "4.1.0", 22 | "scripts": { 23 | "pretest": "eslint specs src && rm -rf testem/compiled", 24 | "test-core": "node specs/support/jasmine-runner.js", 25 | "pretest-browser": "rm -rf testem/compiled", 26 | "test-browser": "testem ci -R dot", 27 | "debug-core": "node debug specs/support/jasmine-runner.js", 28 | "test": "npm run test-core && npm run test-browser", 29 | "test-watch": "npm run pretest && testem dev -R dot", 30 | "pretest-dev": "npm run pretest", 31 | "test-dev": "testem -l Chrome", 32 | "sourcemap": "sourcemap-lookup", 33 | "server": "webpack-dev-server", 34 | "appraise": "appraise run --initial-width 500 --initial-height 300", 35 | "preappraise": "webpack --config webpack.appraise.config.js", 36 | "approve": "appraise approve" 37 | }, 38 | "main": "src/npm-main.js", 39 | "dependencies": { 40 | "jquery": "^3.2.1", 41 | "jquery.hotkeys": "git://github.com/jeresig/jquery.hotkeys", 42 | "hammerjs": "^1.1.3", 43 | "jquery-hammerjs": "^1.1.3", 44 | "underscore": "^1.8.3", 45 | "monotone-convex-hull-2d": "^1.0.1", 46 | "polybooljs": "^1.1.1" 47 | }, 48 | "devDependencies": { 49 | "appraise": "^0.4.0", 50 | "eslint": "^5.4.0", 51 | "eslint-config-crockford": "^1.0.0", 52 | "eslint-config-defaults": "^9.0.0", 53 | "exports-loader": "^0.6.3", 54 | "fs-readdir-recursive": "^1.0.0", 55 | "imports-loader": "^0.7.1", 56 | "jasmine": "^2.7.0", 57 | "jasmine-spec-reporter": "^4.2.1", 58 | "sourcemap-lookup": "0.0.3", 59 | "testem": "^2.9.2", 60 | "webpack": "^2.2.1", 61 | "webpack-dev-middleware": "^1.10.0", 62 | "webpack-dev-server": "^3.1.10" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/core-dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mindmup/mapjs-core-dependencies", 3 | "license": "MIT", 4 | "private": true, 5 | "version": "1.0.0", 6 | "dependencies": { 7 | "underscore": "^1.8.3", 8 | "monotone-convex-hull-2d": "^1.0.1", 9 | "polybooljs": "^1.1.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /performance-todo.md: -------------------------------------------------------------------------------- 1 | * [ ] dimension calculator to use layout similar to mapsvg; don't read overall size then remove image, use functions from layout to calculate size with image depending on metadata 2 | * [ ] alternatively: rewrite dimension calculator to use canvas, then don't calculate via DOM at all 3 | * [ ] cache layout outside DOM 4 | * [ ] initial map load to use a single dom update (eg read from string) 5 | * [ ] remove jquery from critical path on updating node/connectors 6 | * [ ] initial calculation for dom layout 7 | * [ ] deltas/update calculation for dom layout 8 | 9 | ==== 10 | 11 | ## Caching solutions 12 | 13 | * inside JQuery Data 14 | - con: requires a node - can't be warmed up on map load 15 | - con: not good for collapse/uncollapse 16 | - con: jquery 17 | - pro: automatically cleaned up when a node is removed 18 | * outside dom 19 | * pro: can be warmed up on map load 20 | * indexed to ID 21 | - pro: automatically cleaned up 22 | - pro: useful for collapse/uncollapse of large groups of nodes 23 | * indexed by metadata 24 | - pro: good if lots of nodes are similar 25 | - con: as it's not automatically cleaned up (we need to do cache purging somehow) 26 | 27 | ## Dimension calculator options 28 | 29 | Constraint: we don't have access to fonts, browser chooses the font... 30 | 31 | options: 32 | 33 | 1. Use a shadow DOM element to get the overall size 34 | - con: slow 35 | - pro: we use browser text wrapping 36 | - it matches the DOM editor perfectly 37 | - it will work for lots of languages 38 | - con: different text wrapping used on exports and screen 39 | - con: circular dep between updateNodeContent and dimension provider, so we can't strip JQuery out of updateNodeContent and do it async... 40 | 41 | 2. Use canvas getFontMetrics method 42 | - pro: significantly faster as no DOM access needed 43 | - pro: we can use 99% mapjs layout designed for mapsvg... all we need to do is set up a new text engine 44 | - con: we need to use our own text wrapping, which might not match browser 100% 45 | - pro: same text wrapping alg used for exports and screen 46 | 47 | 3. use a shadow DOM element just to extract the text size, build the up the rest from the theme 48 | - pro: matches text wrapping in browser 49 | - pro: we can reuse the whole pipeline for layout from theme... 50 | - pro: removes the circular dependency between updateNodeContent and dimension provider, letting us optimise the updateNodeContent 51 | 52 | 53 | ==== 54 | 55 | - currently, a map with 800 nodes takes about 12 secs to load 56 | - currently, small updates to a large map take a 10 secs to execute 57 | 58 | - updates should take < 2-3s on a huge map 59 | 60 | -------------------------------------------------------------------------------- /specs/browser/build-connection-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, require, beforeEach, jasmine, expect */ 2 | const buildConnection = require('../../src/browser/build-connection'), 3 | jQuery = require('jquery'); 4 | 5 | describe('buildConnection', () => { 6 | 'use strict'; 7 | let element, connectorBuilder, shapeFrom, shapeTo; 8 | beforeEach(() => { 9 | shapeFrom = jQuery('
'); 10 | shapeFrom.data({ 11 | x: 11, 12 | y: 12, 13 | width: 20, 14 | height: 30, 15 | styles: ['green'] 16 | }); 17 | shapeTo = jQuery('
'); 18 | shapeTo.data({ 19 | x: 111, 20 | y: 112, 21 | width: 120, 22 | height: 130, 23 | styles: ['yellow'] 24 | }); 25 | element = jQuery('
'); 26 | element.data({ 27 | nodeFrom: shapeFrom, 28 | nodeTo: shapeTo 29 | }); 30 | connectorBuilder = jasmine.createSpy('connectorBuilder').and.callFake((from, to, theme) => ({from: from, to: to, theme: theme})); 31 | }); 32 | it('creates the connection using the builder by passing from and to boxes and the theme', () => { 33 | expect(buildConnection(element, {theme: 'ugly', connectorBuilder: connectorBuilder})).toEqual({ 34 | from: { top: 12, left: 11, width: 20, height: 30, styles: ['green']}, 35 | to: { top: 112, left: 111, width: 120, height: 130, styles: ['yellow']}, 36 | theme: 'ugly' 37 | }); 38 | }); 39 | it('applies the inner rect for origin node if set', () => { 40 | shapeFrom.data('innerRect', { 41 | dx: 10, 42 | dy: 20, 43 | width: 14, 44 | height: 15 45 | }); 46 | expect(buildConnection(element, {theme: 'ugly', connectorBuilder: connectorBuilder})).toEqual({ 47 | from: { top: 32, left: 21, width: 14, height: 15, styles: ['green']}, 48 | to: { top: 112, left: 111, width: 120, height: 130, styles: ['yellow']}, 49 | theme: 'ugly' 50 | }); 51 | }); 52 | it('applies the inner rect for destination node if set', () => { 53 | shapeTo.data('innerRect', { 54 | dx: 10, 55 | dy: 20, 56 | width: 14, 57 | height: 15 58 | }); 59 | expect(buildConnection(element, {theme: 'ugly', connectorBuilder: connectorBuilder})).toEqual({ 60 | from: { top: 12, left: 11, width: 20, height: 30, styles: ['green']}, 61 | to: { top: 132, left: 121, width: 14, height: 15, styles: ['yellow']}, 62 | theme: 'ugly' 63 | }); 64 | }); 65 | it('adds connector attributes', () => { 66 | element.data('attr', { 67 | theme: 'pretty', 68 | variant: 'super' 69 | }); 70 | expect(buildConnection(element, {theme: 'ugly', connectorBuilder: connectorBuilder})).toEqual({ 71 | from: { top: 12, left: 11, width: 20, height: 30, styles: ['green']}, 72 | to: { top: 112, left: 111, width: 120, height: 130, styles: ['yellow']}, 73 | theme: 'pretty', 74 | variant: 'super' 75 | }); 76 | 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /specs/browser/calc-label-center-point-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, expect, require, beforeEach*/ 2 | const calcLabelCenterPoint = require('../../src/browser/calc-label-center-point'); 3 | describe('calcLabelCenterPoint', () => { 4 | 'use strict'; 5 | let connectorPosition, fromBox, toBox, d; 6 | beforeEach(() => { 7 | connectorPosition = { 8 | top: 100, 9 | left: 200 10 | }; 11 | fromBox = { 12 | top: 100, 13 | left: 200, 14 | width: 50 15 | }; 16 | toBox = { 17 | top: 300, 18 | left: 250, 19 | width: 50 20 | }; 21 | d = 'M20,10L60,30'; 22 | }); 23 | it('returns a percentage point on the curve if the theme position.ratio is set', () => { 24 | const point = calcLabelCenterPoint(connectorPosition, fromBox, toBox, d, { 25 | position: { 26 | ratio: 0.25 27 | } 28 | }); 29 | expect(point.x).toEqual(30); 30 | expect(point.y).toEqual(15); 31 | }); 32 | it('returns a point over the end box relative to connector position if aboveEnd is set', () => { 33 | const point = calcLabelCenterPoint(connectorPosition, fromBox, toBox, d, { 34 | position: { 35 | aboveEnd: 10 36 | } 37 | }); 38 | 39 | //[50, 200] + [25, -10] 40 | expect(point.x).toEqual(75); // 250 + (50 / 2) - 200 41 | expect(point.y).toEqual(190); 42 | }); 43 | it('returns a point between from and toBox centers if both aboveEnd and ratio are set', () => { 44 | const point = calcLabelCenterPoint(connectorPosition, fromBox, toBox, d, { 45 | position: { 46 | aboveEnd: 10, 47 | ratio: 0.5 48 | } 49 | }); 50 | 51 | expect(point.x).toEqual(50); 52 | expect(point.y).toEqual(190); 53 | }); 54 | 55 | it('returns the center of the path if no position set in theme', () => { 56 | const point = calcLabelCenterPoint(connectorPosition, fromBox, toBox, d, { 57 | xposition: {} 58 | }); 59 | 60 | expect(point.x).toEqual(40); 61 | expect(point.y).toEqual(20); 62 | 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /specs/browser/create-node-spec.js: -------------------------------------------------------------------------------- 1 | /*global require, describe, it, expect, beforeEach, afterEach*/ 2 | const jQuery = require('jquery'); 3 | require('../../src/browser/create-node'); 4 | 5 | describe('createNode', () => { 6 | 'use strict'; 7 | let template, node; 8 | beforeEach(() => { 9 | template = jQuery('
').appendTo('body'); 10 | node = { 11 | id: 121, 12 | x: 111.1, 13 | y: 221.9 14 | }; 15 | }); 16 | afterEach(() => template.remove()); 17 | it('should return a div', () => { 18 | const created = template.createNode(node); 19 | expect(created[0].tagName).toEqual('DIV'); 20 | }); 21 | it('should set the id', () => { 22 | const created = template.createNode(node); 23 | expect(created.attr('id')).toEqual('node_121'); 24 | }); 25 | it('should set the tab index', () => { 26 | const created = template.createNode(node); 27 | expect(created.attr('tabindex')).toEqual('0'); 28 | }); 29 | it('should set the mapjs-role to node ', () => { 30 | const created = template.createNode(node); 31 | expect(created.data('mapjs-role')).toEqual('node'); 32 | }); 33 | it('should set the display style to block', () => { 34 | const created = template.createNode(node); 35 | expect(created.css('display')).toEqual('block'); 36 | }); 37 | it('should set the position style to absolute', () => { 38 | const created = template.createNode(node); 39 | expect(created.css('position')).toEqual('absolute'); 40 | }); 41 | it('should set the top style to rounded node y', () => { 42 | const created = template.createNode(node); 43 | expect(created.css('top')).toEqual('222px'); 44 | }); 45 | it('should set the top style to 0 if node y is falsy', () => { 46 | delete node.y; 47 | const created = template.createNode(node); 48 | expect(created.css('top')).toEqual('0px'); 49 | }); 50 | it('should set the left style to rounded node x', () => { 51 | const created = template.createNode(node); 52 | expect(created.css('left')).toEqual('111px'); 53 | }); 54 | it('should set the left style to 0 if node x is falsy', () => { 55 | delete node.x; 56 | const created = template.createNode(node); 57 | expect(created.css('left')).toEqual('0px'); 58 | }); 59 | it('should add a mapjs-node class', () => { 60 | const created = template.createNode(node); 61 | expect(created.hasClass('mapjs-node')).toBeTruthy(); 62 | }); 63 | it('should append the node', () => { 64 | const created = template.createNode(node); 65 | expect(created.parent()[0]).toEqual(template[0]); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /specs/browser/get-box-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach, expect, require */ 2 | const jQuery = require('jquery'); 3 | require('../../src/browser/get-box'); 4 | 5 | describe('getBox', function () { 6 | 'use strict'; 7 | let underTest; 8 | beforeEach(function () { 9 | underTest = jQuery('
').appendTo('body').css({ 10 | position: 'absolute', 11 | top: '200px', 12 | left: '300px', 13 | width: '150px', 14 | height: '20px' 15 | }); 16 | 17 | }); 18 | afterEach(function () { 19 | underTest.remove(); 20 | }); 21 | it('retrieves offset box from a DOM element', function () { 22 | expect(underTest.getBox()).toEqual({ 23 | top: 200, 24 | left: 300, 25 | width: 150, 26 | height: 20 27 | }); 28 | }); 29 | it('retrieves the offset box from the first element of a jQuery selector', function () { 30 | const another = jQuery('
'); 31 | expect(underTest.add(another).getBox()).toEqual({ 32 | top: 200, 33 | left: 300, 34 | width: 150, 35 | height: 20 36 | }); 37 | }); 38 | it('returns false if selector is empty', function () { 39 | expect(jQuery('#non-existent').getBox()).toBeFalsy(); 40 | }); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /specs/browser/get-data-box-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach, expect, require */ 2 | const jQuery = require('jquery'); 3 | require('../../src/browser/get-data-box'); 4 | describe('getDataBox', function () { 5 | 'use strict'; 6 | let underTest, stage; 7 | beforeEach(function () { 8 | stage = jQuery('
').appendTo('body'); 9 | underTest = jQuery('
').appendTo(stage).css({ 10 | position: 'absolute', 11 | top: '200px', 12 | left: '300px', 13 | width: '150px', 14 | height: '20px' 15 | }).data({ 16 | x: 11, 17 | y: 12, 18 | width: 13, 19 | height: 14 20 | }); 21 | }); 22 | afterEach(function () { 23 | underTest.remove(); 24 | stage.remove(); 25 | }); 26 | it('retrieves a pre-calculated box from data attributes if they are present', function () { 27 | expect(underTest.getDataBox()).toEqual({ 28 | left: 11, 29 | top: 12, 30 | width: 13, 31 | height: 14 32 | }); 33 | }); 34 | it('ignores stage offset and zoom', function () { 35 | stage.data({'offsetX': 200, 'offsetY': 300, 'scale': 2}); 36 | expect(underTest.getDataBox()).toEqual({ 37 | left: 11, 38 | top: 12, 39 | width: 13, 40 | height: 14 41 | }); 42 | }); 43 | ['width', 'height'].forEach(function (attrib) { 44 | it('falls back to DOM boxing if data attribute ' + attrib + ' is not present', function () { 45 | underTest.data(attrib, ''); 46 | expect(underTest.getDataBox()).toEqual({ 47 | top: 200, 48 | left: 300, 49 | width: 150, 50 | height: 20 51 | }); 52 | }); 53 | }); 54 | it('returns false if selector is empty', function () { 55 | expect(jQuery('#non-existent').getDataBox()).toBeFalsy(); 56 | }); 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /specs/browser/inner-text-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach, expect, spyOn, require */ 2 | const jQuery = require('jquery'); 3 | 4 | require('../../src/browser/inner-text'); 5 | require('../helpers/jquery-extension-matchers'); 6 | 7 | describe('innerText', function () { 8 | 'use strict'; 9 | let underTest; 10 | beforeEach(function () { 11 | jQuery.fn.innerText.check = false; 12 | underTest = jQuery('').appendTo('body'); 13 | spyOn(jQuery.fn, 'text').and.callThrough(); 14 | }); 15 | afterEach(function () { 16 | underTest.detach(); 17 | }); 18 | it('executes using .text if content does not contain BR elements', function () { 19 | underTest.html('does\nthis\nhave\nbreaks'); 20 | expect(underTest.innerText()).toEqual('does\nthis\nhave\nbreaks'); 21 | expect(jQuery.fn.text).toHaveBeenCalledOnJQueryObject(underTest); 22 | }); 23 | it('removes html tags and replaces BR with newlines if content contains BR elements (broken firefox contenteditable)', function () { 24 | underTest.html('does
this
have
breaks'); 25 | expect(underTest.innerText()).toEqual('does\nthis\nhave\nbreaks'); 26 | expect(jQuery.fn.text).not.toHaveBeenCalledOnJQueryObject(underTest); 27 | }); 28 | it('removes html tags and replaces divs with newlines if content contains div elements (broken safari contenteditable)', function () { 29 | underTest.html('does
this
have
breaks and spaces'); 30 | expect(underTest.innerText()).toEqual('does\nthis\nhave\nbreaks and spaces'); 31 | expect(jQuery.fn.text).not.toHaveBeenCalledOnJQueryObject(underTest); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /specs/browser/jquery-extension-matchers-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach, expect, spyOn, require */ 2 | const jQuery = require('jquery'); 3 | require('../helpers/jquery-extension-matchers'); 4 | describe('toHaveBeenCalledOnJQueryObject matcher', function () { 5 | 'use strict'; 6 | let underTest1, underTest2; 7 | beforeEach(function () { 8 | underTest1 = jQuery('
').appendTo('body'); 9 | underTest2 = jQuery('
').appendTo('body'); 10 | spyOn(jQuery.fn, 'focus'); 11 | }); 12 | afterEach(function () { 13 | underTest1.remove(); 14 | underTest2.remove(); 15 | }); 16 | it('checks that a function was applied to a jQuery selector by comparing elements', function () { 17 | underTest1.focus(); 18 | expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject(underTest1); 19 | expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject(jQuery('#fst')); 20 | expect(jQuery.fn.focus).not.toHaveBeenCalledOnJQueryObject(underTest2); 21 | expect(jQuery.fn.focus).not.toHaveBeenCalledOnJQueryObject(jQuery('#snd')); 22 | }); 23 | }); 24 | describe('toHaveOwnStyle', function () { 25 | 'use strict'; 26 | let underTest; 27 | beforeEach(function () { 28 | underTest = jQuery('
').appendTo('body'); 29 | }); 30 | afterEach(function () { 31 | underTest.remove(); 32 | }); 33 | it('checks that a function was applied to a jQuery selector by comparing elements', function () { 34 | underTest.css('outline', 'none'); 35 | expect(underTest).toHaveOwnStyle('outline'); 36 | expect(underTest).not.toHaveOwnStyle('display'); 37 | }); 38 | it('checks for any of the styles in the array', function () { 39 | underTest.css('outline', 'none'); 40 | expect(underTest).toHaveOwnStyle(['outline', 'display']); 41 | expect(underTest).not.toHaveOwnStyle(['display', 'z-index']); 42 | }); 43 | 44 | }); 45 | 46 | 47 | -------------------------------------------------------------------------------- /specs/browser/node-cache-mark-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, expect, require */ 2 | const nodeCacheMark = require('../../src/browser/node-cache-mark'), 3 | Theme = require('../../src/core/theme/theme'); 4 | describe('nodeCacheMark', function () { 5 | 'use strict'; 6 | 7 | describe('returns the same value for two nodes if they have the same title, icon sizes, levels and positions, groups and collapsed attribute', function () { 8 | [ 9 | ['no icons, just titles', {level: 1, title: 'zeka', x: 1, attr: {ignored: 1}}, {level: 1, title: 'zeka', x: 2, attr: {ignored: 2}}], 10 | ['titles and collapsed', {level: 1, title: 'zeka', x: 1, attr: {ignored: 1, collapsed: true}}, {level: 1, title: 'zeka', x: 2, attr: {ignored: 2, collapsed: true}}], 11 | ['titles and icon', {level: 1, title: 'zeka', x: 1, attr: { ignored: 1, icon: {width: 100, height: 120, position: 'top', url: '1'} }}, {level: 1, title: 'zeka', x: 2, attr: {ignored: 2, icon: {width: 100, height: 120, position: 'top', url: '2'}}}], 12 | ['titles and groups', {level: 1, title: 'zeka', x: 1, attr: {group: 'xx', ignored: 1}}, {level: 1, title: 'zeka', x: 2, attr: {ignored: 2, group: 'xx'}}] 13 | ].forEach(function (testCase) { 14 | const testName = testCase[0], 15 | first = testCase[1], 16 | second = testCase[2]; 17 | it(testName, function () { 18 | expect(nodeCacheMark(first)).toEqual(nodeCacheMark(second)); 19 | }); 20 | }); 21 | }); 22 | describe('returns different values for two nodes if they differ', function () { 23 | [ 24 | ['titles', {title: 'zeka'}, {title: 'zeka2'}], 25 | ['levels', {title: 'zeka', level: 2}, {title: 'zeka', level: 1}], 26 | ['groups', {title: 'zeka', level: 3, attr: {group: 's1'}}, {title: 'zeka', level: 3, attr: { group: 's2' }}], 27 | ['collapsed', {title: 'zeka', attr: {collapsed: true}}, {title: 'zeka', attr: {collapsed: false}}], 28 | ['icon width', {title: 'zeka', attr: { icon: {width: 100, height: 120, position: 'top'} }}, {title: 'zeka', attr: { icon: {width: 101, height: 120, position: 'top'}}}], 29 | ['icon height', {title: 'zeka', attr: { icon: {width: 100, height: 120, position: 'top'} }}, {title: 'zeka', attr: { icon: {width: 100, height: 121, position: 'top'}}}], 30 | ['icon position', {title: 'zeka', attr: { icon: {width: 100, height: 120, position: 'left'} }}, {title: 'zeka', attr: {icon: {width: 100, height: 120, position: 'top'}}}] 31 | ].forEach(function (testCase) { 32 | const testName = testCase[0], 33 | first = testCase[1], 34 | second = testCase[2]; 35 | 36 | it(testName, function () { 37 | expect(nodeCacheMark(first)).not.toEqual(nodeCacheMark(second, {theme: new Theme({})})); 38 | }); 39 | }); 40 | }); 41 | it('ignores group titles', function () { 42 | expect(nodeCacheMark({title: 'zeka', attr: {group: 'supporting'}})).toEqual(nodeCacheMark({title: '', attr: {group: 'supporting'}})); 43 | }); 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /specs/browser/place-caret-at-end-spec.js: -------------------------------------------------------------------------------- 1 | /*global document, describe, it, expect, require, window, afterEach */ 2 | 3 | const jQuery = require('jquery'); 4 | require('../../src/browser/place-caret-at-end'); 5 | 6 | describe('placeCaretAtEnd', () => { 7 | 'use strict'; 8 | let underTest; 9 | afterEach(() => { 10 | underTest.remove(); 11 | }); 12 | it('works on contenteditable divs', () => { 13 | underTest = jQuery('').html('some text').appendTo('body'); 14 | underTest.placeCaretAtEnd(); 15 | const selection = window.getSelection(), 16 | range = selection.getRangeAt(0); 17 | expect(selection.type).toEqual('Caret'); 18 | expect(selection.rangeCount).toEqual(1); 19 | range.surroundContents(document.createElement('i')); 20 | expect(underTest.html()).toEqual('some text'); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /specs/browser/set-theme-class-list-spec.js: -------------------------------------------------------------------------------- 1 | /*global require, describe, expect, beforeEach, afterEach, it */ 2 | const jQuery = require('jquery'); 3 | 4 | require('../../src/browser/set-theme-class-list'); 5 | 6 | describe('setThemeClassList', function () { 7 | 'use strict'; 8 | let underTest, domElement; 9 | beforeEach(function () { 10 | underTest = jQuery('
').appendTo('body'); 11 | domElement = underTest[0]; 12 | domElement.classList.add.apply(domElement.classList, ['level_2', 'attr_foo']); 13 | }); 14 | afterEach(function () { 15 | underTest.remove(); 16 | }); 17 | it('should remove theme classes that are already set on the element', function () { 18 | underTest.setThemeClassList([]); 19 | expect(underTest.attr('class')).toEqual(''); 20 | }); 21 | it('should remove theme classes if no array is supplied', function () { 22 | underTest.setThemeClassList(); 23 | expect(underTest.attr('class')).toEqual(''); 24 | }); 25 | it('should not remove non theme classes that are already set on the element', function () { 26 | domElement.classList.add.apply(domElement.classList, ['foo', 'bar']); 27 | underTest.setThemeClassList([]); 28 | expect(underTest.attr('class')).toEqual('foo bar'); 29 | }); 30 | it('should not add the default class', function () { 31 | underTest.setThemeClassList(['default']); 32 | expect(underTest.attr('class')).toEqual(''); 33 | 34 | }); 35 | it('should add theme classes to the element', function () { 36 | underTest.setThemeClassList(['level_3', 'attr_bar']); 37 | expect(underTest.attr('class')).toEqual('level_3 attr_bar'); 38 | expect(underTest.hasClass('level_3')).toBeTruthy(); 39 | expect(underTest.hasClass('attr_bar')).toBeTruthy(); 40 | }); 41 | it('should not duplicate classes on the element', function () { 42 | underTest.setThemeClassList(['level_2', 'attr_bar']); 43 | expect(underTest.attr('class')).toEqual('level_2 attr_bar'); 44 | 45 | }); 46 | 47 | }); 48 | 49 | -------------------------------------------------------------------------------- /specs/browser/update-reorder-bounds-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach, expect, require */ 2 | const jQuery = require('jquery'); 3 | 4 | require('../../src/browser/update-reorder-bounds'); 5 | 6 | 7 | describe('updateReorderBounds', function () { 8 | 'use strict'; 9 | let underTest; 10 | beforeEach(function () { 11 | underTest = jQuery('
').css({position: 'absolute', width: 6, height: 16}).appendTo('body'); 12 | }); 13 | afterEach(function () { 14 | underTest.remove(); 15 | }); 16 | it('hides the element if the border is not defined', function () { 17 | underTest.updateReorderBounds(false, {}); 18 | expect(underTest.css('display')).toEqual('none'); 19 | }); 20 | it('shows the element if the border is defined', function () { 21 | underTest.css('display', 'none'); 22 | underTest.updateReorderBounds({edge: 'top', minY: 10}, {}, {x: 10}); 23 | expect(underTest.css('display')).not.toEqual('none'); 24 | }); 25 | it('shows the top border at drop coordinates x', function () { 26 | underTest.updateReorderBounds({edge: 'top', minY: 33}, {}, {x: 10}); 27 | expect(underTest.attr('mapjs-edge')).toEqual('top'); 28 | expect(underTest.css('left')).toEqual('7px'); 29 | expect(underTest.css('top')).toEqual('33px'); 30 | }); 31 | it('shows the left border at drop coords (-chevron width, Y)', function () { 32 | underTest.updateReorderBounds({edge: 'left', x: 33}, {}, {y: 10}); 33 | expect(underTest.attr('mapjs-edge')).toEqual('left'); 34 | expect(underTest.css('top')).toEqual('2px'); 35 | expect(underTest.css('left')).toEqual('27px'); 36 | }); 37 | it('shows the right border at drop coords (0, Y)', function () { 38 | underTest.updateReorderBounds({edge: 'right', x: 33}, {}, {y: 10}); 39 | expect(underTest.attr('mapjs-edge')).toEqual('right'); 40 | expect(underTest.css('top')).toEqual('2px'); 41 | expect(underTest.css('left')).toEqual('33px'); 42 | }); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /specs/browser/update-stage-spec.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach, expect, require */ 2 | const jQuery = require('jquery'), 3 | createSVG = require('../../src/browser/create-svg'); 4 | 5 | require('../../src/browser/update-stage'); 6 | 7 | describe('updateStage', function () { 8 | 'use strict'; 9 | let stage, second; 10 | beforeEach(function () { 11 | stage = jQuery('
').appendTo('body'); 12 | second = jQuery('
').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 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /test/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/icon-link-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 28 | 29 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /test/icon-link-inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 28 | 29 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /test/icon-paperclip-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/icon-paperclip-inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Background: 50 | Frames: 51 | Cycle: 52 |
53 | 54 | 58 | 62 | 63 |
64 |
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 | --------------------------------------------------------------------------------