├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ ├── TASK.md │ └── config.yml └── workflows │ ├── CI.yml │ ├── CODE_SCANNING.yml │ └── MERGE_MASTER_TO_DEVELOP.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── diagram-js.css ├── eslint.config.mjs ├── karma.conf.js ├── lib ├── Diagram.js ├── Diagram.spec.ts ├── command │ ├── CommandHandler.spec.ts │ ├── CommandHandler.ts │ ├── CommandInterceptor.js │ ├── CommandInterceptor.spec.ts │ ├── CommandStack.js │ ├── CommandStack.spec.ts │ └── index.js ├── core │ ├── Canvas.js │ ├── Canvas.spec.ts │ ├── ElementFactory.js │ ├── ElementFactory.spec.ts │ ├── ElementRegistry.js │ ├── ElementRegistry.spec.ts │ ├── EventBus.js │ ├── EventBus.spec.ts │ ├── GraphicsFactory.js │ ├── GraphicsFactory.spec.ts │ ├── Types.ts │ └── index.js ├── draw │ ├── BaseRenderer.js │ ├── BaseRenderer.spec.ts │ ├── DefaultRenderer.js │ ├── DefaultRenderer.spec.ts │ ├── Styles.js │ └── index.js ├── features │ ├── align-elements │ │ ├── AlignElements.js │ │ └── index.js │ ├── attach-support │ │ ├── AttachSupport.js │ │ └── index.js │ ├── auto-place │ │ ├── AutoPlace.js │ │ ├── AutoPlaceSelectionBehavior.js │ │ ├── AutoPlaceUtil.js │ │ ├── AutoPlaceUtil.spec.ts │ │ └── index.js │ ├── auto-resize │ │ ├── AutoResize.js │ │ ├── AutoResizeProvider.js │ │ └── index.js │ ├── auto-scroll │ │ ├── AutoScroll.js │ │ └── index.js │ ├── bendpoints │ │ ├── BendpointMove.js │ │ ├── BendpointMovePreview.js │ │ ├── BendpointSnapping.js │ │ ├── BendpointUtil.js │ │ ├── Bendpoints.js │ │ ├── ConnectionSegmentMove.js │ │ ├── GeometricUtil.js │ │ └── index.js │ ├── change-support │ │ ├── ChangeSupport.js │ │ └── index.js │ ├── clipboard │ │ ├── Clipboard.js │ │ └── index.js │ ├── complex-preview │ │ ├── ComplexPreview.js │ │ └── index.js │ ├── connect │ │ ├── Connect.js │ │ ├── ConnectPreview.js │ │ └── index.js │ ├── connection-preview │ │ ├── ConnectionPreview.js │ │ └── index.js │ ├── context-pad │ │ ├── ContextPad.js │ │ ├── ContextPad.spec.ts │ │ ├── ContextPadProvider.spec.ts │ │ ├── ContextPadProvider.ts │ │ └── index.js │ ├── copy-paste │ │ ├── CopyPaste.js │ │ └── index.js │ ├── create │ │ ├── Create.js │ │ ├── CreateConnectPreview.js │ │ ├── CreatePreview.js │ │ └── index.js │ ├── distribute-elements │ │ ├── DistributeElements.js │ │ └── index.js │ ├── dragging │ │ ├── Dragging.js │ │ └── index.js │ ├── editor-actions │ │ ├── EditorActions.js │ │ └── index.js │ ├── global-connect │ │ ├── GlobalConnect.js │ │ └── index.js │ ├── grid-snapping │ │ ├── GridSnapping.js │ │ ├── GridUtil.js │ │ ├── behavior │ │ │ ├── ResizeBehavior.js │ │ │ ├── SpaceToolBehavior.js │ │ │ └── index.js │ │ └── index.js │ ├── hand-tool │ │ ├── HandTool.js │ │ └── index.js │ ├── hover-fix │ │ ├── HoverFix.js │ │ └── index.js │ ├── interaction-events │ │ ├── InteractionEvents.js │ │ └── index.js │ ├── keyboard-move-selection │ │ ├── KeyboardMoveSelection.js │ │ └── index.js │ ├── keyboard │ │ ├── Keyboard.js │ │ ├── Keyboard.spec.ts │ │ ├── KeyboardBindings.js │ │ ├── KeyboardUtil.js │ │ └── index.js │ ├── label-support │ │ ├── LabelSupport.js │ │ └── index.js │ ├── lasso-tool │ │ ├── LassoTool.js │ │ └── index.js │ ├── modeling │ │ ├── Modeling.js │ │ ├── Modeling.spec.ts │ │ ├── cmd │ │ │ ├── AlignElementsHandler.js │ │ │ ├── AppendShapeHandler.js │ │ │ ├── CreateConnectionHandler.js │ │ │ ├── CreateElementsHandler.js │ │ │ ├── CreateLabelHandler.js │ │ │ ├── CreateShapeHandler.js │ │ │ ├── DeleteConnectionHandler.js │ │ │ ├── DeleteElementsHandler.js │ │ │ ├── DeleteShapeHandler.js │ │ │ ├── DistributeElementsHandler.js │ │ │ ├── LayoutConnectionHandler.js │ │ │ ├── MoveConnectionHandler.js │ │ │ ├── MoveElementsHandler.js │ │ │ ├── MoveShapeHandler.js │ │ │ ├── NoopHandler.js │ │ │ ├── ReconnectConnectionHandler.js │ │ │ ├── ReplaceShapeHandler.js │ │ │ ├── ResizeShapeHandler.js │ │ │ ├── SpaceToolHandler.js │ │ │ ├── ToggleShapeCollapseHandler.js │ │ │ ├── UpdateAttachmentHandler.js │ │ │ ├── UpdateWaypointsHandler.js │ │ │ └── helper │ │ │ │ ├── AnchorsHelper.js │ │ │ │ ├── MoveClosure.js │ │ │ │ └── MoveHelper.js │ │ └── index.js │ ├── mouse │ │ ├── Mouse.js │ │ └── index.js │ ├── move │ │ ├── Move.js │ │ ├── MovePreview.js │ │ └── index.js │ ├── ordering │ │ └── OrderingProvider.js │ ├── outline │ │ ├── MultiSelectionOutline.js │ │ ├── Outline.js │ │ ├── OutlineProvider.spec.ts │ │ ├── OutlineProvider.ts │ │ └── index.js │ ├── overlays │ │ ├── Overlays.js │ │ ├── Overlays.spec.ts │ │ └── index.js │ ├── palette │ │ ├── Palette.js │ │ ├── Palette.spec.ts │ │ ├── PaletteProvider.spec.ts │ │ ├── PaletteProvider.ts │ │ └── index.js │ ├── popup-menu │ │ ├── PopupMenu.js │ │ ├── PopupMenu.spec.ts │ │ ├── PopupMenuComponent.js │ │ ├── PopupMenuHeader.js │ │ ├── PopupMenuItem.js │ │ ├── PopupMenuList.js │ │ ├── PopupMenuProvider.spec.ts │ │ ├── PopupMenuProvider.ts │ │ └── index.js │ ├── preview-support │ │ ├── PreviewSupport.js │ │ └── index.js │ ├── replace │ │ ├── Replace.js │ │ ├── ReplaceSelectionBehavior.js │ │ └── index.js │ ├── resize │ │ ├── Resize.js │ │ ├── ResizeHandles.js │ │ ├── ResizePreview.js │ │ ├── ResizeUtil.js │ │ └── index.js │ ├── root-elements │ │ ├── RootElementsBehavior.js │ │ └── index.js │ ├── rules │ │ ├── RuleProvider.js │ │ ├── Rules.js │ │ ├── Rules.test.ts │ │ └── index.js │ ├── scheduler │ │ ├── Scheduler.js │ │ └── index.js │ ├── search-pad │ │ ├── SearchPad.js │ │ ├── SearchPadProvider.ts │ │ ├── SearchProvider.spec.ts │ │ └── index.js │ ├── search │ │ ├── index.js │ │ └── search.js │ ├── selection │ │ ├── Selection.js │ │ ├── Selection.test.ts │ │ ├── SelectionBehavior.js │ │ ├── SelectionVisuals.js │ │ └── index.js │ ├── snapping │ │ ├── CreateMoveSnapping.js │ │ ├── ResizeSnapping.js │ │ ├── SnapContext.js │ │ ├── SnapUtil.js │ │ ├── Snapping.js │ │ └── index.js │ ├── space-tool │ │ ├── SpaceTool.js │ │ ├── SpaceToolPreview.js │ │ ├── SpaceUtil.js │ │ └── index.js │ ├── tool-manager │ │ ├── ToolManager.js │ │ └── index.js │ └── tooltips │ │ ├── Tooltips.js │ │ └── index.js ├── i18n │ ├── I18N.js │ ├── index.js │ └── translate │ │ ├── index.js │ │ ├── translate.js │ │ └── translate.spec.ts ├── layout │ ├── BaseLayouter.js │ ├── ConnectionDocking.js │ ├── CroppingConnectionDocking.js │ ├── LayoutUtil.js │ └── ManhattanLayout.js ├── model │ ├── Types.ts │ ├── index.js │ └── index.spec.ts ├── navigation │ ├── keyboard-move │ │ ├── KeyboardMove.js │ │ └── index.js │ ├── movecanvas │ │ ├── MoveCanvas.js │ │ └── index.js │ └── zoomscroll │ │ ├── ZoomScroll.js │ │ ├── ZoomUtil.js │ │ └── index.js ├── ui │ └── index.js └── util │ ├── AttachUtil.js │ ├── ClickTrap.js │ ├── Collections.js │ ├── Cursor.js │ ├── Elements.js │ ├── EscapeUtil.js │ ├── Event.js │ ├── Geometry.js │ ├── GraphicsUtil.js │ ├── IdGenerator.js │ ├── LineIntersection.js │ ├── Math.js │ ├── ModelUtil.js │ ├── Mouse.js │ ├── Platform.js │ ├── PositionUtil.js │ ├── Removal.js │ ├── RenderUtil.js │ ├── RenderUtil.spec.ts │ ├── SvgTransformUtil.js │ ├── Text.js │ ├── Text.spec.ts │ └── Types.ts ├── package-lock.json ├── package.json ├── renovate.json ├── test ├── TestHelper.js ├── coverageBundle.js ├── helper │ └── index.js ├── matchers │ ├── BoundsMatchers.js │ ├── BoundsMatchersSpec.js │ ├── ConnectionMatchers.js │ └── ConnectionMatchersSpec.js ├── spec │ ├── DiagramSpec.js │ ├── command │ │ ├── CommandInterceptorSpec.js │ │ └── CommandStackSpec.js │ ├── connection-preview │ │ └── ConnectionPreviewSpec.js │ ├── core │ │ ├── CanvasSpec.js │ │ ├── ElementRegistrySpec.js │ │ ├── EventBusSpec.js │ │ └── GraphicsFactorySpec.js │ ├── draw │ │ ├── DefaultRendererSpec.js │ │ └── StylesSpec.js │ ├── environment │ │ └── MockingSpec.js │ ├── features │ │ ├── align-elements │ │ │ ├── AlignElementsSpec.js │ │ │ └── rules │ │ │ │ ├── TestRules.js │ │ │ │ └── index.js │ │ ├── attach-support │ │ │ ├── AttachSupportSpec.js │ │ │ └── rules │ │ │ │ ├── AttachRules.js │ │ │ │ └── index.js │ │ ├── auto-place │ │ │ └── AutoPlaceSpec.js │ │ ├── auto-resize │ │ │ └── AutoResizeSpec.js │ │ ├── auto-scroll │ │ │ └── AutoScrollSpec.js │ │ ├── bendpoints │ │ │ ├── BendpointsMoveSpec.js │ │ │ ├── BendpointsSpec.js │ │ │ ├── ConnectionSegmentMoveSpec.js │ │ │ ├── GeometricUtilSpec.js │ │ │ └── rules │ │ │ │ ├── BendpointRules.js │ │ │ │ └── index.js │ │ ├── change-support │ │ │ └── ChangeSupportSpec.js │ │ ├── clipboard │ │ │ └── ClipboardSpec.js │ │ ├── complex-preview │ │ │ └── ComplexPreviewSpec.js │ │ ├── connect │ │ │ ├── ConnectSpec.js │ │ │ └── rules │ │ │ │ ├── ConnectRules.js │ │ │ │ └── index.js │ │ ├── context-pad │ │ │ ├── ContextPadProvider.js │ │ │ ├── ContextPadSpec.js │ │ │ └── resources │ │ │ │ ├── a.png │ │ │ │ ├── b.png │ │ │ │ └── c.png │ │ ├── copy-paste │ │ │ ├── CopyPasteSpec.js │ │ │ └── rules │ │ │ │ ├── CopyPasteRules.js │ │ │ │ └── index.js │ │ ├── create │ │ │ ├── CreatePreviewSpec.js │ │ │ ├── CreateSpec.js │ │ │ └── rules │ │ │ │ ├── CreateRules.js │ │ │ │ └── index.js │ │ ├── distribute-elements │ │ │ ├── DistributeElementsSpec.js │ │ │ └── rules │ │ │ │ ├── TestRules.js │ │ │ │ └── index.js │ │ ├── dragging │ │ │ └── DraggingSpec.js │ │ ├── editor-actions │ │ │ ├── EditorActionsSpec.js │ │ │ └── rules │ │ │ │ ├── CustomRules.js │ │ │ │ └── index.js │ │ ├── global-connect │ │ │ ├── GlobalConnectSpec.js │ │ │ └── rules │ │ │ │ ├── GlobalConnectRules.js │ │ │ │ └── index.js │ │ ├── grid-snapping │ │ │ ├── GridSnappingSpec.js │ │ │ └── behavior │ │ │ │ ├── ResizeBehaviorSpec.js │ │ │ │ └── SpaceToolBehaviorSpec.js │ │ ├── hand-tool │ │ │ └── HandToolSpec.js │ │ ├── hover-fix │ │ │ └── HoverFixSpec.js │ │ ├── interaction-events │ │ │ └── InteractionEventsSpec.js │ │ ├── keyboard-move-selection │ │ │ ├── KeyboardMoveSelectionSpec.js │ │ │ └── rules │ │ │ │ ├── KeyboardMoveRules.js │ │ │ │ └── index.js │ │ ├── keyboard │ │ │ ├── CopySpec.js │ │ │ ├── KeyboardSpec.js │ │ │ ├── PasteSpec.js │ │ │ ├── RedoSpec.js │ │ │ ├── RemoveSelectionSpec.js │ │ │ ├── UndoSpec.js │ │ │ └── ZoomSpec.js │ │ ├── label-support │ │ │ ├── LabelSupportSpec.js │ │ │ └── rules │ │ │ │ ├── LabelSupportRules.js │ │ │ │ └── index.js │ │ ├── lasso-tool │ │ │ └── LassoToolSpec.js │ │ ├── modeling │ │ │ ├── AppendShapeSpec.js │ │ │ ├── CreateConnectionSpec.js │ │ │ ├── CreateElementsSpec.js │ │ │ ├── CreateLabelSpec.js │ │ │ ├── CreateShapeSpec.js │ │ │ ├── DeleteConnectionSpec.js │ │ │ ├── DeleteElementsSpec.js │ │ │ ├── DeleteShapeSpec.js │ │ │ ├── DropShapeSpec.js │ │ │ ├── LayoutConnectionSpec.js │ │ │ ├── MoveConnectionSpec.js │ │ │ ├── MoveElementsSpec.js │ │ │ ├── MoveShapeSpec.js │ │ │ ├── ReconnectConnectionSpec.js │ │ │ ├── ReplaceShapeSpec.js │ │ │ ├── ResizeShapeSpec.js │ │ │ ├── ToggleShapeCollapseSpec.js │ │ │ ├── UpdateAttachmentSpec.js │ │ │ └── custom │ │ │ │ ├── CustomLayouter.js │ │ │ │ └── index.js │ │ ├── mouse │ │ │ └── MouseSpec.js │ │ ├── move │ │ │ ├── MovePreviewSpec.js │ │ │ ├── MoveSpec.js │ │ │ └── rules │ │ │ │ ├── MoveRules.js │ │ │ │ └── index.js │ │ ├── ordering │ │ │ ├── OrderingProviderSpec.js │ │ │ └── provider │ │ │ │ ├── TestOrderingProvider.js │ │ │ │ └── index.js │ │ ├── outline │ │ │ ├── MultiSelectionOutlineSpec.js │ │ │ └── OutlineSpec.js │ │ ├── overlays │ │ │ ├── OverlaysIntegrationSpec.js │ │ │ └── OverlaysSpec.js │ │ ├── palette │ │ │ └── PaletteSpec.js │ │ ├── popup-menu │ │ │ ├── PopupMenu.example-entries.json │ │ │ ├── PopupMenuComponentSpec.js │ │ │ ├── PopupMenuHeaderSpec.js │ │ │ ├── PopupMenuItemSpec.js │ │ │ ├── PopupMenuListSpec.js │ │ │ ├── PopupMenuSpec.js │ │ │ └── resources │ │ │ │ └── a.png │ │ ├── preview-support │ │ │ ├── nested-renderer │ │ │ │ ├── MarkerRenderer.js │ │ │ │ └── index.js │ │ │ └── renderer │ │ │ │ ├── MarkerRenderer.js │ │ │ │ └── index.js │ │ ├── replace │ │ │ ├── ReplaceSelectionBehaviorSpec.js │ │ │ └── ReplaceSpec.js │ │ ├── resize │ │ │ ├── ResizeSpec.js │ │ │ ├── ResizeUtilSpec.js │ │ │ └── rules │ │ │ │ ├── ResizeRules.js │ │ │ │ └── index.js │ │ ├── root-elements │ │ │ └── RootElementsBehaviorSpec.js │ │ ├── rules │ │ │ ├── RuleProviderSpec.js │ │ │ ├── priority-rules │ │ │ │ ├── PriorityRules.js │ │ │ │ └── index.js │ │ │ ├── rules │ │ │ │ ├── TestRules.js │ │ │ │ └── index.js │ │ │ └── say-no-rules │ │ │ │ ├── SayNoRules.js │ │ │ │ └── index.js │ │ ├── scheduler │ │ │ └── SchedulerSpec.js │ │ ├── search-pad │ │ │ └── SearchPadSpec.js │ │ ├── search │ │ │ └── searchSpec.js │ │ ├── selection │ │ │ ├── SelectionBehaviorSpec.js │ │ │ ├── SelectionSpec.js │ │ │ ├── SelectionVisualsSpec.js │ │ │ └── rules │ │ │ │ ├── ConnectRules.js │ │ │ │ └── index.js │ │ ├── snapping │ │ │ └── SnappingSpec.js │ │ ├── space-tool │ │ │ ├── SpaceToolSpec.js │ │ │ ├── auto-resize │ │ │ │ ├── CustomAutoResizeProvider.js │ │ │ │ └── index.js │ │ │ └── rules │ │ │ │ ├── SpaceToolRules.js │ │ │ │ └── index.js │ │ ├── tool-manager │ │ │ └── ToolManagerSpec.js │ │ └── tooltips │ │ │ └── TooltipsSpec.js │ ├── i18n │ │ ├── I18NSpec.js │ │ └── translate │ │ │ ├── custom-translate │ │ │ ├── custom-translate.js │ │ │ └── index.js │ │ │ └── translateSpec.js │ ├── layout │ │ ├── CroppingConnectionDockingSpec.js │ │ ├── LayoutUtilSpec.js │ │ └── ManhattanLayoutSpec.js │ ├── model │ │ └── ModelSpec.js │ ├── navigation │ │ ├── keyboard-move │ │ │ └── KeyboardMoveSpec.js │ │ ├── movecanvas │ │ │ └── MoveCanvasSpec.js │ │ └── zoomscoll │ │ │ └── ZoomScrollSpec.js │ ├── snapping │ │ ├── CreateMoveSnappingSpec.js │ │ ├── ResizeSnappingSpec.js │ │ ├── SnapContextSpec.js │ │ ├── SnapUtilSpec.js │ │ └── SnappingSpec.js │ ├── ui │ │ └── uiSpec.js │ └── util │ │ ├── AttachUtilSpec.js │ │ ├── ElementIntegrationSpec.js │ │ ├── ElementsSpec.js │ │ ├── EscapeUtilSpec.js │ │ ├── GeometrySpec.js │ │ ├── IdGeneratorSpec.js │ │ ├── LineIntersectionSpec.js │ │ ├── RenderUtilSpec.js │ │ └── TextSpec.js ├── testBundle.js └── util │ ├── KeyEvents.js │ └── MockEvents.js └── tsconfig.json /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a problem and help us fix it. 4 | labels: "bug" 5 | --- 6 | 7 | 8 | ### Describe the Bug 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | 13 | ### Steps to Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. do this 18 | 2. do that 19 | 20 | If possible, try to build [a test case](https://github.com/bpmn-io/diagram-js/tree/main/test/spec) that reproduces your problem. 21 | 22 | 23 | ### Expected Behavior 24 | 25 | A clear and concise description of what you expected to happen. 26 | 27 | 28 | ### Environment 29 | 30 | Please complete the following information: 31 | 32 | - Browser: [e.g. IE 11, Chrome 69] 33 | - OS: [e.g. Windows 7] 34 | - Library version: [e.g. 2.0.0] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or general improvement. 4 | labels: "enhancement" 5 | --- 6 | 7 | 8 | ### Is your feature request related to a problem? Please describe. 9 | 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | 13 | ### Describe the solution you'd like 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | 18 | ### Describe alternatives you've considered 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | 23 | ### Additional context 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/TASK.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Describe a generic activity we should carry out. 4 | --- 5 | 6 | 7 | ### What should we do? 8 | 9 | 10 | 11 | 12 | ### Why should we do it? 13 | 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://forum.bpmn.io 5 | about: Head over to our community forum to ask questions and get answers. -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | 6 | strategy: 7 | matrix: 8 | os: [ macos-latest, ubuntu-latest, windows-latest ] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Use Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | cache: 'npm' 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Setup project 23 | uses: bpmn-io/actions/setup@latest 24 | - name: Build 25 | if: runner.os == 'Linux' 26 | env: 27 | COVERAGE: 1 28 | TEST_BROWSERS: Firefox,ChromeHeadless 29 | run: xvfb-run npm run all 30 | - name: Build 31 | if: runner.os != 'Linux' 32 | env: 33 | TEST_BROWSERS: ChromeHeadless 34 | run: npm run all 35 | - name: Upload coverage 36 | if: runner.os == 'Linux' 37 | uses: codecov/codecov-action@v5 38 | with: 39 | fail_ci_if_error: true 40 | env: 41 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/CODE_SCANNING.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scanning" 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | paths-ignore: 9 | - '**/*.md' 10 | 11 | jobs: 12 | codeql_build: 13 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | # required for all workflows 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | # Initializes the CodeQL tools for scanning. 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: javascript 29 | config: | 30 | paths-ignore: 31 | - '**/test' 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v3 35 | -------------------------------------------------------------------------------- /.github/workflows/MERGE_MASTER_TO_DEVELOP.yml: -------------------------------------------------------------------------------- 1 | name: MERGE_MAIN_TO_DEVELOP 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | 7 | jobs: 8 | merge_main_to_develop: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | 13 | steps: 14 | - name: Checkout develop 15 | uses: actions/checkout@v4 16 | with: 17 | ref: develop 18 | fetch-depth: 0 19 | - name: Merge main to develop and push 20 | run: | 21 | git config user.name '${{ secrets.BPMN_IO_USERNAME }}' 22 | git config user.email '${{ secrets.BPMN_IO_EMAIL }}' 23 | git merge -m 'Merge main to develop' --no-edit origin/main 24 | git push 25 | 26 | - name: Notify failure on Slack 27 | if: failure() 28 | uses: slackapi/slack-github-action@v2 29 | with: 30 | method: chat.postMessage 31 | token: ${{ secrets.SLACK_BOT_TOKEN }} 32 | payload: | 33 | channel: ${{ secrets.SLACK_CHANNEL_ID }} 34 | text: "Automatic merge of to failed." 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | tmp/ 4 | coverage/ 5 | lib/**/*.d.ts 6 | .idea/ 7 | .vscode/ 8 | .DS_Store 9 | .*.swp 10 | .*.swo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present Camunda Services GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import bpmnIoPlugin from 'eslint-plugin-bpmn-io'; 2 | 3 | const files = { 4 | ignored: [ 5 | 'tmp' 6 | ], 7 | build: [ 8 | '*.js', 9 | '*.mjs' 10 | ], 11 | test: [ 12 | '**/test/**/*.js' 13 | ] 14 | }; 15 | 16 | export default [ 17 | { 18 | ignores: files.ignored 19 | }, 20 | 21 | // build 22 | ...bpmnIoPlugin.configs.node.map(config => { 23 | 24 | return { 25 | ...config, 26 | files: files.build 27 | }; 28 | }), 29 | 30 | // lib + test 31 | ...bpmnIoPlugin.configs.browser.map(config => { 32 | 33 | return { 34 | ...config, 35 | ignores: files.build 36 | }; 37 | }), 38 | 39 | // test 40 | ...bpmnIoPlugin.configs.mocha.map(config => { 41 | 42 | return { 43 | ...config, 44 | files: files.test, 45 | }; 46 | }), 47 | { 48 | languageOptions: { 49 | globals: { 50 | sinon: true 51 | } 52 | }, 53 | files: files.test 54 | } 55 | ]; -------------------------------------------------------------------------------- /lib/Diagram.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from './Diagram'; 2 | 3 | import CommandModule from './command'; 4 | 5 | import CoreModule from './core'; 6 | import EventBus from './core/EventBus'; 7 | 8 | import ModelingModule from './features/modeling'; 9 | import Modeling from './features/modeling/Modeling'; 10 | 11 | let diagram = new Diagram(); 12 | 13 | diagram = new Diagram({ 14 | modules: [ 15 | CoreModule, 16 | CommandModule, 17 | ModelingModule 18 | ], 19 | canvas: { 20 | deferUpdate: true 21 | } 22 | }); 23 | 24 | diagram.clear(); 25 | 26 | diagram.destroy(); 27 | 28 | diagram.invoke((eventBus: EventBus) => eventBus.fire('foo')); 29 | 30 | const foo = diagram.invoke((modeling: Modeling, eventBus: EventBus) => { 31 | return { 32 | bar: true 33 | }; 34 | }); 35 | 36 | foo.bar = false; 37 | 38 | type NoneEvent = {}; 39 | 40 | type EventMap = { 41 | 'diagram.init': NoneEvent 42 | }; 43 | 44 | type ServiceMap = { 45 | 'eventBus': EventBus 46 | }; 47 | 48 | const typedDiagram = new Diagram(); 49 | 50 | const eventBus = typedDiagram.get('eventBus'); 51 | 52 | eventBus.on('diagram.init', (event) => { 53 | 54 | // go forth and react to init (!) 55 | }); 56 | -------------------------------------------------------------------------------- /lib/command/CommandHandler.spec.ts: -------------------------------------------------------------------------------- 1 | import CommandHandler from './CommandHandler'; 2 | import { CommandContext } from './CommandStack'; 3 | 4 | import Canvas from '../core/Canvas'; 5 | 6 | export class AddShapeHandler implements CommandHandler { 7 | private _canvas: Canvas; 8 | 9 | static $inject = [ 'canvas' ]; 10 | 11 | constructor(canvas: Canvas) { 12 | this._canvas = canvas; 13 | } 14 | 15 | execute(context: CommandContext) { 16 | const { 17 | parent, 18 | shape 19 | } = context; 20 | 21 | this._canvas.addShape(shape, parent); 22 | 23 | return [ 24 | parent, 25 | shape 26 | ]; 27 | } 28 | 29 | revert(context: CommandContext) { 30 | const { 31 | parent, 32 | shape 33 | } = context; 34 | 35 | this._canvas.removeShape(shape); 36 | 37 | return [ 38 | parent, 39 | shape 40 | ]; 41 | } 42 | 43 | canExecute(context: CommandContext) { 44 | console.log(context); 45 | 46 | return true; 47 | } 48 | 49 | preExecute(context: CommandContext) { 50 | console.log(context); 51 | } 52 | 53 | postExecute(context: CommandContext) { 54 | console.log(context); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/command/CommandHandler.ts: -------------------------------------------------------------------------------- 1 | import type { ElementLike } from '../core/Types'; 2 | import type { CommandContext } from './CommandStack'; 3 | 4 | /** 5 | * A command handler that may be registered via 6 | * {@link CommandStack#registerHandler}. 7 | */ 8 | export default interface CommandHandler { 9 | /** 10 | * Execute changes described in the passed action context. 11 | * 12 | * @param context The execution context. 13 | * 14 | * @return The list of diagram elements that have changed. 15 | */ 16 | execute?(context: CommandContext): ElementLike[]; 17 | 18 | /** 19 | * Revert changes described in the passed action context. 20 | * 21 | * @param context The execution context. 22 | * 23 | * @return The list of diagram elements that have changed. 24 | */ 25 | revert?(context: CommandContext): ElementLike[]; 26 | 27 | /** 28 | * Return true if the handler may execute in the given context. 29 | * 30 | * @param context The execution context. 31 | * 32 | * @return Whether the command can be executed. 33 | */ 34 | canExecute?(context: CommandContext): boolean; 35 | 36 | /** 37 | * Execute actions before the actual command execution but 38 | * grouped together (for undo/redo) with the action. 39 | * 40 | * @param context The execution context. 41 | */ 42 | preExecute?(context: CommandContext): void; 43 | 44 | /** 45 | * Execute actions after the actual command execution but 46 | * grouped together (for undo/redo) with the action. 47 | * 48 | * @param context The execution context. 49 | */ 50 | postExecute?(context: CommandContext): void; 51 | } 52 | -------------------------------------------------------------------------------- /lib/command/CommandInterceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import CommandInterceptor from './CommandInterceptor'; 2 | 3 | import EventBus from '../core/EventBus'; 4 | 5 | import Modeling from '../features/modeling/Modeling'; 6 | 7 | export class AddShapeBehavior extends CommandInterceptor { 8 | static $inject = [ 'eventBus', 'modeling' ]; 9 | 10 | constructor(eventBus: EventBus, modeling: Modeling) { 11 | super(eventBus); 12 | 13 | this.canExecute((context) => {}, true); 14 | 15 | this.canExecute([ 'shape.create' ], (context) => {}, true); 16 | 17 | this.canExecute([ 'shape.create' ], 2000, (context) => {}, true); 18 | 19 | this.preExecute((context) => {}, true); 20 | 21 | this.preExecute([ 'shape.create' ], (context) => {}, true); 22 | 23 | this.preExecute([ 'shape.create' ], 2000, (context) => {}, true); 24 | 25 | this.preExecuted((context) => {}, true); 26 | 27 | this.preExecuted([ 'shape.create' ], (context) => {}, true); 28 | 29 | this.preExecuted([ 'shape.create' ], 2000, (context) => {}, true); 30 | 31 | this.execute((context) => {}, true); 32 | 33 | this.execute([ 'shape.create' ], (context) => {}, true); 34 | 35 | this.execute([ 'shape.create' ], (context) => [], true); 36 | 37 | this.execute([ 'shape.create' ], 2000, (context) => {}, true); 38 | 39 | this.executed((context) => {}, true); 40 | 41 | this.executed([ 'shape.create' ], (context) => {}, true); 42 | 43 | this.executed([ 'shape.create' ], (context) => [], true); 44 | 45 | this.executed([ 'shape.create' ], 2000, (context) => {}, true); 46 | 47 | this.postExecute((context) => {}, true); 48 | 49 | this.postExecute([ 'shape.create' ], (context) => {}, true); 50 | 51 | this.postExecute([ 'shape.create' ], 2000, (context) => {}, true); 52 | 53 | this.postExecuted((context) => {}, true); 54 | 55 | this.postExecuted([ 'shape.create' ], (context) => { 56 | const { shape } = context; 57 | 58 | modeling.moveShape(shape, { x: 100, y: 100 }); 59 | }, true); 60 | 61 | this.postExecuted([ 'shape.create' ], 2000, (context) => {}, true); 62 | 63 | this.revert((context) => {}, true); 64 | 65 | this.revert([ 'shape.create' ], (context) => {}, true); 66 | 67 | this.revert([ 'shape.create' ], (context) => [], true); 68 | 69 | this.revert([ 'shape.create' ], 2000, (context) => {}, true); 70 | 71 | this.reverted((context) => {}, true); 72 | 73 | this.reverted([ 'shape.create' ], (context) => {}, true); 74 | 75 | this.reverted([ 'shape.create' ], (context) => [], true); 76 | 77 | this.reverted([ 'shape.create' ], 2000, (context) => {}, true); 78 | } 79 | } -------------------------------------------------------------------------------- /lib/command/CommandStack.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../Diagram'; 2 | 3 | import CommandModule from '.'; 4 | import CommandStack from './CommandStack'; 5 | 6 | import { AddShapeHandler } from './CommandHandler.spec'; 7 | 8 | const diagram = new Diagram({ 9 | modules: [ 10 | CommandModule 11 | ] 12 | }); 13 | 14 | const commandStack = diagram.get('commandStack'); 15 | 16 | commandStack.registerHandler('shape.add', AddShapeHandler); 17 | 18 | commandStack.canExecute('shape.add', { foo: 'bar' }); 19 | 20 | commandStack.canUndo(); 21 | 22 | commandStack.canRedo(); 23 | 24 | commandStack.clear(); 25 | 26 | commandStack.execute('shape.add', { foo: 'bar' }); 27 | 28 | commandStack.redo(); 29 | 30 | commandStack.undo(); -------------------------------------------------------------------------------- /lib/command/index.js: -------------------------------------------------------------------------------- 1 | import CommandStack from './CommandStack'; 2 | 3 | /** 4 | * @type { import('didi').ModuleDeclaration } 5 | */ 6 | export default { 7 | commandStack: [ 'type', CommandStack ] 8 | }; 9 | -------------------------------------------------------------------------------- /lib/core/ElementFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../Diagram'; 2 | 3 | import ElementFactory from './ElementFactory'; 4 | 5 | import { 6 | Connection, 7 | Label, 8 | Root, 9 | Shape 10 | } from '../model/Types'; 11 | 12 | const diagram = new Diagram(); 13 | 14 | const elementFactory = diagram.get('elementFactory'); 15 | 16 | const shape1 = elementFactory.create('shape', { 17 | id: 'shape1', 18 | x: 100, 19 | y: 100, 20 | width: 100, 21 | height: 100, 22 | foo: 'bar' 23 | }); 24 | 25 | const shape2 = elementFactory.create('shape', { 26 | id: 'shape2', 27 | x: 100, 28 | y: 100, 29 | width: 100, 30 | height: 100, 31 | foo: 'bar' 32 | }); 33 | 34 | elementFactory.create('connection', { 35 | id: 'connection', 36 | source: shape1, 37 | target: shape2, 38 | waypoints: [], 39 | foo: 'bar' 40 | }); 41 | 42 | elementFactory.create('root', { 43 | id: 'root', 44 | foo: 'bar' 45 | }); 46 | 47 | elementFactory.create('label', { 48 | id: 'label', 49 | foo: 'bar' 50 | }); 51 | 52 | elementFactory.create('connection'); 53 | 54 | elementFactory.create('label'); 55 | 56 | elementFactory.create('root'); 57 | 58 | elementFactory.create('shape'); 59 | 60 | elementFactory.createConnection(); 61 | 62 | elementFactory.createLabel(); 63 | 64 | elementFactory.createRoot(); 65 | 66 | elementFactory.createShape(); 67 | 68 | /** 69 | * Customization 70 | */ 71 | 72 | type CustomShape = { 73 | foo: string; 74 | } & Shape; 75 | 76 | class CustomElementFactory extends ElementFactory {} 77 | 78 | const customElementFactory = diagram.get('elementFactory'); 79 | 80 | const customShape = customElementFactory.createShape({ foo: 'bar' }); 81 | 82 | console.log(customShape.foo); 83 | -------------------------------------------------------------------------------- /lib/core/ElementRegistry.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../Diagram'; 2 | 3 | import ElementFactory from './ElementFactory'; 4 | import ElementRegistry from './ElementRegistry'; 5 | import GraphicsFactory from './GraphicsFactory'; 6 | 7 | const diagram = new Diagram(); 8 | 9 | const elementFactory = diagram.get('elementFactory'), 10 | elementRegistry = diagram.get('elementRegistry'), 11 | graphicsFactory = diagram.get('graphicsFactory'); 12 | 13 | const shapeLike = { id: 'shape' }; 14 | 15 | const shape = elementFactory.createShape(), 16 | shapeGfx1 = graphicsFactory.create('shape', shape), 17 | shapeGfx2 = graphicsFactory.create('shape', shape); 18 | 19 | elementRegistry.add(shapeLike, shapeGfx1); 20 | 21 | elementRegistry.add(shape, shapeGfx1); 22 | 23 | elementRegistry.add(shape, shapeGfx1, shapeGfx2); 24 | 25 | elementRegistry.remove('shape'); 26 | 27 | elementRegistry.remove(shapeLike); 28 | 29 | elementRegistry.remove(shape); 30 | 31 | elementRegistry.find((element, gfx) => { 32 | console.log(element, gfx); 33 | 34 | return true; 35 | }); 36 | 37 | elementRegistry.filter((element, gfx) => { 38 | console.log(element, gfx); 39 | 40 | return true; 41 | }); 42 | 43 | 44 | elementRegistry.filter((element, gfx) => { 45 | console.log(element, gfx); 46 | }); 47 | 48 | elementRegistry.forEach((element, gfx) => console.log(element, gfx)); 49 | 50 | elementRegistry.get('shape'); 51 | 52 | elementRegistry.getAll(); 53 | 54 | elementRegistry.getGraphics('shape'); 55 | 56 | elementRegistry.getGraphics(shapeLike); 57 | 58 | elementRegistry.getGraphics(shape); 59 | 60 | elementRegistry.updateGraphics('shape', shapeGfx1); 61 | 62 | elementRegistry.updateGraphics(shapeLike, shapeGfx1); 63 | 64 | elementRegistry.updateGraphics(shape, shapeGfx1); 65 | 66 | elementRegistry.updateGraphics(shape, shapeGfx2, true); 67 | 68 | elementRegistry.updateId(shapeLike, 'foo'); 69 | 70 | elementRegistry.updateId(shape, 'foo'); -------------------------------------------------------------------------------- /lib/core/EventBus.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../Diagram'; 2 | 3 | import EventBus from './EventBus'; 4 | 5 | const diagram = new Diagram(); 6 | 7 | const eventBus = diagram.get('eventBus'); 8 | 9 | const event = eventBus.createEvent({ foo: 'bar' }); 10 | 11 | eventBus.fire('foo'); 12 | 13 | eventBus.fire(event); 14 | 15 | eventBus.fire(event, 'foo', 'bar'); 16 | 17 | eventBus.fire({ foo: 'bar' }); 18 | 19 | eventBus.fire({ foo: 'bar' }, 'foo', 'bar'); 20 | 21 | eventBus.handleError(new Error()); 22 | 23 | const callback = () => {}; 24 | 25 | eventBus.off('foo', callback); 26 | 27 | eventBus.off([ 'foo', 'bar' ], callback); 28 | 29 | eventBus.on('foo', callback); 30 | 31 | eventBus.on([ 'foo', 'bar' ], callback); 32 | 33 | eventBus.on('foo', 2000, callback); 34 | 35 | eventBus.on('foo', callback, this); 36 | 37 | eventBus.on('foo', (event, additional1, additional2) => { 38 | console.log('foo', additional1, additional2); 39 | }); 40 | 41 | type FooEvent = { 42 | foo: string; 43 | }; 44 | 45 | eventBus.on('foo', (event) => { 46 | const { foo } = event; 47 | 48 | console.log(foo); 49 | }); 50 | 51 | eventBus.once('foo', callback); 52 | 53 | eventBus.once('foo', callback); 54 | 55 | eventBus.once('foo', 2000, callback); 56 | 57 | eventBus.once('foo', callback, this); 58 | 59 | eventBus.once('foo', (event, additional1, additional2) => { 60 | console.log('foo', event, additional1, additional2); 61 | }); 62 | 63 | 64 | type EventMap = { 65 | foo: FooEvent 66 | }; 67 | 68 | const typedEventBus = new EventBus(); 69 | 70 | typedEventBus.on('foo', (event: FooEvent) => { 71 | const { foo } = event; 72 | 73 | console.log(foo); 74 | }); 75 | 76 | typedEventBus.on('foo', (event) => { 77 | const { foo } = event; 78 | 79 | console.log(foo); 80 | }); -------------------------------------------------------------------------------- /lib/core/GraphicsFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../Diagram'; 2 | 3 | import ElementFactory from './ElementFactory'; 4 | import GraphicsFactory from './GraphicsFactory'; 5 | 6 | const diagram = new Diagram(); 7 | 8 | const elementFactory = diagram.get('elementFactory'), 9 | graphicsFactory = diagram.get('graphicsFactory'); 10 | 11 | const shape = elementFactory.createShape(); 12 | 13 | const shapeGfx = graphicsFactory.create('shape', shape); 14 | 15 | graphicsFactory.drawShape(shapeGfx, shape); 16 | 17 | graphicsFactory.getShapePath(shape); 18 | 19 | graphicsFactory.remove(shape); 20 | 21 | graphicsFactory.update('shape', shape, shapeGfx); 22 | 23 | graphicsFactory.updateContainments([ shape ]); 24 | 25 | const connection = elementFactory.createConnection(); 26 | 27 | const connectionGfx = graphicsFactory.create('connection', connection); 28 | 29 | graphicsFactory.drawConnection(connectionGfx, connection); 30 | 31 | graphicsFactory.getConnectionPath(connection); 32 | 33 | graphicsFactory.remove(connection); 34 | 35 | const shapeLike = { 36 | id: 'shape', 37 | x: 100, 38 | y: 100, 39 | width: 100, 40 | height: 100 41 | }; 42 | 43 | const shapeLikeGfx = graphicsFactory.create('shape', shapeLike); 44 | 45 | graphicsFactory.drawShape(shapeLikeGfx, shape); 46 | 47 | graphicsFactory.getConnectionPath(connection); 48 | 49 | graphicsFactory.remove(shapeLike); 50 | 51 | const connectionLike = { 52 | id: 'connection', 53 | waypoints: [ 54 | { 55 | x: 100, 56 | y: 100 57 | }, 58 | { 59 | x: 200, 60 | y: 100 61 | } 62 | ] 63 | }; 64 | 65 | graphicsFactory.create('connection', connectionLike); 66 | 67 | const connectionLikeGfx = graphicsFactory.create('connection', connectionLike); 68 | 69 | graphicsFactory.drawConnection(connectionLikeGfx, connectionLike); 70 | 71 | graphicsFactory.getConnectionPath(connectionLike); 72 | 73 | graphicsFactory.remove(connectionLike); -------------------------------------------------------------------------------- /lib/core/Types.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | ElementLike, 3 | ShapeLike, 4 | ParentLike, 5 | RootLike, 6 | ConnectionLike, 7 | LabelLike 8 | } from '../model/Types'; -------------------------------------------------------------------------------- /lib/core/index.js: -------------------------------------------------------------------------------- 1 | import DrawModule from '../draw'; 2 | 3 | import Canvas from './Canvas'; 4 | import ElementRegistry from './ElementRegistry'; 5 | import ElementFactory from './ElementFactory'; 6 | import EventBus from './EventBus'; 7 | import GraphicsFactory from './GraphicsFactory'; 8 | 9 | /** 10 | * @type { import('didi').ModuleDeclaration } 11 | */ 12 | export default { 13 | __depends__: [ DrawModule ], 14 | __init__: [ 'canvas' ], 15 | canvas: [ 'type', Canvas ], 16 | elementRegistry: [ 'type', ElementRegistry ], 17 | elementFactory: [ 'type', ElementFactory ], 18 | eventBus: [ 'type', EventBus ], 19 | graphicsFactory: [ 'type', GraphicsFactory ] 20 | }; -------------------------------------------------------------------------------- /lib/draw/BaseRenderer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Element, Connection, Shape } from '../model'; 2 | 3 | import Diagram from '../Diagram'; 4 | 5 | import ElementFactory from '../core/ElementFactory'; 6 | import GraphicsFactory from '../core/GraphicsFactory'; 7 | 8 | import BaseRenderer from './BaseRenderer'; 9 | 10 | class CustomRenderer extends BaseRenderer { 11 | canRender(element: Element): boolean { 12 | return true; 13 | } 14 | 15 | drawShape(visuals: SVGElement, shape: Shape): SVGElement { 16 | return document.createElementNS('http://www.w3.org/2000/svg', 'rect'); 17 | } 18 | 19 | drawConnection(visuals: SVGElement, connection: Connection): SVGElement { 20 | return document.createElementNS('http://www.w3.org/2000/svg', 'rect'); 21 | } 22 | 23 | getShapePath(shape: Shape): string { 24 | return 'M150 0 L75 200 L225 200 Z'; 25 | } 26 | 27 | getConnectionPath(connection: Connection): string { 28 | return 'M150 0 L75 200 L225 200 Z'; 29 | } 30 | } 31 | 32 | const diagram = new Diagram({ 33 | modules: [ 34 | { 35 | __init__: [ 'customRenderer' ], 36 | customRenderer: [ 'type', CustomRenderer ] 37 | } 38 | ] 39 | }); 40 | 41 | const elementFactory = diagram.get('elementFactory'), 42 | graphicsFactory = diagram.get('graphicsFactory'); 43 | 44 | const shape = elementFactory.createShape(), 45 | connection = elementFactory.createConnection(); 46 | 47 | const shapeGfx = graphicsFactory.create('shape', shape), 48 | connectionGfx = graphicsFactory.create('connection', connection); 49 | 50 | const customRenderer = diagram.get('customRenderer'); 51 | 52 | customRenderer.canRender(shape); 53 | 54 | customRenderer.canRender(connection); 55 | 56 | customRenderer.drawShape(shapeGfx, shape); 57 | 58 | customRenderer.drawConnection(connectionGfx, connection); 59 | 60 | customRenderer.getShapePath(shape); 61 | 62 | customRenderer.getConnectionPath(connection); -------------------------------------------------------------------------------- /lib/draw/DefaultRenderer.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../Diagram'; 2 | 3 | import DefaultRenderer from './DefaultRenderer'; 4 | 5 | import ElementFactory from '../core/ElementFactory'; 6 | import GraphicsFactory from '../core/GraphicsFactory'; 7 | 8 | const diagram = new Diagram(); 9 | 10 | const defaultRenderer = diagram.get('defaultRenderer'); 11 | 12 | const elementFactory = diagram.get('elementFactory'), 13 | graphicsFactory = diagram.get('graphicsFactory'); 14 | 15 | const shape = elementFactory.createShape(), 16 | shapeGfx = graphicsFactory.create('shape', shape); 17 | 18 | const connection = elementFactory.createConnection(), 19 | connectionGfx = graphicsFactory.create('connection', connection); 20 | 21 | defaultRenderer.canRender(shape); 22 | 23 | defaultRenderer.canRender(connection); 24 | 25 | defaultRenderer.drawShape(shapeGfx, shape); 26 | 27 | defaultRenderer.drawConnection(connectionGfx, connection); 28 | 29 | defaultRenderer.getShapePath(shape); 30 | 31 | defaultRenderer.getConnectionPath(connection); 32 | -------------------------------------------------------------------------------- /lib/draw/Styles.js: -------------------------------------------------------------------------------- 1 | import { 2 | isArray, 3 | assign, 4 | reduce 5 | } from 'min-dash'; 6 | 7 | 8 | /** 9 | * A component that manages shape styles 10 | */ 11 | export default function Styles() { 12 | 13 | var defaultTraits = { 14 | 15 | 'no-fill': { 16 | fill: 'none' 17 | }, 18 | 'no-border': { 19 | strokeOpacity: 0.0 20 | }, 21 | 'no-events': { 22 | pointerEvents: 'none' 23 | } 24 | }; 25 | 26 | var self = this; 27 | 28 | /** 29 | * Builds a style definition from a className, a list of traits and an object 30 | * of additional attributes. 31 | * 32 | * @param {string} className 33 | * @param {string[]} [traits] 34 | * @param {Object} [additionalAttrs] 35 | * 36 | * @return {Object} the style definition 37 | */ 38 | this.cls = function(className, traits, additionalAttrs) { 39 | var attrs = this.style(traits, additionalAttrs); 40 | 41 | return assign(attrs, { 'class': className }); 42 | }; 43 | 44 | /** 45 | * Builds a style definition from a list of traits and an object of additional 46 | * attributes. 47 | * 48 | * @param {string[]} [traits] 49 | * @param {Object} additionalAttrs 50 | * 51 | * @return {Object} the style definition 52 | */ 53 | this.style = function(traits, additionalAttrs) { 54 | 55 | if (!isArray(traits) && !additionalAttrs) { 56 | additionalAttrs = traits; 57 | traits = []; 58 | } 59 | 60 | var attrs = reduce(traits, function(attrs, t) { 61 | return assign(attrs, defaultTraits[t] || {}); 62 | }, {}); 63 | 64 | return additionalAttrs ? assign(attrs, additionalAttrs) : attrs; 65 | }; 66 | 67 | 68 | /** 69 | * Computes a style definition from a list of traits and an object of 70 | * additional attributes, with custom style definition object. 71 | * 72 | * @param {Object} custom 73 | * @param {string[]} [traits] 74 | * @param {Object} defaultStyles 75 | * 76 | * @return {Object} the style definition 77 | */ 78 | this.computeStyle = function(custom, traits, defaultStyles) { 79 | if (!isArray(traits)) { 80 | defaultStyles = traits; 81 | traits = []; 82 | } 83 | 84 | return self.style(traits || [], assign({}, defaultStyles, custom || {})); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /lib/draw/index.js: -------------------------------------------------------------------------------- 1 | import DefaultRenderer from './DefaultRenderer'; 2 | import Styles from './Styles'; 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'defaultRenderer' ], 9 | defaultRenderer: [ 'type', DefaultRenderer ], 10 | styles: [ 'type', Styles ] 11 | }; 12 | -------------------------------------------------------------------------------- /lib/features/align-elements/index.js: -------------------------------------------------------------------------------- 1 | import AlignElements from './AlignElements'; 2 | 3 | /** 4 | * @type { import('didi').ModuleDeclaration } 5 | */ 6 | export default { 7 | __init__: [ 'alignElements' ], 8 | alignElements: [ 'type', AlignElements ] 9 | }; 10 | -------------------------------------------------------------------------------- /lib/features/attach-support/index.js: -------------------------------------------------------------------------------- 1 | import RulesModule from '../rules'; 2 | 3 | import AttachSupport from './AttachSupport'; 4 | 5 | /** 6 | * @type { import('didi').ModuleDeclaration } 7 | */ 8 | export default { 9 | __depends__: [ 10 | RulesModule 11 | ], 12 | __init__: [ 'attachSupport' ], 13 | attachSupport: [ 'type', AttachSupport ] 14 | }; 15 | -------------------------------------------------------------------------------- /lib/features/auto-place/AutoPlaceSelectionBehavior.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../../core/EventBus').default} EventBus 3 | * @typedef {import('../selection/Selection').default} Selection 4 | */ 5 | 6 | /** 7 | * Select element after auto placement. 8 | * 9 | * @param {EventBus} eventBus 10 | * @param {Selection} selection 11 | */ 12 | export default function AutoPlaceSelectionBehavior(eventBus, selection) { 13 | 14 | eventBus.on('autoPlace.end', 500, function(e) { 15 | selection.select(e.shape); 16 | }); 17 | 18 | } 19 | 20 | AutoPlaceSelectionBehavior.$inject = [ 21 | 'eventBus', 22 | 'selection' 23 | ]; -------------------------------------------------------------------------------- /lib/features/auto-place/AutoPlaceUtil.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import ElementFactory from '../../core/ElementFactory'; 4 | 5 | import { 6 | findFreePosition, 7 | generateGetNextPosition, 8 | getConnectedDistance 9 | } from './AutoPlaceUtil'; 10 | 11 | const diagram = new Diagram(); 12 | 13 | const elementFactory = diagram.get('elementFactory'); 14 | 15 | const source = elementFactory.createShape(), 16 | element = elementFactory.createShape(); 17 | 18 | const getNextPosition = generateGetNextPosition({ x: 100, y: 100 }); 19 | 20 | findFreePosition(source, element, { x: 100, y: 100 }, getNextPosition); 21 | 22 | getConnectedDistance(source, { 23 | defaultDistance: 100, 24 | direction: 'right', 25 | filter: (connection) => true, 26 | getWeight: (connection) => 1, 27 | maxDistance: 100, 28 | reference: 'center' 29 | }); -------------------------------------------------------------------------------- /lib/features/auto-place/index.js: -------------------------------------------------------------------------------- 1 | import AutoPlace from './AutoPlace'; 2 | import AutoPlaceSelectionBehavior from './AutoPlaceSelectionBehavior'; 3 | 4 | 5 | /** 6 | * @type { import('didi').ModuleDeclaration } 7 | */ 8 | export default { 9 | __init__: [ 'autoPlaceSelectionBehavior' ], 10 | autoPlace: [ 'type', AutoPlace ], 11 | autoPlaceSelectionBehavior: [ 'type', AutoPlaceSelectionBehavior ] 12 | }; -------------------------------------------------------------------------------- /lib/features/auto-resize/AutoResizeProvider.js: -------------------------------------------------------------------------------- 1 | import RuleProvider from '../rules/RuleProvider'; 2 | 3 | import inherits from 'inherits-browser'; 4 | 5 | /** 6 | * @typedef {import('../../model/Types').Shape} Shape 7 | * 8 | * @typedef {import('../../core/EventBus').default} EventBus 9 | */ 10 | 11 | /** 12 | * This is a base rule provider for the element.autoResize rule. 13 | * 14 | * @param {EventBus} eventBus 15 | */ 16 | export default function AutoResizeProvider(eventBus) { 17 | 18 | RuleProvider.call(this, eventBus); 19 | 20 | var self = this; 21 | 22 | this.addRule('element.autoResize', function(context) { 23 | return self.canResize(context.elements, context.target); 24 | }); 25 | } 26 | 27 | AutoResizeProvider.$inject = [ 'eventBus' ]; 28 | 29 | inherits(AutoResizeProvider, RuleProvider); 30 | 31 | /** 32 | * Needs to be implemented by sub classes to allow actual auto resize 33 | * 34 | * @param {Shape[]} elements 35 | * @param {Shape} target 36 | * 37 | * @return {boolean} 38 | */ 39 | AutoResizeProvider.prototype.canResize = function(elements, target) { 40 | return false; 41 | }; -------------------------------------------------------------------------------- /lib/features/auto-resize/index.js: -------------------------------------------------------------------------------- 1 | import AutoResize from './AutoResize'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'autoResize' ], 9 | autoResize: [ 'type', AutoResize ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/auto-scroll/index.js: -------------------------------------------------------------------------------- 1 | import DraggingModule from '../dragging'; 2 | 3 | import AutoScroll from './AutoScroll'; 4 | 5 | 6 | /** 7 | * @type { import('didi').ModuleDeclaration } 8 | */ 9 | export default { 10 | __depends__: [ 11 | DraggingModule, 12 | ], 13 | __init__: [ 'autoScroll' ], 14 | autoScroll: [ 'type', AutoScroll ] 15 | }; -------------------------------------------------------------------------------- /lib/features/bendpoints/index.js: -------------------------------------------------------------------------------- 1 | import DraggingModule from '../dragging'; 2 | import RulesModule from '../rules'; 3 | 4 | import Bendpoints from './Bendpoints'; 5 | import BendpointMove from './BendpointMove'; 6 | import BendpointMovePreview from './BendpointMovePreview'; 7 | import ConnectionSegmentMove from './ConnectionSegmentMove'; 8 | import BendpointSnapping from './BendpointSnapping'; 9 | 10 | 11 | /** 12 | * @type { import('didi').ModuleDeclaration } 13 | */ 14 | export default { 15 | __depends__: [ 16 | DraggingModule, 17 | RulesModule 18 | ], 19 | __init__: [ 'bendpoints', 'bendpointSnapping', 'bendpointMovePreview' ], 20 | bendpoints: [ 'type', Bendpoints ], 21 | bendpointMove: [ 'type', BendpointMove ], 22 | bendpointMovePreview: [ 'type', BendpointMovePreview ], 23 | connectionSegmentMove: [ 'type', ConnectionSegmentMove ], 24 | bendpointSnapping: [ 'type', BendpointSnapping ] 25 | }; 26 | -------------------------------------------------------------------------------- /lib/features/change-support/ChangeSupport.js: -------------------------------------------------------------------------------- 1 | import { 2 | getType as getElementType 3 | } from '../../util/Elements'; 4 | 5 | /** 6 | * @typedef {import('../../core/Canvas').default} Canvas 7 | * @typedef {import('../../core/ElementRegistry').default} ElementRegistry 8 | * @typedef {import('../../core/EventBus').default} EventBus 9 | * @typedef {import('../../core/GraphicsFactory').default} GraphicsFactory 10 | */ 11 | 12 | /** 13 | * Adds change support to the diagram, including 14 | * 15 | *
    16 | *
  • redrawing shapes and connections on change
  • 17 | *
18 | * 19 | * @param {EventBus} eventBus 20 | * @param {Canvas} canvas 21 | * @param {ElementRegistry} elementRegistry 22 | * @param {GraphicsFactory} graphicsFactory 23 | */ 24 | export default function ChangeSupport( 25 | eventBus, canvas, elementRegistry, 26 | graphicsFactory) { 27 | 28 | 29 | // redraw shapes / connections on change 30 | 31 | eventBus.on('element.changed', function(event) { 32 | 33 | var element = event.element; 34 | 35 | // element might have been deleted and replaced by new element with same ID 36 | // thus check for parent of element except for root element 37 | if (element.parent || element === canvas.getRootElement()) { 38 | event.gfx = elementRegistry.getGraphics(element); 39 | } 40 | 41 | // shape + gfx may have been deleted 42 | if (!event.gfx) { 43 | return; 44 | } 45 | 46 | eventBus.fire(getElementType(element) + '.changed', event); 47 | }); 48 | 49 | eventBus.on('elements.changed', function(event) { 50 | 51 | var elements = event.elements; 52 | 53 | elements.forEach(function(e) { 54 | eventBus.fire('element.changed', { element: e }); 55 | }); 56 | 57 | graphicsFactory.updateContainments(elements); 58 | }); 59 | 60 | eventBus.on('shape.changed', function(event) { 61 | graphicsFactory.update('shape', event.element, event.gfx); 62 | }); 63 | 64 | eventBus.on('connection.changed', function(event) { 65 | graphicsFactory.update('connection', event.element, event.gfx); 66 | }); 67 | } 68 | 69 | ChangeSupport.$inject = [ 70 | 'eventBus', 71 | 'canvas', 72 | 'elementRegistry', 73 | 'graphicsFactory' 74 | ]; -------------------------------------------------------------------------------- /lib/features/change-support/index.js: -------------------------------------------------------------------------------- 1 | import ChangeSupport from './ChangeSupport'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'changeSupport' ], 9 | changeSupport: [ 'type', ChangeSupport ] 10 | }; -------------------------------------------------------------------------------- /lib/features/clipboard/Clipboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A clip board stub 3 | */ 4 | export default function Clipboard() {} 5 | 6 | 7 | Clipboard.prototype.get = function() { 8 | return this._data; 9 | }; 10 | 11 | Clipboard.prototype.set = function(data) { 12 | this._data = data; 13 | }; 14 | 15 | Clipboard.prototype.clear = function() { 16 | var data = this._data; 17 | 18 | delete this._data; 19 | 20 | return data; 21 | }; 22 | 23 | Clipboard.prototype.isEmpty = function() { 24 | return !this._data; 25 | }; -------------------------------------------------------------------------------- /lib/features/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './Clipboard'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | clipboard: [ 'type', Clipboard ] 9 | }; 10 | -------------------------------------------------------------------------------- /lib/features/complex-preview/index.js: -------------------------------------------------------------------------------- 1 | import PreviewSupportModule from '../preview-support'; 2 | 3 | import ComplexPreview from './ComplexPreview'; 4 | 5 | /** 6 | * @type { import('didi').ModuleDeclaration } 7 | */ 8 | export default { 9 | __depends__: [ PreviewSupportModule ], 10 | __init__: [ 'complexPreview' ], 11 | complexPreview: [ 'type', ComplexPreview ] 12 | }; -------------------------------------------------------------------------------- /lib/features/connect/index.js: -------------------------------------------------------------------------------- 1 | import SelectionModule from '../selection'; 2 | import RulesModule from '../rules'; 3 | import DraggingModule from '../dragging'; 4 | 5 | import Connect from './Connect'; 6 | import ConnectPreview from './ConnectPreview'; 7 | 8 | 9 | /** 10 | * @type { import('didi').ModuleDeclaration } 11 | */ 12 | export default { 13 | __depends__: [ 14 | SelectionModule, 15 | RulesModule, 16 | DraggingModule 17 | ], 18 | __init__: [ 19 | 'connectPreview' 20 | ], 21 | connect: [ 'type', Connect ], 22 | connectPreview: [ 'type', ConnectPreview ] 23 | }; 24 | -------------------------------------------------------------------------------- /lib/features/connection-preview/index.js: -------------------------------------------------------------------------------- 1 | import ConnectionPreview from './ConnectionPreview'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'connectionPreview' ], 9 | connectionPreview: [ 'type', ConnectionPreview ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/context-pad/ContextPad.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import ElementFactory from '../../core/ElementFactory'; 4 | 5 | import ContextPadModule from '.'; 6 | import ContextPad from './ContextPad'; 7 | 8 | import { FooContextPadProvider } from './ContextPadProvider.spec'; 9 | 10 | const diagram = new Diagram({ 11 | modules: [ 12 | ContextPadModule 13 | ] 14 | }); 15 | 16 | const elementFactory = diagram.get('elementFactory'); 17 | 18 | const shape = elementFactory.createShape(); 19 | 20 | const contextPad = diagram.get('contextPad'); 21 | 22 | contextPad.registerProvider(new FooContextPadProvider()); 23 | 24 | contextPad.registerProvider(1000, new FooContextPadProvider()); 25 | 26 | contextPad.open(shape); 27 | 28 | contextPad.open([ shape ]); 29 | 30 | contextPad.getEntries(shape); 31 | 32 | const entries = contextPad.getEntries([ shape ]); 33 | 34 | for (const key in entries) { 35 | const entry = entries[ key ]; 36 | 37 | console.log(entry.action, entry.title); 38 | } 39 | 40 | contextPad.trigger('foo', new Event('click')); 41 | 42 | contextPad.trigger('foo', new Event('click'), true); 43 | 44 | contextPad.triggerEntry('foo', 'bar', new Event('click')); 45 | 46 | contextPad.triggerEntry('foo', 'bar', new Event('click'), true); 47 | 48 | contextPad.getPad(shape); 49 | 50 | contextPad.getPad([ shape ]); 51 | 52 | contextPad.close(); 53 | 54 | contextPad.isOpen(); 55 | 56 | contextPad.isShown(); -------------------------------------------------------------------------------- /lib/features/context-pad/index.js: -------------------------------------------------------------------------------- 1 | import InteractionEventsModule from '../interaction-events'; 2 | import OverlaysModule from '../overlays'; 3 | import SchedulerModule from '../scheduler'; 4 | 5 | import ContextPad from './ContextPad'; 6 | 7 | 8 | /** 9 | * @type { import('didi').ModuleDeclaration } 10 | */ 11 | export default { 12 | __depends__: [ 13 | InteractionEventsModule, 14 | SchedulerModule, 15 | OverlaysModule 16 | ], 17 | contextPad: [ 'type', ContextPad ] 18 | }; -------------------------------------------------------------------------------- /lib/features/copy-paste/index.js: -------------------------------------------------------------------------------- 1 | import ClipboardModule from '../clipboard'; 2 | import CreateModule from '../create'; 3 | import MouseModule from '../mouse'; 4 | import RulesModule from '../rules'; 5 | 6 | import CopyPaste from './CopyPaste'; 7 | 8 | 9 | /** 10 | * @type { import('didi').ModuleDeclaration } 11 | */ 12 | export default { 13 | __depends__: [ 14 | ClipboardModule, 15 | CreateModule, 16 | MouseModule, 17 | RulesModule 18 | ], 19 | __init__: [ 'copyPaste' ], 20 | copyPaste: [ 'type', CopyPaste ] 21 | }; 22 | -------------------------------------------------------------------------------- /lib/features/create/CreateConnectPreview.js: -------------------------------------------------------------------------------- 1 | var LOW_PRIORITY = 740; 2 | 3 | /** 4 | * @typedef {import('didi').Injector} Injector 5 | * 6 | * @typedef {import('../../core/EventBus').default} EventBus 7 | */ 8 | 9 | /** 10 | * Shows connection preview during create. 11 | * 12 | * @param {Injector} injector 13 | * @param {EventBus} eventBus 14 | */ 15 | export default function CreateConnectPreview(injector, eventBus) { 16 | var connectionPreview = injector.get('connectionPreview', false); 17 | 18 | eventBus.on('create.move', LOW_PRIORITY, function(event) { 19 | var context = event.context, 20 | source = context.source, 21 | shape = context.shape, 22 | canExecute = context.canExecute, 23 | canConnect = canExecute && canExecute.connect; 24 | 25 | // don't draw connection preview if not appending a shape 26 | if (!connectionPreview || !source) { 27 | return; 28 | } 29 | 30 | // place shape's center on cursor 31 | shape.x = Math.round(event.x - shape.width / 2); 32 | shape.y = Math.round(event.y - shape.height / 2); 33 | 34 | connectionPreview.drawPreview(context, canConnect, { 35 | source: source, 36 | target: shape, 37 | waypoints: [], 38 | noNoop: true 39 | }); 40 | }); 41 | 42 | 43 | eventBus.on('create.cleanup', function(event) { 44 | if (connectionPreview) { 45 | connectionPreview.cleanUp(event.context); 46 | } 47 | }); 48 | 49 | } 50 | 51 | CreateConnectPreview.$inject = [ 52 | 'injector', 53 | 'eventBus' 54 | ]; 55 | -------------------------------------------------------------------------------- /lib/features/create/index.js: -------------------------------------------------------------------------------- 1 | import DraggingModule from '../dragging'; 2 | import PreviewSupportModule from '../preview-support'; 3 | import RulesModule from '../rules'; 4 | import SelectionModule from '../selection'; 5 | 6 | import Create from './Create'; 7 | import CreatePreview from './CreatePreview'; 8 | 9 | 10 | /** 11 | * @type { import('didi').ModuleDeclaration } 12 | */ 13 | export default { 14 | __depends__: [ 15 | DraggingModule, 16 | PreviewSupportModule, 17 | RulesModule, 18 | SelectionModule 19 | ], 20 | __init__: [ 21 | 'create', 22 | 'createPreview' 23 | ], 24 | create: [ 'type', Create ], 25 | createPreview: [ 'type', CreatePreview ] 26 | }; 27 | -------------------------------------------------------------------------------- /lib/features/distribute-elements/index.js: -------------------------------------------------------------------------------- 1 | import DistributeElements from './DistributeElements'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'distributeElements' ], 9 | distributeElements: [ 'type', DistributeElements ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/dragging/index.js: -------------------------------------------------------------------------------- 1 | import HoverFixModule from '../hover-fix'; 2 | import SelectionModule from '../selection'; 3 | 4 | import Dragging from './Dragging'; 5 | 6 | 7 | /** 8 | * @type { import('didi').ModuleDeclaration } 9 | */ 10 | export default { 11 | __depends__: [ 12 | HoverFixModule, 13 | SelectionModule, 14 | ], 15 | dragging: [ 'type', Dragging ], 16 | }; -------------------------------------------------------------------------------- /lib/features/editor-actions/index.js: -------------------------------------------------------------------------------- 1 | import EditorActions from './EditorActions'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'editorActions' ], 9 | editorActions: [ 'type', EditorActions ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/global-connect/index.js: -------------------------------------------------------------------------------- 1 | import ConnectModule from '../connect'; 2 | import RulesModule from '../rules'; 3 | import DraggingModule from '../dragging'; 4 | import ToolManagerModule from '../tool-manager'; 5 | import MouseModule from '../mouse'; 6 | 7 | import GlobalConnect from './GlobalConnect'; 8 | 9 | 10 | /** 11 | * @type { import('didi').ModuleDeclaration } 12 | */ 13 | export default { 14 | __depends__: [ 15 | ConnectModule, 16 | RulesModule, 17 | DraggingModule, 18 | ToolManagerModule, 19 | MouseModule 20 | ], 21 | globalConnect: [ 'type', GlobalConnect ] 22 | }; 23 | -------------------------------------------------------------------------------- /lib/features/grid-snapping/GridUtil.js: -------------------------------------------------------------------------------- 1 | export var SPACING = 10; 2 | 3 | export function quantize(value, quantum, fn) { 4 | if (!fn) { 5 | fn = 'round'; 6 | } 7 | 8 | return Math[ fn ](value / quantum) * quantum; 9 | } -------------------------------------------------------------------------------- /lib/features/grid-snapping/behavior/SpaceToolBehavior.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../../../core/EventBus').default} EventBus 3 | * @typedef {import('../../grid-snapping/GridSnapping').default} GridSnapping 4 | */ 5 | 6 | var HIGH_PRIORITY = 2000; 7 | 8 | /** 9 | * Integrates space tool with grid snapping. 10 | * 11 | * @param {EventBus} eventBus 12 | * @param {GridSnapping} gridSnapping 13 | */ 14 | export default function SpaceToolBehavior(eventBus, gridSnapping) { 15 | eventBus.on([ 16 | 'spaceTool.move', 17 | 'spaceTool.end' 18 | ], HIGH_PRIORITY, function(event) { 19 | var context = event.context; 20 | 21 | if (!context.initialized) { 22 | return; 23 | } 24 | 25 | var axis = context.axis; 26 | 27 | var snapped; 28 | 29 | if (axis === 'x') { 30 | 31 | // snap delta x to multiple of 10 32 | snapped = gridSnapping.snapValue(event.dx); 33 | 34 | event.x = event.x + snapped - event.dx; 35 | event.dx = snapped; 36 | } else { 37 | 38 | // snap delta y to multiple of 10 39 | snapped = gridSnapping.snapValue(event.dy); 40 | 41 | event.y = event.y + snapped - event.dy; 42 | event.dy = snapped; 43 | } 44 | }); 45 | } 46 | 47 | SpaceToolBehavior.$inject = [ 48 | 'eventBus', 49 | 'gridSnapping' 50 | ]; -------------------------------------------------------------------------------- /lib/features/grid-snapping/behavior/index.js: -------------------------------------------------------------------------------- 1 | import ResizeBehavior from './ResizeBehavior'; 2 | import SpaceToolBehavior from './SpaceToolBehavior'; 3 | 4 | export default { 5 | __init__: [ 6 | 'gridSnappingResizeBehavior', 7 | 'gridSnappingSpaceToolBehavior' 8 | ], 9 | gridSnappingResizeBehavior: [ 'type', ResizeBehavior ], 10 | gridSnappingSpaceToolBehavior: [ 'type', SpaceToolBehavior ] 11 | }; -------------------------------------------------------------------------------- /lib/features/grid-snapping/index.js: -------------------------------------------------------------------------------- 1 | import GridSnapping from './GridSnapping'; 2 | 3 | import GridSnappingBehaviorModule from './behavior'; 4 | 5 | 6 | /** 7 | * @type { import('didi').ModuleDeclaration } 8 | */ 9 | export default { 10 | __depends__: [ GridSnappingBehaviorModule ], 11 | __init__: [ 'gridSnapping' ], 12 | gridSnapping: [ 'type', GridSnapping ] 13 | }; -------------------------------------------------------------------------------- /lib/features/hand-tool/index.js: -------------------------------------------------------------------------------- 1 | import ToolManagerModule from '../tool-manager'; 2 | import MouseModule from '../mouse'; 3 | 4 | import HandTool from './HandTool'; 5 | 6 | 7 | /** 8 | * @type { import('didi').ModuleDeclaration } 9 | */ 10 | export default { 11 | __depends__: [ 12 | ToolManagerModule, 13 | MouseModule 14 | ], 15 | __init__: [ 'handTool' ], 16 | handTool: [ 'type', HandTool ] 17 | }; 18 | -------------------------------------------------------------------------------- /lib/features/hover-fix/index.js: -------------------------------------------------------------------------------- 1 | import HoverFix from './HoverFix'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 9 | 'hoverFix' 10 | ], 11 | hoverFix: [ 'type', HoverFix ], 12 | }; -------------------------------------------------------------------------------- /lib/features/interaction-events/index.js: -------------------------------------------------------------------------------- 1 | import InteractionEvents from './InteractionEvents'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'interactionEvents' ], 9 | interactionEvents: [ 'type', InteractionEvents ] 10 | }; -------------------------------------------------------------------------------- /lib/features/keyboard-move-selection/index.js: -------------------------------------------------------------------------------- 1 | import KeyboardModule from '../keyboard'; 2 | import SelectionModule from '../selection'; 3 | 4 | import KeyboardMoveSelection from './KeyboardMoveSelection'; 5 | 6 | 7 | /** 8 | * @type { import('didi').ModuleDeclaration } 9 | */ 10 | export default { 11 | __depends__: [ 12 | KeyboardModule, 13 | SelectionModule 14 | ], 15 | __init__: [ 16 | 'keyboardMoveSelection' 17 | ], 18 | keyboardMoveSelection: [ 'type', KeyboardMoveSelection ] 19 | }; 20 | -------------------------------------------------------------------------------- /lib/features/keyboard/Keyboard.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | import Keyboard from './Keyboard'; 3 | 4 | const diagram = new Diagram(); 5 | 6 | let keyboard = diagram.get('keyboard'); 7 | 8 | keyboard.bind(); 9 | 10 | keyboard.unbind(); 11 | 12 | keyboard.bind(document); -------------------------------------------------------------------------------- /lib/features/keyboard/KeyboardUtil.js: -------------------------------------------------------------------------------- 1 | import { isArray } from 'min-dash'; 2 | 3 | export var KEYS_COPY = [ 'c', 'C' ]; 4 | export var KEYS_PASTE = [ 'v', 'V' ]; 5 | export var KEYS_REDO = [ 'y', 'Y' ]; 6 | export var KEYS_UNDO = [ 'z', 'Z' ]; 7 | 8 | /** 9 | * Returns true if event was triggered with any modifier 10 | * @param {KeyboardEvent} event 11 | */ 12 | export function hasModifier(event) { 13 | return (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey); 14 | } 15 | 16 | /** 17 | * @param {KeyboardEvent} event 18 | * @return {boolean} 19 | */ 20 | export function isCmd(event) { 21 | 22 | // ensure we don't react to AltGr 23 | // (mapped to CTRL + ALT) 24 | if (event.altKey) { 25 | return false; 26 | } 27 | 28 | return event.ctrlKey || event.metaKey; 29 | } 30 | 31 | /** 32 | * Checks if key pressed is one of provided keys. 33 | * 34 | * @param {string|string[]} keys 35 | * @param {KeyboardEvent} event 36 | * @return {boolean} 37 | */ 38 | export function isKey(keys, event) { 39 | keys = isArray(keys) ? keys : [ keys ]; 40 | 41 | return keys.indexOf(event.key) !== -1 || keys.indexOf(event.code) !== -1; 42 | } 43 | 44 | /** 45 | * @param {KeyboardEvent} event 46 | */ 47 | export function isShift(event) { 48 | return event.shiftKey; 49 | } 50 | 51 | /** 52 | * @param {KeyboardEvent} event 53 | */ 54 | export function isCopy(event) { 55 | return isCmd(event) && isKey(KEYS_COPY, event); 56 | } 57 | 58 | /** 59 | * @param {KeyboardEvent} event 60 | */ 61 | export function isPaste(event) { 62 | return isCmd(event) && isKey(KEYS_PASTE, event); 63 | } 64 | 65 | /** 66 | * @param {KeyboardEvent} event 67 | */ 68 | export function isUndo(event) { 69 | return isCmd(event) && !isShift(event) && isKey(KEYS_UNDO, event); 70 | } 71 | 72 | /** 73 | * @param {KeyboardEvent} event 74 | */ 75 | export function isRedo(event) { 76 | return isCmd(event) && ( 77 | isKey(KEYS_REDO, event) || ( 78 | isKey(KEYS_UNDO, event) && isShift(event) 79 | ) 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /lib/features/keyboard/index.js: -------------------------------------------------------------------------------- 1 | import Keyboard from './Keyboard'; 2 | import KeyboardBindings from './KeyboardBindings'; 3 | 4 | 5 | /** 6 | * @type { import('didi').ModuleDeclaration } 7 | */ 8 | export default { 9 | __init__: [ 'keyboard', 'keyboardBindings' ], 10 | keyboard: [ 'type', Keyboard ], 11 | keyboardBindings: [ 'type', KeyboardBindings ] 12 | }; 13 | -------------------------------------------------------------------------------- /lib/features/label-support/index.js: -------------------------------------------------------------------------------- 1 | import LabelSupport from './LabelSupport'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'labelSupport' ], 9 | labelSupport: [ 'type', LabelSupport ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/lasso-tool/index.js: -------------------------------------------------------------------------------- 1 | import ToolManagerModule from '../tool-manager'; 2 | import MouseModule from '../mouse'; 3 | 4 | import LassoTool from './LassoTool'; 5 | 6 | 7 | /** 8 | * @type { import('didi').ModuleDeclaration } 9 | */ 10 | export default { 11 | __depends__: [ 12 | ToolManagerModule, 13 | MouseModule 14 | ], 15 | __init__: [ 'lassoTool' ], 16 | lassoTool: [ 'type', LassoTool ] 17 | }; 18 | -------------------------------------------------------------------------------- /lib/features/modeling/cmd/AlignElementsHandler.js: -------------------------------------------------------------------------------- 1 | import { forEach, isDefined } from 'min-dash'; 2 | 3 | /** 4 | * @typedef {import('../../../core/Canvas').default} Canvas 5 | * @typedef {import('../Modeling').default} Modeling 6 | */ 7 | 8 | /** 9 | * A handler that align elements in a certain way. 10 | * 11 | * @param {Modeling} modeling 12 | * @param {Canvas} canvas 13 | */ 14 | export default function AlignElements(modeling, canvas) { 15 | this._modeling = modeling; 16 | this._canvas = canvas; 17 | } 18 | 19 | AlignElements.$inject = [ 'modeling', 'canvas' ]; 20 | 21 | 22 | AlignElements.prototype.preExecute = function(context) { 23 | var modeling = this._modeling; 24 | 25 | var elements = context.elements, 26 | alignment = context.alignment; 27 | 28 | 29 | forEach(elements, function(element) { 30 | var delta = { 31 | x: 0, 32 | y: 0 33 | }; 34 | 35 | if (isDefined(alignment.left)) { 36 | delta.x = alignment.left - element.x; 37 | 38 | } else if (isDefined(alignment.right)) { 39 | delta.x = (alignment.right - element.width) - element.x; 40 | 41 | } else if (isDefined(alignment.center)) { 42 | delta.x = (alignment.center - Math.round(element.width / 2)) - element.x; 43 | 44 | } else if (isDefined(alignment.top)) { 45 | delta.y = alignment.top - element.y; 46 | 47 | } else if (isDefined(alignment.bottom)) { 48 | delta.y = (alignment.bottom - element.height) - element.y; 49 | 50 | } else if (isDefined(alignment.middle)) { 51 | delta.y = (alignment.middle - Math.round(element.height / 2)) - element.y; 52 | } 53 | 54 | modeling.moveElements([ element ], delta, element.parent); 55 | }); 56 | }; 57 | 58 | AlignElements.prototype.postExecute = function(context) { 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /lib/features/modeling/cmd/AppendShapeHandler.js: -------------------------------------------------------------------------------- 1 | import { some } from 'min-dash'; 2 | 3 | /** 4 | * @typedef {import('../../../model/Types').Element} Element 5 | * @typedef {import('../../../model/Types').Parent} Parent 6 | * @typedef {import('../../../model/Types').Shape} Shape 7 | * 8 | * @typedef {import('../../../util/Types').Point} Point 9 | * 10 | * @typedef {import('../Modeling').default} Modeling 11 | */ 12 | 13 | /** 14 | * A handler that implements reversible appending of shapes 15 | * to a source shape. 16 | * 17 | * @param {Modeling} modeling 18 | */ 19 | export default function AppendShapeHandler(modeling) { 20 | this._modeling = modeling; 21 | } 22 | 23 | AppendShapeHandler.$inject = [ 'modeling' ]; 24 | 25 | 26 | // api ////////////////////// 27 | 28 | 29 | /** 30 | * Creates a new shape. 31 | * 32 | * @param {Object} context 33 | * @param {Partial} context.shape The new shape. 34 | * @param {Element} context.source The element to which to append the new shape to. 35 | * @param {Parent} context.parent The parent. 36 | * @param {Point} context.position The position at which to create the new shape. 37 | */ 38 | AppendShapeHandler.prototype.preExecute = function(context) { 39 | 40 | var source = context.source; 41 | 42 | if (!source) { 43 | throw new Error('source required'); 44 | } 45 | 46 | var target = context.target || source.parent, 47 | shape = context.shape, 48 | hints = context.hints || {}; 49 | 50 | shape = context.shape = 51 | this._modeling.createShape( 52 | shape, 53 | context.position, 54 | target, { attach: hints.attach }); 55 | 56 | context.shape = shape; 57 | }; 58 | 59 | AppendShapeHandler.prototype.postExecute = function(context) { 60 | var hints = context.hints || {}; 61 | 62 | if (!existsConnection(context.source, context.shape)) { 63 | 64 | // create connection 65 | if (hints.connectionTarget === context.source) { 66 | this._modeling.connect(context.shape, context.source, context.connection); 67 | } else { 68 | this._modeling.connect(context.source, context.shape, context.connection); 69 | } 70 | } 71 | }; 72 | 73 | 74 | function existsConnection(source, target) { 75 | return some(source.outgoing, function(c) { 76 | return c.target === target; 77 | }); 78 | } -------------------------------------------------------------------------------- /lib/features/modeling/cmd/CreateConnectionHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../../../model/Types').Element} Element 3 | * @typedef {import('../../../model/Types').Shape} Shape 4 | * 5 | * @typedef {import('../../../util/Types').Point} Point 6 | * 7 | * @typedef {import('../Modeling').ModelingHints} ModelingHints 8 | * 9 | * @typedef {import('../../../core/Canvas').default} Canvas 10 | * @typedef {import('../../../layout/BaseLayouter').default} Layouter 11 | */ 12 | 13 | /** 14 | * @param {Canvas} canvas 15 | * @param {Layouter} layouter 16 | */ 17 | export default function CreateConnectionHandler(canvas, layouter) { 18 | this._canvas = canvas; 19 | this._layouter = layouter; 20 | } 21 | 22 | CreateConnectionHandler.$inject = [ 'canvas', 'layouter' ]; 23 | 24 | 25 | // api ////////////////////// 26 | 27 | 28 | /** 29 | * Creates a new connection between two elements. 30 | * 31 | * @param {Object} context 32 | * @param {Element} context.source The source. 33 | * @param {Element} context.target The target. 34 | * @param {Shape} context.parent The parent. 35 | * @param {number} [context.parentIndex] The optional index at which to add the 36 | * connection to the parent's children. 37 | * @param {ModelingHints} [context.hints] The optional hints. 38 | */ 39 | CreateConnectionHandler.prototype.execute = function(context) { 40 | 41 | var connection = context.connection, 42 | source = context.source, 43 | target = context.target, 44 | parent = context.parent, 45 | parentIndex = context.parentIndex, 46 | hints = context.hints; 47 | 48 | if (!source || !target) { 49 | throw new Error('source and target required'); 50 | } 51 | 52 | if (!parent) { 53 | throw new Error('parent required'); 54 | } 55 | 56 | connection.source = source; 57 | connection.target = target; 58 | 59 | if (!connection.waypoints) { 60 | connection.waypoints = this._layouter.layoutConnection(connection, hints); 61 | } 62 | 63 | // add connection 64 | this._canvas.addConnection(connection, parent, parentIndex); 65 | 66 | return connection; 67 | }; 68 | 69 | CreateConnectionHandler.prototype.revert = function(context) { 70 | var connection = context.connection; 71 | 72 | this._canvas.removeConnection(connection); 73 | 74 | connection.source = null; 75 | connection.target = null; 76 | 77 | return connection; 78 | }; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/CreateLabelHandler.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import CreateShapeHandler from './CreateShapeHandler'; 4 | 5 | /** 6 | * @typedef {import('../../../core/Canvas').default} Canvas 7 | * 8 | * @typedef {import('../../../model/Types').Element} Element 9 | * @typedef {import('../../../model/Types').Parent} Parent 10 | * @typedef {import('../../../model/Types').Shape} Shape 11 | * @typedef {import('../../../util/Types').Point} Point 12 | */ 13 | 14 | /** 15 | * A handler that attaches a label to a given target shape. 16 | * 17 | * @param {Canvas} canvas 18 | */ 19 | export default function CreateLabelHandler(canvas) { 20 | CreateShapeHandler.call(this, canvas); 21 | } 22 | 23 | inherits(CreateLabelHandler, CreateShapeHandler); 24 | 25 | CreateLabelHandler.$inject = [ 'canvas' ]; 26 | 27 | 28 | // api ////////////////////// 29 | 30 | 31 | var originalExecute = CreateShapeHandler.prototype.execute; 32 | 33 | /** 34 | * Append label to element. 35 | * 36 | * @param { { 37 | * parent: Parent; 38 | * position: Point; 39 | * shape: Shape; 40 | * target: Element; 41 | * } } context 42 | */ 43 | CreateLabelHandler.prototype.execute = function(context) { 44 | 45 | var label = context.shape; 46 | 47 | ensureValidDimensions(label); 48 | 49 | label.labelTarget = context.labelTarget; 50 | 51 | return originalExecute.call(this, context); 52 | }; 53 | 54 | var originalRevert = CreateShapeHandler.prototype.revert; 55 | 56 | /** 57 | * Revert appending by removing label. 58 | */ 59 | CreateLabelHandler.prototype.revert = function(context) { 60 | context.shape.labelTarget = null; 61 | 62 | return originalRevert.call(this, context); 63 | }; 64 | 65 | 66 | // helpers ////////////////////// 67 | 68 | function ensureValidDimensions(label) { 69 | 70 | // make sure a label has valid { width, height } dimensions 71 | [ 'width', 'height' ].forEach(function(prop) { 72 | if (typeof label[prop] === 'undefined') { 73 | label[prop] = 0; 74 | } 75 | }); 76 | } -------------------------------------------------------------------------------- /lib/features/modeling/cmd/CreateShapeHandler.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'min-dash'; 2 | 3 | /** 4 | * @typedef {import('../../../model/Types').Element} Element 5 | * @typedef {import('../../../util/Types').Point} Point 6 | * 7 | * @typedef {import('../../../core/Canvas').default} Canvas 8 | */ 9 | 10 | var round = Math.round; 11 | 12 | 13 | /** 14 | * A handler that implements reversible addition of shapes. 15 | * 16 | * @param {Canvas} canvas 17 | */ 18 | export default function CreateShapeHandler(canvas) { 19 | this._canvas = canvas; 20 | } 21 | 22 | CreateShapeHandler.$inject = [ 'canvas' ]; 23 | 24 | 25 | // api ////////////////////// 26 | 27 | 28 | /** 29 | * Appends a shape to a target shape 30 | * 31 | * @param {Object} context 32 | * @param {Element} context.parent The parent. 33 | * @param {Point} context.position The position at which to create the new shape. 34 | * @param {number} [context.parentIndex] The optional index at which to add the 35 | * shape to the parent's children. 36 | */ 37 | CreateShapeHandler.prototype.execute = function(context) { 38 | 39 | var shape = context.shape, 40 | positionOrBounds = context.position, 41 | parent = context.parent, 42 | parentIndex = context.parentIndex; 43 | 44 | if (!parent) { 45 | throw new Error('parent required'); 46 | } 47 | 48 | if (!positionOrBounds) { 49 | throw new Error('position required'); 50 | } 51 | 52 | // (1) add at event center position _or_ at given bounds 53 | if (positionOrBounds.width !== undefined) { 54 | assign(shape, positionOrBounds); 55 | } else { 56 | assign(shape, { 57 | x: positionOrBounds.x - round(shape.width / 2), 58 | y: positionOrBounds.y - round(shape.height / 2) 59 | }); 60 | } 61 | 62 | // (2) add to canvas 63 | this._canvas.addShape(shape, parent, parentIndex); 64 | 65 | return shape; 66 | }; 67 | 68 | 69 | /** 70 | * Undo append by removing the shape 71 | */ 72 | CreateShapeHandler.prototype.revert = function(context) { 73 | 74 | var shape = context.shape; 75 | 76 | // (3) remove form canvas 77 | this._canvas.removeShape(shape); 78 | 79 | return shape; 80 | }; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/DeleteConnectionHandler.js: -------------------------------------------------------------------------------- 1 | import { 2 | add as collectionAdd, 3 | indexOf as collectionIdx 4 | } from '../../../util/Collections'; 5 | 6 | import { saveClear } from '../../../util/Removal'; 7 | 8 | /** 9 | * @typedef {import('../../../core/Canvas').default} Canvas 10 | * @typedef {import('../Modeling').default} Modeling 11 | */ 12 | 13 | /** 14 | * A handler that implements reversible deletion of Connections. 15 | */ 16 | export default function DeleteConnectionHandler(canvas, modeling) { 17 | this._canvas = canvas; 18 | this._modeling = modeling; 19 | } 20 | 21 | DeleteConnectionHandler.$inject = [ 22 | 'canvas', 23 | 'modeling' 24 | ]; 25 | 26 | 27 | /** 28 | * - Remove connections 29 | */ 30 | DeleteConnectionHandler.prototype.preExecute = function(context) { 31 | 32 | var modeling = this._modeling; 33 | 34 | var connection = context.connection; 35 | 36 | // remove connections 37 | saveClear(connection.incoming, function(connection) { 38 | 39 | // To make sure that the connection isn't removed twice 40 | // For example if a container is removed 41 | modeling.removeConnection(connection, { nested: true }); 42 | }); 43 | 44 | saveClear(connection.outgoing, function(connection) { 45 | modeling.removeConnection(connection, { nested: true }); 46 | }); 47 | 48 | }; 49 | 50 | 51 | DeleteConnectionHandler.prototype.execute = function(context) { 52 | 53 | var connection = context.connection, 54 | parent = connection.parent; 55 | 56 | context.parent = parent; 57 | 58 | // remember containment 59 | context.parentIndex = collectionIdx(parent.children, connection); 60 | 61 | context.source = connection.source; 62 | context.target = connection.target; 63 | 64 | this._canvas.removeConnection(connection); 65 | 66 | connection.source = null; 67 | connection.target = null; 68 | 69 | return connection; 70 | }; 71 | 72 | /** 73 | * Command revert implementation. 74 | */ 75 | DeleteConnectionHandler.prototype.revert = function(context) { 76 | 77 | var connection = context.connection, 78 | parent = context.parent, 79 | parentIndex = context.parentIndex; 80 | 81 | connection.source = context.source; 82 | connection.target = context.target; 83 | 84 | // restore containment 85 | collectionAdd(parent.children, connection, parentIndex); 86 | 87 | this._canvas.addConnection(connection, parent); 88 | 89 | return connection; 90 | }; 91 | -------------------------------------------------------------------------------- /lib/features/modeling/cmd/DeleteElementsHandler.js: -------------------------------------------------------------------------------- 1 | import { forEach } from 'min-dash'; 2 | 3 | /** 4 | * @typedef {import('../../../core/ElementRegistry').default} ElementRegistry 5 | * @typedef {import('../Modeling').default} Modeling 6 | */ 7 | 8 | /** 9 | * @param {Modeling} modeling 10 | * @param {ElementRegistry} elementRegistry 11 | */ 12 | export default function DeleteElementsHandler(modeling, elementRegistry) { 13 | this._modeling = modeling; 14 | this._elementRegistry = elementRegistry; 15 | } 16 | 17 | DeleteElementsHandler.$inject = [ 18 | 'modeling', 19 | 'elementRegistry' 20 | ]; 21 | 22 | 23 | DeleteElementsHandler.prototype.postExecute = function(context) { 24 | 25 | var modeling = this._modeling, 26 | elementRegistry = this._elementRegistry, 27 | elements = context.elements; 28 | 29 | forEach(elements, function(element) { 30 | 31 | // element may have been removed with previous 32 | // remove operations already (e.g. in case of nesting) 33 | if (!elementRegistry.get(element.id)) { 34 | return; 35 | } 36 | 37 | if (element.waypoints) { 38 | modeling.removeConnection(element); 39 | } else { 40 | modeling.removeShape(element); 41 | } 42 | }); 43 | }; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/LayoutConnectionHandler.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'min-dash'; 2 | 3 | /** 4 | * @typedef {import('../../../core/Canvas').default} Canvas 5 | * @typedef {import('../../../layout/BaseLayouter').default} Layouter 6 | */ 7 | 8 | /** 9 | * A handler that implements reversible moving of shapes. 10 | * 11 | * @param {Layouter} layouter 12 | * @param {Canvas} canvas 13 | */ 14 | export default function LayoutConnectionHandler(layouter, canvas) { 15 | this._layouter = layouter; 16 | this._canvas = canvas; 17 | } 18 | 19 | LayoutConnectionHandler.$inject = [ 'layouter', 'canvas' ]; 20 | 21 | LayoutConnectionHandler.prototype.execute = function(context) { 22 | 23 | var connection = context.connection; 24 | 25 | var oldWaypoints = connection.waypoints; 26 | 27 | assign(context, { 28 | oldWaypoints: oldWaypoints 29 | }); 30 | 31 | connection.waypoints = this._layouter.layoutConnection(connection, context.hints); 32 | 33 | return connection; 34 | }; 35 | 36 | LayoutConnectionHandler.prototype.revert = function(context) { 37 | 38 | var connection = context.connection; 39 | 40 | connection.waypoints = context.oldWaypoints; 41 | 42 | return connection; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/features/modeling/cmd/MoveConnectionHandler.js: -------------------------------------------------------------------------------- 1 | import { forEach } from 'min-dash'; 2 | 3 | 4 | import { 5 | add as collectionAdd, 6 | remove as collectionRemove 7 | } from '../../../util/Collections'; 8 | 9 | 10 | /** 11 | * A handler that implements reversible moving of connections. 12 | * 13 | * The handler differs from the layout connection handler in a sense 14 | * that it preserves the connection layout. 15 | */ 16 | export default function MoveConnectionHandler() { } 17 | 18 | 19 | MoveConnectionHandler.prototype.execute = function(context) { 20 | 21 | var connection = context.connection, 22 | delta = context.delta; 23 | 24 | var newParent = context.newParent || connection.parent, 25 | newParentIndex = context.newParentIndex, 26 | oldParent = connection.parent; 27 | 28 | // save old parent in context 29 | context.oldParent = oldParent; 30 | context.oldParentIndex = collectionRemove(oldParent.children, connection); 31 | 32 | // add to new parent at position 33 | collectionAdd(newParent.children, connection, newParentIndex); 34 | 35 | // update parent 36 | connection.parent = newParent; 37 | 38 | // update waypoint positions 39 | forEach(connection.waypoints, function(p) { 40 | p.x += delta.x; 41 | p.y += delta.y; 42 | 43 | if (p.original) { 44 | p.original.x += delta.x; 45 | p.original.y += delta.y; 46 | } 47 | }); 48 | 49 | return connection; 50 | }; 51 | 52 | MoveConnectionHandler.prototype.revert = function(context) { 53 | 54 | var connection = context.connection, 55 | newParent = connection.parent, 56 | oldParent = context.oldParent, 57 | oldParentIndex = context.oldParentIndex, 58 | delta = context.delta; 59 | 60 | // remove from newParent 61 | collectionRemove(newParent.children, connection); 62 | 63 | // restore previous location in old parent 64 | collectionAdd(oldParent.children, connection, oldParentIndex); 65 | 66 | // restore parent 67 | connection.parent = oldParent; 68 | 69 | // revert to old waypoint positions 70 | forEach(connection.waypoints, function(p) { 71 | p.x -= delta.x; 72 | p.y -= delta.y; 73 | 74 | if (p.original) { 75 | p.original.x -= delta.x; 76 | p.original.y -= delta.y; 77 | } 78 | }); 79 | 80 | return connection; 81 | }; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/MoveElementsHandler.js: -------------------------------------------------------------------------------- 1 | import MoveHelper from './helper/MoveHelper'; 2 | 3 | /** 4 | * @typedef {import('../Modeling').default} Modeling 5 | */ 6 | 7 | /** 8 | * A handler that implements reversible moving of shapes. 9 | * 10 | * @param {Modeling} modeling 11 | */ 12 | export default function MoveElementsHandler(modeling) { 13 | this._helper = new MoveHelper(modeling); 14 | } 15 | 16 | MoveElementsHandler.$inject = [ 'modeling' ]; 17 | 18 | MoveElementsHandler.prototype.preExecute = function(context) { 19 | context.closure = this._helper.getClosure(context.shapes); 20 | }; 21 | 22 | MoveElementsHandler.prototype.postExecute = function(context) { 23 | 24 | var hints = context.hints, 25 | primaryShape; 26 | 27 | if (hints && hints.primaryShape) { 28 | primaryShape = hints.primaryShape; 29 | hints.oldParent = primaryShape.parent; 30 | } 31 | 32 | this._helper.moveClosure( 33 | context.closure, 34 | context.delta, 35 | context.newParent, 36 | context.newHost, 37 | primaryShape 38 | ); 39 | }; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/NoopHandler.js: -------------------------------------------------------------------------------- 1 | export default function NoopHandler() {} 2 | 3 | NoopHandler.prototype.execute = function() {}; 4 | NoopHandler.prototype.revert = function() {}; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/UpdateAttachmentHandler.js: -------------------------------------------------------------------------------- 1 | import { 2 | add as collectionAdd, 3 | remove as collectionRemove 4 | } from '../../../util/Collections'; 5 | 6 | /** 7 | * @typedef {import('../Modeling').default} Modeling 8 | */ 9 | 10 | /** 11 | * A handler that implements reversible attaching/detaching of shapes. 12 | * 13 | * @param {Modeling} modeling 14 | */ 15 | export default function UpdateAttachmentHandler(modeling) { 16 | this._modeling = modeling; 17 | } 18 | 19 | UpdateAttachmentHandler.$inject = [ 'modeling' ]; 20 | 21 | 22 | UpdateAttachmentHandler.prototype.execute = function(context) { 23 | var shape = context.shape, 24 | newHost = context.newHost, 25 | oldHost = shape.host; 26 | 27 | // (0) detach from old host 28 | context.oldHost = oldHost; 29 | context.attacherIdx = removeAttacher(oldHost, shape); 30 | 31 | // (1) attach to new host 32 | addAttacher(newHost, shape); 33 | 34 | // (2) update host 35 | shape.host = newHost; 36 | 37 | return shape; 38 | }; 39 | 40 | UpdateAttachmentHandler.prototype.revert = function(context) { 41 | var shape = context.shape, 42 | newHost = context.newHost, 43 | oldHost = context.oldHost, 44 | attacherIdx = context.attacherIdx; 45 | 46 | // (2) update host 47 | shape.host = oldHost; 48 | 49 | // (1) attach to new host 50 | removeAttacher(newHost, shape); 51 | 52 | // (0) detach from old host 53 | addAttacher(oldHost, shape, attacherIdx); 54 | 55 | return shape; 56 | }; 57 | 58 | 59 | function removeAttacher(host, attacher) { 60 | 61 | // remove attacher from host 62 | return collectionRemove(host && host.attachers, attacher); 63 | } 64 | 65 | function addAttacher(host, attacher, idx) { 66 | 67 | if (!host) { 68 | return; 69 | } 70 | 71 | var attachers = host.attachers; 72 | 73 | if (!attachers) { 74 | host.attachers = attachers = []; 75 | } 76 | 77 | collectionAdd(attachers, attacher, idx); 78 | } 79 | -------------------------------------------------------------------------------- /lib/features/modeling/cmd/UpdateWaypointsHandler.js: -------------------------------------------------------------------------------- 1 | export default function UpdateWaypointsHandler() { } 2 | 3 | UpdateWaypointsHandler.prototype.execute = function(context) { 4 | 5 | var connection = context.connection, 6 | newWaypoints = context.newWaypoints; 7 | 8 | context.oldWaypoints = connection.waypoints; 9 | 10 | connection.waypoints = newWaypoints; 11 | 12 | return connection; 13 | }; 14 | 15 | UpdateWaypointsHandler.prototype.revert = function(context) { 16 | 17 | var connection = context.connection, 18 | oldWaypoints = context.oldWaypoints; 19 | 20 | connection.waypoints = oldWaypoints; 21 | 22 | return connection; 23 | }; -------------------------------------------------------------------------------- /lib/features/modeling/cmd/helper/MoveClosure.js: -------------------------------------------------------------------------------- 1 | import { 2 | assign 3 | } from 'min-dash'; 4 | 5 | import { 6 | getClosure 7 | } from '../../../../util/Elements'; 8 | 9 | /** 10 | * @typedef {import('../../../../model/Types').Connection} Connection 11 | * @typedef {import('../../../../model/Types').Element} Element 12 | * @typedef {import('../../../../model/Types').Shape} Shape 13 | */ 14 | 15 | export default function MoveClosure() { 16 | 17 | /** 18 | * @type {Record} 19 | */ 20 | this.allShapes = {}; 21 | 22 | /** 23 | * @type {Record} 24 | */ 25 | this.allConnections = {}; 26 | 27 | /** 28 | * @type {Record} 29 | */ 30 | this.enclosedElements = {}; 31 | 32 | /** 33 | * @type {Record} 34 | */ 35 | this.enclosedConnections = {}; 36 | 37 | /** 38 | * @type {Record} 39 | */ 40 | this.topLevel = {}; 41 | } 42 | 43 | /** 44 | * @param {Element} element 45 | * @param {boolean} [isTopLevel] 46 | * 47 | * @return {MoveClosure} 48 | */ 49 | MoveClosure.prototype.add = function(element, isTopLevel) { 50 | return this.addAll([ element ], isTopLevel); 51 | }; 52 | 53 | /** 54 | * @param {Element[]} elements 55 | * @param {boolean} [isTopLevel] 56 | * 57 | * @return {MoveClosure} 58 | */ 59 | MoveClosure.prototype.addAll = function(elements, isTopLevel) { 60 | 61 | var newClosure = getClosure(elements, !!isTopLevel, this); 62 | 63 | assign(this, newClosure); 64 | 65 | return this; 66 | }; -------------------------------------------------------------------------------- /lib/features/modeling/index.js: -------------------------------------------------------------------------------- 1 | import CommandModule from '../../command'; 2 | import ChangeSupportModule from '../change-support'; 3 | import SelectionModule from '../selection'; 4 | import RulesModule from '../rules'; 5 | 6 | import Modeling from './Modeling'; 7 | import BaseLayouter from '../../layout/BaseLayouter'; 8 | 9 | 10 | /** 11 | * @type { import('didi').ModuleDeclaration } 12 | */ 13 | export default { 14 | __depends__: [ 15 | CommandModule, 16 | ChangeSupportModule, 17 | SelectionModule, 18 | RulesModule 19 | ], 20 | __init__: [ 'modeling' ], 21 | modeling: [ 'type', Modeling ], 22 | layouter: [ 'type', BaseLayouter ] 23 | }; 24 | -------------------------------------------------------------------------------- /lib/features/mouse/Mouse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../../core/EventBus').default} EventBus 3 | */ 4 | 5 | /** 6 | * @param {EventBus} eventBus 7 | */ 8 | export default function Mouse(eventBus) { 9 | var self = this; 10 | 11 | this._lastMoveEvent = null; 12 | 13 | function setLastMoveEvent(mousemoveEvent) { 14 | self._lastMoveEvent = mousemoveEvent; 15 | } 16 | 17 | eventBus.on('canvas.init', function(context) { 18 | var svg = self._svg = context.svg; 19 | 20 | svg.addEventListener('mousemove', setLastMoveEvent); 21 | }); 22 | 23 | eventBus.on('canvas.destroy', function() { 24 | self._lastMouseEvent = null; 25 | 26 | self._svg.removeEventListener('mousemove', setLastMoveEvent); 27 | }); 28 | } 29 | 30 | Mouse.$inject = [ 'eventBus' ]; 31 | 32 | Mouse.prototype.getLastMoveEvent = function() { 33 | return this._lastMoveEvent || createMoveEvent(0, 0); 34 | }; 35 | 36 | // helpers ////////// 37 | 38 | export function createMoveEvent(x, y) { 39 | var event = document.createEvent('MouseEvent'); 40 | 41 | var screenX = x, 42 | screenY = y, 43 | clientX = x, 44 | clientY = y; 45 | 46 | if (event.initMouseEvent) { 47 | event.initMouseEvent( 48 | 'mousemove', 49 | true, 50 | true, 51 | window, 52 | 0, 53 | screenX, 54 | screenY, 55 | clientX, 56 | clientY, 57 | false, 58 | false, 59 | false, 60 | false, 61 | 0, 62 | null 63 | ); 64 | } 65 | 66 | return event; 67 | } -------------------------------------------------------------------------------- /lib/features/mouse/index.js: -------------------------------------------------------------------------------- 1 | import Mouse from './Mouse'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'mouse' ], 9 | mouse: [ 'type', Mouse ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/move/index.js: -------------------------------------------------------------------------------- 1 | import InteractionEventsModule from '../interaction-events'; 2 | import SelectionModule from '../selection'; 3 | import OutlineModule from '../outline'; 4 | import RulesModule from '../rules'; 5 | import DraggingModule from '../dragging'; 6 | import PreviewSupportModule from '../preview-support'; 7 | 8 | import Move from './Move'; 9 | import MovePreview from './MovePreview'; 10 | 11 | 12 | /** 13 | * @type { import('didi').ModuleDeclaration } 14 | */ 15 | export default { 16 | __depends__: [ 17 | InteractionEventsModule, 18 | SelectionModule, 19 | OutlineModule, 20 | RulesModule, 21 | DraggingModule, 22 | PreviewSupportModule 23 | ], 24 | __init__: [ 25 | 'move', 26 | 'movePreview' 27 | ], 28 | move: [ 'type', Move ], 29 | movePreview: [ 'type', MovePreview ] 30 | }; 31 | -------------------------------------------------------------------------------- /lib/features/outline/MultiSelectionOutline.js: -------------------------------------------------------------------------------- 1 | import { 2 | append as svgAppend, 3 | attr as svgAttr, 4 | create as svgCreate, 5 | classes as svgClasses, 6 | clear as svgClear 7 | } from 'tiny-svg'; 8 | 9 | import { assign } from 'min-dash'; 10 | 11 | import { getBBox } from '../../util/Elements'; 12 | 13 | var SELECTION_OUTLINE_PADDING = 6; 14 | 15 | /** 16 | * @typedef {import('../../model/Types').Element} Element 17 | * 18 | * @typedef {import('../../core/EventBus').default} EventBus 19 | * @typedef {import('../selection/Selection').default} Selection 20 | * @typedef {import('../../core/Canvas').default} Canvas 21 | */ 22 | 23 | /** 24 | * @class 25 | * 26 | * A plugin that adds an outline to shapes and connections that may be activated and styled 27 | * via CSS classes. 28 | * 29 | * @param {EventBus} eventBus 30 | * @param {Canvas} canvas 31 | * @param {Selection} selection 32 | */ 33 | export default function MultiSelectionOutline(eventBus, canvas, selection) { 34 | this._canvas = canvas; 35 | 36 | var self = this; 37 | 38 | eventBus.on('element.changed', function(event) { 39 | if (selection.isSelected(event.element)) { 40 | self._updateMultiSelectionOutline(selection.get()); 41 | } 42 | }); 43 | 44 | eventBus.on('selection.changed', function(event) { 45 | var newSelection = event.newSelection; 46 | 47 | self._updateMultiSelectionOutline(newSelection); 48 | }); 49 | } 50 | 51 | 52 | 53 | MultiSelectionOutline.prototype._updateMultiSelectionOutline = function(selection) { 54 | var layer = this._canvas.getLayer('selectionOutline'); 55 | 56 | svgClear(layer); 57 | 58 | var enabled = selection.length > 1; 59 | 60 | var container = this._canvas.getContainer(); 61 | 62 | svgClasses(container)[enabled ? 'add' : 'remove']('djs-multi-select'); 63 | 64 | if (!enabled) { 65 | return; 66 | } 67 | 68 | var bBox = addSelectionOutlinePadding(getBBox(selection)); 69 | 70 | var rect = svgCreate('rect'); 71 | 72 | svgAttr(rect, assign({ 73 | rx: 3 74 | }, bBox)); 75 | 76 | svgClasses(rect).add('djs-selection-outline'); 77 | 78 | svgAppend(layer, rect); 79 | }; 80 | 81 | 82 | MultiSelectionOutline.$inject = [ 'eventBus', 'canvas', 'selection' ]; 83 | 84 | // helpers ////////// 85 | 86 | function addSelectionOutlinePadding(bBox) { 87 | return { 88 | x: bBox.x - SELECTION_OUTLINE_PADDING, 89 | y: bBox.y - SELECTION_OUTLINE_PADDING, 90 | width: bBox.width + SELECTION_OUTLINE_PADDING * 2, 91 | height: bBox.height + SELECTION_OUTLINE_PADDING * 2 92 | }; 93 | } -------------------------------------------------------------------------------- /lib/features/outline/OutlineProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | attr as svgAttr, 3 | create as svgCreate 4 | } from 'tiny-svg'; 5 | 6 | import { Element } from '../../model/Types'; 7 | 8 | import OutlineProvider, { Outline } from './OutlineProvider'; 9 | 10 | export class FooOutlineProvider implements OutlineProvider { 11 | getOutline(element: Element): Outline { 12 | const outline = svgCreate('circle'); 13 | 14 | svgAttr(outline, { 15 | cx: element.width / 2, 16 | cy: element.height / 2, 17 | r: 23 18 | }); 19 | 20 | return outline; 21 | } 22 | 23 | updateOutline(element: Element, outline: Outline): boolean { 24 | if (element.type === 'Foo') { 25 | svgAttr(outline, { 26 | cx: element.width / 2, 27 | cy: element.height / 2, 28 | r: 23 29 | }); 30 | } 31 | 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/outline/OutlineProvider.ts: -------------------------------------------------------------------------------- 1 | import type { Element } from '../../model/Types'; 2 | 3 | export type Outline = SVGSVGElement | SVGCircleElement | SVGRectElement; 4 | 5 | /** 6 | * An interface to be implemented by an outline provider. 7 | */ 8 | export default interface OutlineProvider { 9 | /** 10 | * Returns outline shape for given element. 11 | * 12 | * @example 13 | * 14 | * ```javascript 15 | * getOutline(element) { 16 | * if (element.type === 'Foo') { 17 | * const outline = svgCreate('circle'); 18 | * 19 | * svgAttr(outline, { 20 | * cx: element.width / 2, 21 | * cy: element.height / 2, 22 | * r: 23 23 | * }); 24 | * 25 | * return outline; 26 | * } 27 | * } 28 | * ``` 29 | * 30 | * @param element 31 | */ 32 | getOutline: (element: Element) => Outline; 33 | 34 | /** 35 | * Updates outline shape based on element's current size. Returns true if 36 | * update was handled by provider. 37 | * 38 | * @example 39 | * 40 | * ```javascript 41 | * updateOutline(element, outline) { 42 | * if (element.type === 'Foo') { 43 | * svgAttr(outline, { 44 | * cx: element.width / 2, 45 | * cy: element.height / 2, 46 | * r: 23 47 | * }); 48 | * } else if (element.type === 'Bar') { 49 | * return true; 50 | * } 51 | * 52 | * return false; 53 | * } 54 | * ``` 55 | * 56 | * @param element 57 | * @param outline 58 | */ 59 | updateOutline: (element: Element, outline: Outline) => boolean; 60 | } 61 | -------------------------------------------------------------------------------- /lib/features/outline/index.js: -------------------------------------------------------------------------------- 1 | import SelectionModule from '../selection'; 2 | 3 | import Outline from './Outline'; 4 | import MultiSelectionOutline from './MultiSelectionOutline'; 5 | 6 | 7 | /** 8 | * @type { import('didi').ModuleDeclaration } 9 | */ 10 | export default { 11 | __depends__: [ 12 | SelectionModule 13 | ], 14 | __init__: [ 'outline', 'multiSelectionOutline' ], 15 | outline: [ 'type', Outline ], 16 | multiSelectionOutline: [ 'type', MultiSelectionOutline ] 17 | }; -------------------------------------------------------------------------------- /lib/features/overlays/Overlays.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import ElementFactory from '../../core/ElementFactory'; 4 | 5 | import OverlaysModule from '.'; 6 | import Overlays, { OverlayAttrs } from './Overlays'; 7 | 8 | const diagram = new Diagram({ 9 | modules: [ 10 | OverlaysModule 11 | ] 12 | }); 13 | 14 | const elementFactory = diagram.get('elementFactory'); 15 | 16 | const shape = elementFactory.createShape(); 17 | 18 | const overlays = diagram.get('overlays'); 19 | 20 | const overlay: OverlayAttrs = { 21 | html: '

Foo

', 22 | position: { 23 | top: 0, 24 | right: 0 25 | }, 26 | show: { 27 | minZoom: 1 28 | }, 29 | scale: true 30 | }; 31 | 32 | overlays.add('shape', overlay); 33 | 34 | overlays.add('shape', 'foo', overlay); 35 | 36 | overlays.get({ id: 'shape' }); 37 | 38 | overlays.get({ element: shape }); 39 | 40 | overlays.get({ type: 'foo' }); 41 | 42 | overlays.get('foo'); 43 | 44 | overlays.remove({ id: 'shape' }); 45 | 46 | overlays.remove({ element: shape }); 47 | 48 | overlays.remove({ type: 'foo' }); 49 | 50 | overlays.remove('foo'); 51 | 52 | overlays.hide(); 53 | 54 | overlays.show(); 55 | 56 | overlays.isShown(); -------------------------------------------------------------------------------- /lib/features/overlays/index.js: -------------------------------------------------------------------------------- 1 | import Overlays from './Overlays'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'overlays' ], 9 | overlays: [ 'type', Overlays ] 10 | }; -------------------------------------------------------------------------------- /lib/features/palette/Palette.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import PaletteModule from '.'; 4 | import Palette from './Palette'; 5 | 6 | import { FooPaletteProvider } from './PaletteProvider.spec'; 7 | 8 | const diagram = new Diagram({ 9 | modules: [ 10 | PaletteModule 11 | ] 12 | }); 13 | 14 | const palette = diagram.get('palette'); 15 | 16 | palette.open(); 17 | 18 | palette.close(); 19 | 20 | palette.isOpen(); 21 | 22 | palette.toggle(); 23 | 24 | palette.isActiveTool('foo'); 25 | 26 | palette.registerProvider(new FooPaletteProvider()); 27 | 28 | const entries = palette.getEntries(); 29 | 30 | for (const key in entries) { 31 | const entry = entries[ key ]; 32 | 33 | console.log(entry.action); 34 | } 35 | 36 | palette.trigger('foo', new Event('click')); 37 | 38 | palette.trigger('foo', new Event('click'), true); 39 | 40 | palette.triggerEntry('foo', 'bar', new Event('click')); 41 | 42 | palette.triggerEntry('foo', 'bar', new Event('click'), true); 43 | 44 | palette.updateToolHighlight('foo'); -------------------------------------------------------------------------------- /lib/features/palette/PaletteProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import PaletteProvider, { PaletteEntries, PaletteEntriesCallback } from './PaletteProvider'; 2 | 3 | export class FooPaletteProvider implements PaletteProvider { 4 | getPaletteEntries(): PaletteEntries { 5 | return { 6 | foo: { 7 | action: (event, autoActivate) => { 8 | console.log(event.target); 9 | console.log(autoActivate); 10 | }, 11 | className: 'foo', 12 | group: 'foo', 13 | html: 'Foo', 14 | imageUrl: 'data:image/svg+xml;', 15 | title: 'Foo' 16 | } 17 | }; 18 | } 19 | } 20 | 21 | export class BarPaletteProvider implements PaletteProvider { 22 | getPaletteEntries(): PaletteEntriesCallback { 23 | return function(entries) { 24 | return { 25 | ...entries, 26 | bar: { 27 | action: { 28 | click: (event, autoActivate) => { 29 | console.log(event.target); 30 | console.log(autoActivate); 31 | }, 32 | dragstart: (event, autoActivate) => { 33 | console.log(event.target); 34 | console.log(autoActivate); 35 | } 36 | }, 37 | className: 'bar', 38 | group: 'bar', 39 | html: 'Bar', 40 | imageUrl: 'data:image/svg+xml;', 41 | title: 'Bar' 42 | } 43 | }; 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/features/palette/PaletteProvider.ts: -------------------------------------------------------------------------------- 1 | export type PaletteEntryAction = (event: Event, autoActivate: boolean) => any; 2 | 3 | export type PaletteEntry = { 4 | action: Record | PaletteEntryAction; 5 | className?: string; 6 | group?: string; 7 | html?: string; 8 | imageUrl?: string; 9 | separator?: boolean; 10 | title?: string; 11 | }; 12 | 13 | export type PaletteEntries = Record; 14 | 15 | export type PaletteEntriesCallback = (entries: PaletteEntries) => PaletteEntries; 16 | 17 | export default interface PaletteProvider { 18 | 19 | /** 20 | * Returns a map of entries or a function that receives, modifies and returns 21 | * a map of entries. 22 | * 23 | * The following example shows how to replace any entries returned by previous 24 | * providers with one entry which alerts the ID of the given element when 25 | * clicking on the entry. 26 | * 27 | * @example 28 | * 29 | * ```javascript 30 | * getPaletteEntries() { 31 | * return function(entries) { 32 | * return { 33 | * foo: { 34 | * action: (event, autoActivate) => { 35 | * alert('Foo'); 36 | * }, 37 | * className: 'foo', 38 | * title: 'Foo' 39 | * } 40 | * }; 41 | * }; 42 | * } 43 | * ``` 44 | */ 45 | getPaletteEntries: () => PaletteEntriesCallback | PaletteEntries; 46 | } 47 | -------------------------------------------------------------------------------- /lib/features/palette/index.js: -------------------------------------------------------------------------------- 1 | import Palette from './Palette'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'palette' ], 9 | palette: [ 'type', Palette ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/popup-menu/PopupMenu.spec.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import ElementFactory from '../../core/ElementFactory'; 4 | 5 | import PopupMenuModule from '.'; 6 | import PopupMenu from './PopupMenu'; 7 | import { PopupMenuEntry } from './PopupMenuProvider'; 8 | 9 | import { FooPopupMenuProvider } from './PopupMenuProvider.spec'; 10 | 11 | const diagram = new Diagram({ 12 | modules: [ 13 | PopupMenuModule 14 | ] 15 | }); 16 | 17 | const elementFactory = diagram.get('elementFactory'); 18 | 19 | const shape = elementFactory.createShape(); 20 | 21 | const popupMenu = diagram.get('popupMenu'); 22 | 23 | popupMenu.open(shape, 'foo', { x: 100, y: 100 }); 24 | 25 | popupMenu.open(shape, 'foo', { x: 100, y: 100 }, { width: 100 }); 26 | 27 | popupMenu.open([ shape ], 'foo', { x: 100, y: 100 }); 28 | 29 | popupMenu.isEmpty(shape, 'foo'); 30 | 31 | popupMenu.isEmpty([ shape ], 'foo'); 32 | 33 | popupMenu.isOpen(); 34 | 35 | popupMenu.registerProvider('foo', new FooPopupMenuProvider()); 36 | 37 | popupMenu.registerProvider('foo', 1000, new FooPopupMenuProvider()); 38 | 39 | popupMenu.reset(); 40 | 41 | popupMenu.close(); 42 | 43 | popupMenu.trigger(new Event('click'), { 44 | action: (event: Event, entry: PopupMenuEntry, action?: string) => { 45 | console.log(event.target); 46 | console.log(entry.label); 47 | console.log(action); 48 | }, 49 | className: 'foo', 50 | label: 'Foo' 51 | }, 'foo'); -------------------------------------------------------------------------------- /lib/features/popup-menu/index.js: -------------------------------------------------------------------------------- 1 | import PopupMenu from './PopupMenu'; 2 | 3 | import Search from '../search'; 4 | 5 | 6 | /** 7 | * @type { import('didi').ModuleDeclaration } 8 | */ 9 | export default { 10 | __depends__: [ Search ], 11 | __init__: [ 'popupMenu' ], 12 | popupMenu: [ 'type', PopupMenu ] 13 | }; 14 | -------------------------------------------------------------------------------- /lib/features/preview-support/index.js: -------------------------------------------------------------------------------- 1 | import PreviewSupport from './PreviewSupport'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'previewSupport' ], 9 | previewSupport: [ 'type', PreviewSupport ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/replace/Replace.js: -------------------------------------------------------------------------------- 1 | import { 2 | assign 3 | } from 'min-dash'; 4 | 5 | /** 6 | * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus 7 | * @typedef {import('../modeling/Modeling').default} Modeling 8 | * 9 | * @typedef {import('../../core/Types').ShapeLike} Shape 10 | */ 11 | 12 | var round = Math.round; 13 | 14 | /** 15 | * Service that allows replacing of elements. 16 | * 17 | * @param {Modeling} modeling 18 | * @param {EventBus} eventBus 19 | */ 20 | export default function Replace(modeling, eventBus) { 21 | this._modeling = modeling; 22 | this._eventBus = eventBus; 23 | } 24 | 25 | Replace.$inject = [ 'modeling', 'eventBus' ]; 26 | 27 | /** 28 | * Replace an element. 29 | * 30 | * @param {Shape} oldElement The element to be replaced. 31 | * @param {Object} attrs Containing information about the new element, for 32 | * example the new bounds and type. 33 | * @param {Object} hints Custom hints that will be attached to the context. It 34 | * can be used to inject data that is needed in the command chain. For example 35 | * it could be used in eventbus.on('commandStack.shape.replace.postExecute') to 36 | * change shape attributes after shape creation. 37 | * 38 | * @return {Shape} 39 | */ 40 | Replace.prototype.replaceElement = function(oldElement, attrs, hints) { 41 | 42 | if (oldElement.waypoints) { 43 | 44 | // TODO(nikku): we do not replace connections, yet 45 | return null; 46 | } 47 | 48 | var modeling = this._modeling; 49 | var eventBus = this._eventBus; 50 | 51 | eventBus.fire('replace.start', { 52 | element: oldElement, 53 | attrs, 54 | hints 55 | }); 56 | 57 | var width = attrs.width || oldElement.width, 58 | height = attrs.height || oldElement.height, 59 | x = attrs.x || oldElement.x, 60 | y = attrs.y || oldElement.y, 61 | centerX = round(x + width / 2), 62 | centerY = round(y + height / 2); 63 | 64 | // modeling API requires center coordinates, 65 | // account for that when handling shape bounds 66 | 67 | var newElement = modeling.replaceShape( 68 | oldElement, 69 | assign( 70 | {}, 71 | attrs, 72 | { 73 | x: centerX, 74 | y: centerY, 75 | width: width, 76 | height: height 77 | } 78 | ), 79 | hints 80 | ); 81 | 82 | eventBus.fire('replace.end', { 83 | element: oldElement, 84 | newElement, 85 | hints 86 | }); 87 | 88 | return newElement; 89 | }; 90 | -------------------------------------------------------------------------------- /lib/features/replace/ReplaceSelectionBehavior.js: -------------------------------------------------------------------------------- 1 | export default function ReplaceSelectionBehavior(selection, eventBus) { 2 | 3 | eventBus.on('replace.end', 500, function(event) { 4 | const { 5 | newElement, 6 | hints = {} 7 | } = event; 8 | 9 | if (hints.select === false) { 10 | return; 11 | } 12 | 13 | selection.select(newElement); 14 | }); 15 | 16 | } 17 | 18 | ReplaceSelectionBehavior.$inject = [ 'selection', 'eventBus' ]; -------------------------------------------------------------------------------- /lib/features/replace/index.js: -------------------------------------------------------------------------------- 1 | import Replace from './Replace'; 2 | import ReplaceSelectionBehavior from './ReplaceSelectionBehavior'; 3 | 4 | 5 | /** 6 | * @type { import('didi').ModuleDeclaration } 7 | */ 8 | export default { 9 | __init__: [ 'replace', 'replaceSelectionBehavior' ], 10 | replaceSelectionBehavior: [ 'type', ReplaceSelectionBehavior ], 11 | replace: [ 'type', Replace ] 12 | }; -------------------------------------------------------------------------------- /lib/features/resize/ResizePreview.js: -------------------------------------------------------------------------------- 1 | var MARKER_RESIZING = 'djs-resizing', 2 | MARKER_RESIZE_NOT_OK = 'resize-not-ok'; 3 | 4 | var LOW_PRIORITY = 500; 5 | 6 | import { 7 | attr as svgAttr, 8 | remove as svgRemove, 9 | classes as svgClasses 10 | } from 'tiny-svg'; 11 | 12 | /** 13 | * @typedef {import('../../core/Canvas').default} Canvas 14 | * @typedef {import('../../core/EventBus').default} EventBus 15 | * @typedef {import('../preview-support/PreviewSupport').default} PreviewSupport 16 | */ 17 | 18 | /** 19 | * Provides previews for resizing shapes when resizing. 20 | * 21 | * @param {EventBus} eventBus 22 | * @param {Canvas} canvas 23 | * @param {PreviewSupport} previewSupport 24 | */ 25 | export default function ResizePreview(eventBus, canvas, previewSupport) { 26 | 27 | /** 28 | * Update resizer frame. 29 | * 30 | * @param {Object} context 31 | */ 32 | function updateFrame(context) { 33 | 34 | var shape = context.shape, 35 | bounds = context.newBounds, 36 | frame = context.frame; 37 | 38 | if (!frame) { 39 | frame = context.frame = previewSupport.addFrame(shape, canvas.getActiveLayer()); 40 | 41 | canvas.addMarker(shape, MARKER_RESIZING); 42 | } 43 | 44 | if (bounds.width > 5) { 45 | svgAttr(frame, { x: bounds.x, width: bounds.width }); 46 | } 47 | 48 | if (bounds.height > 5) { 49 | svgAttr(frame, { y: bounds.y, height: bounds.height }); 50 | } 51 | 52 | if (context.canExecute) { 53 | svgClasses(frame).remove(MARKER_RESIZE_NOT_OK); 54 | } else { 55 | svgClasses(frame).add(MARKER_RESIZE_NOT_OK); 56 | } 57 | } 58 | 59 | /** 60 | * Remove resizer frame. 61 | * 62 | * @param {Object} context 63 | */ 64 | function removeFrame(context) { 65 | var shape = context.shape, 66 | frame = context.frame; 67 | 68 | if (frame) { 69 | svgRemove(context.frame); 70 | } 71 | 72 | canvas.removeMarker(shape, MARKER_RESIZING); 73 | } 74 | 75 | // add and update previews 76 | eventBus.on('resize.move', LOW_PRIORITY, function(event) { 77 | updateFrame(event.context); 78 | }); 79 | 80 | // remove previews 81 | eventBus.on('resize.cleanup', function(event) { 82 | removeFrame(event.context); 83 | }); 84 | 85 | } 86 | 87 | ResizePreview.$inject = [ 88 | 'eventBus', 89 | 'canvas', 90 | 'previewSupport' 91 | ]; -------------------------------------------------------------------------------- /lib/features/resize/index.js: -------------------------------------------------------------------------------- 1 | import RulesModule from '../rules'; 2 | import DraggingModule from '../dragging'; 3 | import PreviewSupportModule from '../preview-support'; 4 | 5 | import Resize from './Resize'; 6 | import ResizePreview from './ResizePreview'; 7 | import ResizeHandles from './ResizeHandles'; 8 | 9 | 10 | /** 11 | * @type { import('didi').ModuleDeclaration } 12 | */ 13 | export default { 14 | __depends__: [ 15 | RulesModule, 16 | DraggingModule, 17 | PreviewSupportModule 18 | ], 19 | __init__: [ 20 | 'resize', 21 | 'resizePreview', 22 | 'resizeHandles' 23 | ], 24 | resize: [ 'type', Resize ], 25 | resizePreview: [ 'type', ResizePreview ], 26 | resizeHandles: [ 'type', ResizeHandles ] 27 | }; 28 | -------------------------------------------------------------------------------- /lib/features/root-elements/RootElementsBehavior.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import CommandInterceptor from '../../command/CommandInterceptor'; 4 | 5 | /** 6 | * @typedef {import('didi').Injector} Injector 7 | * 8 | * @typedef {import('../../core/Canvas').default} Canvas 9 | */ 10 | 11 | /** 12 | * A modeling behavior that ensures we set the correct root element 13 | * as we undo and redo commands. 14 | * 15 | * @param {Canvas} canvas 16 | * @param {Injector} injector 17 | */ 18 | export default function RootElementsBehavior(canvas, injector) { 19 | 20 | injector.invoke(CommandInterceptor, this); 21 | 22 | this.executed(function(event) { 23 | var context = event.context; 24 | 25 | if (context.rootElement) { 26 | canvas.setRootElement(context.rootElement); 27 | } else { 28 | context.rootElement = canvas.getRootElement(); 29 | } 30 | }); 31 | 32 | this.revert(function(event) { 33 | var context = event.context; 34 | 35 | if (context.rootElement) { 36 | canvas.setRootElement(context.rootElement); 37 | } 38 | }); 39 | } 40 | 41 | inherits(RootElementsBehavior, CommandInterceptor); 42 | 43 | RootElementsBehavior.$inject = [ 'canvas', 'injector' ]; -------------------------------------------------------------------------------- /lib/features/root-elements/index.js: -------------------------------------------------------------------------------- 1 | import RootElementsBehavior from './RootElementsBehavior'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'rootElementsBehavior' ], 9 | rootElementsBehavior: [ 'type', RootElementsBehavior ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/rules/Rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('didi').Injector} Injector 3 | */ 4 | 5 | /** 6 | * A service that provides rules for certain diagram actions. 7 | * 8 | * The default implementation will hook into the {@link CommandStack} 9 | * to perform the actual rule evaluation. Make sure to provide the 10 | * `commandStack` service with this module if you plan to use it. 11 | * 12 | * Together with this implementation you may use the {@link import('./RuleProvider').default} 13 | * to implement your own rule checkers. 14 | * 15 | * This module is ment to be easily replaced, thus the tiny foot print. 16 | * 17 | * @param {Injector} injector 18 | */ 19 | export default function Rules(injector) { 20 | this._commandStack = injector.get('commandStack', false); 21 | } 22 | 23 | Rules.$inject = [ 'injector' ]; 24 | 25 | 26 | /** 27 | * Returns whether or not a given modeling action can be executed 28 | * in the specified context. 29 | * 30 | * This implementation will respond with allow unless anyone 31 | * objects. 32 | * 33 | * @param {string} action The action to be allowed or disallowed. 34 | * @param {Object} [context] The context for allowing or disallowing the action. 35 | * 36 | * @return {boolean|null} Wether the action is allowed. Returns `null` if the action 37 | * is to be ignored. 38 | */ 39 | Rules.prototype.allowed = function(action, context) { 40 | var allowed = true; 41 | 42 | var commandStack = this._commandStack; 43 | 44 | if (commandStack) { 45 | allowed = commandStack.canExecute(action, context); 46 | } 47 | 48 | // map undefined to true, i.e. no rules 49 | return allowed === undefined ? true : allowed; 50 | }; -------------------------------------------------------------------------------- /lib/features/rules/Rules.test.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import RulesModule from '.'; 4 | import Rules from './Rules'; 5 | 6 | const diagram = new Diagram({ 7 | modules: [ 8 | RulesModule 9 | ] 10 | }); 11 | 12 | const rules = diagram.get('rules'); 13 | 14 | rules.allowed('foo'); 15 | 16 | rules.allowed('foo', { bar: 'baz' }); -------------------------------------------------------------------------------- /lib/features/rules/index.js: -------------------------------------------------------------------------------- 1 | import Rules from './Rules'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'rules' ], 9 | rules: [ 'type', Rules ] 10 | }; 11 | -------------------------------------------------------------------------------- /lib/features/scheduler/index.js: -------------------------------------------------------------------------------- 1 | import Scheduler from './Scheduler'; 2 | 3 | export default { 4 | scheduler: [ 'type', Scheduler ] 5 | }; -------------------------------------------------------------------------------- /lib/features/search-pad/SearchPadProvider.ts: -------------------------------------------------------------------------------- 1 | import type { Element } from '../../model/Types'; 2 | 3 | type LegacyToken = { 4 | matched?: string; 5 | normal?: string; 6 | }; 7 | 8 | type ModernToken = { 9 | match: boolean; 10 | value: string; 11 | }; 12 | 13 | export type Token = LegacyToken | ModernToken; 14 | 15 | export type SearchResult = { 16 | primaryTokens: Token[]; 17 | secondaryTokens: Token[]; 18 | element: Element; 19 | }; 20 | 21 | export default interface SearchPadProvider { 22 | find(pattern: string): SearchResult[]; 23 | } 24 | -------------------------------------------------------------------------------- /lib/features/search-pad/SearchProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import SearchProvider, { SearchResult } from './SearchPadProvider'; 2 | 3 | import { create } from '../../model'; 4 | 5 | export class FooSearchProvider implements SearchProvider { 6 | find(pattern: string): SearchResult[] { 7 | return [ 8 | { 9 | primaryTokens: [ 10 | { 11 | matched: 'foo', 12 | normal: 'foo' 13 | }, 14 | { 15 | matched: pattern, 16 | normal: pattern 17 | }, 18 | { 19 | match: true, 20 | value: 'bar' 21 | } 22 | ], 23 | secondaryTokens: [ 24 | { 25 | matched: 'bar', 26 | normal: 'bar' 27 | }, 28 | { 29 | match: false, 30 | value: 'foo' 31 | } 32 | ], 33 | element: create('shape') 34 | } 35 | ]; 36 | } 37 | } -------------------------------------------------------------------------------- /lib/features/search-pad/index.js: -------------------------------------------------------------------------------- 1 | import OverlaysModule from '../overlays'; 2 | import SelectionModule from '../selection'; 3 | import TranslateModule from '../../i18n/translate/index.js'; 4 | 5 | import SearchPad from './SearchPad'; 6 | 7 | 8 | /** 9 | * @type { import('didi').ModuleDeclaration } 10 | */ 11 | export default { 12 | __depends__: [ 13 | TranslateModule, 14 | OverlaysModule, 15 | SelectionModule 16 | ], 17 | searchPad: [ 'type', SearchPad ] 18 | }; 19 | -------------------------------------------------------------------------------- /lib/features/search/index.js: -------------------------------------------------------------------------------- 1 | import search from './search'; 2 | 3 | /** 4 | * @type { import('didi').ModuleDeclaration } 5 | */ 6 | export default { 7 | search: [ 'value', search ] 8 | }; -------------------------------------------------------------------------------- /lib/features/selection/Selection.test.ts: -------------------------------------------------------------------------------- 1 | import Diagram from '../../Diagram'; 2 | 3 | import ElementFactory from '../../core/ElementFactory'; 4 | 5 | import SelectionModule from '.'; 6 | import Selection from './Selection'; 7 | 8 | const diagram = new Diagram({ 9 | modules: [ 10 | SelectionModule 11 | ] 12 | }); 13 | 14 | const elementFactory = diagram.get('elementFactory'); 15 | 16 | const shape = elementFactory.createShape(); 17 | 18 | const selection = diagram.get('selection'); 19 | 20 | selection.deselect(shape); 21 | 22 | selection.deselect({}); 23 | 24 | selection.get(); 25 | 26 | selection.isSelected(shape); 27 | 28 | selection.isSelected({}); 29 | 30 | selection.select(shape); 31 | 32 | selection.select({}); 33 | 34 | selection.select([ shape ]); 35 | 36 | selection.select([ shape ], true); -------------------------------------------------------------------------------- /lib/features/selection/SelectionVisuals.js: -------------------------------------------------------------------------------- 1 | import { 2 | forEach 3 | } from 'min-dash'; 4 | 5 | /** 6 | * @typedef {import('../../core/Canvas').default} Canvas 7 | * @typedef {import('../../core/EventBus').default} EventBus 8 | */ 9 | 10 | var MARKER_HOVER = 'hover', 11 | MARKER_SELECTED = 'selected'; 12 | 13 | /** 14 | * A plugin that adds a visible selection UI to shapes and connections 15 | * by appending the hover and selected classes to them. 16 | * 17 | * @class 18 | * 19 | * Makes elements selectable, too. 20 | * 21 | * @param {Canvas} canvas 22 | * @param {EventBus} eventBus 23 | */ 24 | export default function SelectionVisuals(canvas, eventBus) { 25 | this._canvas = canvas; 26 | 27 | function addMarker(e, cls) { 28 | canvas.addMarker(e, cls); 29 | } 30 | 31 | function removeMarker(e, cls) { 32 | canvas.removeMarker(e, cls); 33 | } 34 | 35 | eventBus.on('element.hover', function(event) { 36 | addMarker(event.element, MARKER_HOVER); 37 | }); 38 | 39 | eventBus.on('element.out', function(event) { 40 | removeMarker(event.element, MARKER_HOVER); 41 | }); 42 | 43 | eventBus.on('selection.changed', function(event) { 44 | 45 | function deselect(s) { 46 | removeMarker(s, MARKER_SELECTED); 47 | } 48 | 49 | function select(s) { 50 | addMarker(s, MARKER_SELECTED); 51 | } 52 | 53 | var oldSelection = event.oldSelection, 54 | newSelection = event.newSelection; 55 | 56 | forEach(oldSelection, function(e) { 57 | if (newSelection.indexOf(e) === -1) { 58 | deselect(e); 59 | } 60 | }); 61 | 62 | forEach(newSelection, function(e) { 63 | if (oldSelection.indexOf(e) === -1) { 64 | select(e); 65 | } 66 | }); 67 | }); 68 | } 69 | 70 | SelectionVisuals.$inject = [ 71 | 'canvas', 72 | 'eventBus' 73 | ]; 74 | -------------------------------------------------------------------------------- /lib/features/selection/index.js: -------------------------------------------------------------------------------- 1 | import InteractionEventsModule from '../interaction-events'; 2 | 3 | import Selection from './Selection'; 4 | import SelectionVisuals from './SelectionVisuals'; 5 | import SelectionBehavior from './SelectionBehavior'; 6 | 7 | 8 | /** 9 | * @type { import('didi').ModuleDeclaration } 10 | */ 11 | export default { 12 | __init__: [ 'selectionVisuals', 'selectionBehavior' ], 13 | __depends__: [ 14 | InteractionEventsModule, 15 | ], 16 | selection: [ 'type', Selection ], 17 | selectionVisuals: [ 'type', SelectionVisuals ], 18 | selectionBehavior: [ 'type', SelectionBehavior ] 19 | }; 20 | -------------------------------------------------------------------------------- /lib/features/snapping/index.js: -------------------------------------------------------------------------------- 1 | import CreateMoveSnapping from './CreateMoveSnapping'; 2 | import ResizeSnapping from './ResizeSnapping'; 3 | import Snapping from './Snapping'; 4 | 5 | 6 | /** 7 | * @type { import('didi').ModuleDeclaration } 8 | */ 9 | export default { 10 | __init__: [ 11 | 'createMoveSnapping', 12 | 'resizeSnapping', 13 | 'snapping' 14 | ], 15 | createMoveSnapping: [ 'type', CreateMoveSnapping ], 16 | resizeSnapping: [ 'type', ResizeSnapping ], 17 | snapping: [ 'type', Snapping ] 18 | }; -------------------------------------------------------------------------------- /lib/features/space-tool/index.js: -------------------------------------------------------------------------------- 1 | import DraggingModule from '../dragging'; 2 | import RulesModule from '../rules'; 3 | import ToolManagerModule from '../tool-manager'; 4 | import PreviewSupportModule from '../preview-support'; 5 | import MouseModule from '../mouse'; 6 | 7 | import SpaceTool from './SpaceTool'; 8 | import SpaceToolPreview from './SpaceToolPreview'; 9 | 10 | 11 | /** 12 | * @type { import('didi').ModuleDeclaration } 13 | */ 14 | export default { 15 | __init__: [ 'spaceToolPreview' ], 16 | __depends__: [ 17 | DraggingModule, 18 | RulesModule, 19 | ToolManagerModule, 20 | PreviewSupportModule, 21 | MouseModule 22 | ], 23 | spaceTool: [ 'type', SpaceTool ], 24 | spaceToolPreview: [ 'type', SpaceToolPreview ] 25 | }; 26 | -------------------------------------------------------------------------------- /lib/features/tool-manager/index.js: -------------------------------------------------------------------------------- 1 | import DraggingModule from '../dragging'; 2 | 3 | import ToolManager from './ToolManager'; 4 | 5 | 6 | /** 7 | * @type { import('didi').ModuleDeclaration } 8 | */ 9 | export default { 10 | __depends__: [ 11 | DraggingModule 12 | ], 13 | __init__: [ 'toolManager' ], 14 | toolManager: [ 'type', ToolManager ] 15 | }; 16 | -------------------------------------------------------------------------------- /lib/features/tooltips/index.js: -------------------------------------------------------------------------------- 1 | import Tooltips from './Tooltips'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'tooltips' ], 9 | tooltips: [ 'type', Tooltips ] 10 | }; -------------------------------------------------------------------------------- /lib/i18n/I18N.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../core/EventBus').default} EventBus 3 | */ 4 | 5 | /** 6 | * A component that handles language switching in a unified way. 7 | * 8 | * @param {EventBus} eventBus 9 | */ 10 | export default function I18N(eventBus) { 11 | 12 | /** 13 | * Inform components that the language changed. 14 | * 15 | * Emit a `i18n.changed` event for others to hook into, too. 16 | */ 17 | this.changed = function changed() { 18 | eventBus.fire('i18n.changed'); 19 | }; 20 | } 21 | 22 | I18N.$inject = [ 'eventBus' ]; -------------------------------------------------------------------------------- /lib/i18n/index.js: -------------------------------------------------------------------------------- 1 | import I18N from './I18N'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | i18n: [ 'type', I18N ] 9 | }; -------------------------------------------------------------------------------- /lib/i18n/translate/index.js: -------------------------------------------------------------------------------- 1 | import translate from './translate'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | translate: [ 'value', translate ] 9 | }; -------------------------------------------------------------------------------- /lib/i18n/translate/translate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { { 3 | * [key: string]: string; 4 | * } } TranslateReplacements 5 | */ 6 | 7 | /** 8 | * A simple translation stub to be used for multi-language support 9 | * in diagrams. Can be easily replaced with a more sophisticated 10 | * solution. 11 | * 12 | * @example 13 | * 14 | * ```javascript 15 | * // use it inside any diagram component by injecting `translate`. 16 | * 17 | * function MyService(translate) { 18 | * alert(translate('HELLO {you}', { you: 'You!' })); 19 | * } 20 | * ``` 21 | * 22 | * @param {string} template to interpolate 23 | * @param {TranslateReplacements} [replacements] a map with substitutes 24 | * 25 | * @return {string} the translated string 26 | */ 27 | export default function translate(template, replacements) { 28 | 29 | replacements = replacements || {}; 30 | 31 | return template.replace(/{([^}]+)}/g, function(_, key) { 32 | return replacements[key] || '{' + key + '}'; 33 | }); 34 | } -------------------------------------------------------------------------------- /lib/i18n/translate/translate.spec.ts: -------------------------------------------------------------------------------- 1 | import translate from './translate'; 2 | 3 | translate('foo {{bar}}', { bar: 'baz' }); 4 | translate('foo {{bar}}'); -------------------------------------------------------------------------------- /lib/layout/BaseLayouter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../core/Types').ElementLike} Element 3 | * @typedef {import('../core/Types').ConnectionLike} Connection 4 | * 5 | * @typedef {import('../util').Point} Point 6 | * 7 | * @typedef { { 8 | * connectionStart?: Point; 9 | * connectionEnd?: Point; 10 | * source?: Element; 11 | * target?: Element; 12 | * } } LayoutConnectionHints 13 | */ 14 | 15 | import { 16 | getMid 17 | } from './LayoutUtil'; 18 | 19 | 20 | /** 21 | * A base connection layouter implementation 22 | * that layouts the connection by directly connecting 23 | * mid(source) + mid(target). 24 | */ 25 | export default function BaseLayouter() {} 26 | 27 | 28 | /** 29 | * Return the new layouted waypoints for the given connection. 30 | * 31 | * The connection passed is still unchanged; you may figure out about 32 | * the new connection start / end via the layout hints provided. 33 | * 34 | * @param {Connection} connection 35 | * @param {LayoutConnectionHints} [hints] 36 | * 37 | * @return {Point[]} The waypoints of the laid out connection. 38 | */ 39 | BaseLayouter.prototype.layoutConnection = function(connection, hints) { 40 | 41 | hints = hints || {}; 42 | 43 | return [ 44 | hints.connectionStart || getMid(hints.source || connection.source), 45 | hints.connectionEnd || getMid(hints.target || connection.target) 46 | ]; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/layout/ConnectionDocking.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../../core/Types').ElementLike} Element 3 | * @typedef {import('../../core/Types').ConnectionLike} Connection 4 | * @typedef {import('../../core/Types').ShapeLike} Shape 5 | * 6 | * @typedef {import('../../util/Types').Point} Point 7 | */ 8 | 9 | /** 10 | * @typedef {Object} DockingPointDescriptor 11 | * @property {Point} point 12 | * @property {Point} actual 13 | * @property {number} idx 14 | */ 15 | 16 | /** 17 | * A layout component for connections that retrieves waypoint information. 18 | * 19 | * @class 20 | * @constructor 21 | */ 22 | export default function ConnectionDocking() {} 23 | 24 | 25 | /** 26 | * Return the actual waypoints of the connection (visually). 27 | * 28 | * @param {Connection} connection 29 | * @param {Element} [source] 30 | * @param {Element} [target] 31 | * 32 | * @return {Point[]} 33 | */ 34 | ConnectionDocking.prototype.getCroppedWaypoints = function(connection, source, target) { 35 | return connection.waypoints; 36 | }; 37 | 38 | /** 39 | * Return the connection docking point on the specified shape 40 | * 41 | * @param {Connection} connection 42 | * @param {Shape} shape 43 | * @param {boolean} [dockStart=false] 44 | * 45 | * @return {DockingPointDescriptor} 46 | */ 47 | ConnectionDocking.prototype.getDockingPoint = function(connection, shape, dockStart) { 48 | 49 | var waypoints = connection.waypoints, 50 | dockingIdx, 51 | dockingPoint; 52 | 53 | dockingIdx = dockStart ? 0 : waypoints.length - 1; 54 | dockingPoint = waypoints[dockingIdx]; 55 | 56 | return { 57 | point: dockingPoint, 58 | actual: dockingPoint, 59 | idx: dockingIdx 60 | }; 61 | }; -------------------------------------------------------------------------------- /lib/model/Types.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from '../util/Types'; 2 | 3 | export type ElementLike = { 4 | id: string; 5 | businessObject?: any; 6 | } & Record; 7 | 8 | export type Element = ElementLike & { 9 | label?: Label; 10 | labels: Label[]; 11 | parent?: Element; 12 | incoming: Connection[]; 13 | outgoing: Connection[]; 14 | } 15 | 16 | export type ShapeLike = ElementLike & { 17 | x: number; 18 | y: number; 19 | width: number; 20 | height: number; 21 | }; 22 | 23 | export type Shape = ShapeLike & Element & { 24 | isFrame?: boolean; 25 | children: Element[]; 26 | host?: Shape; 27 | attachers: Shape[]; 28 | } 29 | 30 | export type RootLike = ElementLike & { 31 | isImplicit?: boolean; 32 | }; 33 | 34 | export type Root = RootLike & Element; 35 | 36 | export type LabelLike = ShapeLike; 37 | 38 | export type Label = LabelLike & Shape & { 39 | labelTarget?: Element; 40 | } 41 | 42 | export type ConnectionLike = { 43 | waypoints: Point[]; 44 | } & ElementLike; 45 | 46 | export type Connection = ConnectionLike & Element & { 47 | source?: Element; 48 | target?: Element; 49 | }; 50 | 51 | export type ParentLike = ShapeLike | RootLike; 52 | 53 | export type Parent = Shape | Root; 54 | -------------------------------------------------------------------------------- /lib/model/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { create } from './index'; 2 | 3 | const connection = create('connection', { 4 | id: 'foo', 5 | waypoints: [ 6 | { x: 100, y: 100 }, 7 | { x: 200, y: 100 } 8 | ], 9 | source: create('shape', {}), 10 | target: create('shape', {}) 11 | }); 12 | 13 | connection.businessObject = {}; 14 | 15 | const label = create('label', { 16 | id: 'foo', 17 | x: 100, 18 | y: 100, 19 | width: 100, 20 | height: 100, 21 | labelTarget: create('shape', {}) 22 | }); 23 | 24 | label.businessObject = {}; 25 | 26 | const root = create('root', { 27 | id: 'foo', 28 | x: 100, 29 | y: 100, 30 | width: 100, 31 | height: 100 32 | }); 33 | 34 | root.businessObject = {}; 35 | 36 | const shape = create('shape', { 37 | id: 'foo', 38 | x: 100, 39 | y: 100, 40 | width: 100, 41 | height: 100 42 | }); 43 | 44 | shape.businessObject = {}; -------------------------------------------------------------------------------- /lib/navigation/keyboard-move/index.js: -------------------------------------------------------------------------------- 1 | import KeyboardModule from '../../features/keyboard'; 2 | 3 | import KeyboardMove from './KeyboardMove'; 4 | 5 | 6 | /** 7 | * @type { import('didi').ModuleDeclaration } 8 | */ 9 | export default { 10 | __depends__: [ 11 | KeyboardModule 12 | ], 13 | __init__: [ 'keyboardMove' ], 14 | keyboardMove: [ 'type', KeyboardMove ] 15 | }; -------------------------------------------------------------------------------- /lib/navigation/movecanvas/index.js: -------------------------------------------------------------------------------- 1 | import MoveCanvas from './MoveCanvas'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'moveCanvas' ], 9 | moveCanvas: [ 'type', MoveCanvas ] 10 | }; -------------------------------------------------------------------------------- /lib/navigation/zoomscroll/ZoomUtil.js: -------------------------------------------------------------------------------- 1 | import { 2 | log10 3 | } from '../../util/Math'; 4 | 5 | /** 6 | * Get step size for given range and number of steps. 7 | * 8 | * @param {Object} range 9 | * @param {number} range.min 10 | * @param {number} range.max 11 | * @param {number} steps 12 | */ 13 | export function getStepSize(range, steps) { 14 | 15 | var minLinearRange = log10(range.min), 16 | maxLinearRange = log10(range.max); 17 | 18 | var absoluteLinearRange = Math.abs(minLinearRange) + Math.abs(maxLinearRange); 19 | 20 | return absoluteLinearRange / steps; 21 | } 22 | 23 | /** 24 | * @param {Object} range 25 | * @param {number} range.min 26 | * @param {number} range.max 27 | * @param {number} scale 28 | */ 29 | export function cap(range, scale) { 30 | return Math.max(range.min, Math.min(range.max, scale)); 31 | } 32 | -------------------------------------------------------------------------------- /lib/navigation/zoomscroll/index.js: -------------------------------------------------------------------------------- 1 | import ZoomScroll from './ZoomScroll'; 2 | 3 | 4 | /** 5 | * @type { import('didi').ModuleDeclaration } 6 | */ 7 | export default { 8 | __init__: [ 'zoomScroll' ], 9 | zoomScroll: [ 'type', ZoomScroll ] 10 | }; -------------------------------------------------------------------------------- /lib/ui/index.js: -------------------------------------------------------------------------------- 1 | export * from '@bpmn-io/diagram-js-ui'; -------------------------------------------------------------------------------- /lib/util/ClickTrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../core/EventBus').default} EventBus 3 | */ 4 | 5 | var TRAP_PRIORITY = 5000; 6 | 7 | /** 8 | * Installs a click trap that prevents a ghost click following a dragging operation. 9 | * 10 | * @param {EventBus} eventBus 11 | * @param {string} [eventName='element.click'] 12 | * 13 | * @return {() => void} a function to immediately remove the installed trap. 14 | */ 15 | export function install(eventBus, eventName) { 16 | 17 | eventName = eventName || 'element.click'; 18 | 19 | function trap() { 20 | return false; 21 | } 22 | 23 | eventBus.once(eventName, TRAP_PRIORITY, trap); 24 | 25 | return function() { 26 | eventBus.off(eventName, trap); 27 | }; 28 | } -------------------------------------------------------------------------------- /lib/util/Collections.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Failsafe remove an element from a collection 3 | * 4 | * @param {Array} [collection] 5 | * @param {Object} [element] 6 | * 7 | * @return {number} the previous index of the element 8 | */ 9 | export function remove(collection, element) { 10 | 11 | if (!collection || !element) { 12 | return -1; 13 | } 14 | 15 | var idx = collection.indexOf(element); 16 | 17 | if (idx !== -1) { 18 | collection.splice(idx, 1); 19 | } 20 | 21 | return idx; 22 | } 23 | 24 | /** 25 | * Fail save add an element to the given connection, ensuring 26 | * it does not yet exist. 27 | * 28 | * @param {Array} collection 29 | * @param {Object} element 30 | * @param {number} [idx] 31 | */ 32 | export function add(collection, element, idx) { 33 | 34 | if (!collection || !element) { 35 | return; 36 | } 37 | 38 | if (typeof idx !== 'number') { 39 | idx = -1; 40 | } 41 | 42 | var currentIdx = collection.indexOf(element); 43 | 44 | if (currentIdx !== -1) { 45 | 46 | if (currentIdx === idx) { 47 | 48 | // nothing to do, position has not changed 49 | return; 50 | } else { 51 | 52 | if (idx !== -1) { 53 | 54 | // remove from current position 55 | collection.splice(currentIdx, 1); 56 | } else { 57 | 58 | // already exists in collection 59 | return; 60 | } 61 | } 62 | } 63 | 64 | if (idx !== -1) { 65 | 66 | // insert at specified position 67 | collection.splice(idx, 0, element); 68 | } else { 69 | 70 | // push to end 71 | collection.push(element); 72 | } 73 | } 74 | 75 | 76 | /** 77 | * Fail save get the index of an element in a collection. 78 | * 79 | * @param {Array} collection 80 | * @param {Object} element 81 | * 82 | * @return {number} the index or -1 if collection or element do 83 | * not exist or the element is not contained. 84 | */ 85 | export function indexOf(collection, element) { 86 | 87 | if (!collection || !element) { 88 | return -1; 89 | } 90 | 91 | return collection.indexOf(element); 92 | } 93 | -------------------------------------------------------------------------------- /lib/util/Cursor.js: -------------------------------------------------------------------------------- 1 | import { 2 | classes as domClasses 3 | } from 'min-dom'; 4 | 5 | var CURSOR_CLS_PATTERN = /^djs-cursor-.*$/; 6 | 7 | /** 8 | * @param {string} mode 9 | */ 10 | export function set(mode) { 11 | var classes = domClasses(document.body); 12 | 13 | classes.removeMatching(CURSOR_CLS_PATTERN); 14 | 15 | if (mode) { 16 | classes.add('djs-cursor-' + mode); 17 | } 18 | } 19 | 20 | export function unset() { 21 | set(null); 22 | } 23 | 24 | /** 25 | * @param {string} mode 26 | * 27 | * @return {boolean} 28 | */ 29 | export function has(mode) { 30 | var classes = domClasses(document.body); 31 | 32 | return classes.has('djs-cursor-' + mode); 33 | } 34 | -------------------------------------------------------------------------------- /lib/util/EscapeUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} str 3 | * 4 | * @return {string} 5 | */ 6 | export function escapeCSS(str) { 7 | return CSS.escape(str); 8 | } 9 | 10 | var HTML_ESCAPE_MAP = { 11 | '&': '&', 12 | '<': '<', 13 | '>': '>', 14 | '"': '"', 15 | '\'': ''' 16 | }; 17 | 18 | /** 19 | * @param {string} str 20 | * 21 | * @return {string} 22 | */ 23 | export function escapeHTML(str) { 24 | str = '' + str; 25 | 26 | return str && str.replace(/[&<>"']/g, function(match) { 27 | return HTML_ESCAPE_MAP[match]; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/util/Event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../util/Types').Point} Point 3 | */ 4 | 5 | function __stopPropagation(event) { 6 | if (!event || typeof event.stopPropagation !== 'function') { 7 | return; 8 | } 9 | 10 | event.stopPropagation(); 11 | } 12 | 13 | /** 14 | * @param {import('../core/EventBus').Event} event 15 | * 16 | * @return {Event} 17 | */ 18 | export function getOriginal(event) { 19 | return event.originalEvent || event.srcEvent; 20 | } 21 | 22 | /** 23 | * @param {Event|import('../core/EventBus').Event} event 24 | */ 25 | export function stopPropagation(event) { 26 | __stopPropagation(event); 27 | __stopPropagation(getOriginal(event)); 28 | } 29 | 30 | /** 31 | * @param {Event} event 32 | * 33 | * @return {Point|null} 34 | */ 35 | export function toPoint(event) { 36 | 37 | if (event.pointers && event.pointers.length) { 38 | event = event.pointers[0]; 39 | } 40 | 41 | if (event.touches && event.touches.length) { 42 | event = event.touches[0]; 43 | } 44 | 45 | return event ? { 46 | x: event.clientX, 47 | y: event.clientY 48 | } : null; 49 | } -------------------------------------------------------------------------------- /lib/util/GraphicsUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SVGs for elements are generated by the {@link GraphicsFactory}. 3 | * 4 | * This utility gives quick access to the important semantic 5 | * parts of an element. 6 | */ 7 | 8 | /** 9 | * Returns the visual part of a diagram element. 10 | * 11 | * @param {SVGElement} gfx 12 | * 13 | * @return {SVGElement} 14 | */ 15 | export function getVisual(gfx) { 16 | return gfx.childNodes[0]; 17 | } 18 | 19 | /** 20 | * Returns the children for a given diagram element. 21 | * 22 | * @param {SVGElement} gfx 23 | * @return {SVGElement} 24 | */ 25 | export function getChildren(gfx) { 26 | return gfx.parentNode.childNodes[1]; 27 | } -------------------------------------------------------------------------------- /lib/util/IdGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Util that provides unique IDs. 3 | * 4 | * @class 5 | * @constructor 6 | * 7 | * The ids can be customized via a given prefix and contain a random value to avoid collisions. 8 | * 9 | * @param {string} [prefix] a prefix to prepend to generated ids (for better readability) 10 | */ 11 | export default function IdGenerator(prefix) { 12 | 13 | this._counter = 0; 14 | this._prefix = (prefix ? prefix + '-' : '') + Math.floor(Math.random() * 1000000000) + '-'; 15 | } 16 | 17 | /** 18 | * Returns a next unique ID. 19 | * 20 | * @return {string} the id 21 | */ 22 | IdGenerator.prototype.next = function() { 23 | return this._prefix + (++this._counter); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/util/Math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the logarithm of x with base 10. 3 | * 4 | * @param {number} x 5 | */ 6 | export function log10(x) { 7 | return Math.log(x) / Math.log(10); 8 | } 9 | 10 | export { delta as substract } from './PositionUtil'; 11 | -------------------------------------------------------------------------------- /lib/util/ModelUtil.js: -------------------------------------------------------------------------------- 1 | import { 2 | has, 3 | isNil, 4 | isObject 5 | } from 'min-dash'; 6 | 7 | /** 8 | * Checks whether a value is an instance of Connection. 9 | * 10 | * @param {any} value 11 | * 12 | * @return {boolean} 13 | */ 14 | export function isConnection(value) { 15 | return isObject(value) && has(value, 'waypoints'); 16 | } 17 | 18 | /** 19 | * Checks whether a value is an instance of Label. 20 | * 21 | * @param {any} value 22 | * 23 | * @return {boolean} 24 | */ 25 | export function isLabel(value) { 26 | return isObject(value) && has(value, 'labelTarget'); 27 | } 28 | 29 | /** 30 | * Checks whether a value is an instance of Root. 31 | * 32 | * @param {any} value 33 | * 34 | * @return {boolean} 35 | */ 36 | export function isRoot(value) { 37 | return isObject(value) && isNil(value.parent); 38 | } -------------------------------------------------------------------------------- /lib/util/Mouse.js: -------------------------------------------------------------------------------- 1 | import { 2 | getOriginal as getOriginalEvent 3 | } from './Event'; 4 | 5 | import { 6 | isMac 7 | } from './Platform'; 8 | 9 | export { 10 | isMac 11 | } from './Platform'; 12 | 13 | /** 14 | * @param {MouseEvent} event 15 | * @param {string} button 16 | * 17 | * @return {boolean} 18 | */ 19 | export function isButton(event, button) { 20 | return (getOriginalEvent(event) || event).button === button; 21 | } 22 | 23 | /** 24 | * @param {MouseEvent} event 25 | * 26 | * @return {boolean} 27 | */ 28 | export function isPrimaryButton(event) { 29 | 30 | // button === 0 -> left áka primary mouse button 31 | return isButton(event, 0); 32 | } 33 | 34 | /** 35 | * @param {MouseEvent} event 36 | * 37 | * @return {boolean} 38 | */ 39 | export function isAuxiliaryButton(event) { 40 | 41 | // button === 1 -> auxiliary áka wheel button 42 | return isButton(event, 1); 43 | } 44 | 45 | /** 46 | * @param {MouseEvent} event 47 | * 48 | * @return {boolean} 49 | */ 50 | export function isSecondaryButton(event) { 51 | 52 | // button === 2 -> right áka secondary button 53 | return isButton(event, 2); 54 | } 55 | 56 | /** 57 | * @param {MouseEvent} event 58 | * 59 | * @return {boolean} 60 | */ 61 | export function hasPrimaryModifier(event) { 62 | var originalEvent = getOriginalEvent(event) || event; 63 | 64 | if (!isPrimaryButton(event)) { 65 | return false; 66 | } 67 | 68 | // Use cmd as primary modifier key for mac OS 69 | if (isMac()) { 70 | return originalEvent.metaKey; 71 | } else { 72 | return originalEvent.ctrlKey; 73 | } 74 | } 75 | 76 | /** 77 | * @param {MouseEvent} event 78 | * 79 | * @return {boolean} 80 | */ 81 | export function hasSecondaryModifier(event) { 82 | var originalEvent = getOriginalEvent(event) || event; 83 | 84 | return isPrimaryButton(event) && originalEvent.shiftKey; 85 | } 86 | -------------------------------------------------------------------------------- /lib/util/Platform.js: -------------------------------------------------------------------------------- 1 | export function isMac() { 2 | return (/mac/i).test(navigator.platform); 3 | } -------------------------------------------------------------------------------- /lib/util/PositionUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../util/Types').Point} Point 3 | * @typedef {import('../util/Types').Rect} Rect 4 | */ 5 | 6 | /** 7 | * @param {Rect} bounds 8 | * @return {Point} 9 | */ 10 | export function center(bounds) { 11 | return { 12 | x: bounds.x + (bounds.width / 2), 13 | y: bounds.y + (bounds.height / 2) 14 | }; 15 | } 16 | 17 | 18 | /** 19 | * @param {Point} a 20 | * @param {Point} b 21 | * @return {Point} 22 | */ 23 | export function delta(a, b) { 24 | return { 25 | x: a.x - b.x, 26 | y: a.y - b.y 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /lib/util/Removal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove from the beginning of a collection until it is empty. 3 | * 4 | * This is a null-safe operation that ensures elements 5 | * are being removed from the given collection until the 6 | * collection is empty. 7 | * 8 | * The implementation deals with the fact that a remove operation 9 | * may touch, i.e. remove multiple elements in the collection 10 | * at a time. 11 | * 12 | * @param {Object[]} [collection] 13 | * @param {(element: Object) => void} removeFn 14 | * 15 | * @return {Object[]} the cleared collection 16 | */ 17 | export function saveClear(collection, removeFn) { 18 | 19 | if (typeof removeFn !== 'function') { 20 | throw new Error('removeFn iterator must be a function'); 21 | } 22 | 23 | if (!collection) { 24 | return; 25 | } 26 | 27 | var e; 28 | 29 | while ((e = collection[0])) { 30 | removeFn(e); 31 | } 32 | 33 | return collection; 34 | } 35 | -------------------------------------------------------------------------------- /lib/util/RenderUtil.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | toSVGPoints, 3 | componentsToPath 4 | } from './RenderUtil'; 5 | 6 | toSVGPoints([ 7 | { x: 10, y: 100 }, 8 | { x: 100, y: 100 } 9 | ]); 10 | 11 | componentsToPath([ 12 | [ 'M', 0, 0 ], 13 | [ 'L', 10, 10 ], 14 | [ 'H', 50 ] 15 | ]); -------------------------------------------------------------------------------- /lib/util/SvgTransformUtil.js: -------------------------------------------------------------------------------- 1 | import { 2 | transform as svgTransform, 3 | createTransform 4 | } from 'tiny-svg'; 5 | 6 | 7 | /** 8 | * @param {SVGElement} gfx 9 | * @param {number} x 10 | * @param {number} y 11 | * @param {number} [angle] 12 | * @param {number} [amount] 13 | */ 14 | export function transform(gfx, x, y, angle, amount) { 15 | var translate = createTransform(); 16 | translate.setTranslate(x, y); 17 | 18 | var rotate = createTransform(); 19 | rotate.setRotate(angle || 0, 0, 0); 20 | 21 | var scale = createTransform(); 22 | scale.setScale(amount || 1, amount || 1); 23 | 24 | svgTransform(gfx, [ translate, rotate, scale ]); 25 | } 26 | 27 | 28 | /** 29 | * @param {SVGElement} gfx 30 | * @param {number} x 31 | * @param {number} y 32 | */ 33 | export function translate(gfx, x, y) { 34 | var translate = createTransform(); 35 | translate.setTranslate(x, y); 36 | 37 | svgTransform(gfx, translate); 38 | } 39 | 40 | 41 | /** 42 | * @param {SVGElement} gfx 43 | * @param {number} angle 44 | */ 45 | export function rotate(gfx, angle) { 46 | var rotate = createTransform(); 47 | rotate.setRotate(angle, 0, 0); 48 | 49 | svgTransform(gfx, rotate); 50 | } 51 | 52 | 53 | /** 54 | * @param {SVGElement} gfx 55 | * @param {number} amount 56 | */ 57 | export function scale(gfx, amount) { 58 | var scale = createTransform(); 59 | scale.setScale(amount, amount); 60 | 61 | svgTransform(gfx, scale); 62 | } 63 | -------------------------------------------------------------------------------- /lib/util/Text.spec.ts: -------------------------------------------------------------------------------- 1 | // instantiation 2 | 3 | import Text from './Text'; 4 | 5 | const text = new Text({ 6 | style: { 7 | fontFamily: 'Arial, sans-serif', 8 | fontSize: 1, 9 | fontWeight: 'normal', 10 | lineHeight: 12 11 | } 12 | }); 13 | 14 | // usage 15 | 16 | text.createText('foo bar', { 17 | box: { 18 | width: 100, 19 | height: 50 20 | } 21 | }); 22 | 23 | text.getDimensions('foo bar', { 24 | align: 'center-middle' 25 | }); -------------------------------------------------------------------------------- /lib/util/Types.ts: -------------------------------------------------------------------------------- 1 | export type Point = { 2 | x: number; 3 | y: number; 4 | }; 5 | 6 | export type ScrollDelta = { 7 | dx?: number; 8 | dy?: number; 9 | } 10 | 11 | export type Vector = Point; 12 | 13 | export type Dimension = 'width' | 'height'; 14 | 15 | export type Dimensions = { 16 | width: number; 17 | height: number; 18 | }; 19 | 20 | export type Rect = Dimensions & Point; 21 | 22 | export type RectTRBL = { 23 | top: number; 24 | right: number; 25 | bottom: number; 26 | left: number; 27 | }; 28 | 29 | export type Axis = 'x' | 'y'; 30 | 31 | export type Direction = 'n' | 'w' | 's' | 'e' | 'nw' | 'ne' | 'sw' | 'se'; 32 | 33 | export type DirectionTRBL = 'top' | 'right' | 'bottom' | 'left' | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; 34 | 35 | export type Intersection = 'intersect'; -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>bpmn-io/renovate-config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/TestHelper.js: -------------------------------------------------------------------------------- 1 | export * from './helper'; 2 | 3 | import { 4 | insertCSS 5 | } from './helper'; 6 | 7 | import diagramCSS from '../assets/diagram-js.css'; 8 | 9 | insertCSS('diagram-js.css', diagramCSS); 10 | 11 | insertCSS('diagram-js-testing.css', 12 | 'body .test-container { height: auto }' + 13 | 'body .test-content-container { height: 90vh; }' 14 | ); 15 | 16 | 17 | import BoundsMatchers from './matchers/BoundsMatchers'; 18 | import ConnectionMatchers from './matchers/ConnectionMatchers'; 19 | 20 | /* global chai */ 21 | 22 | // add suite specific matchers 23 | chai.use(BoundsMatchers); 24 | chai.use(ConnectionMatchers); 25 | -------------------------------------------------------------------------------- /test/coverageBundle.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | 3 | var allTests = require.context('.', true, /Spec\.js$/); 4 | 5 | allTests.keys().forEach(allTests); 6 | 7 | var allSources = require.context('../lib', true, /.*\.js$/); 8 | 9 | allSources.keys().forEach(allSources); -------------------------------------------------------------------------------- /test/matchers/BoundsMatchersSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | create 3 | } from 'lib/model'; 4 | 5 | 6 | describe('matchers/BoundsMatchers', function() { 7 | 8 | describe('bounds', function() { 9 | 10 | it('should .have.bounds() with Bounds', function() { 11 | 12 | // given 13 | var bounds = { x: 100, y: 100, width: 200, height: 200 }, 14 | expectedBounds = { x: 100, y: 100, width: 200, height: 200 }; 15 | 16 | // then 17 | expect(bounds).to.have.bounds(expectedBounds); 18 | }); 19 | 20 | 21 | it('should .not.have.bounds() with Bounds', function() { 22 | 23 | // given 24 | var bounds = { x: 100, y: 100, width: 200, height: 200 }, 25 | expectedBounds = { x: 50, y: 100, width: 200, height: 200 }; 26 | 27 | // then 28 | expect(bounds).not.to.have.bounds(expectedBounds); 29 | }); 30 | 31 | 32 | it('should .have.bounds() with Shape', function() { 33 | 34 | // given 35 | var element = create('shape', { 36 | id: 'someShape', 37 | x: 100, y: 100, 38 | width: 200, height: 200 39 | }); 40 | 41 | var expectedBounds = { 42 | x: 100, y: 100, 43 | width: 200, height: 200 44 | }; 45 | 46 | // then 47 | expect(element).to.have.bounds(expectedBounds); 48 | }); 49 | 50 | 51 | it('should .not.have.bounds() with Shape', function() { 52 | 53 | // given 54 | var element = create('shape', { 55 | id: 'someShape', 56 | x: 100, y: 100, 57 | width: 200, height: 200 58 | }); 59 | 60 | var expectedBounds = { 61 | x: 50, y: 100, 62 | width: 200, height: 200 63 | }; 64 | 65 | // then 66 | expect(element).not.to.have.bounds(expectedBounds); 67 | }); 68 | 69 | }); 70 | 71 | }); -------------------------------------------------------------------------------- /test/spec/draw/StylesSpec.js: -------------------------------------------------------------------------------- 1 | import Styles from 'lib/draw/Styles'; 2 | 3 | 4 | describe('draw/Styles', function() { 5 | 6 | var styles = new Styles(); 7 | 8 | describe('#cls', function() { 9 | 10 | it('should create style with traits given', function() { 11 | 12 | // given 13 | var expectedStyle = { 14 | 'class': 'foo', 15 | 'fill': 'none' 16 | }; 17 | 18 | // when 19 | var style = styles.cls('foo', [ 'no-fill' ]); 20 | 21 | // then 22 | expect(style).to.eql(expectedStyle); 23 | }); 24 | 25 | 26 | it('should create style without traits given', function() { 27 | 28 | // given 29 | var expectedStyle = { 30 | 'class': 'foo', 31 | 'fill': 'none' 32 | }; 33 | 34 | // when 35 | var style = styles.cls('foo', { fill: 'none' }); 36 | 37 | // then 38 | expect(style).to.eql(expectedStyle); 39 | }); 40 | 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/spec/environment/MockingSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | 7 | import EventBus from 'lib/core/EventBus'; 8 | 9 | 10 | describe('environment/Mocking', function() { 11 | 12 | var mockEventBus, bootstrapCalled; 13 | 14 | 15 | beforeEach(bootstrapDiagram(function() { 16 | mockEventBus = new EventBus(); 17 | 18 | bootstrapCalled = true; 19 | 20 | return { 21 | eventBus: mockEventBus 22 | }; 23 | })); 24 | 25 | afterEach(function() { 26 | bootstrapCalled = false; 27 | }); 28 | 29 | 30 | it('should use spy', inject(function(eventBus) { 31 | expect(eventBus).to.equal(mockEventBus); 32 | expect(bootstrapCalled).to.equal(true); 33 | })); 34 | 35 | 36 | it('should reparse bootstrap code', inject(function(eventBus) { 37 | expect(bootstrapCalled).to.equal(true); 38 | })); 39 | 40 | }); -------------------------------------------------------------------------------- /test/spec/features/align-elements/rules/TestRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function TestRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | TestRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(TestRules, RuleProvider); 12 | 13 | 14 | TestRules.prototype.init = function() { 15 | var self = this; 16 | 17 | this.addRule('elements.align', function(context) { 18 | return self._result; 19 | }); 20 | }; 21 | 22 | TestRules.prototype.setResult = function(result) { 23 | this._result = result; 24 | }; 25 | -------------------------------------------------------------------------------- /test/spec/features/align-elements/rules/index.js: -------------------------------------------------------------------------------- 1 | import TestRules from './TestRules'; 2 | 3 | export default { 4 | __init__: [ 'testRules' ], 5 | testRules: [ 'type', TestRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/attach-support/rules/AttachRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | import { forEach } from 'min-dash'; 6 | 7 | export default function AttachRules(eventBus) { 8 | RuleProvider.call(this, eventBus); 9 | } 10 | 11 | AttachRules.$inject = [ 'eventBus' ]; 12 | 13 | inherits(AttachRules, RuleProvider); 14 | 15 | 16 | AttachRules.prototype.init = function() { 17 | 18 | this.addRule('elements.move', function(context) { 19 | 20 | if (context.target && context.target.retainAttachmentIds) { 21 | var attachmentIds = context.target.retainAttachmentIds; 22 | 23 | return retainmentAllowed(attachmentIds, context.shapes); 24 | } 25 | }); 26 | 27 | this.addRule('elements.move', function(context) { 28 | var shapes = context.shapes, 29 | target = context.target; 30 | 31 | if (shapes.length === 1 && shapes[0].id === 'attacher' && target) { 32 | 33 | if (target.id === 'host' || target.id === 'host2') { 34 | return 'attach'; 35 | } else if (target.id === 'parent') { 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | } 41 | 42 | if (shapes.length === 1 && shapes[0].id === 'attacher2') { 43 | return false; 44 | } 45 | }); 46 | 47 | this.addRule('connection.reconnect', function(context) { 48 | 49 | // do not allow reconnection to element 50 | // that is attached to #parent element 51 | 52 | var source = context.source, 53 | target = context.target; 54 | 55 | function isChildOfElementWithIdParent(element) { 56 | return element && element.parent.id === 'parent'; 57 | } 58 | 59 | return ![ 60 | source.host, 61 | target.host 62 | ].some(isChildOfElementWithIdParent); 63 | }); 64 | 65 | // restrict resizing only for hosts (defaults to allow all) 66 | this.addRule('shape.resize', function(context) { 67 | var shape = context.shape; 68 | 69 | return shape.attachers.length > 0 && shape.resizable !== false; 70 | }); 71 | }; 72 | 73 | 74 | /** 75 | * Returns 'attach' if all shape ids are contained in the attachmentIds array. 76 | * Returns false if at least one of the shapes is not contained. 77 | * 78 | * @param {Array} attachmentIds 79 | * @param {Array} shapes 80 | */ 81 | function retainmentAllowed(attachmentIds, shapes) { 82 | var allowed = 'attach'; 83 | forEach(shapes, function(shape) { 84 | if (attachmentIds.indexOf(shape.id) === -1) { 85 | allowed = false; 86 | return; 87 | } 88 | }); 89 | return allowed; 90 | } 91 | -------------------------------------------------------------------------------- /test/spec/features/attach-support/rules/index.js: -------------------------------------------------------------------------------- 1 | import AttachRules from './AttachRules'; 2 | 3 | export default { 4 | __init__: [ 'attachRules' ], 5 | attachRules: [ 'type', AttachRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/spec/features/bendpoints/rules/BendpointRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function ConnectRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | ConnectRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(ConnectRules, RuleProvider); 12 | 13 | 14 | ConnectRules.prototype.init = function() { 15 | 16 | this.addRule('connection.reconnect', function(context) { 17 | var source = context.source, 18 | target = context.target; 19 | 20 | return source.type === target.type || source.type === 'C' || target.type === 'D'; 21 | }); 22 | 23 | this.addRule('connection.updateWaypoints', function() { 24 | return true; 25 | }); 26 | 27 | }; -------------------------------------------------------------------------------- /test/spec/features/bendpoints/rules/index.js: -------------------------------------------------------------------------------- 1 | import BendpointRules from './BendpointRules'; 2 | 3 | export default { 4 | __init__: [ 'bendpointRules' ], 5 | bendpointRules: [ 'type', BendpointRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/clipboard/ClipboardSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import clipboardModule from 'lib/features/clipboard'; 7 | 8 | 9 | describe('features/clipboard', function() { 10 | 11 | var contents = { foo: 'bar' }; 12 | 13 | beforeEach(bootstrapDiagram({ 14 | modules: [ clipboardModule ] 15 | })); 16 | 17 | 18 | it('should set element to clipboard', inject(function(clipboard) { 19 | 20 | // when 21 | clipboard.set(contents); 22 | 23 | var result = clipboard.get(); 24 | 25 | // then 26 | expect(result).to.eql(contents); 27 | expect(clipboard.isEmpty()).to.be.false; 28 | })); 29 | 30 | 31 | it('should clear the clipboard', inject(function(clipboard) { 32 | 33 | // when 34 | clipboard.set(contents); 35 | 36 | var oldClipboard = clipboard.clear(); 37 | 38 | // then 39 | expect(clipboard.isEmpty()).to.be.true; 40 | expect(oldClipboard).to.contain(contents); 41 | })); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/spec/features/connect/rules/ConnectRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | import { 6 | isFrameElement 7 | } from 'lib/util/Elements'; 8 | 9 | export default function ConnectRules(eventBus) { 10 | RuleProvider.call(this, eventBus); 11 | } 12 | 13 | ConnectRules.$inject = [ 'eventBus' ]; 14 | 15 | inherits(ConnectRules, RuleProvider); 16 | 17 | 18 | ConnectRules.prototype.init = function() { 19 | 20 | this.addRule('connection.create', function(context) { 21 | var source = context.source, 22 | target = context.target; 23 | 24 | if (isFrameElement(source) || isFrameElement(target)) { 25 | return false; 26 | } 27 | 28 | if (source && target && source.parent === target.parent) { 29 | return { type: 'test:Connection' }; 30 | } 31 | 32 | return false; 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /test/spec/features/connect/rules/index.js: -------------------------------------------------------------------------------- 1 | import ConnectRules from './ConnectRules'; 2 | 3 | export default { 4 | __init__: [ 'connectRules' ], 5 | connectRules: [ 'type', ConnectRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/context-pad/resources/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/diagram-js/c87769f2a275c1882e79b318ddd3cd971574e385/test/spec/features/context-pad/resources/a.png -------------------------------------------------------------------------------- /test/spec/features/context-pad/resources/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/diagram-js/c87769f2a275c1882e79b318ddd3cd971574e385/test/spec/features/context-pad/resources/b.png -------------------------------------------------------------------------------- /test/spec/features/context-pad/resources/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/diagram-js/c87769f2a275c1882e79b318ddd3cd971574e385/test/spec/features/context-pad/resources/c.png -------------------------------------------------------------------------------- /test/spec/features/copy-paste/rules/CopyPasteRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | 6 | export default function CopyPasteRules(eventBus) { 7 | RuleProvider.call(this, eventBus); 8 | } 9 | 10 | CopyPasteRules.$inject = [ 'eventBus' ]; 11 | 12 | inherits(CopyPasteRules, RuleProvider); 13 | 14 | 15 | CopyPasteRules.prototype.init = function() { 16 | 17 | this.addRule('element.copy', function(context) { 18 | 19 | return true; 20 | }); 21 | 22 | this.addRule('element.paste', function(context) { 23 | if (context.source) { 24 | return false; 25 | } 26 | 27 | return true; 28 | }); 29 | 30 | this.addRule('elements.paste', function(context) { 31 | if (context.target.id === 'parent2') { 32 | return false; 33 | } 34 | 35 | return true; 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /test/spec/features/copy-paste/rules/index.js: -------------------------------------------------------------------------------- 1 | import CopyPasteRules from './CopyPasteRules'; 2 | 3 | export default { 4 | __init__: [ 'copyPasteRules' ], 5 | copyPasteRules: [ 'type', CopyPasteRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/spec/features/create/rules/CreateRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function CreateRules(injector) { 6 | injector.invoke(RuleProvider, this); 7 | } 8 | 9 | CreateRules.$inject = [ 'injector' ]; 10 | 11 | inherits(CreateRules, RuleProvider); 12 | 13 | 14 | CreateRules.prototype.init = function() { 15 | 16 | this.addRule('shape.attach', function(context) { 17 | var target = context.target; 18 | 19 | if (target && /ignore/.test(target.id)) { 20 | return null; 21 | } 22 | 23 | if (/host/.test(target.id)) { 24 | return 'attach'; 25 | } 26 | 27 | return false; 28 | }); 29 | 30 | 31 | this.addRule('connection.create', function(context) { 32 | var source = context.source, 33 | hints = context.hints; 34 | 35 | expect(hints).to.have.keys([ 36 | 'targetParent', 37 | 'targetAttach' 38 | ]); 39 | 40 | return /parent|child|newShape/.test(source.id); 41 | }); 42 | 43 | 44 | this.addRule('shape.create', function(context) { 45 | var target = context.target; 46 | 47 | if (/ignore/.test(target.id)) { 48 | return null; 49 | } 50 | 51 | return /parent|root/.test(target.id); 52 | }); 53 | 54 | 55 | this.addRule('elements.create', function(context) { 56 | var target = context.target; 57 | 58 | if (/ignore/.test(target.id)) { 59 | return null; 60 | } 61 | 62 | return /parent|root/.test(target.id); 63 | }); 64 | 65 | }; 66 | -------------------------------------------------------------------------------- /test/spec/features/create/rules/index.js: -------------------------------------------------------------------------------- 1 | import CreateRules from './CreateRules'; 2 | 3 | export default { 4 | __init__: [ 'createRules' ], 5 | createRules: [ 'type', CreateRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/spec/features/distribute-elements/rules/TestRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function TestRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | TestRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(TestRules, RuleProvider); 12 | 13 | 14 | TestRules.prototype.init = function() { 15 | var self = this; 16 | 17 | this.addRule('elements.distribute', function(context) { 18 | return self._result; 19 | }); 20 | }; 21 | 22 | TestRules.prototype.setResult = function(result) { 23 | this._result = result; 24 | }; 25 | -------------------------------------------------------------------------------- /test/spec/features/distribute-elements/rules/index.js: -------------------------------------------------------------------------------- 1 | import TestRules from './TestRules'; 2 | 3 | export default { 4 | __init__: [ 'testRules' ], 5 | testRules: [ 'type', TestRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/editor-actions/rules/CustomRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function CustomRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | CustomRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(CustomRules, RuleProvider); -------------------------------------------------------------------------------- /test/spec/features/editor-actions/rules/index.js: -------------------------------------------------------------------------------- 1 | import CustomRules from './CustomRules'; 2 | 3 | export default { 4 | __init__: [ 'customRules' ], 5 | customRules: [ 'type', CustomRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/spec/features/global-connect/rules/GlobalConnectRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function GlobalConnectRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | GlobalConnectRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(GlobalConnectRules, RuleProvider); 12 | 13 | GlobalConnectRules.prototype.init = function() { 14 | 15 | this.addRule('connection.start', function(context) { 16 | var source = context.source; 17 | 18 | if (source.canStartConnection) { 19 | return true; 20 | } 21 | 22 | return false; 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /test/spec/features/global-connect/rules/index.js: -------------------------------------------------------------------------------- 1 | import GlobalConnectRules from './GlobalConnectRules'; 2 | 3 | export default { 4 | __init__: [ 'globalConnectRules' ], 5 | globalConnectRules: [ 'type', GlobalConnectRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/spec/features/keyboard-move-selection/rules/KeyboardMoveRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | import { find } from 'min-dash'; 6 | 7 | export default function CreateRules(injector) { 8 | injector.invoke(RuleProvider, this); 9 | } 10 | 11 | CreateRules.$inject = [ 'injector' ]; 12 | 13 | inherits(CreateRules, RuleProvider); 14 | 15 | 16 | CreateRules.prototype.init = function() { 17 | 18 | this.addRule('elements.move', function(context) { 19 | var shapes = context.shapes; 20 | 21 | return !find(shapes, function(shape) { 22 | return shape.id === 'shapeDisallowed'; 23 | }); 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /test/spec/features/keyboard-move-selection/rules/index.js: -------------------------------------------------------------------------------- 1 | import KeyboardMoveRules from './KeyboardMoveRules'; 2 | 3 | export default { 4 | __init__: [ 'keyboardMoveRules' ], 5 | keyboardMoveRules: [ 'type', KeyboardMoveRules ] 6 | }; 7 | -------------------------------------------------------------------------------- /test/spec/features/keyboard/CopySpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | forEach 8 | } from 'min-dash'; 9 | 10 | import copyPasteModule from 'lib/features/copy-paste'; 11 | import modelingModule from 'lib/features/modeling'; 12 | import keyboardModule from 'lib/features/keyboard'; 13 | import editorActionsModule from 'lib/features/editor-actions'; 14 | 15 | import { createKeyEvent } from 'test/util/KeyEvents'; 16 | 17 | var KEYS_COPY = [ 'c', 'C' ]; 18 | 19 | 20 | describe('features/keyboard - copy', function() { 21 | 22 | var defaultDiagramConfig = { 23 | modules: [ 24 | copyPasteModule, 25 | modelingModule, 26 | keyboardModule, 27 | modelingModule, 28 | editorActionsModule 29 | ], 30 | canvas: { 31 | deferUpdate: false 32 | } 33 | }; 34 | 35 | var decisionTable = [ 36 | { 37 | desc: 'should call copy', 38 | keys: KEYS_COPY, 39 | ctrlKey: true, 40 | called: true 41 | }, { 42 | desc: 'should not call copy', 43 | keys: KEYS_COPY, 44 | ctrlKey: false, 45 | called: false 46 | } 47 | ]; 48 | 49 | beforeEach(bootstrapDiagram(defaultDiagramConfig)); 50 | 51 | 52 | forEach(decisionTable, function(testCase) { 53 | 54 | forEach(testCase.keys, function(key) { 55 | 56 | it(testCase.desc, inject(function(keyboard, editorActions) { 57 | 58 | // given 59 | var copySpy = sinon.spy(editorActions, 'trigger'); 60 | 61 | var event = createKeyEvent(key, { ctrlKey: testCase.ctrlKey }); 62 | 63 | // when 64 | keyboard._keyHandler(event); 65 | 66 | // then 67 | expect(copySpy.calledWith('copy')).to.be.equal(testCase.called); 68 | })); 69 | 70 | }); 71 | 72 | }); 73 | 74 | }); -------------------------------------------------------------------------------- /test/spec/features/keyboard/PasteSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | forEach 8 | } from 'min-dash'; 9 | 10 | import copyPasteModule from 'lib/features/copy-paste'; 11 | import modelingModule from 'lib/features/modeling'; 12 | import keyboardModule from 'lib/features/keyboard'; 13 | import editorActionsModule from 'lib/features/editor-actions'; 14 | 15 | import { createKeyEvent } from 'test/util/KeyEvents'; 16 | 17 | var KEYS_PASTE = [ 'v', 'V' ]; 18 | 19 | 20 | describe('features/keyboard - paste', function() { 21 | 22 | var defaultDiagramConfig = { 23 | modules: [ 24 | copyPasteModule, 25 | modelingModule, 26 | editorActionsModule, 27 | keyboardModule, 28 | modelingModule 29 | ], 30 | canvas: { 31 | deferUpdate: false 32 | } 33 | }; 34 | 35 | var decisionTable = [ { 36 | desc: 'should call paste', 37 | keys: KEYS_PASTE, 38 | ctrlKey: true, 39 | called: true 40 | }, { 41 | desc: 'should not call paste', 42 | keys: KEYS_PASTE, 43 | ctrlKey: false, 44 | called: false 45 | } ]; 46 | 47 | beforeEach(bootstrapDiagram(defaultDiagramConfig)); 48 | 49 | 50 | forEach(decisionTable, function(testCase) { 51 | 52 | forEach(testCase.keys, function(key) { 53 | 54 | it(testCase.desc, inject(function(keyboard, editorActions) { 55 | 56 | // given 57 | var pasteSpy = sinon.spy(editorActions, 'trigger'); 58 | 59 | var event = createKeyEvent(key, { ctrlKey: testCase.ctrlKey }); 60 | 61 | // when 62 | keyboard._keyHandler(event); 63 | 64 | // then 65 | expect(pasteSpy.calledWith('paste')).to.be.equal(testCase.called); 66 | })); 67 | 68 | }); 69 | 70 | }); 71 | 72 | }); -------------------------------------------------------------------------------- /test/spec/features/keyboard/RedoSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | forEach 8 | } from 'min-dash'; 9 | 10 | import modelingModule from 'lib/features/modeling'; 11 | import editorActionsModule from 'lib/features/editor-actions'; 12 | import keyboardModule from 'lib/features/keyboard'; 13 | 14 | import { createKeyEvent } from 'test/util/KeyEvents'; 15 | 16 | var KEYS_REDO = [ 'y', 'Y' ]; 17 | var KEYS_UNDO = [ 'z', 'Z' ]; 18 | 19 | 20 | describe('features/keyboard - redo', function() { 21 | 22 | var defaultDiagramConfig = { 23 | modules: [ 24 | modelingModule, 25 | keyboardModule, 26 | editorActionsModule 27 | ], 28 | canvas: { 29 | deferUpdate: false 30 | } 31 | }; 32 | 33 | var decisionTable = [ { 34 | desc: 'should call redo', 35 | keys: KEYS_UNDO, 36 | ctrlKey: true, 37 | shiftKey: true, 38 | called: true 39 | }, { 40 | desc: 'should call redo', 41 | keys: KEYS_REDO, 42 | ctrlKey: true, 43 | shiftKey: false, 44 | called: true 45 | }, { 46 | desc: 'should call redo', 47 | keys: KEYS_REDO, 48 | ctrlKey: true, 49 | shiftKey: true, 50 | called: true 51 | }, { 52 | desc: 'should not call redo', 53 | keys: KEYS_UNDO, 54 | ctrlKey: false, 55 | shiftKey: true, 56 | called: false 57 | }, { 58 | desc: 'should not call redo', 59 | keys: KEYS_UNDO, 60 | ctrlKey: true, 61 | shiftKey: false, 62 | called: false 63 | }, { 64 | desc: 'should not call redo', 65 | keys: KEYS_REDO, 66 | ctrlKey: false, 67 | shiftKey: false, 68 | called: false 69 | }, { 70 | desc: 'should not call redo', 71 | keys: KEYS_UNDO, 72 | ctrlKey: false, 73 | shiftKey: false, 74 | called: false 75 | } ]; 76 | 77 | beforeEach(bootstrapDiagram(defaultDiagramConfig)); 78 | 79 | 80 | forEach(decisionTable, function(testCase) { 81 | 82 | forEach(testCase.keys, function(key) { 83 | 84 | it(testCase.desc, inject(function(keyboard, editorActions) { 85 | 86 | // given 87 | var redoSpy = sinon.spy(editorActions, 'trigger'); 88 | 89 | var event = createKeyEvent(key, { 90 | ctrlKey: testCase.ctrlKey, 91 | shiftKey: testCase.shiftKey 92 | }); 93 | 94 | // when 95 | keyboard._keyHandler(event); 96 | 97 | // then 98 | expect(redoSpy.calledWith('redo')).to.be.equal(testCase.called); 99 | })); 100 | 101 | }); 102 | 103 | }); 104 | 105 | }); -------------------------------------------------------------------------------- /test/spec/features/keyboard/RemoveSelectionSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | forEach 8 | } from 'min-dash'; 9 | 10 | import modelingModule from 'lib/features/modeling'; 11 | import editorActionsModule from 'lib/features/editor-actions'; 12 | import keyboardModule from 'lib/features/keyboard'; 13 | 14 | import { createKeyEvent } from 'test/util/KeyEvents'; 15 | 16 | var KEYS = [ 17 | 'Backspace', 18 | 'Delete', 19 | 'Del' 20 | ]; 21 | 22 | 23 | describe('features/keyboard - remove selection', function() { 24 | 25 | var defaultDiagramConfig = { 26 | modules: [ 27 | modelingModule, 28 | keyboardModule, 29 | editorActionsModule 30 | ], 31 | canvas: { 32 | deferUpdate: false 33 | } 34 | }; 35 | 36 | beforeEach(bootstrapDiagram(defaultDiagramConfig)); 37 | 38 | 39 | forEach(KEYS, function(key) { 40 | 41 | it('should call remove selection when ' + key + ' is pressed', 42 | inject(function(keyboard, editorActions) { 43 | 44 | // given 45 | var removeSelectionSpy = sinon.spy(editorActions, 'trigger'); 46 | 47 | var event = createKeyEvent(key); 48 | 49 | // when 50 | keyboard._keyHandler(event); 51 | 52 | // then 53 | expect(removeSelectionSpy.calledWith('removeSelection')).to.be.true; 54 | }) 55 | ); 56 | 57 | }); 58 | 59 | }); -------------------------------------------------------------------------------- /test/spec/features/keyboard/UndoSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | forEach 8 | } from 'min-dash'; 9 | 10 | import modelingModule from 'lib/features/modeling'; 11 | import editorActionsModule from 'lib/features/editor-actions'; 12 | import keyboardModule from 'lib/features/keyboard'; 13 | 14 | import { createKeyEvent } from 'test/util/KeyEvents'; 15 | 16 | var KEYS_UNDO = [ 'z', 'Z' ]; 17 | 18 | 19 | describe('features/keyboard - undo', function() { 20 | 21 | var defaultDiagramConfig = { 22 | modules: [ 23 | modelingModule, 24 | keyboardModule, 25 | editorActionsModule 26 | ], 27 | canvas: { 28 | deferUpdate: false 29 | } 30 | }; 31 | 32 | var decisionTable = [ { 33 | desc: 'should call undo', 34 | keys: KEYS_UNDO, 35 | ctrlKey: true, 36 | shiftKey: false, 37 | called: true 38 | }, { 39 | desc: 'should not call undo', 40 | keys: KEYS_UNDO, 41 | ctrlKey: true, 42 | shiftKey: true, 43 | called: false 44 | }, { 45 | desc: 'should not call undo', 46 | keys: KEYS_UNDO, 47 | ctrlKey: false, 48 | shiftKey: true, 49 | called: false 50 | }, { 51 | desc: 'should not call undo', 52 | keys: KEYS_UNDO, 53 | ctrlKey: false, 54 | shiftKey: false, 55 | called: false 56 | } ]; 57 | 58 | beforeEach(bootstrapDiagram(defaultDiagramConfig)); 59 | 60 | 61 | forEach(decisionTable, function(testCase) { 62 | 63 | forEach(testCase.keys, function(key) { 64 | 65 | it(testCase.desc, inject(function(keyboard, editorActions) { 66 | 67 | // given 68 | var undoSpy = sinon.spy(editorActions, 'trigger'); 69 | 70 | var event = createKeyEvent(key, { 71 | ctrlKey: testCase.ctrlKey, 72 | shiftKey: testCase.shiftKey 73 | }); 74 | 75 | // when 76 | keyboard._keyHandler(event); 77 | 78 | // then 79 | expect(undoSpy.calledWith('undo')).to.be.equal(testCase.called); 80 | })); 81 | 82 | }); 83 | 84 | }); 85 | 86 | }); -------------------------------------------------------------------------------- /test/spec/features/keyboard/ZoomSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | forEach 8 | } from 'min-dash'; 9 | 10 | import editorActionsModule from 'lib/features/editor-actions'; 11 | import zoomScrollModule from 'lib/navigation/zoomscroll'; 12 | import keyboardModule from 'lib/features/keyboard'; 13 | 14 | import { createKeyEvent } from 'test/util/KeyEvents'; 15 | 16 | var KEYS = { 17 | ZOOM_IN: [ '+', 'Add', '=' ], 18 | ZOOM_OUT: [ '-', 'Subtract' ], 19 | ZOOM_DEFAULT: [ '0' ], 20 | }; 21 | 22 | 23 | describe('features/keyboard - zoom', function() { 24 | 25 | var defaultDiagramConfig = { 26 | modules: [ 27 | keyboardModule, 28 | editorActionsModule, 29 | zoomScrollModule 30 | ], 31 | canvas: { 32 | deferUpdate: false 33 | } 34 | }; 35 | 36 | var decisionTable = [ { 37 | desc: 'zoom in', 38 | keys: KEYS.ZOOM_IN, 39 | ctrlKey: true, 40 | defaultZoom: 3, 41 | zoom: 4 42 | }, { 43 | desc: 'zoom out', 44 | keys: KEYS.ZOOM_OUT, 45 | ctrlKey: true, 46 | defaultZoom: 3, 47 | zoom: 2.456 48 | }, { 49 | desc: 'zoom default', 50 | keys: KEYS.ZOOM_DEFAULT, 51 | ctrlKey: true, 52 | defaultZoom: 3, 53 | zoom: 1 54 | } ]; 55 | 56 | beforeEach(bootstrapDiagram(defaultDiagramConfig)); 57 | 58 | 59 | forEach(decisionTable, function(testCase) { 60 | 61 | forEach(testCase.keys, function(key) { 62 | 63 | it('should handle ' + key + ' for ' + testCase.desc, inject(function(canvas, keyboard) { 64 | 65 | // given 66 | canvas.zoom(testCase.defaultZoom); 67 | 68 | var event = createKeyEvent(key, { 69 | ctrlKey: testCase.ctrlKey 70 | }); 71 | 72 | // when 73 | keyboard._keyHandler(event); 74 | 75 | // then 76 | expect(canvas.zoom()).to.be.equal(testCase.zoom); 77 | expect(event.defaultPrevented).to.be.true; 78 | })); 79 | 80 | }); 81 | 82 | }); 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /test/spec/features/label-support/rules/LabelSupportRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function LabelSupportRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | LabelSupportRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(LabelSupportRules, RuleProvider); 12 | 13 | 14 | LabelSupportRules.prototype.init = function() { 15 | 16 | this.addRule('elements.move', function(context) { 17 | var target = context.target, 18 | shapes = context.shapes; 19 | 20 | // check that we do not accidently try to drop elements 21 | // onto themselves or children of themselves 22 | while (target) { 23 | if (shapes.indexOf(target) !== -1) { 24 | return false; 25 | } 26 | 27 | target = target.parent; 28 | } 29 | }); 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /test/spec/features/label-support/rules/index.js: -------------------------------------------------------------------------------- 1 | import LabelSupportRules from './LabelSupportRules'; 2 | 3 | export default { 4 | __init__: [ 'labelSupportRules' ], 5 | labelSupportRules: [ 'type', LabelSupportRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/modeling/custom/CustomLayouter.js: -------------------------------------------------------------------------------- 1 | import { getMid } from 'lib/layout/LayoutUtil'; 2 | 3 | /** 4 | * @typedef {import('../../model').Connection} Connection 5 | */ 6 | 7 | /** 8 | * A base connection layouter implementation 9 | * that layouts the connection by directly connecting 10 | * mid(source) + mid(target). 11 | */ 12 | export default function CustomLayouter() {} 13 | 14 | 15 | /** 16 | * Return the new layouted waypoints for the given connection. 17 | * 18 | * @param {Connection} connection 19 | * @param {Object} [hints] 20 | * @param {boolean} [hints.connectionStart] 21 | * @param {boolean} [hints.connectionEnd] 22 | * 23 | * @return {Array} the layouted connection waypoints 24 | */ 25 | CustomLayouter.prototype.layoutConnection = function(connection, hints) { 26 | 27 | hints = hints || {}; 28 | 29 | var startMid = hints.connectionStart || getMid(connection.source), 30 | endMid = hints.connectionEnd || getMid(connection.target); 31 | 32 | var start = { 33 | x: startMid.x + 50, 34 | y: startMid.y + 50, 35 | original: startMid 36 | }; 37 | 38 | var end = { 39 | x: endMid.x - 50, 40 | y: endMid.y - 50, 41 | original: endMid 42 | }; 43 | 44 | return [ 45 | start, 46 | end 47 | ]; 48 | }; 49 | -------------------------------------------------------------------------------- /test/spec/features/modeling/custom/index.js: -------------------------------------------------------------------------------- 1 | import CommandModule from 'lib/command'; 2 | import ChangeSupportModule from 'lib/features/change-support'; 3 | import SelectionModule from 'lib/features/selection'; 4 | import RulesModule from 'lib/features/rules'; 5 | 6 | import Modeling from 'lib/features/modeling/Modeling'; 7 | import CustomLayouter from './CustomLayouter'; 8 | 9 | 10 | export default { 11 | __depends__: [ 12 | CommandModule, 13 | ChangeSupportModule, 14 | SelectionModule, 15 | RulesModule 16 | ], 17 | __init__: [ 'modeling' ], 18 | modeling: [ 'type', Modeling ], 19 | layouter: [ 'type', CustomLayouter ] 20 | }; 21 | -------------------------------------------------------------------------------- /test/spec/features/mouse/MouseSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import mouseModule from 'lib/features/mouse'; 7 | 8 | import { createMoveEvent } from 'lib/features/mouse/Mouse'; 9 | 10 | 11 | describe('features/mouse', function() { 12 | 13 | beforeEach(bootstrapDiagram({ modules: [ mouseModule ] })); 14 | 15 | 16 | it('should return last mousemove event', inject(function(canvas, mouse) { 17 | 18 | // when 19 | mousemoveEvent(canvas._svg); 20 | 21 | // then 22 | expect(mouse.getLastMoveEvent()).to.exist; 23 | })); 24 | 25 | 26 | it('should always return event', inject(function(mouse) { 27 | 28 | // then 29 | expect(mouse.getLastMoveEvent()).to.exist; 30 | })); 31 | 32 | }); 33 | 34 | // helpers ////////// 35 | 36 | function mousemoveEvent(element) { 37 | 38 | var event = createMoveEvent(0, 0); 39 | 40 | element.dispatchEvent(event); 41 | 42 | return event; 43 | } -------------------------------------------------------------------------------- /test/spec/features/move/rules/MoveRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | import { 6 | isFrameElement 7 | } from 'lib/util/Elements'; 8 | 9 | export default function MoveRules(eventBus) { 10 | RuleProvider.call(this, eventBus); 11 | } 12 | 13 | MoveRules.$inject = [ 'eventBus' ]; 14 | 15 | inherits(MoveRules, RuleProvider); 16 | 17 | 18 | MoveRules.prototype.init = function() { 19 | 20 | this.addRule('elements.move', function(context) { 21 | var target = context.target, 22 | shapes = context.shapes; 23 | 24 | if (target && /ignore/.test(target.id)) { 25 | return null; 26 | } 27 | 28 | // not allowed to move on frame elements 29 | if (isFrameElement(target)) { 30 | return false; 31 | } 32 | 33 | // check that we do not accidently try to drop elements 34 | // onto themselves or children of themselves 35 | while (target) { 36 | if (shapes.indexOf(target) !== -1) { 37 | return false; 38 | } 39 | 40 | target = target.parent; 41 | } 42 | }); 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /test/spec/features/move/rules/index.js: -------------------------------------------------------------------------------- 1 | import MoveRules from './MoveRules'; 2 | 3 | export default { 4 | __init__: [ 'moveRules' ], 5 | moveRules: [ 'type', MoveRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/ordering/provider/TestOrderingProvider.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import OrderingProvider from 'lib/features/ordering/OrderingProvider'; 4 | 5 | import { 6 | findIndex 7 | } from 'min-dash'; 8 | 9 | 10 | /** 11 | * a simple ordering provider that makes sure: 12 | * 13 | * (1) elements are ordered by a {level} property 14 | * (2) elements with {alwaysTopLevel} are always added to the root 15 | */ 16 | export default function TestOrderingProvider(eventBus) { 17 | 18 | OrderingProvider.call(this, eventBus); 19 | 20 | 21 | this.getOrdering = function(element, newParent) { 22 | 23 | if (element.alwaysTopLevel) { 24 | while (newParent.parent) { 25 | newParent = newParent.parent; 26 | } 27 | } 28 | 29 | 30 | var currentIndex = newParent.children.indexOf(element); 31 | 32 | var insertIndex = findIndex(newParent.children, function(c) { 33 | return element.level < c.level; 34 | }); 35 | 36 | 37 | if (insertIndex !== -1) { 38 | if (currentIndex !== -1 && currentIndex < insertIndex) { 39 | insertIndex -= 1; 40 | } 41 | } 42 | 43 | return { 44 | index: insertIndex, 45 | parent: newParent 46 | }; 47 | }; 48 | } 49 | 50 | inherits(TestOrderingProvider, OrderingProvider); -------------------------------------------------------------------------------- /test/spec/features/ordering/provider/index.js: -------------------------------------------------------------------------------- 1 | import TestOrderingProvider from './TestOrderingProvider'; 2 | 3 | export default { 4 | __init__: [ 'testOrderingProvider' ], 5 | testOrderingProvider: [ 'type', TestOrderingProvider ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/popup-menu/PopupMenuItemSpec.js: -------------------------------------------------------------------------------- 1 | import PopupMenuItem from 'lib/features/popup-menu/PopupMenuItem'; 2 | 3 | import { 4 | html, 5 | render 6 | } from 'lib/ui'; 7 | 8 | import { 9 | query as domQuery 10 | } from 'min-dom'; 11 | 12 | 13 | describe('features/popup-menu - ', function() { 14 | let container; 15 | 16 | beforeEach(function() { 17 | container = document.createElement('div'); 18 | document.body.appendChild(container); 19 | }); 20 | 21 | afterEach(function() { 22 | container.parentNode.removeChild(container); 23 | }); 24 | 25 | 26 | it('should render', function() { 27 | 28 | // when 29 | createPopupMenu({ container }); 30 | 31 | // expect 32 | expect(domQuery('.entry', container)).to.exist; 33 | }); 34 | 35 | }); 36 | 37 | 38 | // helpers 39 | function createPopupMenu(options) { 40 | const { 41 | container, 42 | ...restProps 43 | } = options; 44 | 45 | const props = { 46 | entry: { id: 'foo', label: 'bar' }, 47 | ...restProps 48 | }; 49 | 50 | return render( 51 | html`<${PopupMenuItem} ...${ props } />`, 52 | container 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /test/spec/features/popup-menu/PopupMenuListSpec.js: -------------------------------------------------------------------------------- 1 | import PopupMenuList from 'lib/features/popup-menu/PopupMenuList'; 2 | 3 | import { 4 | html, 5 | render 6 | } from 'lib/ui'; 7 | 8 | import { 9 | query as domQuery 10 | } from 'min-dom'; 11 | 12 | 13 | describe('features/popup-menu - ', function() { 14 | let container; 15 | 16 | beforeEach(function() { 17 | container = document.createElement('div'); 18 | document.body.appendChild(container); 19 | }); 20 | 21 | afterEach(function() { 22 | container.parentNode.removeChild(container); 23 | }); 24 | 25 | 26 | it('should render', function() { 27 | 28 | // when 29 | createPopupMenuList({ container }); 30 | 31 | // then 32 | expect(domQuery('.djs-popup-results', container)).to.exist; 33 | }); 34 | 35 | }); 36 | 37 | 38 | // helpers 39 | function createPopupMenuList(options) { 40 | const { 41 | container, 42 | ...restProps 43 | } = options; 44 | 45 | const props = { 46 | entries: [ ], 47 | selectedEntry: null, 48 | setSelectedEntry: () => {}, 49 | onSelect: () => {}, 50 | ...restProps 51 | }; 52 | 53 | return render( 54 | html`<${PopupMenuList} ...${props} />`, 55 | container 56 | ); 57 | } -------------------------------------------------------------------------------- /test/spec/features/popup-menu/resources/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/diagram-js/c87769f2a275c1882e79b318ddd3cd971574e385/test/spec/features/popup-menu/resources/a.png -------------------------------------------------------------------------------- /test/spec/features/preview-support/nested-renderer/index.js: -------------------------------------------------------------------------------- 1 | import MarkerRenderer from './MarkerRenderer'; 2 | 3 | export default { 4 | __init__: [ 'defaultRenderer' ], 5 | defaultRenderer: [ 'type', MarkerRenderer ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/preview-support/renderer/index.js: -------------------------------------------------------------------------------- 1 | import MarkerRenderer from './MarkerRenderer'; 2 | 3 | export default { 4 | __init__: [ 'defaultRenderer' ], 5 | defaultRenderer: [ 'type', MarkerRenderer ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/replace/ReplaceSelectionBehaviorSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import modelingModule from 'lib/features/modeling'; 7 | import replaceModule from 'lib/features/replace'; 8 | 9 | 10 | describe('features/replace - ReplaceSelectionBehavior', function() { 11 | 12 | beforeEach(bootstrapDiagram({ 13 | modules: [ 14 | modelingModule, 15 | replaceModule 16 | ] 17 | })); 18 | 19 | var rootShape, parentShape, originalShape; 20 | 21 | beforeEach(inject(function(elementFactory, canvas) { 22 | 23 | rootShape = elementFactory.createRoot({ 24 | id: 'root' 25 | }); 26 | 27 | canvas.setRootElement(rootShape); 28 | 29 | parentShape = elementFactory.createShape({ 30 | id: 'parent', 31 | x: 100, y: 100, width: 300, height: 300 32 | }); 33 | 34 | canvas.addShape(parentShape, rootShape); 35 | 36 | originalShape = elementFactory.createShape({ 37 | id: 'originalShape', 38 | x: 110, y: 110, width: 100, height: 100 39 | }); 40 | 41 | canvas.addShape(originalShape, parentShape); 42 | })); 43 | 44 | 45 | describe('#replaceElement', function() { 46 | 47 | it('should select after replacement', inject(function(replace, selection) { 48 | 49 | // given 50 | var replacement = { 51 | id: 'replacement', 52 | width: 200, 53 | height: 200 54 | }; 55 | 56 | // when 57 | var newShape = replace.replaceElement(originalShape, replacement); 58 | 59 | // then 60 | expect(newShape).to.exist; 61 | 62 | // expect added 63 | expect(selection.get()).to.eql([ newShape ]); 64 | })); 65 | 66 | 67 | it('should NOT select after replacement if ', inject(function(replace, selection) { 68 | 69 | // given 70 | var replacement = { 71 | id: 'replacement', 72 | width: 200, 73 | height: 200 74 | }; 75 | 76 | // when 77 | var newShape = replace.replaceElement(originalShape, replacement, { select: false }); 78 | 79 | // then 80 | expect(newShape).to.exist; 81 | 82 | // expect added 83 | expect(selection.get()).not.to.include(newShape); 84 | })); 85 | 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /test/spec/features/resize/rules/ResizeRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function ResizeRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | ResizeRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(ResizeRules, RuleProvider); 12 | 13 | 14 | ResizeRules.prototype.init = function() { 15 | 16 | this.addRule('shape.resize', function(context) { 17 | 18 | var shape = context.shape; 19 | 20 | if (!context.newBounds) { 21 | 22 | // check general resizability 23 | if (!shape.resizable) { 24 | return false; 25 | } 26 | } else { 27 | if (shape.resizable === 'always') { 28 | return true; 29 | } 30 | 31 | // element must have minimum size of 50*50 points 32 | return context.newBounds.width > 50 && context.newBounds.height > 50; 33 | } 34 | }); 35 | }; -------------------------------------------------------------------------------- /test/spec/features/resize/rules/index.js: -------------------------------------------------------------------------------- 1 | import ResizeRules from './ResizeRules'; 2 | 3 | export default { 4 | __init__: [ 'resizeRules' ], 5 | resizeRules: [ 'type', ResizeRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/root-elements/RootElementsBehaviorSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import coreModule from 'lib/core'; 7 | import commandModule from 'lib/command'; 8 | import modelingModule from 'lib/features/modeling'; 9 | import rootElementsModule from 'lib/features/root-elements'; 10 | 11 | 12 | describe('features/planes/RootElementsBehavior', function() { 13 | 14 | beforeEach(bootstrapDiagram({ 15 | modules: [ 16 | coreModule, 17 | commandModule, 18 | modelingModule, 19 | rootElementsModule 20 | ] 21 | })); 22 | 23 | var shape1; 24 | 25 | beforeEach(inject(function(canvas) { 26 | 27 | // given 28 | shape1 = canvas.addShape({ 29 | id: 'shape1', 30 | x: 10, 31 | y: 10, 32 | width: 100, 33 | height: 100 34 | }); 35 | })); 36 | 37 | 38 | describe('undo', function() { 39 | 40 | it('should switch to affected root', inject(function(canvas, modeling, commandStack) { 41 | 42 | // given 43 | modeling.removeShape(shape1); 44 | 45 | canvas.setRootElement(canvas.addRootElement(null)); 46 | 47 | // when 48 | commandStack.undo(); 49 | 50 | // then 51 | expect(canvas.getRootElement()).to.equal(shape1.parent); 52 | })); 53 | 54 | }); 55 | 56 | 57 | describe('redo', function() { 58 | 59 | it('should switch to affected root', inject(function(canvas, modeling, commandStack) { 60 | 61 | // given 62 | var rootElement = canvas.getRootElement(); 63 | 64 | var otherRootElement = canvas.addRootElement(null); 65 | 66 | modeling.removeShape(shape1); 67 | commandStack.undo(); 68 | canvas.setRootElement(otherRootElement); 69 | 70 | // when 71 | commandStack.redo(); 72 | 73 | // then 74 | expect(canvas.getRootElement()).to.equal(rootElement); 75 | })); 76 | 77 | }); 78 | 79 | }); 80 | 81 | -------------------------------------------------------------------------------- /test/spec/features/rules/priority-rules/PriorityRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | 6 | var HIGH_PRIORITY = 1500; 7 | 8 | 9 | export default function PriorityRules(eventBus) { 10 | RuleProvider.call(this, eventBus); 11 | } 12 | 13 | PriorityRules.$inject = [ 'eventBus' ]; 14 | 15 | inherits(PriorityRules, RuleProvider); 16 | 17 | 18 | PriorityRules.prototype.init = function() { 19 | 20 | // a allow+block list containing 21 | // element ids 22 | var allowList = [ 23 | 'always-resizable' 24 | ]; 25 | 26 | var blockList = [ 27 | 'never-resizable' 28 | ]; 29 | 30 | 31 | this.addRule('shape.resize', function(context) { 32 | return context.shape.resizable; 33 | }); 34 | 35 | 36 | this.addRule('shape.resize', HIGH_PRIORITY, function(context) { 37 | var shape = context.shape; 38 | 39 | if (allowList.indexOf(shape.id) !== -1) { 40 | return true; 41 | } 42 | 43 | if (blockList.indexOf(shape.id) !== -1) { 44 | return false; 45 | } 46 | }); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /test/spec/features/rules/priority-rules/index.js: -------------------------------------------------------------------------------- 1 | import PriorityRules from './PriorityRules'; 2 | 3 | export default { 4 | __init__: [ 'priorityRules' ], 5 | priorityRules: [ 'type', PriorityRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/rules/rules/TestRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function TestRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | TestRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(TestRules, RuleProvider); 12 | 13 | 14 | TestRules.prototype.init = function() { 15 | 16 | this.addRule('shape.resize', function(context) { 17 | 18 | var shape = context.shape; 19 | 20 | if (shape.ignoreResize) { 21 | return null; 22 | } 23 | 24 | return shape.resizable !== undefined ? shape.resizable : undefined; 25 | }); 26 | }; -------------------------------------------------------------------------------- /test/spec/features/rules/rules/index.js: -------------------------------------------------------------------------------- 1 | import TestRules from './TestRules'; 2 | 3 | export default { 4 | __init__: [ 'testRules' ], 5 | testRules: [ 'type', TestRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/rules/say-no-rules/SayNoRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function SayNoRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | SayNoRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(SayNoRules, RuleProvider); 12 | 13 | 14 | SayNoRules.prototype.init = function() { 15 | 16 | this.addRule('shape.resize', function(context) { 17 | return false; 18 | }); 19 | }; -------------------------------------------------------------------------------- /test/spec/features/rules/say-no-rules/index.js: -------------------------------------------------------------------------------- 1 | import SayNoRules from './SayNoRules'; 2 | 3 | export default { 4 | __init__: [ 'sayNoRules' ], 5 | sayNoRules: [ 'type', SayNoRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/selection/rules/ConnectRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function ConnectRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | ConnectRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(ConnectRules, RuleProvider); 12 | 13 | 14 | ConnectRules.prototype.init = function() { 15 | 16 | this.addRule('connection.create', function(context) { 17 | var source = context.source, 18 | target = context.target; 19 | 20 | if (source.targetOnly) { 21 | return false; 22 | } 23 | 24 | if (source.notConnectable || target.notConnectable) { 25 | return false; 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /test/spec/features/selection/rules/index.js: -------------------------------------------------------------------------------- 1 | import ConnectRules from './ConnectRules'; 2 | 3 | export default { 4 | __init__: [ 'connectRules' ], 5 | connectRules: [ 'type', ConnectRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/snapping/SnappingSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | 7 | import modelingModule from 'lib/features/modeling'; 8 | import snappingModule from 'lib/features/snapping'; 9 | import moveModule from 'lib/features/move'; 10 | 11 | 12 | describe('features/snapping', function() { 13 | 14 | beforeEach(bootstrapDiagram({ 15 | modules: [ 16 | modelingModule, 17 | snappingModule, 18 | moveModule 19 | ] 20 | })); 21 | 22 | 23 | var rootShape, parentShape, childShape, childShape2, label, connection; 24 | 25 | beforeEach(inject(function(elementFactory, canvas) { 26 | 27 | rootShape = elementFactory.createRoot({ 28 | id: 'root' 29 | }); 30 | 31 | canvas.setRootElement(rootShape); 32 | 33 | parentShape = elementFactory.createShape({ 34 | id: 'parent', 35 | x: 100, y: 100, width: 300, height: 300 36 | }); 37 | 38 | canvas.addShape(parentShape, rootShape); 39 | 40 | childShape = elementFactory.createShape({ 41 | id: 'child', 42 | x: 110, y: 110, width: 100, height: 100 43 | }); 44 | 45 | canvas.addShape(childShape, parentShape); 46 | 47 | childShape2 = elementFactory.createShape({ 48 | id: 'child2', 49 | x: 200, y: 110, width: 100, height: 100 50 | }); 51 | 52 | canvas.addShape(childShape2, parentShape); 53 | 54 | label = elementFactory.createLabel({ 55 | id: 'label1', 56 | x: 250, y: 110, width: 40, height: 40, 57 | hidden: true 58 | }); 59 | 60 | canvas.addShape(label, parentShape); 61 | 62 | connection = elementFactory.createConnection({ 63 | id: 'connection', 64 | waypoints: [ { x: 150, y: 150 }, { x: 150, y: 200 }, { x: 350, y: 150 } ], 65 | source: childShape, 66 | target: childShape2 67 | }); 68 | 69 | canvas.addConnection(connection, parentShape); 70 | })); 71 | 72 | 73 | describe('bootstrap', function() { 74 | 75 | it('should bootstrap diagram with component', inject(function(snapping) { 76 | expect(snapping).to.exist; 77 | })); 78 | 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /test/spec/features/space-tool/auto-resize/CustomAutoResizeProvider.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import AutoResizeProvider from 'lib/features/auto-resize/AutoResizeProvider'; 4 | 5 | 6 | export default function CustomAutoResizeProvider(eventBus) { 7 | AutoResizeProvider.call(this, eventBus); 8 | 9 | this.canResize = function(elements, target) { 10 | return target.parent; 11 | }; 12 | } 13 | 14 | inherits(CustomAutoResizeProvider, AutoResizeProvider); -------------------------------------------------------------------------------- /test/spec/features/space-tool/auto-resize/index.js: -------------------------------------------------------------------------------- 1 | import CustomAutoResizeProvider from './CustomAutoResizeProvider'; 2 | 3 | export default { 4 | __init__: [ 'customAutoResizeProvider' ], 5 | customAutoResizeProvider: [ 'type', CustomAutoResizeProvider ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/space-tool/rules/SpaceToolRules.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import RuleProvider from 'lib/features/rules/RuleProvider'; 4 | 5 | export default function SpaceToolRules(eventBus) { 6 | RuleProvider.call(this, eventBus); 7 | } 8 | 9 | SpaceToolRules.$inject = [ 'eventBus' ]; 10 | 11 | inherits(SpaceToolRules, RuleProvider); 12 | 13 | 14 | SpaceToolRules.prototype.init = function() { 15 | this.addRule('shape.resize', function(context) { 16 | var shape = context.shape; 17 | 18 | return shape.id.includes('resizable'); 19 | }); 20 | }; -------------------------------------------------------------------------------- /test/spec/features/space-tool/rules/index.js: -------------------------------------------------------------------------------- 1 | import SpaceToolRules from './SpaceToolRules'; 2 | 3 | export default { 4 | __init__: [ 'spaceToolRules' ], 5 | spaceToolRules: [ 'type', SpaceToolRules ] 6 | }; -------------------------------------------------------------------------------- /test/spec/features/tool-manager/ToolManagerSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import { 7 | createCanvasEvent as canvasEvent 8 | } from '../../../util/MockEvents'; 9 | 10 | import toolManagerModule from 'lib/features/tool-manager'; 11 | import handToolModule from 'lib/features/hand-tool'; 12 | import draggingModule from 'lib/features/dragging'; 13 | 14 | 15 | describe('features/tool-manager', function() { 16 | 17 | beforeEach(bootstrapDiagram({ modules: [ toolManagerModule, handToolModule, draggingModule ] })); 18 | 19 | beforeEach(inject(function(dragging) { 20 | dragging.setOptions({ manual: true }); 21 | })); 22 | 23 | describe('basics', function() { 24 | 25 | it('should register a tool', inject(function(toolManager) { 26 | 27 | // when 28 | toolManager.registerTool('lasso', { 29 | tool: 'lasso.selection', 30 | dragging: 'lasso' 31 | }); 32 | 33 | // then 34 | expect(toolManager.length()).to.equal(2); 35 | })); 36 | 37 | 38 | it('should throw error when registering a tool without events', inject(function(toolManager) { 39 | 40 | // when 41 | function result() { 42 | toolManager.registerTool('hand'); 43 | } 44 | 45 | // then 46 | expect(result).to.throw('A tool has to be registered with it\'s "events"'); 47 | })); 48 | 49 | 50 | it('should have hand-tool as active', inject(function(toolManager, handTool) { 51 | 52 | // when 53 | handTool.activateHand(canvasEvent({ x: 150, y: 150 })); 54 | 55 | expect(toolManager.isActive('hand')).to.be.true; 56 | })); 57 | 58 | 59 | it('should have no active tool', inject(function(toolManager, handTool, dragging) { 60 | 61 | // when 62 | handTool.activateHand(canvasEvent({ x: 150, y: 150 })); 63 | 64 | dragging.end(); 65 | 66 | expect(toolManager.isActive('hand')).to.be.false; 67 | })); 68 | 69 | 70 | it('should not interfere with dragging', inject(function(toolManager, handTool, dragging) { 71 | 72 | // given 73 | toolManager.registerTool('customTool', { 74 | tool: 'my-custom-event' 75 | }); 76 | 77 | toolManager.setActive('customTool'); 78 | 79 | // when 80 | const startDrag = () => 81 | dragging.init(canvasEvent({ x: 150, y: 150 }), 'my-custom-event', { 82 | autoActivate: true, 83 | data: { 84 | context: {} 85 | } 86 | }); 87 | 88 | // then 89 | expect(startDrag).not.to.throw(); 90 | 91 | })); 92 | 93 | }); 94 | 95 | }); 96 | -------------------------------------------------------------------------------- /test/spec/i18n/I18NSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import paletteModule from 'lib/features/palette'; 7 | import i18nModule from 'lib/i18n'; 8 | 9 | 10 | describe('i18n', function() { 11 | 12 | describe('events', function() { 13 | 14 | beforeEach(bootstrapDiagram({ modules: [ i18nModule ] })); 15 | 16 | 17 | it('should emit i18n.changed event', inject(function(i18n, eventBus) { 18 | 19 | // given 20 | var listener = sinon.spy(); 21 | 22 | eventBus.on('i18n.changed', listener); 23 | 24 | // when 25 | i18n.changed(); 26 | 27 | // then 28 | expect(listener).to.have.been.called; 29 | })); 30 | 31 | }); 32 | 33 | 34 | describe('integration', function() { 35 | 36 | beforeEach(bootstrapDiagram({ modules: [ i18nModule, paletteModule ] })); 37 | 38 | 39 | it('should update palette', inject(function(palette, i18n) { 40 | 41 | // given 42 | var paletteUpdate = sinon.spy(palette, '_update'); 43 | palette._init(); 44 | 45 | // when 46 | i18n.changed(); 47 | 48 | // then 49 | expect(paletteUpdate).to.have.been.called; 50 | })); 51 | 52 | }); 53 | 54 | }); -------------------------------------------------------------------------------- /test/spec/i18n/translate/custom-translate/custom-translate.js: -------------------------------------------------------------------------------- 1 | import translate from 'lib/i18n/translate/translate'; 2 | 3 | export default function customTranslate(template, replacements) { 4 | if (template === 'Remove') { 5 | template = 'Eliminar'; 6 | } 7 | 8 | if (template === 'Activate the hand tool') { 9 | template = 'Activar herramienta mano'; 10 | } 11 | 12 | return translate(template, replacements); 13 | } -------------------------------------------------------------------------------- /test/spec/i18n/translate/custom-translate/index.js: -------------------------------------------------------------------------------- 1 | import customTranslate from './custom-translate'; 2 | 3 | export default { 4 | translate: [ 'value', customTranslate ] 5 | }; -------------------------------------------------------------------------------- /test/spec/i18n/translate/translateSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import translateModule from 'lib/i18n/translate'; 7 | import customTranslateModule from './custom-translate'; 8 | 9 | 10 | describe('i18n - translate', function() { 11 | 12 | describe('basics', function() { 13 | 14 | beforeEach(bootstrapDiagram({ modules: [ translateModule ] })); 15 | 16 | 17 | it('should provide translate helper', inject(function(translate) { 18 | expect(translate).to.be.a('function'); 19 | })); 20 | 21 | 22 | it('should pass through', inject(function(translate) { 23 | expect(translate('FOO BAR')).to.eql('FOO BAR'); 24 | })); 25 | 26 | 27 | it('should replace patterns', inject(function(translate) { 28 | expect(translate('FOO {bar}!', { bar: 'BAR' })).to.eql('FOO BAR!'); 29 | })); 30 | 31 | 32 | it('should handle missing replacement', inject(function(translate) { 33 | expect(translate('FOO {bar}!')).to.eql('FOO {bar}!'); 34 | })); 35 | 36 | 37 | it('should NOT escape', inject(function(translate) { 38 | expect(translate('
')).to.eql('
'); 39 | })); 40 | 41 | }); 42 | 43 | 44 | describe('custom translate / override', function() { 45 | 46 | beforeEach(bootstrapDiagram({ modules: [ translateModule, customTranslateModule ] })); 47 | 48 | it('should override translate', inject(function(translate) { 49 | expect(translate('Remove')).to.eql('Eliminar'); 50 | })); 51 | 52 | }); 53 | 54 | }); -------------------------------------------------------------------------------- /test/spec/snapping/SnapUtilSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapDiagram, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import modelingModule from 'lib/features/modeling'; 7 | 8 | import { getChildren } from 'lib/features/snapping/SnapUtil'; 9 | 10 | 11 | describe('features/snapping - SnapUtil', function() { 12 | 13 | beforeEach(bootstrapDiagram({ 14 | modules: [ 15 | modelingModule 16 | ] 17 | })); 18 | 19 | var rootElement, shape1, shape2, connection; 20 | 21 | beforeEach(inject(function(canvas, elementFactory) { 22 | rootElement = elementFactory.createRoot({ 23 | id: 'root' 24 | }); 25 | 26 | canvas.setRootElement(rootElement); 27 | 28 | shape1 = canvas.addShape(elementFactory.createShape({ 29 | id: 'shape1', 30 | x: 100, y: 100, width: 100, height: 100 31 | })); 32 | 33 | shape2 = canvas.addShape(elementFactory.createShape({ 34 | id: 'shape2', 35 | x: 300, y: 100, width: 100, height: 100 36 | })); 37 | 38 | connection = canvas.addConnection(elementFactory.createConnection({ 39 | id: 'connection', 40 | source: shape1, 41 | target: shape2, 42 | waypoints: [ 43 | { x: 150, y: 200 }, 44 | { x: 150, y: 300 } 45 | ] 46 | })); 47 | })); 48 | 49 | 50 | describe('#getChildren', function() { 51 | 52 | it('root', function() { 53 | 54 | // when 55 | var children = getChildren(rootElement); 56 | 57 | // then 58 | expect(children).to.have.length(3); 59 | }); 60 | 61 | 62 | it('shape', function() { 63 | 64 | // when 65 | var children = getChildren(shape1); 66 | 67 | // then 68 | expect(children).to.have.length(0); 69 | }); 70 | 71 | 72 | it('connection', function() { 73 | 74 | // when 75 | var children = getChildren(connection); 76 | 77 | // then 78 | expect(children).to.have.length(0); 79 | }); 80 | }); 81 | 82 | }); -------------------------------------------------------------------------------- /test/spec/ui/uiSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | html, 3 | render, 4 | useState 5 | } from 'lib/ui'; 6 | 7 | 8 | describe('ui', function() { 9 | 10 | Object.entries({ 11 | html, 12 | render, 13 | useState 14 | }).map(([ name, value ]) => { 15 | 16 | it(`should export ${ name }`, function() { 17 | expect(value, `export <${ name }>`).to.exist; 18 | }); 19 | 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/util/AttachUtilSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | getNewAttachPoint 3 | } from 'lib/util/AttachUtil'; 4 | 5 | 6 | describe('AttachUtil', function() { 7 | 8 | describe('#getNewAttachPoint', function() { 9 | 10 | it('should return new point\'s position delta for growing', function() { 11 | 12 | // given 13 | var point = { 14 | x: -2, 15 | y: 2 16 | }; 17 | 18 | var oldBounds = { 19 | x: -2, 20 | y: -2, 21 | width: 4, 22 | height: 4 23 | }; 24 | 25 | var newBounds = { 26 | x: -2, 27 | y: -2, 28 | width: 8, 29 | height: 8 30 | }; 31 | 32 | // then 33 | expect(getNewAttachPoint(point, oldBounds, newBounds)).to.eql({ x: -2, y: 6 }); 34 | }); 35 | 36 | 37 | it('should return new point\'s position delta for shrinking', function() { 38 | 39 | // given 40 | var point = { 41 | x: -4, 42 | y: 4 43 | }; 44 | 45 | var oldBounds = { 46 | x: -4, 47 | y: -4, 48 | width: 8, 49 | height: 8 50 | }; 51 | 52 | var newBounds = { 53 | x: -4, 54 | y: -4, 55 | width: 4, 56 | height: 4 57 | }; 58 | 59 | // then 60 | expect(getNewAttachPoint(point, oldBounds, newBounds)).to.eql({ x: -4, y: 0 }); 61 | }); 62 | 63 | 64 | it('should return new point\'s position delta', function() { 65 | 66 | // given 67 | var point = { 68 | x: 21, 69 | y: 12 70 | }; 71 | 72 | var oldBounds = { 73 | x: 18, 74 | y: 8, 75 | width: 4, 76 | height: 4 77 | }; 78 | 79 | var newBounds = { 80 | x: 18, 81 | y: 8, 82 | width: 10, 83 | height: 2 84 | }; 85 | 86 | // then 87 | expect(getNewAttachPoint(point, oldBounds, newBounds)).to.eql({ x: 26, y: 10 }); 88 | }); 89 | 90 | }); 91 | 92 | }); 93 | 94 | -------------------------------------------------------------------------------- /test/spec/util/EscapeUtilSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | escapeCSS, 3 | escapeHTML 4 | } from 'lib/util/EscapeUtil'; 5 | 6 | 7 | describe('util/EscapeUtil', function() { 8 | 9 | it('escapeCSS', function() { 10 | expect(escapeCSS('..ab')).to.eql('\\.\\.ab'); 11 | }); 12 | 13 | 14 | it('should escape HTML', function() { 15 | var htmlStr = '