diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 81eee51efc..0000000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": [ "env" ] -} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index e86ef84941..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage -dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index f8ce66a665..0000000000 --- a/.eslintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "plugin:bpmn-io/es6", - "env": { - "browser": true - }, - "globals": { - "Promise": true - } -} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3e3966511c..c64dae917b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,20 +2,17 @@ Great to see you! Help us out by [filing bugs or feature requests](#work-with-issues), assisting others [in our forums](https://forum.bpmn.io/), or [contributing improvements](#contribute-improvements). - ## Table of Contents * [Work with Issues](#work-with-issues) - * [Create an Issue](#Create-an-issue) - * [Help Out](#help-out) + * [Create an Issue](#create-an-issue) + * [Help Out](#help-out) * [Contribute Improvements](#contribute-improvements) - * [Setup the Project](#Setup-the-project) - * [Build and Run the Project](#build-and-run-the-project) - * [Discuss Code Changes](#discuss-code-changes) - * [Adhere to the Code Style](#adhere-to-the-code-style) - * [Adhere to the Unit Test Style](#adhere-to-the-unit-test-style) - * [Create a Pull Request](#create-a-pull-request) - + * [Setup the Project](#setup-the-project) + * [Build and Run the Project](#build-and-run-the-project) + * [Discuss Code Changes](#discuss-code-changes) + * [Adhere to the Unit Test Style](#adhere-to-the-unit-test-style) + * [Create a Pull Request](#create-a-pull-request) ## Work with Issues @@ -30,28 +27,26 @@ File bug reports or feature requests via [our issue tracker](https://github.com/ * Share your perspective on issues * Be helpful and respect others when commenting - ## Contribute Improvements Learn how to set up the project locally, make changes, and contribute bug fixes and new features through pull requests. ### Setup the Project -The project development runs on top of the [diagram-js](https://github.com/bpmn-io/diagram-js) master branch. The following code snippet sets up both libraries linking diagram-js to bpmn-js. +The project development runs on top of the [diagram-js](https://github.com/bpmn-io/diagram-js) `develop` branch. The following code snippet sets up both libraries linking diagram-js to bpmn-js. ```sh mkdir bpmn.io cd bpmn.io -git clone git@github.com:bpmn-io/diagram-js.git +git clone git@github.com:bpmn-io/diagram-js.git -b develop (cd diagram-js && npm i) git clone git@github.com:bpmn-io/bpmn-js.git (cd bpmn-js && npm install && npm link ../diagram-js) ``` -For details consult our in depth [setup guide](https://github.com/bpmn-io/bpmn-js/blob/master/docs/project/SETUP.md). - +For details consult our in depth [setup guide](../docs/project/SETUP.md). ### Build and Run the Project @@ -64,7 +59,13 @@ npm start Spin up the development environment, re-run tests with every file change: ```sh -TEST_BROWSERS=(Chrome|Firefox|IE) npm run dev +npm run dev +``` + +You may also run against different browsers: + +```sh +TEST_BROWSERS=Firefox npm run dev ``` Build, lint, and test the project, just as the CI does. @@ -73,24 +74,13 @@ Build, lint, and test the project, just as the CI does. npm run all ``` - ### Discuss Code Changes -Create a [pull request](#Create-a-pull-request) if you would like to have an in-depth discussion about some piece of code. - - -### Adhere to the Code Style - -In addition to our automatically enforced [lint rules](https://github.com/bpmn-io/eslint-plugin-bpmn-io), please adhere to the following conventions: - -* Use modules (`import` / `export (default)`) -* __Do NOT__ use ES language constructs (`class`, `const`, ...) in sources - -__Rationale:__ People should be able to consume parts of the library with an ES module aware bundler such as [Webpack](https://webpack.js.org/) or [Rollup](https://rollupjs.org) without the need to use a transpiler such as [Babel](https://babeljs.io/). +Create a [pull request](#create-a-pull-request) if you would like to have an in-depth discussion about some piece of code. ### Adhere to the Unit Test Style -In order to retrieve a sign-off for your contribution, it needs to be sufficiently and well tested. Please structure your unit tests into **given**, **when** and **then** ([ModelerSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/ModelerSpec.js#L116), [ResizeBehaviorSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/features/modeling/behavior/ResizeBehaviorSpec.js#L38)). To increase overall readability and understandability please also leave two empty lines before `describe(...)`, `it(...)` or *setup* blocks on the same indentation level ([ModelerSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/ModelerSpec.js#L49), [ResizeBehaviorSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/features/modeling/behavior/ResizeBehaviorSpec.js#L36)). +In order to retrieve a sign-off for your contribution, it needs to be sufficiently and well tested. Please structure your unit tests into __given__, __when__ and __then__ ([ModelerSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/ModelerSpec.js#L116), [ResizeBehaviorSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/features/modeling/behavior/ResizeBehaviorSpec.js#L38)). To increase overall readability and understandability please also leave two empty lines before `describe(...)`, `it(...)` or _setup_ blocks on the same indentation level ([ModelerSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/ModelerSpec.js#L49), [ResizeBehaviorSpec example](https://github.com/bpmn-io/bpmn-js/blob/develop/test/spec/features/modeling/behavior/ResizeBehaviorSpec.js#L36)). ### Create a Pull Request @@ -99,18 +89,16 @@ We use pull requests for feature additions and bug fixes. If you are not yet fam Some things that make it easier for us to accept your pull requests * The code adheres to our conventions - * spaces instead of tabs - * single-quotes - * ... + * spaces instead of tabs + * single-quotes + * ... * The code is tested * The `npm run all` build passes (executes tests + linting) * The work is combined into a single commit * The commit messages adhere to the [conventional commits guidelines](https://www.conventionalcommits.org) - We'd be glad to assist you if you do not get these things right in the first place. - --- Thanks for your interest in our library. diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index d10b05a7de..fcc7465149 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -5,12 +5,12 @@ labels: "bug" --- -__Describe the Bug__ +### Describe the Bug -__Steps to Reproduce__ +### Steps to Reproduce 1. do this 2. do that @@ -21,12 +21,12 @@ If you report a modeling related issue, ensure you can reproduce it on [demo.bpm When reporting a library error, try to build an example that reproduces your problem. You can use our playgrounds for [viewer](https://jsfiddle.net/07envcu1/) or [modeler](https://jsfiddle.net/bg97r61t/) as a starting point or put a demo up on [GitHub](https://github.com/) for inspection. --> -__Expected Behavior__ +### Expected Behavior -__Environment__ +### Environment - Browser: [e.g. IE 11, Chrome 69] - OS: [e.g. Windows 7] diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md index e72eeedc18..e8254ad405 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -5,20 +5,20 @@ labels: "enhancement" --- -__Is your feature request related to a problem? Please describe.__ +### Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -__Describe the solution you'd like__ +### Describe the solution you'd like A clear and concise description of what you want to happen. -__Describe alternatives you've considered__ +### Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. -__Additional context__ +### Additional context Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/TASK.md b/.github/ISSUE_TEMPLATE/TASK.md index 759e0f96a3..09b19bef68 100644 --- a/.github/ISSUE_TEMPLATE/TASK.md +++ b/.github/ISSUE_TEMPLATE/TASK.md @@ -4,11 +4,11 @@ about: Describe a generic activity we should carry out. --- -__What should we do?__ +### What should we do? -__Why should we do it?__ +### Why should we do it? \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 775d941360..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,20 +0,0 @@ -# configure dependabot automatic dependency upgrades (PRs) -# -# @see https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "monthly" - day: "monday" - time: "09:00" - timezone: "Europe/Berlin" - reviewers: # Automatically assign reviewer - - "bpmn-io/modeling-dev" - commit-message: - prefix: "deps:" - versioning-strategy: "increase-if-necessary" - # Disable version updates unless they are for security reasons. - open-pull-requests-limit: 0 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4066a6f9f3..3fe3c1e0c2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,46 +1,70 @@ name: CI on: [ push, pull_request ] jobs: - Build: + build_browsers: strategy: + fail-fast: false matrix: - os: [ macos-latest, ubuntu-latest, windows-latest ] - node-version: [ 14 ] + os: [ ubuntu-latest ] + browser: [ Firefox, ChromeHeadless ] runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v6 with: - node-version: ${{ matrix.node-version }} - - name: Cache Node.js modules - uses: actions/cache@v2 - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node- - ${{ runner.OS }}- + node-version: 24 + cache: 'npm' - name: Install dependencies run: npm ci - - name: Wire dependencies - run: ./tasks/wiredeps - - name: Build - if: matrix.os == 'ubuntu-latest' + - name: Setup project + uses: bpmn-io/actions/setup@latest + - name: Build (with coverage) + if: matrix.browser == 'ChromeHeadless' env: COVERAGE: 1 - TEST_BROWSERS: Firefox,PhantomJS,ChromeHeadless + TEST_BROWSERS: ${{ matrix.browser }} + run: npm run all + - name: Build + if: matrix.browser == 'Firefox' + env: + TEST_BROWSERS: ${{ matrix.browser }} run: xvfb-run npm run all + - name: Upload coverage + if: matrix.browser == 'ChromeHeadless' + uses: codecov/codecov-action@v6 + with: + fail_ci_if_error: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + build_os: + + strategy: + fail-fast: false + matrix: + os: [ macos-latest, windows-latest ] + browser: [ ChromeHeadless ] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Setup project + uses: bpmn-io/actions/setup@latest - name: Build - if: matrix.os != 'ubuntu-latest' env: - TEST_BROWSERS: ChromeHeadless + TEST_BROWSERS: ${{ matrix.browser }} run: npm run all - - name: Upload Coverage - run: npx codecov - if: matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/CODE_SCANNING.yml b/.github/workflows/CODE_SCANNING.yml new file mode 100644 index 0000000000..a5f9261da1 --- /dev/null +++ b/.github/workflows/CODE_SCANNING.yml @@ -0,0 +1,34 @@ +name: "Code Scanning" + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + paths-ignore: + - '**/*.md' + +jobs: + codeql-build: + # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest + runs-on: ubuntu-latest + + permissions: + # required for all workflows + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: javascript + config: | + paths-ignore: + - '**/test' + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/COMMENT_TARGETS_MAIN.yml b/.github/workflows/COMMENT_TARGETS_MAIN.yml new file mode 100644 index 0000000000..cda28df65f --- /dev/null +++ b/.github/workflows/COMMENT_TARGETS_MAIN.yml @@ -0,0 +1,24 @@ +name: COMMENT_TARGETS_MAIN +on: + pull_request: + types: + - opened + branches: + - main +permissions: + pull-requests: write +jobs: + comment: + name: Comment on targeting main branch + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Create comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.number }} + BODY: | + This pull request targets the `main` branch. Please target `main` for bug fixes only. Target `develop` for regular feature development. + run: gh issue comment "$NUMBER" --body "$BODY" \ No newline at end of file diff --git a/.github/workflows/MERGE_MAIN_TO_DEVELOP.yml b/.github/workflows/MERGE_MAIN_TO_DEVELOP.yml new file mode 100644 index 0000000000..1fb92f2059 --- /dev/null +++ b/.github/workflows/MERGE_MAIN_TO_DEVELOP.yml @@ -0,0 +1,36 @@ +name: MERGE_MAIN_TO_DEVELOP +on: + push: + branches: + - "main" + +jobs: + merge_main_to_develop: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout develop + uses: actions/checkout@v6 + with: + token: ${{ secrets.BPMN_IO_TOKEN }} + persist-credentials: true + ref: develop + fetch-depth: 0 + - name: Merge main to develop and push + run: | + git config user.name '${{ secrets.BPMN_IO_USERNAME }}' + git config user.email '${{ secrets.BPMN_IO_EMAIL }}' + git merge -m 'chore: merge main to develop' --no-edit origin/main + git push + + - name: Notify failure on Slack + if: failure() + uses: slackapi/slack-github-action@v3 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "Automatic merge of to failed." diff --git a/.github/workflows/MERGE_MASTER_TO_DEVELOP.yml b/.github/workflows/MERGE_MASTER_TO_DEVELOP.yml deleted file mode 100644 index cdcafa24c7..0000000000 --- a/.github/workflows/MERGE_MASTER_TO_DEVELOP.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: MERGE_MASTER_TO_DEVELOP -on: - push: - branches: - - "master" - -jobs: - Merge_master_to_develop: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout develop - uses: actions/checkout@v2 - with: - ref: develop - fetch-depth: 0 - - name: Merge master to develop and push - run: | - git config user.name '${{ secrets.BPMN_IO_USERNAME }}' - git config user.email '${{ secrets.BPMN_IO_EMAIL }}' - git merge -m 'Merge master to develop' --no-edit origin/master - git push - - - name: Notify failure on Slack - if: failure() - uses: slackapi/slack-github-action@v1.15.0 - with: - channel-id: ${{ secrets.SLACK_CHANNEL_ID }} - slack-message: "Automatic merge of to failed." - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/POST_RELEASE.yml b/.github/workflows/POST_RELEASE.yml index 3a3ca52c92..81d845dfa7 100644 --- a/.github/workflows/POST_RELEASE.yml +++ b/.github/workflows/POST_RELEASE.yml @@ -2,69 +2,73 @@ name: POST_RELEASE on: push: tags: - - 'v[0-9]+.*' + - 'v[0-9]+.[0-9]+.[0-9]+' + jobs: - post-release: + post_release: strategy: matrix: os: [ ubuntu-latest ] - node-version: [ 14 ] + node-version: [ 20 ] runs-on: ${{ matrix.os }} - steps: + - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Setup project + uses: bpmn-io/actions/setup@latest + - name: Set TAG run: echo "TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV + - name: Wait for published env: PKG: 'bpmn-js@${{ env.TAG }}' run: tasks/stages/await-published - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - name: Cache Node.js modules - uses: actions/cache@v2 - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node- - ${{ runner.OS }}- - - name: Check for stable release - run: | - if [[ ${{ env.TAG }} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] - then echo "STABLE_RELEASE=true" >> $GITHUB_ENV - fi + - name: Update integration test env: BPMN_IO_TOKEN: ${{ secrets.BPMN_IO_TOKEN }} BPMN_IO_EMAIL: ${{ secrets.BPMN_IO_EMAIL }} BPMN_IO_USERNAME: ${{ secrets.BPMN_IO_USERNAME }} run: tasks/stages/update-integration-test + - name: Update demo - if: ${{ env.STABLE_RELEASE == 'true' }} env: BPMN_IO_TOKEN: ${{ secrets.BPMN_IO_TOKEN }} BPMN_IO_EMAIL: ${{ secrets.BPMN_IO_EMAIL }} BPMN_IO_USERNAME: ${{ secrets.BPMN_IO_USERNAME }} BPMN_IO_DEMO_ENDPOINT: ${{ secrets.BPMN_IO_DEMO_ENDPOINT }} run: tasks/stages/update-demo + - name: Update examples - if: ${{ env.STABLE_RELEASE == 'true' }} env: BPMN_IO_TOKEN: ${{ secrets.BPMN_IO_TOKEN }} BPMN_IO_EMAIL: ${{ secrets.BPMN_IO_EMAIL }} BPMN_IO_USERNAME: ${{ secrets.BPMN_IO_USERNAME }} run: tasks/stages/update-examples + - name: Update website - if: ${{ env.STABLE_RELEASE == 'true' }} env: BPMN_IO_TOKEN: ${{ secrets.BPMN_IO_TOKEN }} BPMN_IO_EMAIL: ${{ secrets.BPMN_IO_EMAIL }} BPMN_IO_USERNAME: ${{ secrets.BPMN_IO_USERNAME }} run: tasks/stages/update-website + + - name: Update translations + env: + GITHUB_TOKEN: ${{ secrets.BPMN_IO_TOKEN }} + BPMN_IO_EMAIL: ${{ secrets.BPMN_IO_EMAIL }} + BPMN_IO_USERNAME: ${{ secrets.BPMN_IO_USERNAME }} + REVIEWERS: 'bpmn-io/modeling-dev' + TAG: ${{ env.TAG }} + run: tasks/stages/update-translations diff --git a/.github/workflows/RELEASE.yml b/.github/workflows/RELEASE.yml new file mode 100644 index 0000000000..32e1e7c9ba --- /dev/null +++ b/.github/workflows/RELEASE.yml @@ -0,0 +1,43 @@ +name: RELEASE +on: + push: + branches: + - main + - develop +jobs: + release_please: + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag: ${{ steps.release.outputs.tag_name }} + permissions: + contents: write # to create release commit (google-github-actions/release-please-action) + pull-requests: write # to create release PR (google-github-actions/release-please-action) + + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + target-branch: main + release-type: node + + publish: + needs: release_please + if: ${{ needs.release_please.outputs.release_created }} + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ce111f611f..0fa215154e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ coverage/ +lib/**/*.d.ts .idea *.iml .DS_Store \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000000..3b1e3d0589 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,6 @@ +{ + "pull-request-title-pattern": "chore: release v${version}", + "changelog-sections": "[{\"type\":\"feat\",\"section\":\"Features\",\"hidden\":false},{\"type\":\"fix\",\"section\":\"Bug Fixes\",\"hidden\":false},{\"type\":\"deps\",\"section\":\"Dependency Updates\",\"hidden\":false}]", + "changelog-type": "github", + ".": "18.16.1" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..b09d2ff860 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md + +**Read and apply our central [`AGENTS.md`](https://raw.githubusercontent.com/bpmn-io/.github/refs/heads/main/AGENTS.md) file before proceeding.** Treat the central file's contents as if they were written directly in this file. + +Instructions below (if any) extend those guidelines and take precedence if there is any conflict. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da49a4892..c5d664e87c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,753 @@ All notable changes to [bpmn-js](https://github.com/bpmn-io/bpmn-js) are documented here. We use [semantic versioning](http://semver.org/) for releases. - ## Unreleased ___Note:__ Yet to be released changes appear here._ +### 18.16.1 + +* `FIX`: render data output with filled arrow ([#2425](https://github.com/bpmn-io/bpmn-js/pull/2425)) + +### 18.16.0 + +* `FEAT`: make external labels resizable ([#2414](https://github.com/bpmn-io/bpmn-js/pull/2414)) + +### 18.15.0 + +* `FEAT`: show outline around lasso-selected elements ([#173](https://github.com/bpmn-io/bpmn-js/issues/173), [bpmn-io/diagram-js#1021](https://github.com/bpmn-io/diagram-js/pull/1021)) +* `FEAT`: improve text rendering performance ([bpmn-io/diagram-js#1026](https://github.com/bpmn-io/diagram-js/issues/1026), [bpmn-io/diagram-js#1027](https://github.com/bpmn-io/diagram-js/pull/1027)) +* `FEAT`: improve diagram import performance ([bpmn-io/diagram-js#1026](https://github.com/bpmn-io/diagram-js/issues/1026), [bpmn-io/diagram-js#1027](https://github.com/bpmn-io/diagram-js/pull/1027)) +* `FEAT`: move enclosed artifacts with participants / sub-processes ([#1929](https://github.com/bpmn-io/bpmn-js/issues/1929), [#2419](https://github.com/bpmn-io/bpmn-js/issues/2419)) +* `FIX`: ignore broken `BPMNDI` when setting label colors ([#2418](https://github.com/bpmn-io/bpmn-js/pull/2418)) +* `DEPS`: update to `diagram-js@15.13.0` +* `DEPS`: update to `min-dom@5.3.0` + +## 18.14.0 + +* `FEAT`: prioritize full word matches in search ([bpmn-io/diagram-js#1017](https://github.com/bpmn-io/diagram-js/pull/1017)) +* `FEAT`: factor match density into search ([bpmn-io/diagram-js#1017](https://github.com/bpmn-io/diagram-js/pull/1017)) +* `CHORE`: prioritize later search matches slightly lower ([bpmn-io/diagram-js#1017](https://github.com/bpmn-io/diagram-js/pull/1017)) +* `DEPS`: update to `diagram-js@15.11.0` +* `DEPS`: update to `ids@3.0.3` + +## 18.13.2 + +* `FIX`: disable grouping in popup menu during search ([bpmn-io/diagram-js#1014](https://github.com/bpmn-io/diagram-js/pull/1014)) +* `FIX`: correct handling of annotations during sub-process collapse/expand, copy/paste, and remove actions ([#2388](https://github.com/bpmn-io/bpmn-js/pull/2388)) +* `FIX`: allow undo of pasted sub-process ([#2388](https://github.com/bpmn-io/bpmn-js/pull/2388), [#2269](https://github.com/bpmn-io/bpmn-js/issues/2269)) +* `DEPS`: update to `diagram-js@15.10.0` + +## 18.13.1 + +* `FIX`: correct sequence flow layout for corner boundary events whose target is strictly axis-aligned ([#2270](https://github.com/bpmn-io/bpmn-js/issues/2270)) + +## 18.13.0 + +* `FEAT`: allow to create child elements from the context pad ([#2391](https://github.com/bpmn-io/bpmn-js/issues/2391)) + +## 18.12.1 + +* `FIX`: correctly replace non-interrupting event with an interrupting one ([#2313](https://github.com/bpmn-io/bpmn-js/issues/2313)) + +## 18.12.0 + +* `FEAT`: activate wheel zoom/scoll on `mouseover` ([#1008](https://github.com/bpmn-io/diagram-js/pull/1008)) +* `FEAT`: prevent keyboard movement for boundary events without host ([#2386](https://github.com/bpmn-io/bpmn-js/pull/2386)) +* `FIX`: prevent accidental creation of intermediate events during keyboard move ([#1803](https://github.com/bpmn-io/bpmn-js/issues/1803), [#1876](https://github.com/bpmn-io/bpmn-js/issues/1876)) +* `DEPS`: update to `diagram-js@15.9.0` + +## 18.11.0 + +* `FEAT`: add `cut` action and keyboard shortcut ([bpmn-io/diagram-js#1006](https://github.com/bpmn-io/diagram-js/pull/1006)) +* `DEPS`: update to `diagram-js@15.7.0` +* `DEPS`: update to `bpmn-moddle@10.0.0` +* `DEPS`: update to `min-dash@5.0.0` +* `DEPS`: update to `ids@3.0.0` +* `DEPS`: update to `tiny-svg@4.1.4` +* `DEPS`: update to `diagram-js-direct-editing@3.3.0` +* `DEPS`: update to `min-dom@5.2.0` + +## 18.10.1 + +* `DEPS`: update to `min-dash@4.2.3` +* `DEPS`: update to `tiny-svg@3.1.3` + +## 18.10.0 + +* `FEAT`: add ability to duplicate elements ([bpmn-io/diagram-js#998](https://github.com/bpmn-io/diagram-js/pull/998)) +* `DEPS`: update to `diagram-js@15.5.0` + +## 18.9.1 + +* `FIX`: only draw links for currently selected elements ([#2365](https://github.com/bpmn-io/bpmn-js/pull/2365)) + +## 18.9.0 + +* `FEAT`: visually link external label with its target ([#2328](https://github.com/bpmn-io/bpmn-js/pull/2328)) +* `FEAT`: add support for labels to `OutlineProvider#getOutline` +* `FIX`: ensure `BpmnRenderer#getShapePath` returns correct path for labels + +## 18.8.0 + +* `FEAT`: allow copying data object references and `isCollection` property ([#2348](https://github.com/bpmn-io/bpmn-js/pull/2348)) + +## 18.7.0 + +* `FEAT`: support disabled entries in popup menu ([bpmn-io/diagram-js#987](https://github.com/bpmn-io/diagram-js/pull/987)) +* `DEPS`: update to `diagram-js@15.4.0` + +## 18.6.5 + +* `FIX`: ensure popup menu keyboard navigation accounts for group order ([bpmn-io/diagram-js#989](https://github.com/bpmn-io/diagram-js/pull/989)) +* `DEPS`: update to `diagram-js@15.3.1` + +## 18.6.4 + +* `FIX`: revert `AdHocSubProcess#cancelRemainingInstances` default value removal ([bpmn-io/bpmn-moddle#132](https://github.com/bpmn-io/bpmn-moddle/pull/132)) +* `DEPS`: update to `bpmn-moddle@9.0.4` + +## 18.6.3 + +* `FIX`: remove `AdHocSubProcess#cancelRemainingInstances` default value ([bpmn-io/bpmn-moddle#131](https://github.com/bpmn-io/bpmn-moddle/issues/131)) +* `DEPS`: update to `bpmn-moddle@9.0.3` + +## 18.6.2 + +* `FIX`: center task markers ([#1995](https://github.com/bpmn-io/bpmn-js/issues/1995)) + +## 18.6.1 + +* `FIX`: copy error, escalation, message and signal references when copying elements ([#1906](https://github.com/bpmn-io/bpmn-js/issues/1906), [#2249](https://github.com/bpmn-io/bpmn-js/issues/2249), [#2301](https://github.com/bpmn-io/bpmn-js/pull/2301)) + +## 18.6.0 + +* `FEAT`: support searching through arrays in popup menu ([bpmn-io/diagram-js#970](https://github.com/bpmn-io/diagram-js/pull/970)) +* `FEAT`: prioritize `search` over `description` when matching popup menu entries ([bpmn-io/diagram-js#963](https://github.com/bpmn-io/diagram-js/pull/963)) +* `FEAT`: sort `search` terms across all keys ([bpmn-io/diagram-js#963](https://github.com/bpmn-io/diagram-js/pull/963)) +* `FIX`: always select first search entry ([bpmn-io/diagram-js#967](https://github.com/bpmn-io/diagram-js/pull/967)) +* `DEPS`: update to `diagram-js@15.3.0` + +## 18.5.0 + +* `FEAT`: allow text annotations for message flows ([#2281](https://github.com/bpmn-io/bpmn-js/issues/2281)) + +## 18.4.0 + +* `FEAT`: render collapsed event subprocess icons ([#50](https://github.com/bpmn-io/bpmn-js/issues/50)) + +## 18.3.2 + +* `FIX`: remove default start event for ad-hoc subprocess ([#2295](https://github.com/bpmn-io/bpmn-js/issues/2295)) +* `FIX`: show modeling feedback error for data objects ([#2290](https://github.com/bpmn-io/bpmn-js/pull/2290)) + +## 18.3.1 + +* `FIX`: move artifacts when a participant is resized by space tool ([#2285](https://github.com/bpmn-io/bpmn-js/pull/2285)) + +## 18.3.0 + +* `FEAT`: allow to replace between variants of typed events ([#2282](https://github.com/bpmn-io/bpmn-js/pull/2282)) + +## 18.2.0 + +* `FEAT`: add ad-hoc subprocess option to replace menu ([#2276](https://github.com/bpmn-io/bpmn-js/pull/2276)) + +## 18.1.2 + +* `FIX`: canvas `autoFocus` must explicitly be enabled ([bpmn-io/diagram-js#956](https://github.com/bpmn-io/diagram-js/pull/956)) +* `FIX`: properly integrate `zoomscroll` with canvas focus ([bpmn-io/diagram-js#956](https://github.com/bpmn-io/diagram-js/pull/956)) +* `FIX`: properly integrate `movecanvas` with canvas focus ([bpmn-io/diagram-js#956](https://github.com/bpmn-io/diagram-js/pull/956)) + +## 18.1.1 + +* `FIX`: adjust search to prioritize start of word and exact matches ([bpmn-io/diagram-js#953](https://github.com/bpmn-io/diagram-js/pull/953)) +* `FIX`: ignore whitespace when searching ([bpmn-io/diagram-js#954](https://github.com/bpmn-io/diagram-js/pull/954)) + +## 18.1.0 + +* `FIX`: clear selection when opening search pad ([bpmn-io/diagram-js#947](https://github.com/bpmn-io/diagram-js/pull/947)) +* `FIX`: correct dangling selection after search pad interaction ([bpmn-io/diagram-js#947](https://github.com/bpmn-io/diagram-js/pull/947)) +* `DEPS`: update to `diagram-js@15.2.2` + +## 18.0.0 + +* `FEAT`: remove `outline` from `Viewer` modules ([#2135](https://github.com/bpmn-io/bpmn-js/issues/2135)) +* `FEAT`: make `Canvas` a focusable element ([bpmn-io/diagram-js#662](https://github.com/bpmn-io/diagram-js/pull/662)) +* `FEAT`: implicit keyboard binding ([bpmn-io/diagram-js#662](https://github.com/bpmn-io/diagram-js/pull/662)) +* `FEAT`: integrate with global `search` ([#2235](https://github.com/bpmn-io/bpmn-js/pull/2235)) +* `FEAT`: integrate `popup-menu` with `search` ([bpmn-io/diagram-js#932](https://github.com/bpmn-io/diagram-js/pull/932)) +* `FEAT`: recognize modern `search` tokens in `search-pad` ([bpmn-io/diagram-js#932](https://github.com/bpmn-io/diagram-js/pull/932)) +* `FIX`: correctly handle duplicate entries and whitespace in `search` ([bpmn-io/diagram-js#932](https://github.com/bpmn-io/diagram-js/pull/932)) +* `FIX`: find `search` terms across all keys ([bpmn-io/diagram-js#932](https://github.com/bpmn-io/diagram-js/pull/932)) +* `FIX`: `search` always returns tokens for matched items ([bpmn-io/diagram-js#932](https://github.com/bpmn-io/diagram-js/pull/932)) +* `FIX`: prevent crash during label adjustment ([#2239](https://github.com/bpmn-io/bpmn-js/issues/2239)) +* `FIX`: keep existing loop characteristics when toggling through the replace menu ([#2251](https://github.com/bpmn-io/bpmn-js/pull/2251)) +* `FIX`: prevent covering multi selection with black box in `Viewer` ([#2135](https://github.com/bpmn-io/bpmn-js/issues/2135)) +* `FIX`: generate types for main entry ([`986e2bb`](https://github.com/bpmn-io/bpmn-js/commit/986e2bb51ea301e6e0df56f3606a27424fb90179)) +* `FIX`: correct handling of group name with whitespace only ([#2231](https://github.com/bpmn-io/bpmn-js/issues/2231)) +* `DEPS`: update to `bpmn-moddle@9` ([#2114](https://github.com/bpmn-io/bpmn-js/pull/2114)) +* `DEPS`: update to `diagram-js@15.1.0` +* `DEPS`: update to `diagram-js-direct-editing@3.2.0` + +### Breaking Changes + +* Require `Node >= 20` +* `Canvas` is now a focusable element and provides better support for native browser behaviors. Focus can be controlled with new `focus` and `restoreFocus` APIs ([bpmn-io/diagram-js#662](https://github.com/bpmn-io/diagram-js/pull/662)). +* Keyboard is now implicitly bound to canvas SVG element. Calls to `keyboard.bind` and `keyboard.bindTo` now result with a descriptive console error and have no effect ([bpmn-io/diagram-js#662](https://github.com/bpmn-io/diagram-js/pull/662)). +* Selection outline is no longer included in the viewer. If needed, add it as an additional module ([#2253](https://github.com/bpmn-io/bpmn-js/pull/2253)). + +## 17.11.1 + +* `FIX`: handle searching elements without labels ([#2232](https://github.com/bpmn-io/bpmn-js/issues/2232), [#2234](https://github.com/bpmn-io/bpmn-js/pull/2234)) + +## 17.11.0 + +* `FEAT`: align search styles with other popups ([#2187](https://github.com/bpmn-io/bpmn-js/pull/2187)) +* `FEAT`: prioritize start of tokens in search results ([#2187](https://github.com/bpmn-io/bpmn-js/pull/2187)) +* `FIX`: do not commit viewport changes on `ESC` ([#2189](https://github.com/bpmn-io/bpmn-js/issues/2189), [#2187](https://github.com/bpmn-io/bpmn-js/pull/2187)) +* `DEPS`: update to `diagram-js@14.10.0` + +## 17.10.0 + +* `CHORE`: correct various type hints ([#2228](https://github.com/bpmn-io/bpmn-js/issues/2228)) +* `FIX`: pasting compensation activity without boundary event ([#2070](https://github.com/bpmn-io/bpmn-js/issues/2070)) +* `FIX`: lane resize constraints for se and nw direction ([#2209](https://github.com/bpmn-io/bpmn-js/issues/2209)) +* `FIX`: auto place elements vertically in sub-processes ([#2127](https://github.com/bpmn-io/bpmn-js/issues/2127)) +* `FIX`: hide lane label during direct editing +* `DEPS`: update to `diagram-js@14.9.0` + +## 17.9.2 + +* `FIX`: keep direction when collapsing pools ([#2208](https://github.com/bpmn-io/bpmn-js/issues/2208)) + +## 17.9.1 + +* `FIX`: show delete action for labels ([#2163](https://github.com/bpmn-io/bpmn-js/issues/2163)) + +## 17.9.0 + +* `FIX`: remove incorrect attribute in replace menu ([#2196](https://github.com/bpmn-io/bpmn-js/pull/2196)) +* `DEPS`: update to diagram-js@14.7.2 + +## 17.8.3 + +* `FIX`: add accessible label to drill down button ([#2194](https://github.com/bpmn-io/bpmn-js/pull/2194)) + +## 17.8.2 + +* `FIX`: do not suggest root elements in search ([#2143](https://github.com/bpmn-io/bpmn-js/issues/2143)) + +## 17.8.1 + +* `FIX`: gracefully handle missing process DI in drilldown ([#2180](https://github.com/bpmn-io/bpmn-js/pull/2180)) +* `FIX`: do not cause HTML validation errors on move preview ([#2179](https://github.com/bpmn-io/bpmn-js/issues/2179)) +* `DEPS`: update to `diagram-js@14.7.1` + +## 17.8.0 + +* `FEAT`: keep global elements when deleting last participant ([#2175](https://github.com/bpmn-io/bpmn-js/pull/2175)) +* `FIX`: allow undo after deleting last participants and data store ([#1676](https://github.com/bpmn-io/bpmn-js/issues/1676)) +* `FIX`: allow styling markers with `canvas.addMarker` and css ([#2173](https://github.com/bpmn-io/bpmn-js/pull/2173)) +* `CHORE`: render flow markers as part of `djs-visual` ([#2173](https://github.com/bpmn-io/bpmn-js/pull/2173)) +* `DEPS`: update to `diagram-js@14.7.0` + +## 17.7.1 + +* `FIX`: correct call activity outline ([#2167](https://github.com/bpmn-io/bpmn-js/issues/2167)) +* `FIX`: gracefully handle missing `BPMNDiagram#plane` ([#2172](https://github.com/bpmn-io/bpmn-js/pull/2172), [#2171](https://github.com/bpmn-io/bpmn-js/pull/2171)) + +## 17.7.0 + +* `DEPS`: update to `diagram-js@14.6.0` + +## 17.6.4 + +* `DEPS`: update to `diagram-js@14.5.4` + +## 17.6.3 + +* `DEPS`: update to `diagram-js@14.5.3` + +## 17.6.2 + +* `DEPS`: update to `diagram-js@14.5.2` ([#2158](https://github.com/bpmn-io/bpmn-js/pull/2158)) + +## 17.6.1 + +* `DEPS`: update to `diagram-js@14.5.1` ([#2157](https://github.com/bpmn-io/bpmn-js/pull/2157)) + +## 17.6.0 + +* `FEAT`: add ability to type services and events ([#2121](https://github.com/bpmn-io/bpmn-js/issues/2121), [#2153](https://github.com/bpmn-io/bpmn-js/pull/2153)) +* `FIX`: remove preview on context pad close ([#2150](https://github.com/bpmn-io/bpmn-js/issues/2150)) +* `FIX`: use tagged template in error logging ([#2151](https://github.com/bpmn-io/bpmn-js/pull/2151)) + +## 17.5.0 + +* `FEAT`: remove direct editing outline for embedded labels ([#2147](https://github.com/bpmn-io/bpmn-js/pull/2147)) +* `FEAT`: do not translate technical errors ([#2145](https://github.com/bpmn-io/bpmn-js/pull/2145)) +* `DEPS`: update to `diagram-js-direct-editing@3.0.1` + +## 17.4.0 + +* `FEAT`: do not scale popup menu and context pad +* `DEPS`: update to `diagram-js@14.4.1` + +## 17.3.0 + +* `FEAT`: auto-place elements vertically ([#2110](https://github.com/bpmn-io/bpmn-js/issues/2110)) + +## 17.2.2 + +* `FIX`: correct navigated viewer outline ([#2133](https://github.com/bpmn-io/bpmn-js/issues/2133)) + +## 17.2.1 + +* `FIX`: render popup menu on top +* `DEPS`: update to `diagram-js@14.3.1` + +## 17.2.0 + +* `FEAT`: make popup menu keyboard navigatable +* `FIX`: address various accessibility issues +* `FIX`: correct various typing issues +* `DEPS`: update to `diagram-js@14.3.0` +* `DEPS`: update to `diagram-js-direct-editing@2.1.2` + +## 17.1.0 + +* `FEAT`: handle splitting vertical lanes ([#2101](https://github.com/bpmn-io/bpmn-js/pull/2101)) + +## 17.0.2 + +* `FIX`: create hit boxes for vertical lanes ([#2093](https://github.com/bpmn-io/bpmn-js/issues/2093)) + +## 17.0.1 + +* `FIX`: fix rendering of gateway without marker ([#2102](https://github.com/bpmn-io/bpmn-js/pull/2102)) + +## 17.0.0 + +* `FEAT`: add to selection through SHIFT ([bpmn-io/diagram-js#796](https://github.com/bpmn-io/diagram-js/pull/851), [#2053](https://github.com/bpmn-io/bpmn-js/issues/2053)) +* `CHORE`: remove broken touch interaction ([bpmn-io/diagram-js#796](https://github.com/bpmn-io/diagram-js/issues/796)) +* `DEPS`: update to `diagram-js@14.0.0` + +### Breaking Changes + +* Migrated to `diagram-js@14` which removes touch interaction module, and dependency on unsupported `hammerjs` package. If you rely on touch interaction, you need to support touch interaction on your own. + +## 16.5.0 + +* `FEAT`: handle adding vertical lanes ([#2086](https://github.com/bpmn-io/bpmn-js/issues/2086)) +* `FIX`: don't fill multiple parallel events ([#2085](https://github.com/bpmn-io/bpmn-js/issues/2085)) + +## 16.4.0 + +* `FEAT`: handle resizing of vertical lanes ([#2062](https://github.com/bpmn-io/bpmn-js/issues/2062)) +* `FEAT`: allow text annotations to overlap with the borders of subprocesses and pools ([#2049](https://github.com/bpmn-io/bpmn-js/issues/2049)) +* `FEAT`: support modeling of gateway without marker ([#2063](https://github.com/bpmn-io/bpmn-js/issues/2063)) +* `FIX`: correctly remove vertical lanes ([#2081](https://github.com/bpmn-io/bpmn-js/pull/2081)) +* `FIX`: do not set label on planes ([#2033](https://github.com/bpmn-io/bpmn-js/issues/2033)) + +## 16.3.2 + +* `FIX`: support core replace in compensation behavior ([#2073](https://github.com/bpmn-io/bpmn-js/issues/2073)) + +## 16.3.1 + +* `FIX`: do not remove connection that is being created when pasting compensation boundary event and handler ([#2069](https://github.com/bpmn-io/bpmn-js/pull/2069)) + +## 16.3.0 + +* `FEAT`: improve handling of compensation association ([#2038](https://github.com/bpmn-io/bpmn-js/issues/2038)) + +## 16.2.0 + +* `DEPS`: update to `bpmn-moddle@8.1.0` + +## 16.1.0 + +* `DEPS`: update to `diagram-js@13.4.0` +* `DEPS`: update to `diagram-js-direct-editing@2.1.1` +* `DEPS`: drop unused `object-refs` dependency + +## 16.0.0 + +* `FEAT`: render vertical pools and lanes ([#2024](https://github.com/bpmn-io/bpmn-js/pull/2024)) +* `FEAT`: sentence case titles and labels ([#2023](https://github.com/bpmn-io/bpmn-js/issues/2023)) +* `FIX`: ensure all error translations are collected ([#2040](https://github.com/bpmn-io/bpmn-js/pull/2040)) +* `DEPS` update to diagram-js@13.0.0 + +### Breaking Changes + +* Major updates to [diagram-js@13](https://github.com/bpmn-io/diagram-js/blob/develop/CHANGELOG.md#1300) and [didi@10](https://github.com/nikku/didi/blob/main/CHANGELOG.md#1000). Make sure to check out the linked changelog updates. +* Multiple translation labels has been updated to sentence case. If you rely on the old casing, you need to update your translations. + +## 15.2.2 + +* `FIX`: use correct types in BpmnRenderUtil ([#2036](https://github.com/bpmn-io/bpmn-js/pull/2036)) + +## 15.2.1 + +* `DEPS`: update to `diagram-js@13.8.1` + +## 15.2.0 + +* `FEAT`: remove selection outline from connections ([diagram-js#826](https://github.com/bpmn-io/diagram-js/pull/826)) +* `FEAT`: position context pad according to last waypoint for connections ([diagram-js#826](https://github.com/bpmn-io/diagram-js/pull/826)) +* `FIX`: prevent access of non-existing connection bounds ([diagram-js#824](https://github.com/bpmn-io/diagram-js/pull/824)) +* `FIX`: correct selection outline size for end event ([#2026](https://github.com/bpmn-io/bpmn-js/pull/2026)) +* `DEPS`: update to `diagram-js@13.8.0` + +## 15.1.3 + +* `FIX`: revert `djs-dragging` CSS class changes ([#2016](https://github.com/bpmn-io/bpmn-js/pull/2016)) +* `FIX`: clear context pad hover timeout on close ([#2016](https://github.com/bpmn-io/bpmn-js/pull/2016)) +* `DEPS`: update to `diagram-js@12.7.2` + +## 15.1.2 + +* `FIX`: revert selection outline removal for connections ([#2011](https://github.com/bpmn-io/bpmn-js/pull/2011)) +* `DEPS`: update to `diagram-js@12.7.1` + +## 15.1.1 + +* `FIX`: adjust selection outline to external label ([#2001](https://github.com/bpmn-io/bpmn-js/issues/2001)) + +## 15.1.0 + +* `FEAT`: add toggle for non-interrupting events ([#2000](https://github.com/bpmn-io/bpmn-js/pull/2000)) +* `FEAT`: keep events non-interrupting when using `bpmnReplace` by default ([#2000](https://github.com/bpmn-io/bpmn-js/pull/2000)) +* `DEPS`: update to `diagram-js@12.7.0` + +## 15.0.0 + +* `FEAT`: align selection outline with element's shape ([#1996](https://github.com/bpmn-io/bpmn-js/issues/1996)) +* `FEAT`: preview append on hover ([#1985](https://github.com/bpmn-io/bpmn-js/pull/1985)) +* `FEAT`: allow overriding `fill`, `stroke`, `width` and `height` when rendering elements ([#1985](https://github.com/bpmn-io/bpmn-js/pull/1985)) +* `FIX`: renderer only renders actual flow elements ([#1985](https://github.com/bpmn-io/bpmn-js/pull/1985)) +* `DEPS`: update to `diagram-js@12.6.0` + +### Breaking Changes + +* `BpmnRenderer` only renders actual flow elements (e.g. `bpmn:IntermediateCatchEvent` but not `bpmn:MessageEventDefinition`) + +## 14.2.0 + +* `FEAT`: make spacetool local per default ([bpmn-io/diagram-js#811](https://github.com/bpmn-io/diagram-js/pull/811), [#1975](https://github.com/bpmn-io/bpmn-js/issues/1975)) +* `FEAT`: add complex preview feature ([bpmn-io/diagram-js#807](https://github.com/bpmn-io/diagram-js/pull/807)) +* `CHORE`: mark connection as dragging when moving bendpoint ([bpmn-io/diagram-js#807](https://github.com/bpmn-io/diagram-js/pull/807)) +* `DEPS`: update to `diagram-js@12.5.0` + +## 14.1.3 + +* `CHORE`: correctly output tag in [#1982](https://github.com/bpmn-io/bpmn-js/pull/1982) + +## 14.1.2 + +* `CHORE`: fix POST_RELEASE job in [#1980](https://github.com/bpmn-io/bpmn-js/pull/1980) + +## 14.1.1 + +* `FIX`: asset path by [__@nikku__](https://github.com/nikku) in [#1977](https://github.com/bpmn-io/bpmn-js/pull/1977) + +## 14.1.0 + +* `FEAT`: ensure lanes aren't resized when using space tool in [#1972](https://github.com/bpmn-io/bpmn-js/pull/1972) +* `DOCS`: update translations for v14.0.0 by [__@bpmn-io-bot__](https://github.com/bpmn-io-bot) in [#1948](https://github.com/bpmn-io/bpmn-js/pull/1948) + +## 14.0.0 + +* `FEAT`: do not hide overlays on canvas move per default ([diagram-js#798](https://github.com/bpmn-io/diagram-js/issues/798)) +* `FEAT`: translate _Append TextAnnotation_ context pad action ([#1932](https://github.com/bpmn-io/bpmn-js/pull/1932)) +* `FIX`: allow to create connection + event-based gateway ([#1490](https://github.com/bpmn-io/bpmn-js/issues/1490)) +* `FIX`: make breadcrumb styling more robust ([#1945](https://github.com/bpmn-io/bpmn-js/pull/1945)) +* `FIX`: correct copy of default sequence flow elements ([#1935](https://github.com/bpmn-io/bpmn-js/issues/1935)) +* `CHORE`: extract `modeling-feedback` into dedicated module ([#1940](https://github.com/bpmn-io/bpmn-js/pull/1940)) +* `CHORE`: drop deprecated callback support from public API +* `CHORE`: drop deprecated `import.parse.complete` event member `context` +* `DEPS`: update to `diagram-js@12.3.0` +* `DEPS`: update to `bpmn-moddle@8.0.1` +* `DEPS`: update to `ids@1.0.3` + +### Breaking Changes + +* Deprecated callback style API removed. Migrate to promise-based APIs, released with `v7.0.0`. +* Deprecated `import.parse.complete` event member `context` removed. Access the same information via the event itself, as released with `v7.0.0`. + +## 13.2.2 + +* `FIX`: do not vertically resize empty pools using the space tool ([#1769](https://github.com/bpmn-io/bpmn-js/issues/1769)) + +## 13.2.1 + +* `FIX`: improve regular expression ([#1927](https://github.com/bpmn-io/bpmn-js/pull/1927)) +* `FIX`: show non-interrupting event version in replace menu ([#1924](https://github.com/bpmn-io/bpmn-js/pull/1924)) + +## 13.2.0 + +* `CHORE`: provide align and distribute context pad and popup menu icons as html ([#1920](https://github.com/bpmn-io/bpmn-js/pull/1920)) +* `DEPS`: update to `diagram-js@12.2.0` + +## 13.1.0 + +* `FEAT`: allow event rendering without icons ([#1917](https://github.com/bpmn-io/bpmn-js/pull/1917)) + +## 13.0.9 + +* `CHORE`: update translations infra + +## 13.0.8 + +_Republish of v13.0.7._ + +## 13.0.7 + +_Republish of v13.0.6._ + +## 13.0.6 + +* `DOCS`: update translations + +## 13.0.5 + +* `DEPS`: update to `diagram-js@12.1.0` + +## 13.0.4 + +* `DEPS`: bump to `diagram-js@12.0.2` + +## 13.0.3 + +* `FIX`: update label on `modeling.updateModdleProperties` ([#1872](https://github.com/bpmn-io/bpmn-js/issues/1872)) + +## 13.0.2 + +* `FIX`: export types as `type` ([#1897](https://github.com/bpmn-io/bpmn-js/pull/1897)) +* `DEPS`: bump to `diagram-js@12.0.1` + +## 13.0.1 + +* `FIX`: correct some type definitions ([#1896](https://github.com/bpmn-io/bpmn-js/pull/1896)) + +## 13.0.0 + +* `FEAT`: rework and complete type definitions ([#1886](https://github.com/bpmn-io/bpmn-js/pull/1886)) +* `DEPS`: update to `diagram-js@12.0.0` + +## 12.1.1 + +* `DEPS`: update to `diagram-js@11.13.0` + +## 12.1.0 + +* `FIX`: correct `Viewer#saveXML` type definition ([#1885](https://github.com/bpmn-io/bpmn-js/pull/1885)) +* `FIX`: correct `Viewer` constructor type definition ([#1882](https://github.com/bpmn-io/bpmn-js/issues/1882)) + +## 12.0.0 + +* `FEAT`: move `create-append-anything` to [external module](https://github.com/bpmn-io/bpmn-js-create-append-anything) ([#1873](https://github.com/bpmn-io/bpmn-js/pull/1873), [#1862](https://github.com/bpmn-io/bpmn-js/issues/1862)) +* `CHORE`: use `diagram-js@11.11.0` built-in selection after replace feature ([#1857](https://github.com/bpmn-io/bpmn-js/pull/1857)) +* `DEPS`: update to `diagram-js@11.12.0` + +### Breaking Changes + +* The create/append anything features moved to an [external module](https://github.com/bpmn-io/bpmn-js-create-append-anything). Include it to restore the `v11` create/append behavior. + +## 11.5.0 + +* `FEAT`: add root elements to definitions when provided via `modeling#update(Moddle)Properties` + +## 11.4.1 + +* `FIX`: correct redo triggering on international keyboard layouts ([#1842](https://github.com/bpmn-io/bpmn-js/issues/1842)) + +## 11.4.0 + +* `FEAT`: translate append menu entry labels and groups ([#1810](https://github.com/bpmn-io/bpmn-js/pull/1810)) +* `FEAT`: activate direct editing on participant creation ([#1845](https://github.com/bpmn-io/bpmn-js/pull/1845)) +* `FIX`: dragging append menu entries creates element connection ([#1843](https://github.com/bpmn-io/bpmn-js/pull/1843)) +* `FIX`: append shortcut triggers create menu if append not allowed ([#1840](https://github.com/bpmn-io/bpmn-js/issues/1840)) +* `FIX`: restore marker rendering workaround ([`9c6e475`](https://github.com/bpmn-io/bpmn-js/commit/9c6e475681dd6b6a418b2fbc1dac19a9df360953)) + +## 11.3.1 + +_Republish of `v11.3.0`._ + +## 11.3.0 + +* `FEAT`: feature `service` and `user` tasks more prominently in replace menu ([#1836](https://github.com/bpmn-io/bpmn-js/pull/1836)) +* `FEAT`: hide rare items initially from create/append menus ([#1836](https://github.com/bpmn-io/bpmn-js/pull/1836)) +* `FEAT`: retrieve instantiation modules with context ([#1835](https://github.com/bpmn-io/bpmn-js/pull/1835)) +* `DEPS`: update to `diagram-js@11.9.0` + +## 11.2.0 + +_Adds create/append anything._ + +* `FEAT`: append menu available via context pad ([#1802](https://github.com/bpmn-io/bpmn-js/pull/1802), [#1809](https://github.com/bpmn-io/bpmn-js/pull/1809), [#1815](https://github.com/bpmn-io/bpmn-js/pull/1815), [#1818](https://github.com/bpmn-io/bpmn-js/pull/1818), [#1831](https://github.com/bpmn-io/bpmn-js/pull/1831)) +* `FEAT`: create menu available via palette ([#1811](https://github.com/bpmn-io/bpmn-js/pull/1811), [#1809](https://github.com/bpmn-io/bpmn-js/pull/1809), [#1817](https://github.com/bpmn-io/bpmn-js/pull/1817)) +* `FEAT`: simplify connection-multi icon ([#1822](https://github.com/bpmn-io/bpmn-js/pull/1822)) +* `FEAT`: join paths `round` by default ([1827](https://github.com/bpmn-io/bpmn-js/pull/1827)) +* `FEAT`: improved BPMN symbol rendering ([#1830](https://github.com/bpmn-io/bpmn-js/pull/1830)) +* `FEAT`: round connection corners ([#1828](https://github.com/bpmn-io/bpmn-js/pull/1828)) +* `FEAT`: improve visibility of popup menu ([#1812](https://github.com/bpmn-io/bpmn-js/issues/1812)) +* `FIX`: missing special attributes in `bpmnElementFactory` ([#1807](https://github.com/bpmn-io/bpmn-js/pull/1807)) +* `FIX`: handle `bpmn:DataObjectReference` without data object in replace menu ([#1823](https://github.com/bpmn-io/bpmn-js/pull/1823)) +* `DEPS`: update to `diagram-js@11.8.0` + +## 11.1.1 + +* `FIX`: correct popup menu display in fullscreen ([#1795](https://github.com/bpmn-io/bpmn-js/issues/1795)) +* `DEPS`: update to `diagram-js@11.4.3` + +## 11.1.0 + +* `FEAT`: add replace element keyboard binding ([#1785](https://github.com/bpmn-io/bpmn-js/pull/1785)) +* `FEAT`: add `replaceElement` editor action ([#1785](https://github.com/bpmn-io/bpmn-js/pull/1785)) +* `DEPS`: update to `diagram-js@11.4.1` + +## 11.0.5 + +* `DEPS`: update to `diagram-js@11.3.0` + +## 11.0.4 + +* `DEPS`: update to `diagram-js@11.2.0` + +## 11.0.3 + +_Re-release of `v11.0.2`._ + +## 11.0.2 + +* `FIX`: correct test for replace options ([#1787](https://github.com/bpmn-io/bpmn-js/pull/1787)) + +## 11.0.1 + +* `DEPS`: update to `diagram-js@11.1.1` + +## 11.0.0 + +_Reworks popup menu UI._ + +* `FEAT`: integrate new popup menu UI ([#1776](https://github.com/bpmn-io/bpmn-js/pull/1776)) +* `DEPS`: update to `diagram-js@11.1.0` ([#1776](https://github.com/bpmn-io/bpmn-js/pull/1776)) + +### Breaking Changes + +* New popup menu UI introduced with `diagram-js@11`. See [`diagram-js` breaking changes and migration guide](https://github.com/bpmn-io/diagram-js/blob/develop/CHANGELOG.md#breaking-changes). +* Keyboard-related features no longer use `KeyboardEvent#keyCode`. Use a polyfill (e.g. [keyboardevent-key-polyfill](https://www.npmjs.com/package/keyboardevent-key-polyfill)) if you need to support old browsers. + +## 10.3.0 + +* `FEAT`: add BPMN specific space tool ([#1344](https://github.com/bpmn-io/bpmn-js/pull/1344)) +* `FIX`: do not resize `bpmn:TextAnnotation` when using space tool ([#1344](https://github.com/bpmn-io/bpmn-js/pull/1344)) +* `FIX`: correct attachers left hanging when using space tool ([#1344](https://github.com/bpmn-io/bpmn-js/pull/1344)) +* `FIX`: stick labels to label targets when using space tool ([#1344](https://github.com/bpmn-io/bpmn-js/pull/1344), [#1302](https://github.com/bpmn-io/bpmn-js/issues/1302)) +* `DEPS`: update to `diagram-js@10` + +## 10.2.1 + +* `FIX`: correct preserving of outgoing connections on event-based gateway morph ([#1738](https://github.com/bpmn-io/bpmn-js/issues/1738)) + +## 10.2.0 + +* `DEPS`: update to `bpmn-moddle@8` + +## 10.1.0 + +* `DEPS`: update to `diagram-js@9.1.0` + +## 10.0.0 + +_Updates the library target to ES2018._ + +* `FEAT`: use ES2018 syntax ([#1737](https://github.com/bpmn-io/bpmn-js/pull/1737)) + +### Breaking Changes + +* Migrated to ES2018 syntax. [Read the blog post with details and a migration guide](https://bpmn.io/blog/posts/2022-migration-to-es2018.html). + +## 9.4.1 + +* `FIX`: ignore elements which cannot be colored ([#1734](https://github.com/bpmn-io/bpmn-js/pull/1734)) + +## 9.4.0 + +* `FEAT`: allow clipboard to be serialized ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FEAT`: allow cloning of elements ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FEAT`: copy groups in a safe manner ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FIX`: make clipboard contents immutable ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FIX`: do not alter inputs passed to `ElementFactory#create` ([#1711](https://github.com/bpmn-io/bpmn-js/pull/1711)) +* `FIX`: prevent bogus meta-data to be attached on paste ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FIX`: only claim existing IDs ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FIX`: prevent double paste on label creation ([#1707](https://github.com/bpmn-io/bpmn-js/pull/1707)) +* `FIX`: move labels when collapsing sub-process ([#1695](https://github.com/bpmn-io/bpmn-js/issues/1695)) +* `FIX`: assign default size when expanding element ([#1687](https://github.com/bpmn-io/bpmn-js/issues/1687)) +* `FIX`: render sequence flows always on top ([#1716](https://github.com/bpmn-io/bpmn-js/issues/1716)) +* `DEPS`: update to `diagram-js@8.9.0` +* `DEPS`: update to `bpmn-moddle@7.1.3` + +## 9.3.2 + +* `FIX`: prevent unnecessary scrollbar ([#1692](https://github.com/bpmn-io/bpmn-js/issues/1692)) +* `FIX`: check for replacement using actual target ([#1699](https://github.com/bpmn-io/bpmn-js/pull/1699)) +* `DEPS`: update to `diagram-js@8.7.1` + +## 9.3.1 + +* `FIX`: properly size icons for distribute/align menu ([#1694](https://github.com/bpmn-io/bpmn-js/pull/1694)) + +## 9.3.0 + +* `FEAT`: add aligment and distribution menu ([#1680](https://github.com/bpmn-io/bpmn-js/issues/1680), [#1691](https://github.com/bpmn-io/bpmn-js/issues/1691)) +* `DEPS`: update to `diagram-js@8.7.0` + +## 9.2.2 + +* `FIX`: correctly toggle loop characteristics ([#1673](https://github.com/bpmn-io/bpmn-js/issues/1673)) + +## 9.2.1 + +* `FIX`: cancel direct editing before shape deletion ([#1677](https://github.com/bpmn-io/bpmn-js/issues/1677)) + +## 9.2.0 + +* `FEAT`: rework select and hover interaction on the diagram ([#1616](https://github.com/bpmn-io/bpmn-js/issues/1616), [#640](https://github.com/bpmn-io/diagram-js/pull/640), [#643](https://github.com/bpmn-io/diagram-js/pull/643)) +* `FEAT`: rework diagram interaction handles ([#640](https://github.com/bpmn-io/diagram-js/pull/640)) +* `FEAT`: clearly distinguish select and hover states ([#1616](https://github.com/bpmn-io/bpmn-js/issues/1616)) +* `FEAT`: allow text annotation on sequence flows ([#1652](https://github.com/bpmn-io/bpmn-js/pull/1652)) +* `FEAT`: add multi-element context pad ([#1525](https://github.com/bpmn-io/bpmn-js/pull/1525)) +* `FEAT`: change default color to off black ([#1656](https://github.com/bpmn-io/bpmn-js/pull/1656)) +* `FEAT`: select connection after connect ([#644](https://github.com/bpmn-io/diagram-js/pull/644)) +* `FIX`: copy elements with `string` extension properties ([#1518](https://github.com/bpmn-io/bpmn-js/issues/1518)) +* `FIX`: cancel direct editing before shape deletion ([#1664](https://github.com/bpmn-io/bpmn-js/issues/1664)) +* `FIX`: remove connection on source connection deletion ([#1663](https://github.com/bpmn-io/bpmn-js/issues/1663)) +* `FIX`: set correct label color when batch coloring elements ([#1653](https://github.com/bpmn-io/bpmn-js/issues/1653)) +* `FIX`: always reconnect labels and associations ([#1659](https://github.com/bpmn-io/bpmn-js/pull/1659)) +* `FIX`: correct connection drop highlighting +* `DEPS`: replace `inherits` with `inherits-browser` +* `DEPS`: bump to `diagram-js@8.5.0` + +## 9.1.0 + +* `FEAT`: allow to select participant and subprocess via click on body ([#1646](https://github.com/bpmn-io/bpmn-js/pull/1646)) +* `FIX`: comply with strict style-src CSP ([#1625](https://github.com/bpmn-io/bpmn-js/issues/1625)) +* `FIX`: complete direct editing when selection changes ([#1648](https://github.com/bpmn-io/bpmn-js/pull/1648)) +* `DEPS`: update to `diagram-js@8.3.0` +* `DEPS`: update to `min-dom@3.2.0` + +## 9.0.4 + +* `FIX`: remove `label` property on empty label ([#1637](https://github.com/bpmn-io/bpmn-js/issues/1637)) +* `FIX`: create drilldown overlays on `viewer.open` ([`574a67438`](https://github.com/bpmn-io/bpmn-js/commit/574a674381d6449b509396b6d17c4ca94674ea1c)) +* `FIX`: render data association inside collapsed sub-processes ([#1619](https://github.com/bpmn-io/bpmn-js/issues/1619)) +* `FIX`: preserve multi-instance properties when toggling between parallel and sequential ([#1581](https://github.com/bpmn-io/bpmn-js/issues/1581)) +* `FIX`: correct hanging sequence flow label after collapsing sub-process ([#1617](https://github.com/bpmn-io/bpmn-js/issues/1617)) +* `FIX`: correct start event not added to newly created sub-process ([#1631](https://github.com/bpmn-io/bpmn-js/issues/1631)) + ## 9.0.3 * `FIX`: submit direct editing result on drilldown ([#1609](https://github.com/bpmn-io/bpmn-js/issues/1609)) @@ -40,8 +782,8 @@ ___Note:__ Yet to be released changes appear here._ ## 8.10.0 -* `CHORE`: provide `ModelUtil#isAny` utility ([#https://github.com/bpmn-io/bpmn-js/pull/1604](https://github.com/bpmn-io/bpmn-js/pull/1604)) -* `CHORE`: provide `ModelUtil#getDi` utility ([#https://github.com/bpmn-io/bpmn-js/pull/1604](https://github.com/bpmn-io/bpmn-js/pull/1604)) +* `CHORE`: provide `ModelUtil#isAny` utility ([#1604](https://github.com/bpmn-io/bpmn-js/pull/1604)) +* `CHORE`: provide `ModelUtil#getDi` utility ([#1604](https://github.com/bpmn-io/bpmn-js/pull/1604)) ## 8.9.1 @@ -57,7 +799,7 @@ ___Note:__ Yet to be released changes appear here._ * `FIX`: set label color on `bpmndi:BPMNLabel#color` ([#1543](https://github.com/bpmn-io/bpmn-js/pull/1543)) * `FIX`: don't create illegal `bpmndi:BPMNEdge#waypoints` property ([#1544](https://github.com/bpmn-io/bpmn-js/issues/1544)) * `FIX`: correct direct editing on touch devices -* `DEPS`: update to diagram-js@7.8.2 +* `DEPS`: update to `diagram-js@7.8.2` ## 8.8.3 @@ -482,7 +1224,7 @@ Copy and paste as well as create is completely reworked: ### Breaking Changes -* `CHORE`: bump to [`diagram-js@4.0.0`](https://github.com/bpmn-io/diagram-js/blob/master/CHANGELOG.md#400) +* `CHORE`: bump to [`diagram-js@4.0.0`](https://github.com/bpmn-io/diagram-js/blob/main/CHANGELOG.md#400) ## 3.5.0 @@ -518,7 +1260,7 @@ _Republish of `v3.4.0` without `.git` folder._ * `FEAT`: consistently layout connection on reconnect start and end ([#971](https://github.com/bpmn-io/bpmn-js/pull/971)) * `FEAT`: layout connection on element removal ([#989](https://github.com/bpmn-io/bpmn-js/issues/989)) * `FIX`: properly crop sequence flow ends on undo/redo ([#940](https://github.com/bpmn-io/bpmn-js/issues/940)) -* `CHORE`: bump to [`diagram-js@3.3.0`](https://github.com/bpmn-io/diagram-js/blob/master/CHANGELOG.md#330) +* `CHORE`: bump to [`diagram-js@3.3.0`](https://github.com/bpmn-io/diagram-js/blob/main/CHANGELOG.md#330) ## 3.3.1 @@ -580,7 +1322,7 @@ _Republish of `v3.4.0` without `.git` folder._ * `FEAT`: require `Ctrl/Cmd` to be pressed as a modifier key to move the canvas via keyboard errors * `FEAT`: auto-expand elements when children resize ([#786](https://github.com/bpmn-io/bpmn-js/issues/786)) * `CHORE`: bind editor actions and keyboard shortcuts for explicitly added features only ([#887](https://github.com/bpmn-io/bpmn-js/pull/887)) -* `CHORE`: update to [`diagram-js@3.0.0`](https://github.com/bpmn-io/diagram-js/blob/master/CHANGELOG.md#300) +* `CHORE`: update to [`diagram-js@3.0.0`](https://github.com/bpmn-io/diagram-js/blob/main/CHANGELOG.md#300) * `FIX`: disallow attaching of `BoundaryEvent` to a `ReceiveTask` following an `EventBasedGateway` ([#874](https://github.com/bpmn-io/bpmn-js/issues/874)) * `FIX`: fix date in license ([#882](https://github.com/bpmn-io/bpmn-js/pull/882)) @@ -654,7 +1396,7 @@ _Republish of `v2.0.0` due to registry error._ ## 2.0.0 * `FEAT`: allow data store to be modeled between participants ([#483](https://github.com/bpmn-io/bpmn-js/issues/483)) -* `CHORE`: update to [`diagram-js@2.0.0`](https://github.com/bpmn-io/diagram-js/blob/master/CHANGELOG.md#200) +* `CHORE`: update to [`diagram-js@2.0.0`](https://github.com/bpmn-io/diagram-js/blob/main/CHANGELOG.md#200) * `FIX`: correctly handle missing `bpmndi:Label` bounds during model updating ([#794](https://github.com/bpmn-io/bpmn-js/issues/794)) ### Breaking Changes @@ -663,7 +1405,7 @@ _Republish of `v2.0.0` due to registry error._ ## 1.3.3 -* `CHORE`: update to [`bpmn-moddle@5.1.5`](https://github.com/bpmn-io/bpmn-moddle/blob/master/CHANGELOG.md#515) +* `CHORE`: update to [`bpmn-moddle@5.1.5`](https://github.com/bpmn-io/bpmn-moddle/blob/main/CHANGELOG.md#515) ## 1.3.2 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..208f62a36c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,4 @@ +# CLAUDE.md + +Claude does not natively support `AGENTS.md` yet. +Until it does, please read and follow [`./AGENTS.md`](./AGENTS.md) before starting any task. \ No newline at end of file diff --git a/README.md b/README.md index 1ee6a3763c..badc0958f7 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,10 @@ View and edit BPMN 2.0 diagrams in the browser. [![bpmn-js screencast](./resources/screencast.gif "bpmn-js in action")](http://demo.bpmn.io/s/start) - ## Installation -Use the library [pre-packaged](https://github.com/bpmn-io/bpmn-js-examples/tree/master/pre-packaged) -or include it [via npm](https://github.com/bpmn-io/bpmn-js-examples/tree/master/bundling) +Use the library [pre-packaged](https://github.com/bpmn-io/bpmn-js-examples/tree/main/pre-packaged) +or include it [via npm](https://github.com/bpmn-io/bpmn-js-examples/tree/main/bundling) into your node-style web-application. ## Usage @@ -36,22 +35,6 @@ try { Checkout our [examples](https://github.com/bpmn-io/bpmn-js-examples) for many more supported usage scenarios. - -### Dynamic Attach/Detach - -You may attach or detach the viewer dynamically to any element on the page, too: - -```javascript -const viewer = new BpmnJS(); - -// attach it to some element -viewer.attachTo('#container'); - -// detach the panel -viewer.detach(); -``` - - ## Resources * [Demo](http://demo.bpmn.io) @@ -60,7 +43,6 @@ viewer.detach(); * [Forum](https://forum.bpmn.io) * [Changelog](./CHANGELOG.md) - ## Build and Run Prepare the project by installing all dependencies: @@ -85,7 +67,6 @@ npm run dev You may need to perform [additional project setup](./docs/project/SETUP.md) when building the latest development snapshot. - ## Related bpmn-js builds on top of a few powerful tools: @@ -93,17 +74,7 @@ bpmn-js builds on top of a few powerful tools: * [bpmn-moddle](https://github.com/bpmn-io/bpmn-moddle): Read / write support for BPMN 2.0 XML in the browsers * [diagram-js](https://github.com/bpmn-io/diagram-js): Diagram rendering and editing toolkit - -## Contributing - -Please checkout our [contributing guidelines](./.github/CONTRIBUTING.md) if you plan to -file an issue or pull request. - - -## Code of Conduct - -By participating to this project, please uphold to our [Code of Conduct](https://github.com/bpmn-io/.github/blob/master/.github/CODE_OF_CONDUCT.md). - +It is an extensible toolkit, complemented by many [additional utilities](https://github.com/bpmn-io/awesome-bpmn-io). ## License diff --git a/assets/bpmn-js.css b/assets/bpmn-js.css index ff933e144d..ac1c97de98 100644 --- a/assets/bpmn-js.css +++ b/assets/bpmn-js.css @@ -8,7 +8,7 @@ --color-grey-225-10-80: hsl(225, 10%, 80%); --color-grey-225-10-85: hsl(225, 10%, 85%); --color-grey-225-10-90: hsl(225, 10%, 90%); - --color-grey-225-10-95: hsl(225, 10%, 95%); + --color-grey-225-10-95: hsl(225, 10%, 95%); --color-grey-225-10-97: hsl(225, 10%, 97%); --color-blue-205-100-45: hsl(205, 100%, 45%); @@ -24,8 +24,8 @@ --color-red-360-100-97: hsl(360, 100%, 97%); --color-white: hsl(0, 0%, 100%); - --color-black: hsl(0, 0%, 0%); - --color-black-opacity-05: hsla(0, 0%, 0%, 5%); + --color-black: hsl(0, 0%, 0%); + --color-black-opacity-05: hsla(0, 0%, 0%, 5%); --color-black-opacity-10: hsla(0, 0%, 0%, 10%); --breadcrumbs-font-family: var(--bjs-font-family); @@ -64,6 +64,7 @@ .bjs-breadcrumbs li { display: inline-flex; padding-bottom: 5px; + align-items: center; } .bjs-breadcrumbs li a { @@ -113,4 +114,32 @@ .selected .bjs-drilldown-empty { display: inherit; -} \ No newline at end of file +} + +[data-popup="align-elements"] .djs-popup-results { + display: flex; +} + +[data-popup="align-elements"] .djs-popup-body [data-group] + [data-group] { + border-left: 1px solid var(--popup-border-color); +} + +[data-popup="align-elements"] [data-group="align"] { + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +[data-popup="align-elements"] .djs-popup-body .entry { + padding: 6px 8px; +} + +[data-popup="align-elements"] .djs-popup-body .entry:not(:first-child) { + margin-top: 0; +} + +[data-popup="align-elements"] .djs-popup-entry-icon { + display: block; + margin: 0; + height: 20px; + width: 20px; +} diff --git a/docs/project/SETUP.md b/docs/project/SETUP.md index a4e02dd932..5ffc2773ba 100644 --- a/docs/project/SETUP.md +++ b/docs/project/SETUP.md @@ -2,16 +2,13 @@ This document describes the necessary steps to setup a `bpmn-js` development environment. - ## TLDR; -On Linux, OS X or Windows? [git](http://git-scm.com/), [NodeJS](nodejs.org) and [npm](https://www.npmjs.org/doc/cli/npm.html) ready? Check out the [setup script section](https://github.com/bpmn-io/bpmn-js/blob/master/docs/project/SETUP.md#setup-via-script) below. - +On Linux, OS X or Windows? [git](http://git-scm.com), [NodeJS](https://nodejs.org) and [npm](https://www.npmjs.org/doc/cli/npm.html) ready? Check out the [setup script section](#setup-via-script) below. ## Manual Steps -Make sure you have [git](http://git-scm.com/), [NodeJS](nodejs.org) and [npm](https://www.npmjs.org/doc/cli/npm.html) installed before you continue. - +Make sure you have [git](http://git-scm.com), [NodeJS](https://nodejs.org) and [npm](https://www.npmjs.org/doc/cli/npm.html) installed before you continue. ### Get Project + Dependencies @@ -23,7 +20,7 @@ The following projects from the [bpmn-io](https://github.com/bpmn-io) project on and clone them into a common directory via -``` +```sh git clone git@github.com:bpmn-io/bpmn-js.git git clone git@github.com:bpmn-io/diagram-js.git git clone git@github.com:bpmn-io/bpmn-moddle.git @@ -33,7 +30,7 @@ git clone git@github.com:bpmn-io/bpmn-moddle.git [Link dependent projects](https://docs.npmjs.com/cli/link) between each other to pick up changes immediately. -``` +```plain . ├─bpmn-js │ └─node_modules @@ -55,12 +52,10 @@ Use `mklink /d ` [(docs)](http://technet.microsoft.com/en-us/libr Execute `npm install` on each of the projects to grab their dependencies. - ### Verify Things are O.K. Execute `npm run all` on each project. Things should be fine. - ### Setup via Script -The whole setup can be automated through setup scripts for [Linux/OS X](https://github.com/bpmn-io/bpmn-js/blob/master/docs/project/setup.sh) and [Windows](https://github.com/bpmn-io/bpmn-js/blob/master/docs/project/setup.bat). \ No newline at end of file +The whole setup can be automated through setup scripts for [Linux/OS X](./setup.sh) and [Windows](./setup.bat). diff --git a/docs/translations.json b/docs/translations.json index 814a62f1c4..3c90b322b8 100644 --- a/docs/translations.json +++ b/docs/translations.json @@ -1,121 +1,129 @@ [ - "Activate the create/remove space tool", - "Activate the global connect tool", - "Activate the hand tool", - "Activate the lasso tool", - "Ad-hoc", - "Add Lane above", - "Add Lane below", - "Append ConditionIntermediateCatchEvent", - "Append EndEvent", - "Append Gateway", - "Append Intermediate/Boundary Event", - "Append MessageIntermediateCatchEvent", - "Append ReceiveTask", - "Append SignalIntermediateCatchEvent", - "Append Task", - "Append TimerIntermediateCatchEvent", + "Activate create/remove space tool", + "Activate global connect tool", + "Activate hand tool", + "Activate lasso tool", + "Ad-hoc sub-process", + "Ad-hoc sub-process (collapsed)", + "Ad-hoc sub-process (expanded)", + "Add lane above", + "Add lane below", + "Add text annotation", + "Align elements", + "Align elements bottom", + "Align elements center", + "Align elements left", + "Align elements middle", + "Align elements right", + "Align elements top", "Append compensation activity", - "Append {type}", - "Business Rule Task", - "Call Activity", - "Cancel Boundary Event", - "Change type", + "Append conditional intermediate catch event", + "Append end event", + "Append gateway", + "Append intermediate/boundary event", + "Append message intermediate catch event", + "Append receive task", + "Append signal intermediate catch event", + "Append task", + "Append timer intermediate catch event", + "Business rule task", + "Call activity", + "Cancel boundary event", + "Cancel end event", + "Change element", "Collection", - "Compensation Boundary Event", - "Compensation End Event", - "Compensation Intermediate Throw Event", - "Compensation Start Event", - "Complex Gateway", - "Conditional Boundary Event", - "Conditional Boundary Event (non-interrupting)", - "Conditional Flow", - "Conditional Intermediate Catch Event", - "Conditional Start Event", - "Conditional Start Event (non-interrupting)", - "Connect using Association", - "Connect using DataInputAssociation", - "Connect using Sequence/MessageFlow or Association", - "Create DataObjectReference", - "Create DataStoreReference", - "Create EndEvent", - "Create Gateway", - "Create Group", - "Create Intermediate/Boundary Event", - "Create Pool/Participant", - "Create StartEvent", - "Create Task", - "Create expanded SubProcess", - "Data Object Reference", - "Data Store Reference", - "Default Flow", - "Divide into three Lanes", - "Divide into two Lanes", - "Empty Pool", - "Empty Pool (removes content)", - "End Event", - "Error Boundary Event", - "Error End Event", - "Error Start Event", - "Escalation Boundary Event", - "Escalation Boundary Event (non-interrupting)", - "Escalation End Event", - "Escalation Intermediate Throw Event", - "Escalation Start Event", - "Escalation Start Event (non-interrupting)", - "Event Sub Process", - "Event based Gateway", - "Exclusive Gateway", - "Inclusive Gateway", - "Intermediate Throw Event", - "Link Intermediate Catch Event", - "Link Intermediate Throw Event", + "Compensation boundary event", + "Compensation end event", + "Compensation intermediate throw event", + "Compensation start event", + "Complex gateway", + "Conditional boundary event", + "Conditional boundary event (non-interrupting)", + "Conditional flow", + "Conditional intermediate catch event", + "Conditional start event", + "Conditional start event (non-interrupting)", + "Connect to other element", + "Connect using association", + "Connect using data input association", + "Create data object reference", + "Create data store reference", + "Create end event", + "Create expanded sub-process", + "Create gateway", + "Create group", + "Create intermediate/boundary event", + "Create pool/participant", + "Create start event", + "Create task", + "Data object must be placed within a pool/participant.", + "Data object reference", + "Data store reference", + "Default flow", + "Delete", + "Distribute elements horizontally", + "Distribute elements vertically", + "Divide into three lanes", + "Divide into two lanes", + "Empty pool/participant", + "Empty pool/participant (removes content)", + "End event", + "Error boundary event", + "Error end event", + "Error start event", + "Escalation boundary event", + "Escalation boundary event (non-interrupting)", + "Escalation end event", + "Escalation intermediate throw event", + "Escalation start event", + "Escalation start event (non-interrupting)", + "Event sub-process", + "Event-based gateway", + "Exclusive gateway", + "Inclusive gateway", + "Intermediate throw event", + "Link intermediate catch event", + "Link intermediate throw event", "Loop", - "Manual Task", - "Message Boundary Event", - "Message Boundary Event (non-interrupting)", - "Message End Event", - "Message Intermediate Catch Event", - "Message Intermediate Throw Event", - "Message Start Event", - "Message Start Event (non-interrupting)", - "Parallel Gateway", - "Parallel Multi Instance", - "Participant Multiplicity", - "Receive Task", - "Remove", - "Script Task", - "Send Task", - "Sequence Flow", - "Sequential Multi Instance", - "Service Task", - "Signal Boundary Event", - "Signal Boundary Event (non-interrupting)", - "Signal End Event", - "Signal Intermediate Catch Event", - "Signal Intermediate Throw Event", - "Signal Start Event", - "Signal Start Event (non-interrupting)", - "Start Event", - "Sub Process", - "Sub Process (collapsed)", - "Sub Process (expanded)", + "Manual task", + "Message boundary event", + "Message boundary event (non-interrupting)", + "Message end event", + "Message intermediate catch event", + "Message intermediate throw event", + "Message start event", + "Message start event (non-interrupting)", + "Open {element}", + "Parallel gateway", + "Parallel multi-instance", + "Participant multiplicity", + "Receive task", + "Script task", + "Search in diagram", + "Send task", + "Sequence flow", + "Sequential multi-instance", + "Service task", + "Signal boundary event", + "Signal boundary event (non-interrupting)", + "Signal end event", + "Signal intermediate catch event", + "Signal intermediate throw event", + "Signal start event", + "Signal start event (non-interrupting)", + "Start event", + "Sub-process", + "Sub-process (collapsed)", + "Sub-process (expanded)", "Task", - "Terminate End Event", - "Timer Boundary Event", - "Timer Boundary Event (non-interrupting)", - "Timer Intermediate Catch Event", - "Timer Start Event", - "Timer Start Event (non-interrupting)", + "Terminate end event", + "Timer boundary event", + "Timer boundary event (non-interrupting)", + "Timer intermediate catch event", + "Timer start event", + "Timer start event (non-interrupting)", + "Toggle non-interrupting", "Transaction", - "User Task", - "correcting missing bpmnElement on {plane} to {rootElement}", - "element {element} referenced by {referenced}#{property} not yet drawn", - "failed to import {element}", - "flow elements must be children of pools/participants", - "missing {semantic}#attachedToRef", - "multiple DI elements defined for {element}", - "no bpmnElement referenced in {element}", - "no diagram to display", - "no process or collaboration to display" + "User task", + "flow elements must be children of pools/participants" ] \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..cf4535ea45 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,59 @@ +import bpmnIoPlugin from 'eslint-plugin-bpmn-io'; + +const files = { + ignored: [ + 'dist', + 'coverage' + ], + build: [ + 'test/config/*.js', + 'tasks/**/*.mjs', + '*.js', + '*.mjs' + ], + test: [ + 'test/**/*.js' + ] +}; + +export default [ + { + ignores: files.ignored + }, + + // build + ...bpmnIoPlugin.configs.node.map(config => { + + return { + ...config, + files: files.build + }; + }), + + // lib + test + ...bpmnIoPlugin.configs.browser.map(config => { + + return { + ...config, + ignores: files.build + }; + }), + + // test + ...bpmnIoPlugin.configs.mocha.map(config => { + + return { + ...config, + files: files.test, + }; + }), + { + languageOptions: { + globals: { + sinon: true, + require: true + } + }, + files: files.test + } +]; \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 180f579e7b..0000000000 --- a/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { - default -} from './lib/Viewer'; \ No newline at end of file diff --git a/lib/.eslintrc b/lib/.eslintrc deleted file mode 100644 index 68555a9b33..0000000000 --- a/lib/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "plugins": [ - "import" - ], - "rules": { - "import/no-unresolved": "error" - } -} \ No newline at end of file diff --git a/lib/BaseModeler.js b/lib/BaseModeler.js index 4d242a05ac..c22d78c3cf 100644 --- a/lib/BaseModeler.js +++ b/lib/BaseModeler.js @@ -1,22 +1,27 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; -import Ids from 'ids'; +import { Ids } from 'ids'; import BaseViewer from './BaseViewer'; +/** + * @typedef {import('./BaseViewer').BaseViewerOptions} BaseViewerOptions + * @typedef {import('./BaseViewer').ModdleElementsById} ModdleElementsById + * + * @typedef {import('./model/Types').ModdleElement} ModdleElement + */ + /** * A base modeler for BPMN 2.0 diagrams. * - * Have a look at {@link Modeler} for a bundle that includes actual features. + * See {@link bpmn-js/lib/Modeler} for a fully-featured modeler. * - * @param {Object} [options] configuration options to pass to the viewer - * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. - * @param {string|number} [options.width] the width of the viewer - * @param {string|number} [options.height] the height of the viewer - * @param {Object} [options.moddleExtensions] extension packages to provide - * @param {Array} [options.modules] a list of modules to override the default modules - * @param {Array} [options.additionalModules] a list of modules to use with the default modules + * @template [ServiceMap=null] + * + * @extends BaseViewer + * + * @param {BaseViewerOptions} [options] The options to configure the modeler. */ export default function BaseModeler(options) { BaseViewer.call(this, options); @@ -37,27 +42,27 @@ inherits(BaseModeler, BaseViewer); /** - * Create a moddle instance, attaching ids to it. + * Create a moddle instance, attaching IDs to it. + * + * @param {BaseViewerOptions} options * - * @param {Object} options + * @return {Moddle} */ BaseModeler.prototype._createModdle = function(options) { var moddle = BaseViewer.prototype._createModdle.call(this, options); - // attach ids to moddle to be able to track - // and validated ids in the BPMN 2.0 XML document - // tree + // attach ids to moddle to be able to track and validated ids in the BPMN 2.0 + // XML document tree moddle.ids = new Ids([ 32, 36, 1 ]); return moddle; }; /** - * Collect ids processed during parsing of the - * definitions object. + * Collect IDs processed during parsing of the definitions object. * * @param {ModdleElement} definitions - * @param {Context} context + * @param {ModdleElementsById} elementsById */ BaseModeler.prototype._collectIds = function(definitions, elementsById) { @@ -69,6 +74,6 @@ BaseModeler.prototype._collectIds = function(definitions, elementsById) { ids.clear(); for (id in elementsById) { - ids.claim(id, elementsById[id]); + ids.claim(id, elementsById[ id ]); } }; diff --git a/lib/BaseModeler.spec.ts b/lib/BaseModeler.spec.ts new file mode 100644 index 0000000000..a5fb6ad5dd --- /dev/null +++ b/lib/BaseModeler.spec.ts @@ -0,0 +1,59 @@ +import Canvas from 'diagram-js/lib/core/Canvas'; +import EventBus from 'diagram-js/lib/core/EventBus'; + +import BaseModeler from './BaseModeler'; + +import { testViewer } from './BaseViewer.spec'; + +const modeler = new BaseModeler({ + container: 'container' +}); + +testViewer(modeler); + + +const otherModeler = new BaseModeler({ + container: 'container' +}); + +const extendedModeler = new BaseModeler({ + container: 'container', + alignToOrigin: false, + propertiesPanel: { + attachTo: '#properties-panel' + } +}); + + +// typed API usage + +type FooEvent = { + /** + * Very cool field! + */ + foo: string; +}; + +type EventMap = { + + foo: FooEvent +}; + +type TypeMap = { + canvas: Canvas, + eventBus: EventBus +}; + +const typedModeler = new BaseModeler(); + +const bus = typedModeler.get('eventBus'); + +const canvas = typedModeler.get('canvas'); + +canvas.zoom('fit-viewport'); + +typedModeler.on('foo', event => { + console.log(event.foo); +}); + +typedModeler.get('eventBus').on('foo', e => console.log(e.foo)); \ No newline at end of file diff --git a/lib/BaseViewer.js b/lib/BaseViewer.js index d727c31e3d..8d22eef0c8 100644 --- a/lib/BaseViewer.js +++ b/lib/BaseViewer.js @@ -13,6 +13,7 @@ import { import { domify, + assignStyle, query as domQuery, remove as domRemove } from 'min-dom'; @@ -22,67 +23,151 @@ import { } from 'tiny-svg'; import Diagram from 'diagram-js'; -import BpmnModdle from 'bpmn-moddle'; +import { BpmnModdle } from 'bpmn-moddle'; -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import { importBpmnDiagram } from './import/Importer'; -import { - wrapForCompatibility -} from './util/CompatibilityUtil'; +/** + * @template T + * + * @typedef { import('diagram-js/lib/core/EventBus').default } EventBus + */ + +/** + * @template T + * + * @typedef {import('diagram-js/lib/core/EventBus').EventBusEventCallback} EventBusEventCallback + */ + +/** + * @typedef {import('didi').ModuleDeclaration} ModuleDeclaration + * + * @typedef {import('./model/Types').Moddle} Moddle + * @typedef {import('./model/Types').ModdleElement} ModdleElement + * @typedef {import('./model/Types').ModdleExtension} ModdleExtension + * + * @typedef { { + * width?: number|string; + * height?: number|string; + * position?: string; + * container?: string|HTMLElement; + * moddleExtensions?: ModdleExtensions; + * additionalModules?: ModuleDeclaration[]; + * } & Record } BaseViewerOptions + * + * @typedef {Record} ModdleElementsById + * + * @typedef { { + * [key: string]: ModdleExtension; + * } } ModdleExtensions + * + * @typedef { { + * warnings: string[]; + * } } ImportXMLResult + * + * @typedef {ImportXMLResult & Error} ImportXMLError + * + * @typedef {ImportXMLResult} ImportDefinitionsResult + * + * @typedef {ImportXMLError} ImportDefinitionsError + * + * @typedef {ImportXMLResult} OpenResult + * + * @typedef {ImportXMLError} OpenError + * + * @typedef { { + * format?: boolean; + * preamble?: boolean; + * } } SaveXMLOptions + * + * @typedef { { + * xml?: string; + * error?: Error; + * } } SaveXMLResult + * + * @typedef { { + * svg: string; + * } } SaveSVGResult + * + * @typedef { { + * xml: string; + * } } ImportParseStartEvent + * + * @typedef { { + * error?: ImportXMLError; + * definitions?: ModdleElement; + * elementsById?: ModdleElementsById; + * references?: ModdleElement[]; + * warnings: string[]; + * } } ImportParseCompleteEvent + * + * @typedef { { + * error?: ImportXMLError; + * warnings: string[]; + * } } ImportDoneEvent + * + * @typedef { { + * definitions: ModdleElement; + * } } SaveXMLStartEvent + * + * @typedef {SaveXMLResult} SaveXMLDoneEvent + * + * @typedef { { + * error?: Error; + * svg: string; + * } } SaveSVGDoneEvent + */ + +/** + * @template Type + * + * @typedef { Type extends { eventBus: EventBus } ? X : never } EventMap + */ /** * A base viewer for BPMN 2.0 diagrams. * - * Have a look at {@link Viewer}, {@link NavigatedViewer} or {@link Modeler} for + * Have a look at {@link bpmn-js/lib/Viewer}, {@link bpmn-js/lib/NavigatedViewer} or {@link bpmn-js/lib/Modeler} for * bundles that include actual features. * - * @param {Object} [options] configuration options to pass to the viewer - * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. - * @param {string|number} [options.width] the width of the viewer - * @param {string|number} [options.height] the height of the viewer - * @param {Object} [options.moddleExtensions] extension packages to provide - * @param {Array} [options.modules] a list of modules to override the default modules - * @param {Array} [options.additionalModules] a list of modules to use with the default modules + * @template [ServiceMap=null] + * + * @extends Diagram + * + * @param {BaseViewerOptions} [options] The options to configure the viewer. */ export default function BaseViewer(options) { + /** + * @type {BaseViewerOptions} + */ options = assign({}, DEFAULT_OPTIONS, options); + /** + * @type {Moddle} + */ this._moddle = this._createModdle(options); + /** + * @type {HTMLElement} + */ this._container = this._createContainer(options); + this._init(this._container, this._moddle, options); + /* */ addProjectLogo(this._container); /* */ - - this._init(this._container, this._moddle, options); } inherits(BaseViewer, Diagram); -/** -* The importXML result. -* -* @typedef {Object} ImportXMLResult -* -* @property {Array} warnings -*/ - -/** -* The importXML error. -* -* @typedef {Error} ImportXMLError -* -* @property {Array} warnings -*/ - /** * Parse and render a BPMN 2.0 diagram. * @@ -93,7 +178,7 @@ inherits(BaseViewer, Diagram); * * During import the viewer will fire life-cycle events: * - * * import.parse.start (about to read model from xml) + * * import.parse.start (about to read model from XML) * * import.parse.complete (model read; may have worked or not) * * import.render.start (graphical import start) * * import.render.complete (graphical import finished) @@ -101,105 +186,102 @@ inherits(BaseViewer, Diagram); * * You can use these events to hook into the life-cycle. * - * @param {string} xml the BPMN 2.0 xml - * @param {ModdleElement|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) + * @throws {ImportXMLError} An error thrown during the import of the XML. + * + * @fires BaseViewer#ImportParseStartEvent + * @fires BaseViewer#ImportParseCompleteEvent + * @fires Importer#ImportRenderStartEvent + * @fires Importer#ImportRenderCompleteEvent + * @fires BaseViewer#ImportDoneEvent * - * Returns {Promise} + * @param {string} xml The BPMN 2.0 XML to be imported. + * @param {ModdleElement|string} [bpmnDiagram] The optional diagram or Id of the BPMN diagram to open. + * + * @return {Promise} A promise resolving with warnings that were produced during the import. */ -BaseViewer.prototype.importXML = wrapForCompatibility(function importXML(xml, bpmnDiagram) { +BaseViewer.prototype.importXML = async function importXML(xml, bpmnDiagram) { - var self = this; + const self = this; function ParseCompleteEvent(data) { - - var event = self.get('eventBus').createEvent(data); - - // TODO(nikku): remove with future bpmn-js version - Object.defineProperty(event, 'context', { - enumerable: true, - get: function() { - - console.warn(new Error( - 'import.parse.complete is deprecated ' + - 'and will be removed in future library versions' - )); - - return { - warnings: data.warnings, - references: data.references, - elementsById: data.elementsById - }; - } - }); - - return event; + return self.get('eventBus').createEvent(data); } - return new Promise(function(resolve, reject) { + let aggregatedWarnings = []; + try { // hook in pre-parse listeners + // allow xml manipulation - xml = self._emit('import.parse.start', { xml: xml }) || xml; - - self._moddle.fromXML(xml, 'bpmn:Definitions').then(function(result) { - var definitions = result.rootElement; - var references = result.references; - var parseWarnings = result.warnings; - var elementsById = result.elementsById; - - // hook in post parse listeners + - // allow definitions manipulation - definitions = self._emit('import.parse.complete', ParseCompleteEvent({ - error: null, - definitions: definitions, - elementsById: elementsById, - references: references, - warnings: parseWarnings - })) || definitions; - self.importDefinitions(definitions, bpmnDiagram).then(function(result) { - var allWarnings = [].concat(parseWarnings, result.warnings || []); + /** + * A `import.parse.start` event. + * + * @event BaseViewer#ImportParseStartEvent + * @type {ImportParseStartEvent} + */ + xml = this._emit('import.parse.start', { xml: xml }) || xml; - self._emit('import.done', { error: null, warnings: allWarnings }); - - return resolve({ warnings: allWarnings }); - }).catch(function(err) { - var allWarnings = [].concat(parseWarnings, err.warnings || []); - - self._emit('import.done', { error: err, warnings: allWarnings }); - - return reject(addWarningsToError(err, allWarnings)); - }); - }).catch(function(err) { - - self._emit('import.parse.complete', { - error: err + let parseResult; + try { + parseResult = await this._moddle.fromXML(xml, 'bpmn:Definitions'); + } catch (error) { + this._emit('import.parse.complete', { + error }); - err = checkValidationError(err); - - self._emit('import.done', { error: err, warnings: err.warnings }); - - return reject(err); - }); - }); -}); + throw error; + } -/** -* The importDefinitions result. -* -* @typedef {Object} ImportDefinitionsResult -* -* @property {Array} warnings -*/ + let definitions = parseResult.rootElement; + const references = parseResult.references; + const parseWarnings = parseResult.warnings; + const elementsById = parseResult.elementsById; + + aggregatedWarnings = aggregatedWarnings.concat(parseWarnings); + + // hook in post parse listeners + + // allow definitions manipulation + + /** + * A `import.parse.complete` event. + * + * @event BaseViewer#ImportParseCompleteEvent + * @type {ImportParseCompleteEvent} + */ + definitions = this._emit('import.parse.complete', ParseCompleteEvent({ + error: null, + definitions: definitions, + elementsById: elementsById, + references: references, + warnings: aggregatedWarnings + })) || definitions; + + const importResult = await this.importDefinitions(definitions, bpmnDiagram); + + aggregatedWarnings = aggregatedWarnings.concat(importResult.warnings); + + /** + * A `import.parse.complete` event. + * + * @event BaseViewer#ImportDoneEvent + * @type {ImportDoneEvent} + */ + this._emit('import.done', { error: null, warnings: aggregatedWarnings }); + + return { warnings: aggregatedWarnings }; + } catch (err) { + let error = err; + aggregatedWarnings = aggregatedWarnings.concat(error.warnings || []); + addWarningsToError(error, aggregatedWarnings); + + error = checkValidationError(error); + + this._emit('import.done', { error, warnings: error.warnings }); + + throw error; + } +}; -/** -* The importDefinitions error. -* -* @typedef {Error} ImportDefinitionsError -* -* @property {Array} warnings -*/ /** * Import parsed definitions and render a BPMN 2.0 diagram. @@ -216,46 +298,20 @@ BaseViewer.prototype.importXML = wrapForCompatibility(function importXML(xml, bp * * You can use these events to hook into the life-cycle. * - * @param {ModdleElement} definitions parsed BPMN 2.0 definitions - * @param {ModdleElement|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) + * @throws {ImportDefinitionsError} An error thrown during the import of the definitions. * - * Returns {Promise} - */ -BaseViewer.prototype.importDefinitions = wrapForCompatibility(function importDefinitions(definitions, bpmnDiagram) { - - var self = this; - - return new Promise(function(resolve, reject) { - - self._setDefinitions(definitions); - - self.open(bpmnDiagram).then(function(result) { - - var warnings = result.warnings; - - return resolve({ warnings: warnings }); - }).catch(function(err) { - - return reject(err); - }); - }); -}); - -/** - * The open result. - * - * @typedef {Object} OpenResult + * @param {ModdleElement} definitions The definitions. + * @param {ModdleElement|string} [bpmnDiagram] The optional diagram or ID of the BPMN diagram to open. * - * @property {Array} warnings + * @return {Promise} A promise resolving with warnings that were produced during the import. */ +BaseViewer.prototype.importDefinitions = async function importDefinitions(definitions, bpmnDiagram) { + this._setDefinitions(definitions); + const result = await this.open(bpmnDiagram); + + return { warnings: result.warnings }; +}; -/** -* The open error. -* -* @typedef {Error} OpenError -* -* @property {Array} warnings -*/ /** * Open diagram of previously imported XML. @@ -272,63 +328,50 @@ BaseViewer.prototype.importDefinitions = wrapForCompatibility(function importDef * * You can use these events to hook into the life-cycle. * - * @param {string|ModdleElement} [bpmnDiagramOrId] id or the diagram to open + * @throws {OpenError} An error thrown during opening. * - * Returns {Promise} + * @param {ModdleElement|string} bpmnDiagramOrId The diagram or Id of the BPMN diagram to open. + * + * @return {Promise} A promise resolving with warnings that were produced during opening. */ -BaseViewer.prototype.open = wrapForCompatibility(function open(bpmnDiagramOrId) { - - var definitions = this._definitions; - var bpmnDiagram = bpmnDiagramOrId; - - var self = this; +BaseViewer.prototype.open = async function open(bpmnDiagramOrId) { - return new Promise(function(resolve, reject) { - if (!definitions) { - var err1 = new Error('no XML imported'); - - return reject(addWarningsToError(err1, [])); - } + const definitions = this._definitions; + let bpmnDiagram = bpmnDiagramOrId; - if (typeof bpmnDiagramOrId === 'string') { - bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId); + if (!definitions) { + const error = new Error('no XML imported'); + addWarningsToError(error, []); - if (!bpmnDiagram) { - var err2 = new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found'); + throw error; + } - return reject(addWarningsToError(err2, [])); - } - } + if (typeof bpmnDiagramOrId === 'string') { + bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId); - // clear existing rendered diagram - // catch synchronous exceptions during #clear() - try { - self.clear(); - } catch (error) { + if (!bpmnDiagram) { + const error = new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found'); + addWarningsToError(error, []); - return reject(addWarningsToError(error, [])); + throw error; } + } - // perform graphical import - importBpmnDiagram(self, definitions, bpmnDiagram).then(function(result) { - - var warnings = result.warnings; + // clear existing rendered diagram + // catch synchronous exceptions during #clear() + try { + this.clear(); + } catch (error) { + addWarningsToError(error, []); - return resolve({ warnings: warnings }); - }).catch(function(err) { + throw error; + } - return reject(err); - }); - }); -}); + // perform graphical import + const { warnings } = await importBpmnDiagram(this, definitions, bpmnDiagram); -/** - * The saveXML result. - * - * @typedef {Object} SaveXMLResult - * - * @property {string} xml - */ + return { warnings }; +}; /** * Export the currently displayed BPMN 2.0 diagram as @@ -344,68 +387,66 @@ BaseViewer.prototype.open = wrapForCompatibility(function open(bpmnDiagramOrId) * * You can use these events to hook into the life-cycle. * - * @param {Object} [options] export options - * @param {boolean} [options.format=false] output formatted XML - * @param {boolean} [options.preamble=true] output preamble + * @throws {Error} An error thrown during export. + * + * @fires BaseViewer#SaveXMLStart + * @fires BaseViewer#SaveXMLDone * - * Returns {Promise} + * @param {SaveXMLOptions} [options] The options. + * + * @return {Promise} A promise resolving with the XML. */ -BaseViewer.prototype.saveXML = wrapForCompatibility(function saveXML(options) { +BaseViewer.prototype.saveXML = async function saveXML(options) { options = options || {}; - var self = this; - - var definitions = this._definitions; - - return new Promise(function(resolve) { + let definitions = this._definitions, + error, xml; + try { if (!definitions) { - return resolve({ - error: new Error('no definitions loaded') - }); + throw new Error('no definitions loaded'); } // allow to fiddle around with definitions - definitions = self._emit('saveXML.start', { - definitions: definitions - }) || definitions; - self._moddle.toXML(definitions, options).then(function(result) { + /** + * A `saveXML.start` event. + * + * @event BaseViewer#SaveXMLStartEvent + * @type {SaveXMLStartEvent} + */ + definitions = this._emit('saveXML.start', { + definitions + }) || definitions; - var xml = result.xml; + const result = await this._moddle.toXML(definitions, options); + xml = result.xml; - xml = self._emit('saveXML.serialized', { - xml: xml - }) || xml; + xml = this._emit('saveXML.serialized', { + xml + }) || xml; + } catch (err) { + error = err; + } - return resolve({ - xml: xml - }); - }); - }).catch(function(error) { - return { error: error }; - }).then(function(result) { + const result = error ? { error } : { xml }; - self._emit('saveXML.done', result); + /** + * A `saveXML.done` event. + * + * @event BaseViewer#SaveXMLDoneEvent + * @type {SaveXMLDoneEvent} + */ + this._emit('saveXML.done', result); - var error = result.error; + if (error) { + throw error; + } - if (error) { - return Promise.reject(error); - } + return result; +}; - return result; - }); -}); - -/** - * The saveSVG result. - * - * @typedef {Object} SaveSVGResult - * - * @property {string} svg - */ /** * Export the currently displayed BPMN 2.0 diagram as @@ -420,95 +461,68 @@ BaseViewer.prototype.saveXML = wrapForCompatibility(function saveXML(options) { * * You can use these events to hook into the life-cycle. * - * @param {Object} [options] + * @throws {Error} An error thrown during export. + * + * @fires BaseViewer#SaveSVGDone * - * Returns {Promise} + * @return {Promise} A promise resolving with the SVG. */ -BaseViewer.prototype.saveSVG = wrapForCompatibility(function saveSVG(options) { - - options = options || {}; - - var self = this; +BaseViewer.prototype.saveSVG = async function saveSVG() { + this._emit('saveSVG.start'); - return new Promise(function(resolve, reject) { + let svg, err; - self._emit('saveSVG.start'); + try { + const canvas = this.get('canvas'); - var svg, err; + const contentNode = canvas.getActiveLayer(), + defsNode = domQuery(':scope > defs', canvas._svg); - try { - var canvas = self.get('canvas'); - - var contentNode = canvas.getActiveLayer(), - defsNode = domQuery('defs', canvas._svg); - - var contents = innerSVG(contentNode), + const contents = innerSVG(contentNode), defs = defsNode ? '' + innerSVG(defsNode) + '' : ''; - var bbox = contentNode.getBBox(); - - svg = - '\n' + - '\n' + - '\n' + - '' + - defs + contents + - ''; - } catch (e) { - err = e; - } - - self._emit('saveSVG.done', { - error: err, - svg: svg - }); - - if (!err) { - return resolve({ svg: svg }); - } + const bbox = contentNode.getBBox(); + + svg = + '\n' + + '\n' + + '\n' + + '' + + defs + contents + + ''; + } catch (e) { + err = e; + } - return reject(err); + /** + * A `saveSVG.done` event. + * + * @event BaseViewer#SaveSVGDoneEvent + * @type {SaveSVGDoneEvent} + */ + this._emit('saveSVG.done', { + error: err, + svg: svg }); -}); -/** - * Get a named diagram service. - * - * @example - * - * var elementRegistry = viewer.get('elementRegistry'); - * var startEventShape = elementRegistry.get('StartEvent_1'); - * - * @param {string} name - * - * @return {Object} diagram service instance - * - * @method BaseViewer#get - */ - -/** - * Invoke a function in the context of this viewer. - * - * @example - * - * viewer.invoke(function(elementRegistry) { - * var startEventShape = elementRegistry.get('StartEvent_1'); - * }); - * - * @param {Function} fn to be invoked - * - * @return {Object} the functions return value - * - * @method BaseViewer#invoke - */ + if (err) { + throw err; + } + return { svg }; +}; BaseViewer.prototype._setDefinitions = function(definitions) { this._definitions = definitions; }; +/** + * Return modules to instantiate with. + * + * @return {ModuleDeclaration[]} The modules. + */ BaseViewer.prototype.getModules = function() { return this._modules; }; @@ -516,10 +530,8 @@ BaseViewer.prototype.getModules = function() { /** * Remove all drawn elements from the viewer. * - * After calling this method the viewer can still - * be reused for opening another diagram. - * - * @method BaseViewer#clear + * After calling this method the viewer can still be reused for opening another + * diagram. */ BaseViewer.prototype.clear = function() { if (!this.getDefinitions()) { @@ -533,8 +545,8 @@ BaseViewer.prototype.clear = function() { }; /** - * Destroy the viewer instance and remove all its - * remainders from the document tree. + * Destroy the viewer instance and remove all its remainders from the document + * tree. */ BaseViewer.prototype.destroy = function() { @@ -546,29 +558,83 @@ BaseViewer.prototype.destroy = function() { }; /** - * Register an event listener + * @overlord + * + * Register an event listener for events with the given name. + * + * The callback will be invoked with `event, ...additionalArguments` + * that have been passed to {@link EventBus#fire}. + * + * Returning false from a listener will prevent the events default action + * (if any is specified). To stop an event from being processed further in + * other listeners execute {@link Event#stopPropagation}. + * + * Returning anything but `undefined` from a listener will stop the listener propagation. + * + * @template {keyof EventMap} EventName + * + * @param {EventName} events to subscribe to + * @param {number} [priority=1000] listen priority + * @param {EventBusEventCallback<(EventMap)[EventName]>} callback + * @param {any} [that] callback context + */ +/** + * @overlord + * + * Register an event listener for events with the given name. + * + * The callback will be invoked with `event, ...additionalArguments` + * that have been passed to {@link EventBus#fire}. + * + * Returning false from a listener will prevent the events default action + * (if any is specified). To stop an event from being processed further in + * other listeners execute {@link Event#stopPropagation}. + * + * Returning anything but `undefined` from a listener will stop the listener propagation. * - * Remove a previously added listener via {@link #off(event, callback)}. + * @template T * - * @param {string} event - * @param {number} [priority] - * @param {Function} callback - * @param {Object} [that] + * @param {string|string[]} events The event(s) to listen to. + * @param {number} [priority] The priority with which to listen. + * @param {EventBusEventCallback} callback The callback. + * @param {any} [that] Value of `this` the callback will be called with. */ -BaseViewer.prototype.on = function(event, priority, callback, target) { - return this.get('eventBus').on(event, priority, callback, target); +/** + * Register an event listener for events with the given name. + * + * The callback will be invoked with `event, ...additionalArguments` + * that have been passed to {@link EventBus#fire}. + * + * Returning false from a listener will prevent the events default action + * (if any is specified). To stop an event from being processed further in + * other listeners execute {@link Event#stopPropagation}. + * + * Returning anything but `undefined` from a listener will stop the listener propagation. + * + * @param {string|string[]} events The event(s) to listen to. + * @param {number} [priority] The priority with which to listen. + * @param {Function} callback The callback. + * @param {any} [that] Value of `this` the callback will be called with. + */ +BaseViewer.prototype.on = function(events, priority, callback, that) { + return this.get('eventBus').on(events, priority, callback, that); }; /** - * De-register an event listener + * Remove an event listener. * - * @param {string} event - * @param {Function} callback + * @param {string|string[]} events The event(s). + * @param {Function} [callback] The callback. */ -BaseViewer.prototype.off = function(event, callback) { - this.get('eventBus').off(event, callback); +BaseViewer.prototype.off = function(events, callback) { + this.get('eventBus').off(events, callback); }; +/** + * Attach the viewer to an HTML element. + * + * @param {HTMLElement} parentNode The parent node to attach to. + */ BaseViewer.prototype.attachTo = function(parentNode) { if (!parentNode) { @@ -595,19 +661,35 @@ BaseViewer.prototype.attachTo = function(parentNode) { this.get('canvas').resized(); }; +/** + * Get the definitions model element. + * + * @return {ModdleElement} The definitions model element. + */ BaseViewer.prototype.getDefinitions = function() { return this._definitions; }; +/** + * Detach the viewer. + * + * @fires BaseViewer#DetachEvent + */ BaseViewer.prototype.detach = function() { - var container = this._container, - parentNode = container.parentNode; + const container = this._container, + parentNode = container.parentNode; if (!parentNode) { return; } + /** + * A `detach` event. + * + * @event BaseViewer#DetachEvent + * @type {Object} + */ this._emit('detach', {}); parentNode.removeChild(container); @@ -615,18 +697,18 @@ BaseViewer.prototype.detach = function() { BaseViewer.prototype._init = function(container, moddle, options) { - var baseModules = options.modules || this.getModules(), - additionalModules = options.additionalModules || [], - staticModules = [ - { - bpmnjs: [ 'value', this ], - moddle: [ 'value', moddle ] - } - ]; + const baseModules = options.modules || this.getModules(options), + additionalModules = options.additionalModules || [], + staticModules = [ + { + bpmnjs: [ 'value', this ], + moddle: [ 'value', moddle ] + } + ]; - var diagramModules = [].concat(staticModules, baseModules, additionalModules); + const diagramModules = [].concat(staticModules, baseModules, additionalModules); - var diagramOptions = assign(omit(options, [ 'additionalModules' ]), { + const diagramOptions = assign(omit(options, [ 'additionalModules' ]), { canvas: assign({}, options.canvas, { container: container }), modules: diagramModules }); @@ -645,17 +727,22 @@ BaseViewer.prototype._init = function(container, moddle, options) { * @param {string} type * @param {Object} event * - * @return {Object} event processing result (if any) + * @return {Object} The return value after calling all event listeners. */ BaseViewer.prototype._emit = function(type, event) { return this.get('eventBus').fire(type, event); }; +/** + * @param {BaseViewerOptions} options + * + * @return {HTMLElement} + */ BaseViewer.prototype._createContainer = function(options) { - var container = domify('
'); + const container = domify('
'); - assign(container.style, { + assignStyle(container, { width: ensureUnit(options.width), height: ensureUnit(options.height), position: options.position @@ -664,8 +751,13 @@ BaseViewer.prototype._createContainer = function(options) { return container; }; +/** + * @param {BaseViewerOptions} options + * + * @return {Moddle} + */ BaseViewer.prototype._createModdle = function(options) { - var moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions); + const moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions); return new BpmnModdle(moddleOptions); }; @@ -684,8 +776,8 @@ function checkValidationError(err) { // check if we can help the user by indicating wrong BPMN 2.0 xml // (in case he or the exporting tool did not get that right) - var pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/; - var match = pattern.exec(err.message); + const pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/; + const match = pattern.exec(err.message); if (match) { err.message = @@ -696,7 +788,7 @@ function checkValidationError(err) { return err; } -var DEFAULT_OPTIONS = { +const DEFAULT_OPTIONS = { width: '100%', height: '100%', position: 'relative' @@ -735,7 +827,8 @@ function findBPMNDiagram(definitions, diagramId) { import { open as openPoweredBy, BPMNIO_IMG, - LINK_STYLES as BPMNIO_LINK_STYLES + LOGO_STYLES, + LINK_STYLES } from './util/PoweredByUtil'; import { @@ -751,18 +844,26 @@ import { * @param {Element} container */ function addProjectLogo(container) { - var img = BPMNIO_IMG; + const img = BPMNIO_IMG; - var linkMarkup = + const linkMarkup = '' + - img + + 'target="_blank" ' + + 'class="bjs-powered-by" ' + + 'title="Powered by bpmn.io" ' + + '>' + + img + ''; - var linkElement = domify(linkMarkup); + const linkElement = domify(linkMarkup); + + assignStyle(domQuery('svg', linkElement), LOGO_STYLES); + assignStyle(linkElement, LINK_STYLES, { + position: 'absolute', + bottom: '15px', + right: '15px', + zIndex: '100' + }); container.appendChild(linkElement); diff --git a/lib/BaseViewer.spec.ts b/lib/BaseViewer.spec.ts new file mode 100644 index 0000000000..170e0f8430 --- /dev/null +++ b/lib/BaseViewer.spec.ts @@ -0,0 +1,207 @@ +import CommandStack from 'diagram-js/lib/command/CommandStack'; + +import EventBus, { Event } from 'diagram-js/lib/core/EventBus'; + +import BaseViewer, { + ImportDoneEvent, + ImportParseCompleteEvent, + ImportParseStartEvent, + SaveXMLDoneEvent, + SaveXMLStartEvent +} from './BaseViewer'; + +import OverlaysModule from 'diagram-js/lib/features/overlays'; +import Canvas from 'diagram-js/lib/core/Canvas'; + +const viewer = new BaseViewer(); + +const configuredViewer = new BaseViewer({ + width: 100, + height: 100, + position: 'absolute', + container: 'container', + moddleExtensions: { + foo: {} + }, + additionalModules: [ + OverlaysModule + ] +}); + +testViewer(viewer); + +const extendedViewer = new BaseViewer({ + container: 'container', + alignToOrigin: false, + propertiesPanel: { + attachTo: '#properties-panel' + } +}); + +export function testViewer(viewer: BaseViewer) { + viewer.importXML('', 'BPMNDiagram_1'); + + viewer.importXML('') + .then(({ warnings }) => { + console.log(warnings); + }) + .catch(error => { + const { + message, + warnings + } = error; + + console.log(message, warnings); + }); + + viewer.importDefinitions({ $type: 'bpmn:Definitions' }, 'BPMNDiagram_1'); + + viewer.importDefinitions({ $type: 'bpmn:Definitions' }) + .then(({ warnings }) => { + console.log(warnings); + }) + .catch(error => { + const { + message, + warnings + } = error; + + console.log(message, warnings); + }); + + viewer.open('BPMNDiagram_1'); + + viewer.open({ $type: 'bpmn:BPMNDiagram' }) + .then(({ warnings }) => { + console.log(warnings); + }) + .catch(error => { + const { + message, + warnings + } = error; + + console.log(message, warnings); + }); + + viewer.saveXML({ format: true, preamble: false }) + .then(({ xml, error }) => { + if (error) { + console.log(error); + } else { + console.log(xml); + } + }) + .catch(error => { + console.log(error); + }); + + viewer.saveXML(); + + viewer.saveSVG(); + + viewer.getModules(); + + viewer.clear(); + + viewer.destroy(); + + viewer.get('commandStack').undo(); + + viewer.invoke((commandStack: CommandStack) => commandStack.undo()); + + viewer.on('foo', () => console.log('foo')); + + viewer.on([ 'foo', 'bar' ], () => console.log('foo')); + + viewer.on('foo', 2000, () => console.log('foo')); + + viewer.on('foo', 2000, () => console.log('foo'), { foo: 'bar' }); + + viewer.off('foo', () => console.log('foo')); + + viewer.attachTo(document.createElement('div')); + + viewer.getDefinitions(); + + viewer.detach(); + + viewer.on('import.parse.start', ({ xml }) => { + console.log(xml); + }); + + viewer.on('import.parse.complete', ({ + error, + definitions, + elementsById, + references, + warnings + }) => { + if (error) { + console.error(error); + } + + if (warnings.length) { + warnings.forEach(warning => console.log(warning)); + } + + console.log(definitions, elementsById, references); + }); + + viewer.on('import.done', ({ error, warnings }) => { + if (error) { + console.error(error); + } + + if (warnings.length) { + warnings.forEach(warning => console.log(warning)); + } + }); + + viewer.on('saveXML.start', ({ definitions }) => { + console.log(definitions); + }); + + viewer.on('saveXML.done', ({ error, xml }) => { + if (error) { + console.error(error); + } else { + console.log(xml); + } + }); + + viewer.on('detach', () => {}); +} + +// typed API usage + +type FooEvent = { + /** + * Very cool field! + */ + foo: string; +}; + +type EventMap = { + + foo: FooEvent +}; + +type TypeMap = { + canvas: Canvas, + eventBus: EventBus +}; + +const typedViewer = new BaseViewer(); + +const bus = typedViewer.get('eventBus'); + +const canvas = typedViewer.get('canvas'); + +canvas.zoom('fit-viewport'); + +typedViewer.on('foo', event => { + console.log(event.foo); +}); + +typedViewer.get('eventBus').on('foo', e => console.log(e.foo)); diff --git a/lib/Modeler.js b/lib/Modeler.js index b0ec29d847..cda05f8968 100644 --- a/lib/Modeler.js +++ b/lib/Modeler.js @@ -1,4 +1,4 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import BaseModeler from './BaseModeler'; @@ -7,10 +7,9 @@ import NavigatedViewer from './NavigatedViewer'; import KeyboardMoveModule from 'diagram-js/lib/navigation/keyboard-move'; import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'; -import TouchModule from 'diagram-js/lib/navigation/touch'; import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'; -import AlignElementsModule from 'diagram-js/lib/features/align-elements'; +import AlignElementsModule from './features/align-elements'; import AutoPlaceModule from './features/auto-place'; import AutoResizeModule from './features/auto-resize'; import AutoScrollModule from 'diagram-js/lib/features/auto-scroll'; @@ -27,17 +26,16 @@ import InteractionEventsModule from './features/interaction-events'; import KeyboardModule from './features/keyboard'; import KeyboardMoveSelectionModule from 'diagram-js/lib/features/keyboard-move-selection'; import LabelEditingModule from './features/label-editing'; +import LabelLink from './features/label-link'; import ModelingModule from './features/modeling'; +import ModelingFeedbackModule from './features/modeling-feedback'; import MoveModule from 'diagram-js/lib/features/move'; import PaletteModule from './features/palette'; import ReplacePreviewModule from './features/replace-preview'; import ResizeModule from 'diagram-js/lib/features/resize'; import SnappingModule from './features/snapping'; import SearchModule from './features/search'; - -import { - wrapForCompatibility -} from './util/CompatibilityUtil'; +import OutlineModule from './features/outline'; var initialDiagram = '' + @@ -60,10 +58,14 @@ var initialDiagram = ''; +/** + * @typedef {import('./BaseViewer').BaseViewerOptions} BaseViewerOptions + * @typedef {import('./BaseViewer').ImportXMLResult} ImportXMLResult + */ + /** * A modeler for BPMN 2.0 diagrams. * - * * ## Extending the Modeler * * In order to extend the viewer pass extension modules to bootstrap via the @@ -124,13 +126,11 @@ var initialDiagram = * var bpmnModeler = new Modeler({ additionalModules: [ overrideModule ]}); * ``` * - * @param {Object} [options] configuration options to pass to the viewer - * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. - * @param {string|number} [options.width] the width of the viewer - * @param {string|number} [options.height] the height of the viewer - * @param {Object} [options.moddleExtensions] extension packages to provide - * @param {Array} [options.modules] a list of modules to override the default modules - * @param {Array} [options.additionalModules] a list of modules to use with the default modules + * @template [ServiceMap=null] + * + * @extends BaseModeler + * + * @param {BaseViewerOptions} [options] The options to configure the modeler. */ export default function Modeler(options) { BaseModeler.call(this, options); @@ -142,30 +142,16 @@ inherits(Modeler, BaseModeler); Modeler.Viewer = Viewer; Modeler.NavigatedViewer = NavigatedViewer; -/** -* The createDiagram result. -* -* @typedef {Object} CreateDiagramResult -* -* @property {Array} warnings -*/ - -/** -* The createDiagram error. -* -* @typedef {Error} CreateDiagramError -* -* @property {Array} warnings -*/ - /** * Create a new diagram to start modeling. * - * Returns {Promise} + * @throws {ImportXMLError} An error thrown during the import of the XML. + * + * @return {Promise} A promise resolving with warnings that were produced during the import. */ -Modeler.prototype.createDiagram = wrapForCompatibility(function createDiagram() { +Modeler.prototype.createDiagram = function createDiagram() { return this.importXML(initialDiagram); -}); +}; Modeler.prototype._interactionModules = [ @@ -173,7 +159,6 @@ Modeler.prototype._interactionModules = [ // non-modeling components KeyboardMoveModule, MoveCanvasModule, - TouchModule, ZoomScrollModule ]; @@ -197,13 +182,16 @@ Modeler.prototype._modelingModules = [ KeyboardModule, KeyboardMoveSelectionModule, LabelEditingModule, + LabelLink, ModelingModule, + ModelingFeedbackModule, MoveModule, PaletteModule, ReplacePreviewModule, ResizeModule, SnappingModule, - SearchModule + SearchModule, + OutlineModule ]; diff --git a/lib/Modeler.spec.ts b/lib/Modeler.spec.ts new file mode 100644 index 0000000000..b9b0d6cbed --- /dev/null +++ b/lib/Modeler.spec.ts @@ -0,0 +1,61 @@ +import Canvas from 'diagram-js/lib/core/Canvas'; +import EventBus from 'diagram-js/lib/core/EventBus'; + +import Modeler from './Modeler'; + +import { testViewer } from './BaseViewer.spec'; + +const modeler = new Modeler({ + container: 'container' +}); + +testViewer(modeler); + +modeler.createDiagram(); + + +const otherModeler = new Modeler({ + container: 'container' +}); + +const extendedModeler = new Modeler({ + container: 'container', + alignToOrigin: false, + propertiesPanel: { + attachTo: '#properties-panel' + } +}); + + +// typed API usage + +type FooEvent = { + /** + * Very cool field! + */ + foo: string; +}; + +type EventMap = { + + foo: FooEvent +}; + +type TypeMap = { + canvas: Canvas, + eventBus: EventBus +}; + +const typedViewer = new Modeler(); + +const bus = typedViewer.get('eventBus'); + +const canvas = typedViewer.get('canvas'); + +canvas.zoom('fit-viewport'); + +typedViewer.on('foo', event => { + console.log(event.foo); +}); + +typedViewer.get('eventBus').on('foo', e => console.log(e.foo)); \ No newline at end of file diff --git a/lib/NavigatedViewer.js b/lib/NavigatedViewer.js index 592f59dc34..535c24a816 100644 --- a/lib/NavigatedViewer.js +++ b/lib/NavigatedViewer.js @@ -1,4 +1,4 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import Viewer from './Viewer'; @@ -6,11 +6,18 @@ import KeyboardMoveModule from 'diagram-js/lib/navigation/keyboard-move'; import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'; import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'; +/** + * @typedef { import('./BaseViewer').BaseViewerOptions } BaseViewerOptions + */ /** - * A viewer that includes mouse navigation facilities + * A viewer with mouse and keyboard navigation features. + * + * @template [ServiceMap=null] + * + * @extends Viewer * - * @param {Object} options + * @param {BaseViewerOptions} [options] */ export default function NavigatedViewer(options) { Viewer.call(this, options); diff --git a/lib/NavigatedViewer.spec.ts b/lib/NavigatedViewer.spec.ts new file mode 100644 index 0000000000..268188ca2d --- /dev/null +++ b/lib/NavigatedViewer.spec.ts @@ -0,0 +1,53 @@ +import Canvas from 'diagram-js/lib/core/Canvas'; +import EventBus from 'diagram-js/lib/core/EventBus'; + +import NavigatedViewer from './NavigatedViewer'; + +import { testViewer } from './BaseViewer.spec'; + +const viewer = new NavigatedViewer({ + container: 'container' +}); + +testViewer(viewer); + +const extendedViewer = new NavigatedViewer({ + container: 'container', + alignToOrigin: false, + propertiesPanel: { + attachTo: '#properties-panel' + } +}); + +// typed API usage + +type FooEvent = { + /** + * Very cool field! + */ + foo: string; +}; + +type EventMap = { + + foo: FooEvent +}; + +type TypeMap = { + canvas: Canvas, + eventBus: EventBus +}; + +const typedViewer = new NavigatedViewer(); + +const bus = typedViewer.get('eventBus'); + +const canvas = typedViewer.get('canvas'); + +canvas.zoom('fit-viewport'); + +typedViewer.on('foo', event => { + console.log(event.foo); +}); + +typedViewer.get('eventBus').on('foo', e => console.log(e.foo)); \ No newline at end of file diff --git a/lib/Viewer.js b/lib/Viewer.js index 27a81bde38..64bb0b55a3 100644 --- a/lib/Viewer.js +++ b/lib/Viewer.js @@ -1,18 +1,22 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import CoreModule from './core'; -import TranslateModule from 'diagram-js/lib/i18n/translate'; -import SelectionModule from 'diagram-js/lib/features/selection'; -import OverlaysModule from 'diagram-js/lib/features/overlays'; import DrilldownModdule from './features/drilldown'; +import OverlaysModule from 'diagram-js/lib/features/overlays'; +import SelectionModule from 'diagram-js/lib/features/selection'; +import TranslateModule from 'diagram-js/lib/i18n/translate'; import BaseViewer from './BaseViewer'; +/** + * @typedef { import('./BaseViewer').BaseViewerOptions } BaseViewerOptions + */ + /** * A viewer for BPMN 2.0 diagrams. * - * Have a look at {@link NavigatedViewer} or {@link Modeler} for bundles that include + * Have a look at {@link bpmn-js/lib/NavigatedViewer} or {@link bpmn-js/lib/Modeler} for bundles that include * additional features. * * @@ -48,13 +52,11 @@ import BaseViewer from './BaseViewer'; * bpmnViewer.importXML(...); * ``` * - * @param {Object} [options] configuration options to pass to the viewer - * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. - * @param {string|number} [options.width] the width of the viewer - * @param {string|number} [options.height] the height of the viewer - * @param {Object} [options.moddleExtensions] extension packages to provide - * @param {Array} [options.modules] a list of modules to override the default modules - * @param {Array} [options.additionalModules] a list of modules to use with the default modules + * @template [ServiceMap=null] + * + * @extends BaseViewer + * + * @param {BaseViewerOptions} [options] The options to configure the viewer. */ export default function Viewer(options) { BaseViewer.call(this, options); @@ -65,10 +67,10 @@ inherits(Viewer, BaseViewer); // modules the viewer is composed of Viewer.prototype._modules = [ CoreModule, - TranslateModule, - SelectionModule, + DrilldownModdule, OverlaysModule, - DrilldownModdule + SelectionModule, + TranslateModule ]; // default moddle extensions the viewer is composed of diff --git a/lib/Viewer.spec.ts b/lib/Viewer.spec.ts new file mode 100644 index 0000000000..b7b2ed605b --- /dev/null +++ b/lib/Viewer.spec.ts @@ -0,0 +1,54 @@ +import Canvas from 'diagram-js/lib/core/Canvas'; +import EventBus from 'diagram-js/lib/core/EventBus'; + +import Viewer from './Viewer'; + +import { testViewer } from './BaseViewer.spec'; + +const viewer = new Viewer({ + container: 'container' +}); + +testViewer(viewer); + +const extendedViewer = new Viewer({ + container: 'container', + alignToOrigin: false, + propertiesPanel: { + attachTo: '#properties-panel' + } +}); + + +// typed API usage + +type FooEvent = { + /** + * Very cool field! + */ + foo: string; +}; + +type EventMap = { + + foo: FooEvent +}; + +type TypeMap = { + canvas: Canvas, + eventBus: EventBus +}; + +const typedViewer = new Viewer(); + +const bus = typedViewer.get('eventBus'); + +const canvas = typedViewer.get('canvas'); + +canvas.zoom('fit-viewport'); + +typedViewer.on('foo', event => { + console.log(event.foo); +}); + +typedViewer.get('eventBus').on('foo', e => console.log(e.foo)); \ No newline at end of file diff --git a/lib/draw/BpmnRenderUtil.js b/lib/draw/BpmnRenderUtil.js index 0bc24091ad..91c4c6dce7 100644 --- a/lib/draw/BpmnRenderUtil.js +++ b/lib/draw/BpmnRenderUtil.js @@ -1,5 +1,5 @@ import { - every, + has, some } from 'min-dash'; @@ -11,72 +11,119 @@ import { componentsToPath } from 'diagram-js/lib/util/RenderUtil'; -// re-export getDi for compatibility -export { getDi }; - -// element utils ////////////////////// /** - * Checks if eventDefinition of the given element matches with semantic type. + * @typedef {import('../model').ModdleElement} ModdleElement + * @typedef {import('../model').Element} Element + * + * @typedef {import('../model').ShapeLike} ShapeLike * - * @return {boolean} true if element is of the given semantic type + * @typedef {import('diagram-js/lib/util/Types').Dimensions} Dimensions + * @typedef {import('diagram-js/lib/util/Types').Rect} Rect */ -export function isTypedEvent(event, eventDefinitionType, filter) { - function matches(definition, filter) { - return every(filter, function(val, key) { +// re-export for compatibility +export { + getDi, + getBusinessObject as getSemantic +} from '../util/ModelUtil'; + - // we want a == conversion here, to be able to catch - // undefined == false and friends - /* jshint -W116 */ - return definition[key] == val; - }); - } +export var black = 'hsl(225, 10%, 15%)'; +export var white = 'white'; + +// element utils ////////////////////// +/** + * Checks if eventDefinition of the given element matches with semantic type. + * + * @param {ModdleElement} event + * @param {string} eventDefinitionType + * + * @return {boolean} + */ +export function isTypedEvent(event, eventDefinitionType) { return some(event.eventDefinitions, function(definition) { - return definition.$type === eventDefinitionType && matches(event, filter); + return definition.$type === eventDefinitionType; }); } +/** + * Check if element is a throw event. + * + * @param {ModdleElement} event + * + * @return {boolean} + */ export function isThrowEvent(event) { return (event.$type === 'bpmn:IntermediateThrowEvent') || (event.$type === 'bpmn:EndEvent'); } +/** + * Check if element is a throw event. + * + * @param {ModdleElement} element + * + * @return {boolean} + */ export function isCollection(element) { var dataObject = element.dataObjectRef; return element.isCollection || (dataObject && dataObject.isCollection); } -export function getSemantic(element) { - return element.businessObject; -} - // color access ////////////////////// -export function getFillColor(element, defaultColor) { +/** + * @param {Element} element + * @param {string} [defaultColor] + * @param {string} [overrideColor] + * + * @return {string} + */ +export function getFillColor(element, defaultColor, overrideColor) { var di = getDi(element); - return di.get('color:background-color') || di.get('bioc:fill') || defaultColor || 'white'; + return overrideColor || di.get('color:background-color') || di.get('bioc:fill') || defaultColor || white; } -export function getStrokeColor(element, defaultColor) { +/** + * @param {Element} element + * @param {string} [defaultColor] + * @param {string} [overrideColor] + * + * @return {string} + */ +export function getStrokeColor(element, defaultColor, overrideColor) { var di = getDi(element); - return di.get('color:border-color') || di.get('bioc:stroke') || defaultColor || 'black'; + return overrideColor || di.get('color:border-color') || di.get('bioc:stroke') || defaultColor || black; } -export function getLabelColor(element, defaultColor, defaultStrokeColor) { +/** + * @param {Element} element + * @param {string} [defaultColor] + * @param {string} [defaultStrokeColor] + * @param {string} [overrideColor] + * + * @return {string} + */ +export function getLabelColor(element, defaultColor, defaultStrokeColor, overrideColor) { var di = getDi(element), label = di.get('label'); - return label && label.get('color:color') || defaultColor || + return overrideColor || (label && label.get('color:color')) || defaultColor || getStrokeColor(element, defaultStrokeColor); } // cropping path customizations ////////////////////// +/** + * @param {ShapeLike} shape + * + * @return {string} path + */ export function getCirclePath(shape) { var cx = shape.x + shape.width / 2, @@ -84,16 +131,22 @@ export function getCirclePath(shape) { radius = shape.width / 2; var circlePath = [ - ['M', cx, cy], - ['m', 0, -radius], - ['a', radius, radius, 0, 1, 1, 0, 2 * radius], - ['a', radius, radius, 0, 1, 1, 0, -2 * radius], - ['z'] + [ 'M', cx, cy ], + [ 'm', 0, -radius ], + [ 'a', radius, radius, 0, 1, 1, 0, 2 * radius ], + [ 'a', radius, radius, 0, 1, 1, 0, -2 * radius ], + [ 'z' ] ]; return componentsToPath(circlePath); } +/** + * @param {ShapeLike} shape + * @param {number} [borderRadius] + * + * @return {string} path + */ export function getRoundRectPath(shape, borderRadius) { var x = shape.x, @@ -102,21 +155,26 @@ export function getRoundRectPath(shape, borderRadius) { height = shape.height; var roundRectPath = [ - ['M', x + borderRadius, y], - ['l', width - borderRadius * 2, 0], - ['a', borderRadius, borderRadius, 0, 0, 1, borderRadius, borderRadius], - ['l', 0, height - borderRadius * 2], - ['a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, borderRadius], - ['l', borderRadius * 2 - width, 0], - ['a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, -borderRadius], - ['l', 0, borderRadius * 2 - height], - ['a', borderRadius, borderRadius, 0, 0, 1, borderRadius, -borderRadius], - ['z'] + [ 'M', x + borderRadius, y ], + [ 'l', width - borderRadius * 2, 0 ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, borderRadius, borderRadius ], + [ 'l', 0, height - borderRadius * 2 ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, borderRadius ], + [ 'l', borderRadius * 2 - width, 0 ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, -borderRadius ], + [ 'l', 0, borderRadius * 2 - height ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, borderRadius, -borderRadius ], + [ 'z' ] ]; return componentsToPath(roundRectPath); } +/** + * @param {ShapeLike} shape + * + * @return {string} path + */ export function getDiamondPath(shape) { var width = shape.width, @@ -127,16 +185,21 @@ export function getDiamondPath(shape) { halfHeight = height / 2; var diamondPath = [ - ['M', x + halfWidth, y], - ['l', halfWidth, halfHeight], - ['l', -halfWidth, halfHeight], - ['l', -halfWidth, -halfHeight], - ['z'] + [ 'M', x + halfWidth, y ], + [ 'l', halfWidth, halfHeight ], + [ 'l', -halfWidth, halfHeight ], + [ 'l', -halfWidth, -halfHeight ], + [ 'z' ] ]; return componentsToPath(diamondPath); } +/** + * @param {ShapeLike} shape + * + * @return {string} path + */ export function getRectPath(shape) { var x = shape.x, y = shape.y, @@ -144,12 +207,51 @@ export function getRectPath(shape) { height = shape.height; var rectPath = [ - ['M', x, y], - ['l', width, 0], - ['l', 0, height], - ['l', -width, 0], - ['z'] + [ 'M', x, y ], + [ 'l', width, 0 ], + [ 'l', 0, height ], + [ 'l', -width, 0 ], + [ 'z' ] ]; return componentsToPath(rectPath); +} + +/** + * Get width and height from element or overrides. + * + * @param {Dimensions|Rect|ShapeLike} bounds + * @param {Object} overrides + * + * @returns {Dimensions} + */ +export function getBounds(bounds, overrides = {}) { + return { + width: getWidth(bounds, overrides), + height: getHeight(bounds, overrides) + }; +} + +/** + * Get width from element or overrides. + * + * @param {Dimensions|Rect|ShapeLike} bounds + * @param {Object} overrides + * + * @returns {number} + */ +export function getWidth(bounds, overrides = {}) { + return has(overrides, 'width') ? overrides.width : bounds.width; +} + +/** + * Get height from element or overrides. + * + * @param {Dimensions|Rect|ShapeLike} bounds + * @param {Object} overrides + * + * @returns {number} + */ +export function getHeight(bounds, overrides = {}) { + return has(overrides, 'height') ? overrides.height : bounds.height; } \ No newline at end of file diff --git a/lib/draw/BpmnRenderer.js b/lib/draw/BpmnRenderer.js index ee9bb64947..f2bd0b7c07 100644 --- a/lib/draw/BpmnRenderer.js +++ b/lib/draw/BpmnRenderer.js @@ -1,23 +1,31 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import { - isObject, assign, - forEach + forEach, + isObject } from 'min-dash'; import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'; import { isExpanded, + isHorizontal, isEventSubProcess } from '../util/DiUtil'; import { - getLabel -} from '../features/label-editing/LabelUtil'; + getLabel, + isLabel +} from '../util/LabelUtil'; -import { is } from '../util/ModelUtil'; +import { + TEXT_ANNOTATION_PADDING +} from '../util/AnnotationUtil'; + +import { + is +} from '../util/ModelUtil'; import { createLine @@ -27,6 +35,7 @@ import { isTypedEvent, isThrowEvent, isCollection, + getBounds, getDi, getSemantic, getCirclePath, @@ -35,7 +44,9 @@ import { getRectPath, getFillColor, getStrokeColor, - getLabelColor + getLabelColor, + getHeight, + getWidth } from './BpmnRenderUtil'; import { @@ -55,18 +66,54 @@ import { translate } from 'diagram-js/lib/util/SvgTransformUtil'; -import Ids from 'ids'; - -var RENDERER_IDS = new Ids(); - -var TASK_BORDER_RADIUS = 10; -var INNER_OUTER_DIST = 3; - -var DEFAULT_FILL_OPACITY = .95, - HIGH_FILL_OPACITY = .35; - -var ELEMENT_LABEL_DISTANCE = 10; - +import { Ids } from 'ids'; + +import { black } from './BpmnRenderUtil'; + +var markerIds = new Ids(); + +var ELEMENT_LABEL_DISTANCE = 10, + INNER_OUTER_DIST = 3, + PARTICIPANT_STROKE_WIDTH = 1.5, + TASK_BORDER_RADIUS = 10, + EXTERNAL_LABEL_BORDER_RADIUS = 4; + +var DEFAULT_OPACITY = 0.95, + FULL_OPACITY = 1, + LOW_OPACITY = 0.25; + +/** + * @typedef { Partial<{ + * defaultFillColor: string, + * defaultStrokeColor: string, + * defaultLabelColor: string + * }> } BpmnRendererConfig + * + * @typedef { Partial<{ + * fill: string, + * stroke: string, + * width: string, + * height: string + * }> } Attrs + */ + +/** + * @typedef { import('../model/Types').Element } Element + * @typedef { import('../model/Types').Shape } Shape + * @typedef { import('../model/Types').Connection } Connection + */ + +/** + * A renderer for BPMN elements + * + * @param {BpmnRendererConfig} config + * @param {import('diagram-js/lib/core/EventBus').default} eventBus + * @param {import('diagram-js/lib/draw/Styles').default} styles + * @param {import('./PathMap').default} pathMap + * @param {import('diagram-js/lib/core/Canvas').default} canvas + * @param {import('./TextRenderer').default} textRenderer + * @param {number} [priority] + */ export default function BpmnRenderer( config, eventBus, styles, pathMap, canvas, textRenderer, priority) { @@ -77,37 +124,34 @@ export default function BpmnRenderer( defaultStrokeColor = config && config.defaultStrokeColor, defaultLabelColor = config && config.defaultLabelColor; - var rendererId = RENDERER_IDS.next(); - - var markers = {}; - - var computeStyle = styles.computeStyle; - - function addMarker(id, options) { - var attrs = assign({ - fill: 'black', - strokeWidth: 1, + function shapeStyle(attrs) { + return styles.computeStyle(attrs, { strokeLinecap: 'round', - strokeDasharray: 'none' - }, options.attrs); - - var ref = options.ref || { x: 0, y: 0 }; - - var scale = options.scale || 1; - - // fix for safari / chrome / firefox bug not correctly - // resetting stroke dash array - if (attrs.strokeDasharray === 'none') { - attrs.strokeDasharray = [10000, 1]; - } - - var marker = svgCreate('marker'); - - svgAttr(options.element, attrs); + strokeLinejoin: 'round', + stroke: black, + strokeWidth: 2, + fill: 'white' + }); + } - svgAppend(marker, options.element); + function lineStyle(attrs) { + return styles.computeStyle(attrs, [ 'no-fill' ], { + strokeLinecap: 'round', + strokeLinejoin: 'round', + stroke: black, + strokeWidth: 2 + }); + } - svgAttr(marker, { + function addMarker(id, options) { + var { + ref = { x: 0, y: 0 }, + scale = 1, + element, + parentGfx = canvas._svg + } = options; + + var marker = svgCreate('marker', { id: id, viewBox: '0 0 20 20', refX: ref.x, @@ -117,144 +161,173 @@ export default function BpmnRenderer( orient: 'auto' }); - var defs = domQuery('defs', canvas._svg); + svgAppend(marker, element); + + var defs = domQuery(':scope > defs', parentGfx); if (!defs) { defs = svgCreate('defs'); - svgAppend(canvas._svg, defs); + svgAppend(parentGfx, defs); } svgAppend(defs, marker); - - markers[id] = marker; } - function colorEscape(str) { + function marker(parentGfx, type, fill, stroke) { - // only allow characters and numbers - return str.replace(/[^0-9a-zA-z]+/g, '_'); - } - function marker(type, fill, stroke) { - var id = type + '-' + colorEscape(fill) + '-' + colorEscape(stroke) + '-' + rendererId; + var id = markerIds.nextPrefixed('marker-'); - if (!markers[id]) { - createMarker(id, type, fill, stroke); - } + createMarker(parentGfx, id, type, fill, stroke); return 'url(#' + id + ')'; } - function createMarker(id, type, fill, stroke) { + function createMarker(parentGfx, id, type, fill, stroke) { if (type === 'sequenceflow-end') { - var sequenceflowEnd = svgCreate('path'); - svgAttr(sequenceflowEnd, { d: 'M 1 5 L 11 10 L 1 15 Z' }); + var sequenceflowEnd = svgCreate('path', { + d: 'M 1 5 L 11 10 L 1 15 Z', + ...shapeStyle({ + fill: stroke, + stroke: stroke, + strokeWidth: 1 + }) + }); addMarker(id, { element: sequenceflowEnd, ref: { x: 11, y: 10 }, scale: 0.5, - attrs: { - fill: stroke, - stroke: stroke - } + parentGfx }); } if (type === 'messageflow-start') { - var messageflowStart = svgCreate('circle'); - svgAttr(messageflowStart, { cx: 6, cy: 6, r: 3.5 }); + var messageflowStart = svgCreate('circle', { + cx: 6, + cy: 6, + r: 3.5, + ...shapeStyle({ + fill, + stroke: stroke, + strokeWidth: 1, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); addMarker(id, { element: messageflowStart, - attrs: { - fill: fill, - stroke: stroke - }, - ref: { x: 6, y: 6 } + ref: { x: 6, y: 6 }, + parentGfx }); } if (type === 'messageflow-end') { - var messageflowEnd = svgCreate('path'); - svgAttr(messageflowEnd, { d: 'm 1 5 l 0 -3 l 7 3 l -7 3 z' }); + var messageflowEnd = svgCreate('path', { + d: 'm 1 5 l 0 -3 l 7 3 l -7 3 z', + ...shapeStyle({ + fill, + stroke: stroke, + strokeWidth: 1, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); addMarker(id, { element: messageflowEnd, - attrs: { - fill: fill, - stroke: stroke, - strokeLinecap: 'butt' - }, - ref: { x: 8.5, y: 5 } + ref: { x: 8.5, y: 5 }, + parentGfx }); } if (type === 'association-start') { - var associationStart = svgCreate('path'); - svgAttr(associationStart, { d: 'M 11 5 L 1 10 L 11 15' }); + var associationStart = svgCreate('path', { + d: 'M 11 5 L 1 10 L 11 15', + ...lineStyle({ + fill: 'none', + stroke, + strokeWidth: 1.5, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); addMarker(id, { element: associationStart, - attrs: { - fill: 'none', - stroke: stroke, - strokeWidth: 1.5 - }, ref: { x: 1, y: 10 }, - scale: 0.5 + scale: 0.5, + parentGfx }); } if (type === 'association-end') { - var associationEnd = svgCreate('path'); - svgAttr(associationEnd, { d: 'M 1 5 L 11 10 L 1 15' }); + var associationEnd = svgCreate('path', { + d: 'M 1 5 L 11 10 L 1 15', + ...lineStyle({ + fill: 'none', + stroke, + strokeWidth: 1.5, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); addMarker(id, { element: associationEnd, - attrs: { - fill: 'none', - stroke: stroke, - strokeWidth: 1.5 - }, - ref: { x: 12, y: 10 }, - scale: 0.5 + ref: { x: 11, y: 10 }, + scale: 0.5, + parentGfx }); } if (type === 'conditional-flow-marker') { - var conditionalflowMarker = svgCreate('path'); - svgAttr(conditionalflowMarker, { d: 'M 0 10 L 8 6 L 16 10 L 8 14 Z' }); + var conditionalFlowMarker = svgCreate('path', { + d: 'M 0 10 L 8 6 L 16 10 L 8 14 Z', + ...shapeStyle({ + fill, + stroke: stroke + }) + }); addMarker(id, { - element: conditionalflowMarker, - attrs: { - fill: fill, - stroke: stroke - }, + element: conditionalFlowMarker, ref: { x: -1, y: 10 }, - scale: 0.5 + scale: 0.5, + parentGfx }); } if (type === 'conditional-default-flow-marker') { - var conditionaldefaultflowMarker = svgCreate('path'); - svgAttr(conditionaldefaultflowMarker, { d: 'M 6 4 L 10 16' }); + var defaultFlowMarker = svgCreate('path', { + d: 'M 6 4 L 10 16', + ...shapeStyle({ + stroke: stroke, + fill: 'none' + }) + }); addMarker(id, { - element: conditionaldefaultflowMarker, - attrs: { - stroke: stroke - }, + element: defaultFlowMarker, ref: { x: 0, y: 10 }, - scale: 0.5 + scale: 0.5, + parentGfx }); } } - function drawCircle(parentGfx, width, height, offset, attrs) { + function drawCircle(parentGfx, width, height, offset, attrs = {}) { if (isObject(offset)) { attrs = offset; @@ -263,26 +336,17 @@ export default function BpmnRenderer( offset = offset || 0; - attrs = computeStyle(attrs, { - stroke: 'black', - strokeWidth: 2, - fill: 'white' - }); - - if (attrs.fill === 'none') { - delete attrs.fillOpacity; - } + attrs = shapeStyle(attrs); var cx = width / 2, cy = height / 2; - var circle = svgCreate('circle'); - svgAttr(circle, { + var circle = svgCreate('circle', { cx: cx, cy: cy, - r: Math.round((width + height) / 4 - offset) + r: Math.round((width + height) / 4 - offset), + ...attrs }); - svgAttr(circle, attrs); svgAppend(parentGfx, circle); @@ -298,22 +362,17 @@ export default function BpmnRenderer( offset = offset || 0; - attrs = computeStyle(attrs, { - stroke: 'black', - strokeWidth: 2, - fill: 'white' - }); + attrs = shapeStyle(attrs); - var rect = svgCreate('rect'); - svgAttr(rect, { + var rect = svgCreate('rect', { x: offset, y: offset, width: width - offset * 2, height: height - offset * 2, rx: r, - ry: r + ry: r, + ...attrs }); - svgAttr(rect, attrs); svgAppend(parentGfx, rect); @@ -325,54 +384,66 @@ export default function BpmnRenderer( var x_2 = width / 2; var y_2 = height / 2; - var points = [{ x: x_2, y: 0 }, { x: width, y: y_2 }, { x: x_2, y: height }, { x: 0, y: y_2 }]; + var points = [ + { x: x_2, y: 0 }, + { x: width, y: y_2 }, + { x: x_2, y: height }, + { x: 0, y: y_2 } + ]; var pointsString = points.map(function(point) { return point.x + ',' + point.y; }).join(' '); - attrs = computeStyle(attrs, { - stroke: 'black', - strokeWidth: 2, - fill: 'white' - }); + attrs = shapeStyle(attrs); - var polygon = svgCreate('polygon'); - svgAttr(polygon, { + var polygon = svgCreate('polygon', { + ...attrs, points: pointsString }); - svgAttr(polygon, attrs); svgAppend(parentGfx, polygon); return polygon; } - function drawLine(parentGfx, waypoints, attrs) { - attrs = computeStyle(attrs, [ 'no-fill' ], { - stroke: 'black', - strokeWidth: 2, - fill: 'none' - }); + /** + * @param {SVGElement} parentGfx + * @param {Point[]} waypoints + * @param {any} attrs + * @param {number} [radius] + * + * @return {SVGElement} + */ + function drawLine(parentGfx, waypoints, attrs, radius) { + attrs = lineStyle(attrs); - var line = createLine(waypoints, attrs); + var line = createLine(waypoints, attrs, radius); svgAppend(parentGfx, line); return line; } + /** + * @param {SVGElement} parentGfx + * @param {Point[]} waypoints + * @param {any} attrs + * + * @return {SVGElement} + */ + function drawConnectionSegments(parentGfx, waypoints, attrs) { + return drawLine(parentGfx, waypoints, attrs, 5); + } + function drawPath(parentGfx, d, attrs) { + attrs = lineStyle(attrs); - attrs = computeStyle(attrs, [ 'no-fill' ], { - strokeWidth: 2, - stroke: 'black' + var path = svgCreate('path', { + ...attrs, + d }); - var path = svgCreate('path'); - svgAttr(path, { d: d }); - svgAttr(path, attrs); - svgAppend(parentGfx, path); return path; @@ -387,214 +458,58 @@ export default function BpmnRenderer( } function as(type) { - return function(parentGfx, element) { - return renderer(type)(parentGfx, element); - }; - } - - function renderEventContent(element, parentGfx) { - - var event = getSemantic(element); - var isThrowing = isThrowEvent(event); - - if (event.eventDefinitions && event.eventDefinitions.length>1) { - if (event.parallelMultiple) { - return renderer('bpmn:ParallelMultipleEventDefinition')(parentGfx, element, isThrowing); - } - else { - return renderer('bpmn:MultipleEventDefinition')(parentGfx, element, isThrowing); - } - } - - if (isTypedEvent(event, 'bpmn:MessageEventDefinition')) { - return renderer('bpmn:MessageEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:TimerEventDefinition')) { - return renderer('bpmn:TimerEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:ConditionalEventDefinition')) { - return renderer('bpmn:ConditionalEventDefinition')(parentGfx, element); - } - - if (isTypedEvent(event, 'bpmn:SignalEventDefinition')) { - return renderer('bpmn:SignalEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:EscalationEventDefinition')) { - return renderer('bpmn:EscalationEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:LinkEventDefinition')) { - return renderer('bpmn:LinkEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:ErrorEventDefinition')) { - return renderer('bpmn:ErrorEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:CancelEventDefinition')) { - return renderer('bpmn:CancelEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:CompensateEventDefinition')) { - return renderer('bpmn:CompensateEventDefinition')(parentGfx, element, isThrowing); - } - - if (isTypedEvent(event, 'bpmn:TerminateEventDefinition')) { - return renderer('bpmn:TerminateEventDefinition')(parentGfx, element, isThrowing); - } - - return null; - } - - function renderLabel(parentGfx, label, options) { - - options = assign({ - size: { - width: 100 - } - }, options); - - var text = textRenderer.createText(label || '', options); - - svgClasses(text).add('djs-label'); - - svgAppend(parentGfx, text); - - return text; - } - - function renderEmbeddedLabel(parentGfx, element, align) { - var semantic = getSemantic(element); - - return renderLabel(parentGfx, semantic.name, { - box: element, - align: align, - padding: 5, - style: { - fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) - } - }); - } - - function renderExternalLabel(parentGfx, element) { - - var box = { - width: 90, - height: 30, - x: element.width / 2 + element.x, - y: element.height / 2 + element.y + return function(parentGfx, element, attrs) { + return renderer(type)(parentGfx, element, attrs); }; - - return renderLabel(parentGfx, getLabel(element), { - box: box, - fitBox: true, - style: assign( - {}, - textRenderer.getExternalStyle(), - { - fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) - } - ) - }); - } - - function renderLaneLabel(parentGfx, text, element) { - var textBox = renderLabel(parentGfx, text, { - box: { - height: 30, - width: element.height - }, - align: 'center-middle', - style: { - fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) - } - }); - - var top = -1 * element.height; - - transform(textBox, 0, -top, 270); - } - - function createPathFromConnection(connection) { - var waypoints = connection.waypoints; - - var pathData = 'm ' + waypoints[0].x + ',' + waypoints[0].y; - for (var i = 1; i < waypoints.length; i++) { - pathData += 'L' + waypoints[i].x + ',' + waypoints[i].y + ' '; - } - return pathData; } - var handlers = this.handlers = { - 'bpmn:Event': function(parentGfx, element, attrs) { - - if (!('fillOpacity' in attrs)) { - attrs.fillOpacity = DEFAULT_FILL_OPACITY; - } - - return drawCircle(parentGfx, element.width, element.height, attrs); - }, - 'bpmn:StartEvent': function(parentGfx, element) { - var attrs = { - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }; - - var semantic = getSemantic(element); - - if (!semantic.isInterrupting) { - attrs = { - strokeDasharray: '6', - strokeLinecap: 'round', - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }; - } - - var circle = renderer('bpmn:Event')(parentGfx, element, attrs); - - renderEventContent(element, parentGfx); - - return circle; - }, - 'bpmn:MessageEventDefinition': function(parentGfx, element, isThrowing) { + var eventIconRenderers = { + 'bpmn:MessageEventDefinition': function(parentGfx, element, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_MESSAGE', { xScaleFactor: 0.9, yScaleFactor: 0.9, - containerWidth: element.width, - containerHeight: element.height, + containerWidth: attrs.width || element.width, + containerHeight: attrs.height || element.height, position: { mx: 0.235, my: 0.315 } }); - var fill = isThrowing ? getStrokeColor(element, defaultStrokeColor) : getFillColor(element, defaultFillColor); - var stroke = isThrowing ? getFillColor(element, defaultFillColor) : getStrokeColor(element, defaultStrokeColor); + var fill = isThrowing + ? getStrokeColor(element, defaultStrokeColor, attrs.stroke) + : getFillColor(element, defaultFillColor, attrs.fill); + + var stroke = isThrowing + ? getFillColor(element, defaultFillColor, attrs.fill) + : getStrokeColor(element, defaultStrokeColor, attrs.stroke); var messagePath = drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill, - stroke: stroke + fill, + stroke, + strokeWidth: 1 }); return messagePath; }, - 'bpmn:TimerEventDefinition': function(parentGfx, element) { - var circle = drawCircle(parentGfx, element.width, element.height, 0.2 * element.height, { - strokeWidth: 2, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + 'bpmn:TimerEventDefinition': function(parentGfx, element, attrs = {}) { + var baseWidth = attrs.width || element.width; + var baseHeight = attrs.height || element.height; + + // use a lighter stroke for event suprocess icons + var strokeWidth = attrs.width ? 1 : 2; + + var circle = drawCircle(parentGfx, baseWidth, baseHeight, 0.2 * baseHeight, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: strokeWidth }); var pathData = pathMap.getScaledPath('EVENT_TIMER_WH', { xScaleFactor: 0.75, yScaleFactor: 0.75, - containerWidth: element.width, - containerHeight: element.height, + containerWidth: baseWidth, + containerHeight: baseHeight, position: { mx: 0.5, my: 0.5 @@ -602,63 +517,62 @@ export default function BpmnRenderer( }); drawPath(parentGfx, pathData, { - strokeWidth: 2, - strokeLinecap: 'square', - stroke: getStrokeColor(element, defaultStrokeColor) + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: strokeWidth }); - for (var i = 0;i < 12; i++) { - + for (var i = 0; i < 12; i++) { var linePathData = pathMap.getScaledPath('EVENT_TIMER_LINE', { xScaleFactor: 0.75, yScaleFactor: 0.75, - containerWidth: element.width, - containerHeight: element.height, + containerWidth: baseWidth, + containerHeight: baseHeight, position: { mx: 0.5, my: 0.5 } }); - var width = element.width / 2; - var height = element.height / 2; + var width = baseWidth / 2, + height = baseHeight / 2; drawPath(parentGfx, linePathData, { strokeWidth: 1, - strokeLinecap: 'square', - transform: 'rotate(' + (i * 30) + ',' + height + ',' + width + ')', - stroke: getStrokeColor(element, defaultStrokeColor) + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + transform: 'rotate(' + (i * 30) + ',' + height + ',' + width + ')' }); } return circle; }, - 'bpmn:EscalationEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:EscalationEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_ESCALATION', { xScaleFactor: 1, yScaleFactor: 1, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { mx: 0.5, my: 0.2 } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill, - stroke: getStrokeColor(event, defaultStrokeColor) + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); }, - 'bpmn:ConditionalEventDefinition': function(parentGfx, event) { + 'bpmn:ConditionalEventDefinition': function(parentGfx, event, attrs = {}) { var pathData = pathMap.getScaledPath('EVENT_CONDITIONAL', { xScaleFactor: 1, yScaleFactor: 1, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { mx: 0.5, my: 0.222 @@ -666,11 +580,12 @@ export default function BpmnRenderer( }); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - stroke: getStrokeColor(event, defaultStrokeColor) + fill: getFillColor(event, defaultFillColor, attrs.fill), + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); }, - 'bpmn:LinkEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:LinkEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_LINK', { xScaleFactor: 1, yScaleFactor: 1, @@ -682,35 +597,39 @@ export default function BpmnRenderer( } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill, - stroke: getStrokeColor(event, defaultStrokeColor) + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); }, - 'bpmn:ErrorEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:ErrorEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_ERROR', { xScaleFactor: 1.1, yScaleFactor: 1.1, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { mx: 0.2, my: 0.722 } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill, - stroke: getStrokeColor(event, defaultStrokeColor) + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); }, - 'bpmn:CancelEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:CancelEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_CANCEL_45', { xScaleFactor: 1.0, yScaleFactor: 1.0, @@ -722,83 +641,90 @@ export default function BpmnRenderer( } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) : 'none'; var path = drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill, - stroke: getStrokeColor(event, defaultStrokeColor) + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); rotate(path, 45); return path; }, - 'bpmn:CompensateEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:CompensateEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_COMPENSATION', { xScaleFactor: 1, yScaleFactor: 1, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { mx: 0.22, my: 0.5 } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill, - stroke: getStrokeColor(event, defaultStrokeColor) + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); }, - 'bpmn:SignalEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:SignalEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_SIGNAL', { xScaleFactor: 0.9, yScaleFactor: 0.9, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { mx: 0.5, my: 0.2 } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); return drawPath(parentGfx, pathData, { strokeWidth: 1, - fill: fill, - stroke: getStrokeColor(event, defaultStrokeColor) + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke) }); }, - 'bpmn:MultipleEventDefinition': function(parentGfx, event, isThrowing) { + 'bpmn:MultipleEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_MULTIPLE', { xScaleFactor: 1.1, yScaleFactor: 1.1, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { - mx: 0.222, + mx: 0.211, my: 0.36 } }); - var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor) : 'none'; + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: fill + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); }, - 'bpmn:ParallelMultipleEventDefinition': function(parentGfx, event) { + 'bpmn:ParallelMultipleEventDefinition': function(parentGfx, event, attrs = {}) { var pathData = pathMap.getScaledPath('EVENT_PARALLEL_MULTIPLE', { xScaleFactor: 1.2, yScaleFactor: 1.2, - containerWidth: event.width, - containerHeight: event.height, + containerWidth: attrs.width || event.width, + containerHeight: attrs.height || event.height, position: { mx: 0.458, my: 0.194 @@ -806,432 +732,708 @@ export default function BpmnRenderer( }); return drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: getStrokeColor(event, defaultStrokeColor), - stroke: getStrokeColor(event, defaultStrokeColor) - }); - }, - 'bpmn:EndEvent': function(parentGfx, element) { - var circle = renderer('bpmn:Event')(parentGfx, element, { - strokeWidth: 4, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + fill: getFillColor(event, defaultFillColor, attrs.fill), + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); - - renderEventContent(element, parentGfx, true); - - return circle; }, - 'bpmn:TerminateEventDefinition': function(parentGfx, element) { + 'bpmn:TerminateEventDefinition': function(parentGfx, element, attrs = {}) { var circle = drawCircle(parentGfx, element.width, element.height, 8, { - strokeWidth: 4, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getStrokeColor(element, defaultStrokeColor) + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 4 }); return circle; - }, - 'bpmn:IntermediateEvent': function(parentGfx, element) { - var outer = renderer('bpmn:Event')(parentGfx, element, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); + } + }; - /* inner */ - drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, { - strokeWidth: 1, - fill: getFillColor(element, 'none'), - stroke: getStrokeColor(element, defaultStrokeColor) - }); + function renderEventIcon(element, parentGfx, attrs = {}, proxyElement) { + var semantic = getSemantic(element), + isThrowing = isThrowEvent(semantic); - renderEventContent(element, parentGfx); + var nodeElement = proxyElement || element; - return outer; - }, - 'bpmn:IntermediateCatchEvent': as('bpmn:IntermediateEvent'), - 'bpmn:IntermediateThrowEvent': as('bpmn:IntermediateEvent'), + if (semantic.get('eventDefinitions') && semantic.get('eventDefinitions').length > 1) { + if (semantic.get('parallelMultiple')) { + return eventIconRenderers[ 'bpmn:ParallelMultipleEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } + else { + return eventIconRenderers[ 'bpmn:MultipleEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } + } - 'bpmn:Activity': function(parentGfx, element, attrs) { + if (isTypedEvent(semantic, 'bpmn:MessageEventDefinition')) { + return eventIconRenderers[ 'bpmn:MessageEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - attrs = attrs || {}; + if (isTypedEvent(semantic, 'bpmn:TimerEventDefinition')) { + return eventIconRenderers[ 'bpmn:TimerEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - if (!('fillOpacity' in attrs)) { - attrs.fillOpacity = DEFAULT_FILL_OPACITY; - } + if (isTypedEvent(semantic, 'bpmn:ConditionalEventDefinition')) { + return eventIconRenderers[ 'bpmn:ConditionalEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, attrs); - }, + if (isTypedEvent(semantic, 'bpmn:SignalEventDefinition')) { + return eventIconRenderers[ 'bpmn:SignalEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - 'bpmn:Task': function(parentGfx, element) { - var attrs = { - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }; + if (isTypedEvent(semantic, 'bpmn:EscalationEventDefinition')) { + return eventIconRenderers[ 'bpmn:EscalationEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - var rect = renderer('bpmn:Activity')(parentGfx, element, attrs); + if (isTypedEvent(semantic, 'bpmn:LinkEventDefinition')) { + return eventIconRenderers[ 'bpmn:LinkEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - renderEmbeddedLabel(parentGfx, element, 'center-middle'); - attachTaskMarkers(parentGfx, element); + if (isTypedEvent(semantic, 'bpmn:ErrorEventDefinition')) { + return eventIconRenderers[ 'bpmn:ErrorEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - return rect; - }, - 'bpmn:ServiceTask': function(parentGfx, element) { - var task = renderer('bpmn:Task')(parentGfx, element); + if (isTypedEvent(semantic, 'bpmn:CancelEventDefinition')) { + return eventIconRenderers[ 'bpmn:CancelEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - var pathDataBG = pathMap.getScaledPath('TASK_TYPE_SERVICE', { - abspos: { - x: 12, - y: 18 - } - }); + if (isTypedEvent(semantic, 'bpmn:CompensateEventDefinition')) { + return eventIconRenderers[ 'bpmn:CompensateEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - /* service bg */ drawPath(parentGfx, pathDataBG, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); + if (isTypedEvent(semantic, 'bpmn:TerminateEventDefinition')) { + return eventIconRenderers[ 'bpmn:TerminateEventDefinition' ](parentGfx, nodeElement, attrs, isThrowing); + } - var fillPathData = pathMap.getScaledPath('TASK_TYPE_SERVICE_FILL', { - abspos: { - x: 17.2, - y: 18 - } - }); + return null; + } - /* service fill */ drawPath(parentGfx, fillPathData, { - strokeWidth: 0, - fill: getFillColor(element, defaultFillColor) - }); + var taskMarkerRenderers = { + 'ParticipantMultiplicityMarker': function(parentGfx, element, attrs = {}) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); - var pathData = pathMap.getScaledPath('TASK_TYPE_SERVICE', { - abspos: { - x: 17, - y: 22 + var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 - 6) / width), + my: (height - 15) / height } }); - /* service */ drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawMarker('participant-multiplicity', parentGfx, markerPath, { + strokeWidth: 2, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) }); - - return task; }, - 'bpmn:UserTask': function(parentGfx, element) { - var task = renderer('bpmn:Task')(parentGfx, element); + 'SubProcessMarker': function(parentGfx, element, attrs = {}) { + var markerRect = drawRect(parentGfx, 14, 14, 0, { + strokeWidth: 1, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); - var x = 15; - var y = 12; + translate(markerRect, element.width / 2 - 7.5, element.height - 20); - var pathData = pathMap.getScaledPath('TASK_TYPE_USER_1', { - abspos: { - x: x, - y: y + var markerPath = pathMap.getScaledPath('MARKER_SUB_PROCESS', { + xScaleFactor: 1.5, + yScaleFactor: 1.5, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: (element.width / 2 - 7.5) / element.width, + my: (element.height - 20) / element.height } }); - /* user path */ drawPath(parentGfx, pathData, { - strokeWidth: 0.5, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawMarker('sub-process', parentGfx, markerPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) }); + }, + 'ParallelMarker': function(parentGfx, element, attrs) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); - var pathData2 = pathMap.getScaledPath('TASK_TYPE_USER_2', { - abspos: { - x: x, - y: y + var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 + attrs.parallel) / width), + my: (height - 20) / height } }); - /* user2 path */ drawPath(parentGfx, pathData2, { - strokeWidth: 0.5, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawMarker('parallel', parentGfx, markerPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) }); - - var pathData3 = pathMap.getScaledPath('TASK_TYPE_USER_3', { - abspos: { - x: x, - y: y + }, + 'SequentialMarker': function(parentGfx, element, attrs) { + var markerPath = pathMap.getScaledPath('MARKER_SEQUENTIAL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: ((element.width / 2 + attrs.seq) / element.width), + my: (element.height - 19) / element.height } }); - /* user3 path */ drawPath(parentGfx, pathData3, { - strokeWidth: 0.5, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawMarker('sequential', parentGfx, markerPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + }, + 'CompensationMarker': function(parentGfx, element, attrs) { + var markerMath = pathMap.getScaledPath('MARKER_COMPENSATION', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: ((element.width / 2 + attrs.compensation) / element.width), + my: (element.height - 13) / element.height + } }); - return task; + drawMarker('compensation', parentGfx, markerMath, { + strokeWidth: 1, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); }, - 'bpmn:ManualTask': function(parentGfx, element) { - var task = renderer('bpmn:Task')(parentGfx, element); + 'LoopMarker': function(parentGfx, element, attrs) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); - var pathData = pathMap.getScaledPath('TASK_TYPE_MANUAL', { - abspos: { - x: 17, - y: 15 + var markerPath = pathMap.getScaledPath('MARKER_LOOP', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 + attrs.loop) / width), + my: (height - 7) / height } }); - /* manual path */ drawPath(parentGfx, pathData, { - strokeWidth: 0.5, // 0.25, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawMarker('loop', parentGfx, markerPath, { + strokeWidth: 1.5, + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeMiterlimit: 0.5 }); - - return task; }, - 'bpmn:SendTask': function(parentGfx, element) { - var task = renderer('bpmn:Task')(parentGfx, element); + 'AdhocMarker': function(parentGfx, element, attrs) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); - var pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { + var markerPath = pathMap.getScaledPath('MARKER_ADHOC', { xScaleFactor: 1, yScaleFactor: 1, - containerWidth: 21, - containerHeight: 14, + containerWidth: width, + containerHeight: height, position: { - mx: 0.285, - my: 0.357 + mx: ((width / 2 + attrs.adhoc) / width), + my: (height - 15) / height } }); - /* send path */ drawPath(parentGfx, pathData, { + drawMarker('adhoc', parentGfx, markerPath, { strokeWidth: 1, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getFillColor(element, defaultFillColor) + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) }); + } + }; - return task; - }, - 'bpmn:ReceiveTask' : function(parentGfx, element) { - var semantic = getSemantic(element); + function renderTaskMarker(type, parentGfx, element, attrs) { + taskMarkerRenderers[ type ](parentGfx, element, attrs); + } - var task = renderer('bpmn:Task')(parentGfx, element); - var pathData; + function renderTaskMarkers(parentGfx, element, taskMarkers = [], attrs = {}) { + attrs = { + fill: attrs.fill, + stroke: attrs.stroke, + width: getWidth(element, attrs), + height: getHeight(element, attrs) + }; - if (semantic.instantiate) { - drawCircle(parentGfx, 28, 28, 20 * 0.22, { strokeWidth: 1 }); + var semantic = getSemantic(element); - pathData = pathMap.getScaledPath('TASK_TYPE_INSTANTIATING_SEND', { - abspos: { - x: 7.77, - y: 9.52 - } - }); - } else { + var subprocess = taskMarkers.includes('SubProcessMarker'); - pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { - xScaleFactor: 0.9, - yScaleFactor: 0.9, - containerWidth: 21, - containerHeight: 14, - position: { - mx: 0.3, - my: 0.4 - } - }); + if (subprocess) { + attrs = { + ...attrs, + seq: -21, + parallel: -22, + compensation: -25, + loop: -18, + adhoc: 10 + }; + } else { + attrs = { + ...attrs, + seq: -5, + parallel: -6, + compensation: -7, + loop: 0, + adhoc: -8 + }; + } + + if (semantic.get('isForCompensation')) { + taskMarkers.push('CompensationMarker'); + } + + if (is(semantic, 'bpmn:AdHocSubProcess')) { + taskMarkers.push('AdhocMarker'); + + if (!subprocess) { + assign(attrs, { compensation: attrs.compensation - 18 }); } + } - /* receive path */ drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); + var loopCharacteristics = semantic.get('loopCharacteristics'), + isSequential = loopCharacteristics && loopCharacteristics.get('isSequential'); - return task; - }, - 'bpmn:ScriptTask': function(parentGfx, element) { - var task = renderer('bpmn:Task')(parentGfx, element); + if (loopCharacteristics) { - var pathData = pathMap.getScaledPath('TASK_TYPE_SCRIPT', { - abspos: { - x: 15, - y: 20 - } + assign(attrs, { + compensation: attrs.compensation - 18, }); - /* script path */ drawPath(parentGfx, pathData, { - strokeWidth: 1, - stroke: getStrokeColor(element, defaultStrokeColor) + if (taskMarkers.includes('AdhocMarker')) { + assign(attrs, { + seq: -23, + loop: -18, + parallel: -24 + }); + } + + if (isSequential === undefined) { + taskMarkers.push('LoopMarker'); + } + + if (isSequential === false) { + taskMarkers.push('ParallelMarker'); + } + + if (isSequential === true) { + taskMarkers.push('SequentialMarker'); + } + } + + if (taskMarkers.includes('CompensationMarker') && taskMarkers.length === 1) { + assign(attrs, { + compensation: -8 }); + } - return task; - }, - 'bpmn:BusinessRuleTask': function(parentGfx, element) { - var task = renderer('bpmn:Task')(parentGfx, element); + forEach(taskMarkers, function(marker) { + renderTaskMarker(marker, parentGfx, element, attrs); + }); + } - var headerPathData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_HEADER', { - abspos: { - x: 8, - y: 8 + function renderLabel(parentGfx, label, attrs = {}) { + attrs = assign({ + size: { + width: 100 + } + }, attrs); + + var text = textRenderer.createText(label || '', attrs); + + svgClasses(text).add('djs-label'); + + svgAppend(parentGfx, text); + + return text; + } + + function renderEmbeddedLabel(parentGfx, element, align, attrs = {}) { + var semantic = getSemantic(element); + + var box = getBounds({ + x: element.x, + y: element.y, + width: element.width, + height: element.height + }, attrs); + + return renderLabel(parentGfx, semantic.name, { + align, + box, + padding: 7, + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + } + + function renderExternalLabel(parentGfx, element, attrs = {}) { + var box = { + width: element.width, + height: element.height, + x: element.width / 2 + element.x, + y: element.height / 2 + element.y + }; + + return renderLabel(parentGfx, getLabel(element), { + box: box, + style: assign( + {}, + textRenderer.getExternalStyle(), + { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) } - }); + ) + }); + } - var businessHeaderPath = drawPath(parentGfx, headerPathData); - svgAttr(businessHeaderPath, { - strokeWidth: 1, - fill: getFillColor(element, '#aaaaaa'), - stroke: getStrokeColor(element, defaultStrokeColor) - }); + function renderLaneLabel(parentGfx, text, element, attrs = {}) { + var isHorizontalLane = isHorizontal(element); - var headerData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_MAIN', { - abspos: { - x: 8, - y: 8 + var textBox = renderLabel(parentGfx, text, { + box: { + height: 30, + width: isHorizontalLane ? getHeight(element, attrs) : getWidth(element, attrs), + }, + align: 'center-middle', + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + + if (isHorizontalLane) { + var top = -1 * getHeight(element, attrs); + transform(textBox, 0, -top, 270); + } + } + + function renderActivity(parentGfx, element, attrs = {}) { + var { + width, + height + } = getBounds(element, attrs); + + return drawRect(parentGfx, width, height, TASK_BORDER_RADIUS, { + ...attrs, + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } + + function renderAssociation(parentGfx, element, attrs = {}) { + var semantic = getSemantic(element); + + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + if (semantic.get('associationDirection') === 'One' || + semantic.get('associationDirection') === 'Both') { + attrs.markerEnd = marker(parentGfx, 'association-end', fill, stroke); + } + + if (semantic.get('associationDirection') === 'Both') { + attrs.markerStart = marker(parentGfx, 'association-start', fill, stroke); + } + + attrs = pickAttrs(attrs, [ + 'markerStart', + 'markerEnd' + ]); + + return drawConnectionSegments(parentGfx, element.waypoints, { + ...attrs, + stroke, + strokeDasharray: '0, 5' + }); + } + + function renderDataObject(parentGfx, element, attrs = {}) { + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + var pathData = pathMap.getScaledPath('DATA_OBJECT_PATH', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.474, + my: 0.296 + } + }); + + var dataObject = drawPath(parentGfx, pathData, { + fill, + fillOpacity: DEFAULT_OPACITY, + stroke + }); + + var semantic = getSemantic(element); + + if (isCollection(semantic)) { + var collectionPathData = pathMap.getScaledPath('DATA_OBJECT_COLLECTION_PATH', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.33, + my: (element.height - 18) / element.height } }); - var businessPath = drawPath(parentGfx, headerData); - svgAttr(businessPath, { - strokeWidth: 1, - stroke: getStrokeColor(element, defaultStrokeColor) + drawPath(parentGfx, collectionPathData, { + strokeWidth: 2, + fill, + stroke }); + } - return task; - }, - 'bpmn:SubProcess': function(parentGfx, element, attrs) { - attrs = assign({ - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }, attrs); + return dataObject; + } - var rect = renderer('bpmn:Activity')(parentGfx, element, attrs); + function renderEvent(parentGfx, element, attrs = {}) { + return drawCircle(parentGfx, element.width, element.height, { + fillOpacity: DEFAULT_OPACITY, + ...attrs, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } - var expanded = isExpanded(element); + function renderGateway(parentGfx, element, attrs = {}) { + return drawDiamond(parentGfx, element.width, element.height, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } - if (isEventSubProcess(element)) { - svgAttr(rect, { - strokeDasharray: '1,2' - }); + function renderLane(parentGfx, element, attrs = {}) { + var lane = drawRect(parentGfx, getWidth(element, attrs), getHeight(element, attrs), 0, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: attrs.fillOpacity || DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5 + }); + + var semantic = getSemantic(element); + + if (is(semantic, 'bpmn:Lane')) { + var text = semantic.get('name'); + + renderLaneLabel(parentGfx, text, element, attrs); + } + + return lane; + } + + function renderSubProcess(parentGfx, element, attrs = {}) { + var activity = renderActivity(parentGfx, element, attrs); + + var expanded = isExpanded(element); + + if (isEventSubProcess(element)) { + svgAttr(activity, { + strokeDasharray: '0, 5.5', + strokeWidth: 2.5 + }); + + if (!expanded) { + var flowElements = getSemantic(element).flowElements || []; + var startEvents = flowElements.filter(e => is(e, 'bpmn:StartEvent')); + + if (startEvents.length === 1) { + renderEventSubProcessIcon(startEvents[0], parentGfx, attrs, element); + } } + } - renderEmbeddedLabel(parentGfx, element, expanded ? 'center-top' : 'center-middle'); + renderEmbeddedLabel(parentGfx, element, expanded ? 'center-top' : 'center-middle', attrs); - if (expanded) { - attachTaskMarkers(parentGfx, element); + if (expanded) { + renderTaskMarkers(parentGfx, element, undefined, attrs); + } else { + renderTaskMarkers(parentGfx, element, [ 'SubProcessMarker' ], attrs); + } + + return activity; + } + + function renderEventSubProcessIcon(startEvent, parentGfx, attrs, proxyElement) { + var iconSize = 22; + + // match the colors of the enclosing subprocess + var proxyAttrs = { + fill: getFillColor(proxyElement, defaultFillColor, attrs.fill), + stroke: getStrokeColor(proxyElement, defaultStrokeColor, attrs.stroke), + width: iconSize, + height: iconSize + }; + + var interrupting = getSemantic(startEvent).isInterrupting; + var strokeDasharray = interrupting ? 0 : 3; + + // better visibility for non-interrupting events + var strokeWidth = interrupting ? 1 : 1.2; + + // make the icon look larger by drawing a smaller circle + var circleSize = 20; + var shift = (iconSize - circleSize) / 2; + var transform = 'translate(' + shift + ',' + shift + ')'; + + drawCircle(parentGfx, circleSize, circleSize, { + fill: proxyAttrs.fill, + stroke: proxyAttrs.stroke, + strokeWidth, + strokeDasharray, + transform + }); + + renderEventIcon(startEvent, parentGfx, proxyAttrs, proxyElement); + } + + function renderTask(parentGfx, element, attrs = {}) { + var activity = renderActivity(parentGfx, element, attrs); + + renderEmbeddedLabel(parentGfx, element, 'center-middle', attrs); + + renderTaskMarkers(parentGfx, element, undefined, attrs); + + return activity; + } + + var handlers = this.handlers = { + 'bpmn:AdHocSubProcess': function(parentGfx, element, attrs = {}) { + if (isExpanded(element)) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); } else { - attachTaskMarkers(parentGfx, element, ['SubProcessMarker']); + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); } - return rect; + return renderSubProcess(parentGfx, element, attrs); }, - 'bpmn:AdHocSubProcess': function(parentGfx, element) { - return renderer('bpmn:SubProcess')(parentGfx, element); - }, - 'bpmn:Transaction': function(parentGfx, element) { - var outer = renderer('bpmn:SubProcess')(parentGfx, element); - - var innerAttrs = styles.style([ 'no-fill', 'no-events' ], { - stroke: getStrokeColor(element, defaultStrokeColor) - }); + 'bpmn:Association': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - /* inner path */ drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS - 2, INNER_OUTER_DIST, innerAttrs); - - return outer; - }, - 'bpmn:CallActivity': function(parentGfx, element) { - return renderer('bpmn:SubProcess')(parentGfx, element, { - strokeWidth: 5 - }); + return renderAssociation(parentGfx, element, attrs); }, - 'bpmn:Participant': function(parentGfx, element) { + 'bpmn:BoundaryEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; - var attrs = { - fillOpacity: DEFAULT_FILL_OPACITY, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }; - - var lane = renderer('bpmn:Lane')(parentGfx, element, attrs); + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - var expandedPool = isExpanded(element); + var semantic = getSemantic(element), + cancelActivity = semantic.get('cancelActivity'); - if (expandedPool) { - drawLine(parentGfx, [ - { x: 30, y: 0 }, - { x: 30, y: element.height } - ], { - stroke: getStrokeColor(element, defaultStrokeColor) - }); - var text = getSemantic(element).name; - renderLaneLabel(parentGfx, text, element); - } else { + attrs = { + strokeWidth: 1.5, + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: FULL_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }; - // Collapsed pool draw text inline - var text2 = getSemantic(element).name; - renderLabel(parentGfx, text2, { - box: element, align: 'center-middle', - style: { - fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) - } - }); + if (!cancelActivity) { + attrs.strokeDasharray = '6'; } - var participantMultiplicity = !!(getSemantic(element).participantMultiplicity); + var event = renderEvent(parentGfx, element, attrs); + + drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, { + ...attrs, + fill: 'none' + }); - if (participantMultiplicity) { - renderer('ParticipantMultiplicityMarker')(parentGfx, element); + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); } - return lane; + return event; }, - 'bpmn:Lane': function(parentGfx, element, attrs) { - var rect = drawRect(parentGfx, element.width, element.height, 0, assign({ - fill: getFillColor(element, defaultFillColor), - fillOpacity: HIGH_FILL_OPACITY, - stroke: getStrokeColor(element, defaultStrokeColor) - }, attrs)); + 'bpmn:BusinessRuleTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - var semantic = getSemantic(element); + var task = renderTask(parentGfx, element, attrs); - if (semantic.$type === 'bpmn:Lane') { - var text = semantic.name; - renderLaneLabel(parentGfx, text, element); - } + var headerData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_MAIN', { + abspos: { + x: 8, + y: 8 + } + }); - return rect; - }, - 'bpmn:InclusiveGateway': function(parentGfx, element) { - var diamond = renderer('bpmn:Gateway')(parentGfx, element); + var businessPath = drawPath(parentGfx, headerData); - /* circle path */ - drawCircle(parentGfx, element.width, element.height, element.height * 0.24, { - strokeWidth: 2.5, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + svgAttr(businessPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); - return diamond; - }, - 'bpmn:ExclusiveGateway': function(parentGfx, element) { - var diamond = renderer('bpmn:Gateway')(parentGfx, element); - - var pathData = pathMap.getScaledPath('GATEWAY_EXCLUSIVE', { - xScaleFactor: 0.4, - yScaleFactor: 0.4, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: 0.32, - my: 0.3 + var headerPathData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_HEADER', { + abspos: { + x: 8, + y: 8 } }); - if ((getDi(element).isMarkerVisible)) { - drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); - } + var businessHeaderPath = drawPath(parentGfx, headerPathData); - return diamond; + svgAttr(businessHeaderPath, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:CallActivity': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderSubProcess(parentGfx, element, { + strokeWidth: 5, + ...attrs + }); }, - 'bpmn:ComplexGateway': function(parentGfx, element) { - var diamond = renderer('bpmn:Gateway')(parentGfx, element); + 'bpmn:ComplexGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var gateway = renderGateway(parentGfx, element, attrs); var pathData = pathMap.getScaledPath('GATEWAY_COMPLEX', { xScaleFactor: 0.5, @@ -1244,50 +1446,142 @@ export default function BpmnRenderer( } }); - /* complex path */ drawPath(parentGfx, pathData, { + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return gateway; + }, + 'bpmn:DataInput': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var arrowPathData = pathMap.getRawPath('DATA_ARROW'); + + var dataObject = renderDataObject(parentGfx, element, attrs); + + drawPath(parentGfx, arrowPathData, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return dataObject; + }, + 'bpmn:DataInputAssociation': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderAssociation(parentGfx, element, { + ...attrs, + markerEnd: marker(parentGfx, 'association-end', getFillColor(element, defaultFillColor, attrs.fill), getStrokeColor(element, defaultStrokeColor, attrs.stroke)) + }); + }, + 'bpmn:DataObject': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderDataObject(parentGfx, element, attrs); + }, + 'bpmn:DataObjectReference': as('bpmn:DataObject'), + 'bpmn:DataOutput': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var arrowPathData = pathMap.getRawPath('DATA_ARROW'); + + var dataObject = renderDataObject(parentGfx, element, attrs); + + drawPath(parentGfx, arrowPathData, { strokeWidth: 1, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getStrokeColor(element, defaultStrokeColor) + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) }); - return diamond; + return dataObject; }, - 'bpmn:ParallelGateway': function(parentGfx, element) { - var diamond = renderer('bpmn:Gateway')(parentGfx, element); + 'bpmn:DataOutputAssociation': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { - xScaleFactor: 0.6, - yScaleFactor:0.6, + return renderAssociation(parentGfx, element, { + ...attrs, + markerEnd: marker(parentGfx, 'association-end', getFillColor(element, defaultFillColor, attrs.fill), getStrokeColor(element, defaultStrokeColor, attrs.stroke)) + }); + }, + 'bpmn:DataStoreReference': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var dataStorePath = pathMap.getScaledPath('DATA_STORE', { + xScaleFactor: 1, + yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { - mx: 0.46, - my: 0.2 + mx: 0, + my: 0.133 } }); - /* parallel path */ drawPath(parentGfx, pathData, { - strokeWidth: 1, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getStrokeColor(element, defaultStrokeColor) + return drawPath(parentGfx, dataStorePath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2 }); + }, + 'bpmn:EndEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; - return diamond; + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var event = renderEvent(parentGfx, element, { + ...attrs, + strokeWidth: 4 + }); + + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); + } + + return event; }, - 'bpmn:EventBasedGateway': function(parentGfx, element) { + 'bpmn:EventBasedGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); var semantic = getSemantic(element); - var diamond = renderer('bpmn:Gateway')(parentGfx, element); + var diamond = renderGateway(parentGfx, element, attrs); - /* outer circle path */ drawCircle(parentGfx, element.width, element.height, element.height * 0.20, { - strokeWidth: 1, - fill: 'none', - stroke: getStrokeColor(element, defaultStrokeColor) + drawCircle(parentGfx, element.width, element.height, element.height * 0.20, { + fill: getFillColor(element, 'none', attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); - var type = semantic.eventGatewayType; - var instantiate = !!semantic.instantiate; + var type = semantic.get('eventGatewayType'), + instantiate = !!semantic.get('instantiate'); function drawEvent() { @@ -1302,20 +1596,17 @@ export default function BpmnRenderer( } }); - var attrs = { - strokeWidth: 2, - fill: getFillColor(element, 'none'), - stroke: getStrokeColor(element, defaultStrokeColor) - }; - - /* event path */ drawPath(parentGfx, pathData, attrs); + drawPath(parentGfx, pathData, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2 + }); } if (type === 'Parallel') { - var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { xScaleFactor: 0.4, - yScaleFactor:0.4, + yScaleFactor: 0.4, containerWidth: element.width, containerHeight: element.height, position: { @@ -1324,19 +1615,17 @@ export default function BpmnRenderer( } }); - var parallelPath = drawPath(parentGfx, pathData); - svgAttr(parallelPath, { - strokeWidth: 1, - fill: 'none' + drawPath(parentGfx, pathData, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); } else if (type === 'Exclusive') { - if (!instantiate) { - var innerCircle = drawCircle(parentGfx, element.width, element.height, element.height * 0.26); - svgAttr(innerCircle, { - strokeWidth: 1, + drawCircle(parentGfx, element.width, element.height, element.height * 0.26, { fill: 'none', - stroke: getStrokeColor(element, defaultStrokeColor) + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); } @@ -1346,118 +1635,163 @@ export default function BpmnRenderer( return diamond; }, - 'bpmn:Gateway': function(parentGfx, element) { - var attrs = { - fill: getFillColor(element, defaultFillColor), - fillOpacity: DEFAULT_FILL_OPACITY, - stroke: getStrokeColor(element, defaultStrokeColor) - }; - - return drawDiamond(parentGfx, element.width, element.height, attrs); - }, - 'bpmn:SequenceFlow': function(parentGfx, element) { - var pathData = createPathFromConnection(element); + 'bpmn:ExclusiveGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - var fill = getFillColor(element, defaultFillColor), - stroke = getStrokeColor(element, defaultStrokeColor); + var gateway = renderGateway(parentGfx, element, attrs); - var attrs = { - strokeLinejoin: 'round', - markerEnd: marker('sequenceflow-end', fill, stroke), - stroke: getStrokeColor(element, defaultStrokeColor) - }; + var pathData = pathMap.getScaledPath('GATEWAY_EXCLUSIVE', { + xScaleFactor: 0.4, + yScaleFactor: 0.4, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.32, + my: 0.3 + } + }); - var path = drawPath(parentGfx, pathData, attrs); + var di = getDi(element); - var sequenceFlow = getSemantic(element); + if (di.get('isMarkerVisible')) { + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + } - var source; + return gateway; + }, + 'bpmn:Gateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - if (element.source) { - source = element.source.businessObject; + return renderGateway(parentGfx, element, attrs); + }, + 'bpmn:Group': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5, + strokeDasharray: '10, 6, 0, 6', + fill: 'none', + pointerEvents: 'none', + width: getWidth(element, attrs), + height: getHeight(element, attrs) + }); + }, + 'bpmn:InclusiveGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - // conditional flow marker - if (sequenceFlow.conditionExpression && source.$instanceOf('bpmn:Activity')) { - svgAttr(path, { - markerStart: marker('conditional-flow-marker', fill, stroke) - }); - } + var gateway = renderGateway(parentGfx, element, attrs); - // default marker - if (source.default && (source.$instanceOf('bpmn:Gateway') || source.$instanceOf('bpmn:Activity')) && - source.default === sequenceFlow) { - svgAttr(path, { - markerStart: marker('conditional-default-flow-marker', fill, stroke) - }); - } - } + drawCircle(parentGfx, element.width, element.height, element.height * 0.24, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2.5 + }); - return path; + return gateway; }, - 'bpmn:Association': function(parentGfx, element, attrs) { - - var semantic = getSemantic(element); + 'bpmn:IntermediateEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; - var fill = getFillColor(element, defaultFillColor), - stroke = getStrokeColor(element, defaultStrokeColor); + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - attrs = assign({ - strokeDasharray: '0.5, 5', - strokeLinecap: 'round', - strokeLinejoin: 'round', - stroke: getStrokeColor(element, defaultStrokeColor) - }, attrs || {}); + var outer = renderEvent(parentGfx, element, { + ...attrs, + strokeWidth: 1.5 + }); - if (semantic.associationDirection === 'One' || - semantic.associationDirection === 'Both') { - attrs.markerEnd = marker('association-end', fill, stroke); - } + drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5 + }); - if (semantic.associationDirection === 'Both') { - attrs.markerStart = marker('association-start', fill, stroke); + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); } - return drawLine(parentGfx, element.waypoints, attrs); + return outer; }, - 'bpmn:DataInputAssociation': function(parentGfx, element) { - var fill = getFillColor(element, defaultFillColor), - stroke = getStrokeColor(element, defaultStrokeColor); - - return renderer('bpmn:Association')(parentGfx, element, { - markerEnd: marker('association-end', fill, stroke) + 'bpmn:IntermediateCatchEvent': as('bpmn:IntermediateEvent'), + 'bpmn:IntermediateThrowEvent': as('bpmn:IntermediateEvent'), + 'bpmn:Lane': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + return renderLane(parentGfx, element, { + ...attrs, + fillOpacity: LOW_OPACITY }); }, - 'bpmn:DataOutputAssociation': function(parentGfx, element) { - var fill = getFillColor(element, defaultFillColor), - stroke = getStrokeColor(element, defaultStrokeColor); + 'bpmn:ManualTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('TASK_TYPE_MANUAL', { + abspos: { + x: 17, + y: 15 + } + }); - return renderer('bpmn:Association')(parentGfx, element, { - markerEnd: marker('association-end', fill, stroke) + drawPath(parentGfx, pathData, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 }); + + return task; }, - 'bpmn:MessageFlow': function(parentGfx, element) { + 'bpmn:MessageFlow': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); var semantic = getSemantic(element), di = getDi(element); - var fill = getFillColor(element, defaultFillColor), - stroke = getStrokeColor(element, defaultStrokeColor); + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); - var pathData = createPathFromConnection(element); - - var attrs = { - markerEnd: marker('messageflow-end', fill, stroke), - markerStart: marker('messageflow-start', fill, stroke), - strokeDasharray: '10, 12', - strokeLinecap: 'round', - strokeLinejoin: 'round', - strokeWidth: '1.5px', - stroke: getStrokeColor(element, defaultStrokeColor) - }; - - var path = drawPath(parentGfx, pathData, attrs); + var path = drawConnectionSegments(parentGfx, element.waypoints, { + markerEnd: marker(parentGfx, 'messageflow-end', fill, stroke), + markerStart: marker(parentGfx, 'messageflow-start', fill, stroke), + stroke, + strokeDasharray: '10, 11', + strokeWidth: 1.5 + }); - if (semantic.messageRef) { + if (semantic.get('messageRef')) { var midPoint = path.getPointAtLength(path.getTotalLength() / 2); var markerPathData = pathMap.getScaledPath('MESSAGE_FLOW_MARKER', { @@ -1467,173 +1801,394 @@ export default function BpmnRenderer( } }); - var messageAttrs = { strokeWidth: 1 }; + var messageAttrs = { + strokeWidth: 1 + }; - if (di.messageVisibleKind === 'initiating') { - messageAttrs.fill = 'white'; - messageAttrs.stroke = 'black'; + if (di.get('messageVisibleKind') === 'initiating') { + messageAttrs.fill = fill; + messageAttrs.stroke = stroke; } else { - messageAttrs.fill = '#888'; - messageAttrs.stroke = 'white'; + messageAttrs.fill = stroke; + messageAttrs.stroke = fill; } var message = drawPath(parentGfx, markerPathData, messageAttrs); - var labelText = semantic.messageRef.name; - var label = renderLabel(parentGfx, labelText, { + var messageRef = semantic.get('messageRef'), + name = messageRef.get('name'); + + var label = renderLabel(parentGfx, name, { align: 'center-top', fitBox: true, style: { - fill: getStrokeColor(element, defaultLabelColor, defaultStrokeColor) + fill: stroke } }); var messageBounds = message.getBBox(), labelBounds = label.getBBox(); - var translateX = midPoint.x - labelBounds.width / 2, - translateY = midPoint.y + messageBounds.height / 2 + ELEMENT_LABEL_DISTANCE; + var translateX = midPoint.x - labelBounds.width / 2, + translateY = midPoint.y + messageBounds.height / 2 + ELEMENT_LABEL_DISTANCE; + + transform(label, translateX, translateY, 0); + } + + return path; + }, + 'bpmn:ParallelGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var diamond = renderGateway(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { + xScaleFactor: 0.6, + yScaleFactor: 0.6, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.46, + my: 0.2 + } + }); + + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return diamond; + }, + 'bpmn:Participant': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + var participant = renderLane(parentGfx, element, attrs); + + var expandedParticipant = isExpanded(element); + var horizontalParticipant = isHorizontal(element); + + var semantic = getSemantic(element), + name = semantic.get('name'); + + if (expandedParticipant) { + var waypoints = horizontalParticipant ? [ + { + x: 30, + y: 0 + }, + { + x: 30, + y: getHeight(element, attrs) + } + ] : [ + { + x: 0, + y: 30 + }, + { + x: getWidth(element, attrs), + y: 30 + } + ]; + + drawLine(parentGfx, waypoints, { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: PARTICIPANT_STROKE_WIDTH + }); + + renderLaneLabel(parentGfx, name, element, attrs); + } else { + var bounds = getBounds(element, attrs); + + if (!horizontalParticipant) { + bounds.height = getWidth(element, attrs); + bounds.width = getHeight(element, attrs); + } + + var textBox = renderLabel(parentGfx, name, { + box: bounds, + align: 'center-middle', + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + + if (!horizontalParticipant) { + var top = -1 * getHeight(element, attrs); + transform(textBox, 0, -top, 270); + } + } + + if (semantic.get('participantMultiplicity')) { + renderTaskMarker('ParticipantMultiplicityMarker', parentGfx, element, attrs); + } + + return participant; + }, + 'bpmn:ReceiveTask' : function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var semantic = getSemantic(element); + + var task = renderTask(parentGfx, element, attrs); + + var pathData; + + if (semantic.get('instantiate')) { + drawCircle(parentGfx, 28, 28, 20 * 0.22, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + pathData = pathMap.getScaledPath('TASK_TYPE_INSTANTIATING_SEND', { + abspos: { + x: 7.77, + y: 9.52 + } + }); + } else { + pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { + xScaleFactor: 0.9, + yScaleFactor: 0.9, + containerWidth: 21, + containerHeight: 14, + position: { + mx: 0.3, + my: 0.4 + } + }); + } + + drawPath(parentGfx, pathData, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:ScriptTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - transform(label, translateX, translateY, 0); + var task = renderTask(parentGfx, element, attrs); - } + var pathData = pathMap.getScaledPath('TASK_TYPE_SCRIPT', { + abspos: { + x: 15, + y: 20 + } + }); - return path; + drawPath(parentGfx, pathData, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; }, - 'bpmn:DataObject': function(parentGfx, element) { - var pathData = pathMap.getScaledPath('DATA_OBJECT_PATH', { + 'bpmn:SendTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { xScaleFactor: 1, yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, + containerWidth: 21, + containerHeight: 14, position: { - mx: 0.474, - my: 0.296 + mx: 0.285, + my: 0.357 } }); - var elementObject = drawPath(parentGfx, pathData, { - fill: getFillColor(element, defaultFillColor), - fillOpacity: DEFAULT_FILL_OPACITY, - stroke: getStrokeColor(element, defaultStrokeColor) + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getFillColor(element, defaultFillColor, attrs.fill), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:SequenceFlow': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + var connection = drawConnectionSegments(parentGfx, element.waypoints, { + markerEnd: marker(parentGfx, 'sequenceflow-end', fill, stroke), + stroke }); var semantic = getSemantic(element); - if (isCollection(semantic)) { - renderDataItemCollection(parentGfx, element); + var { source } = element; + + if (source) { + var sourceSemantic = getSemantic(source); + + // conditional flow marker + if (semantic.get('conditionExpression') && is(sourceSemantic, 'bpmn:Activity')) { + svgAttr(connection, { + markerStart: marker(parentGfx, 'conditional-flow-marker', fill, stroke) + }); + } + + // default marker + if (sourceSemantic.get('default') && (is(sourceSemantic, 'bpmn:Gateway') || is(sourceSemantic, 'bpmn:Activity')) && + sourceSemantic.get('default') === semantic) { + svgAttr(connection, { + markerStart: marker(parentGfx, 'conditional-default-flow-marker', fill, stroke) + }); + } } - return elementObject; + return connection; }, - 'bpmn:DataObjectReference': as('bpmn:DataObject'), - 'bpmn:DataInput': function(parentGfx, element) { - - var arrowPathData = pathMap.getRawPath('DATA_ARROW'); + 'bpmn:ServiceTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - // page - var elementObject = renderer('bpmn:DataObject')(parentGfx, element); + var task = renderTask(parentGfx, element, attrs); - /* input arrow path */ drawPath(parentGfx, arrowPathData, { strokeWidth: 1 }); + drawCircle(parentGfx, 10, 10, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: 'none', + transform: 'translate(6, 6)' + }); - return elementObject; - }, - 'bpmn:DataOutput': function(parentGfx, element) { - var arrowPathData = pathMap.getRawPath('DATA_ARROW'); + var pathDataService1 = pathMap.getScaledPath('TASK_TYPE_SERVICE', { + abspos: { + x: 12, + y: 18 + } + }); - // page - var elementObject = renderer('bpmn:DataObject')(parentGfx, element); + drawPath(parentGfx, pathDataService1, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); - /* output arrow path */ drawPath(parentGfx, arrowPathData, { - strokeWidth: 1, - fill: 'black' + drawCircle(parentGfx, 10, 10, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: 'none', + transform: 'translate(11, 10)' }); - return elementObject; - }, - 'bpmn:DataStoreReference': function(parentGfx, element) { - var DATA_STORE_PATH = pathMap.getScaledPath('DATA_STORE', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: 0, - my: 0.133 + var pathDataService2 = pathMap.getScaledPath('TASK_TYPE_SERVICE', { + abspos: { + x: 17, + y: 22 } }); - var elementStore = drawPath(parentGfx, DATA_STORE_PATH, { - strokeWidth: 2, - fill: getFillColor(element, defaultFillColor), - fillOpacity: DEFAULT_FILL_OPACITY, - stroke: getStrokeColor(element, defaultStrokeColor) + drawPath(parentGfx, pathDataService2, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 }); - return elementStore; + return task; }, - 'bpmn:BoundaryEvent': function(parentGfx, element) { + 'bpmn:StartEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; - var semantic = getSemantic(element), - cancel = semantic.cancelActivity; + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - var attrs = { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }; + var semantic = getSemantic(element); - if (!cancel) { - attrs.strokeDasharray = '6'; - attrs.strokeLinecap = 'round'; + if (!semantic.get('isInterrupting')) { + attrs = { + ...attrs, + strokeDasharray: '6' + }; } - // apply fillOpacity - var outerAttrs = assign({}, attrs, { - fillOpacity: 1 - }); - - // apply no-fill - var innerAttrs = assign({}, attrs, { - fill: 'none' - }); - - var outer = renderer('bpmn:Event')(parentGfx, element, outerAttrs); + var event = renderEvent(parentGfx, element, attrs); - /* inner path */ drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, innerAttrs); + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); + } - renderEventContent(element, parentGfx); + return event; + }, + 'bpmn:SubProcess': function(parentGfx, element, attrs = {}) { + if (isExpanded(element)) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + } else { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + } - return outer; + return renderSubProcess(parentGfx, element, attrs); }, - 'bpmn:Group': function(parentGfx, element) { + 'bpmn:Task': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - var group = drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, { - stroke: getStrokeColor(element, defaultStrokeColor), - strokeWidth: 1, - strokeDasharray: '8,3,1,3', + return renderTask(parentGfx, element, attrs); + }, + 'bpmn:TextAnnotation': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var { + width, + height + } = getBounds(element, attrs); + + var textElement = drawRect(parentGfx, width, height, 0, 0, { fill: 'none', - pointerEvents: 'none' + stroke: 'none' }); - return group; - }, - 'label': function(parentGfx, element) { - return renderExternalLabel(parentGfx, element); - }, - 'bpmn:TextAnnotation': function(parentGfx, element) { - var style = { - 'fill': 'none', - 'stroke': 'none' - }; - - var textElement = drawRect(parentGfx, element.width, element.height, 0, 0, style); - var textPathData = pathMap.getScaledPath('TEXT_ANNOTATION', { xScaleFactor: 1, yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, + containerWidth: width, + containerHeight: height, position: { mx: 0.0, my: 0.0 @@ -1641,233 +2196,121 @@ export default function BpmnRenderer( }); drawPath(parentGfx, textPathData, { - stroke: getStrokeColor(element, defaultStrokeColor) + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) }); - var text = getSemantic(element).text || ''; + var semantic = getSemantic(element), + text = semantic.get('text') || ''; + renderLabel(parentGfx, text, { - box: element, align: 'left-top', - padding: 5, + box: getBounds(element, attrs), + padding: TEXT_ANNOTATION_PADDING, style: { - fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) } }); return textElement; }, - 'ParticipantMultiplicityMarker': function(parentGfx, element) { - var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: ((element.width / 2) / element.width), - my: (element.height - 15) / element.height - } - }); + 'bpmn:Transaction': function(parentGfx, element, attrs = {}) { + if (isExpanded(element)) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + } else { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + } - drawMarker('participant-multiplicity', parentGfx, markerPath, { - strokeWidth: 2, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + var outer = renderSubProcess(parentGfx, element, { + strokeWidth: 1.5, + ...attrs }); - }, - 'SubProcessMarker': function(parentGfx, element) { - var markerRect = drawRect(parentGfx, 14, 14, 0, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + + var innerAttrs = styles.style([ 'no-fill', 'no-events' ], { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5 }); - // Process marker is placed in the middle of the box - // therefore fixed values can be used here - translate(markerRect, element.width / 2 - 7.5, element.height - 20); + var expanded = isExpanded(element); - var markerPath = pathMap.getScaledPath('MARKER_SUB_PROCESS', { - xScaleFactor: 1.5, - yScaleFactor: 1.5, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: (element.width / 2 - 7.5) / element.width, - my: (element.height - 20) / element.height - } - }); + if (!expanded) { + attrs = {}; + } - drawMarker('sub-process', parentGfx, markerPath, { - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); - }, - 'ParallelMarker': function(parentGfx, element, position) { - var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: ((element.width / 2 + position.parallel) / element.width), - my: (element.height - 20) / element.height - } - }); + drawRect( + parentGfx, + getWidth(element, attrs), + getHeight(element, attrs), + TASK_BORDER_RADIUS - INNER_OUTER_DIST, + INNER_OUTER_DIST, + innerAttrs + ); - drawMarker('parallel', parentGfx, markerPath, { - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); + return outer; }, - 'SequentialMarker': function(parentGfx, element, position) { - var markerPath = pathMap.getScaledPath('MARKER_SEQUENTIAL', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: ((element.width / 2 + position.seq) / element.width), - my: (element.height - 19) / element.height - } - }); + 'bpmn:UserTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); - drawMarker('sequential', parentGfx, markerPath, { - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) - }); - }, - 'CompensationMarker': function(parentGfx, element, position) { - var markerMath = pathMap.getScaledPath('MARKER_COMPENSATION', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: ((element.width / 2 + position.compensation) / element.width), - my: (element.height - 13) / element.height + var task = renderTask(parentGfx, element, attrs); + + var x = 15; + var y = 12; + + var pathDataUser1 = pathMap.getScaledPath('TASK_TYPE_USER_1', { + abspos: { + x: x, + y: y } }); - drawMarker('compensation', parentGfx, markerMath, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawPath(parentGfx, pathDataUser1, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 }); - }, - 'LoopMarker': function(parentGfx, element, position) { - var markerPath = pathMap.getScaledPath('MARKER_LOOP', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: ((element.width / 2 + position.loop) / element.width), - my: (element.height - 7) / element.height + + var pathDataUser2 = pathMap.getScaledPath('TASK_TYPE_USER_2', { + abspos: { + x: x, + y: y } }); - drawMarker('loop', parentGfx, markerPath, { - strokeWidth: 1, - fill: getFillColor(element, defaultFillColor), - stroke: getStrokeColor(element, defaultStrokeColor), - strokeLinecap: 'round', - strokeMiterlimit: 0.5 + drawPath(parentGfx, pathDataUser2, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 }); - }, - 'AdhocMarker': function(parentGfx, element, position) { - var markerPath = pathMap.getScaledPath('MARKER_ADHOC', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: ((element.width / 2 + position.adhoc) / element.width), - my: (element.height - 15) / element.height + + var pathDataUser3 = pathMap.getScaledPath('TASK_TYPE_USER_3', { + abspos: { + x: x, + y: y } }); - drawMarker('adhoc', parentGfx, markerPath, { - strokeWidth: 1, - fill: getStrokeColor(element, defaultStrokeColor), - stroke: getStrokeColor(element, defaultStrokeColor) + drawPath(parentGfx, pathDataUser3, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 }); - } - }; - - function attachTaskMarkers(parentGfx, element, taskMarkers) { - var obj = getSemantic(element); - - var subprocess = taskMarkers && taskMarkers.indexOf('SubProcessMarker') !== -1; - var position; - - if (subprocess) { - position = { - seq: -21, - parallel: -22, - compensation: -42, - loop: -18, - adhoc: 10 - }; - } else { - position = { - seq: -3, - parallel: -6, - compensation: -27, - loop: 0, - adhoc: 10 - }; - } - - forEach(taskMarkers, function(marker) { - renderer(marker)(parentGfx, element, position); - }); - - if (obj.isForCompensation) { - renderer('CompensationMarker')(parentGfx, element, position); - } - - if (obj.$type === 'bpmn:AdHocSubProcess') { - renderer('AdhocMarker')(parentGfx, element, position); - } - - var loopCharacteristics = obj.loopCharacteristics, - isSequential = loopCharacteristics && loopCharacteristics.isSequential; - - if (loopCharacteristics) { - - if (isSequential === undefined) { - renderer('LoopMarker')(parentGfx, element, position); - } - - if (isSequential === false) { - renderer('ParallelMarker')(parentGfx, element, position); - } - if (isSequential === true) { - renderer('SequentialMarker')(parentGfx, element, position); - } + return task; + }, + 'label': function(parentGfx, element, attrs = {}) { + return renderExternalLabel(parentGfx, element, attrs); } - } - - function renderDataItemCollection(parentGfx, element) { - - var yPosition = (element.height - 18) / element.height; - - var pathData = pathMap.getScaledPath('DATA_OBJECT_COLLECTION_PATH', { - xScaleFactor: 1, - yScaleFactor: 1, - containerWidth: element.width, - containerHeight: element.height, - position: { - mx: 0.33, - my: yPosition - } - }); - - /* collection path */ drawPath(parentGfx, pathData, { - strokeWidth: 2 - }); - } - + }; // extension API, use at your own risk this._drawPath = drawPath; @@ -1888,39 +2331,91 @@ BpmnRenderer.$inject = [ ]; +/** + * @param {Element} element + * + * @return {boolean} + */ BpmnRenderer.prototype.canRender = function(element) { return is(element, 'bpmn:BaseElement'); }; -BpmnRenderer.prototype.drawShape = function(parentGfx, element) { - var type = element.type; - var h = this._renderer(type); - - /* jshint -W040 */ - return h(parentGfx, element); +/** + * Draw shape into parentGfx. + * + * @param {SVGElement} parentGfx + * @param {Shape} shape + * @param {Attrs} [attrs] + * + * @return {SVGElement} mainGfx + */ +BpmnRenderer.prototype.drawShape = function(parentGfx, shape, attrs = {}) { + var { type } = shape; + + var handler = this._renderer(type); + + return handler(parentGfx, shape, attrs); }; -BpmnRenderer.prototype.drawConnection = function(parentGfx, element) { - var type = element.type; - var h = this._renderer(type); - - /* jshint -W040 */ - return h(parentGfx, element); +/** + * Draw connection into parentGfx. + * + * @param {SVGElement} parentGfx + * @param {Connection} connection + * @param {Attrs} [attrs] + * + * @return {SVGElement} mainGfx + */ +BpmnRenderer.prototype.drawConnection = function(parentGfx, connection, attrs = {}) { + var { type } = connection; + + var handler = this._renderer(type); + + return handler(parentGfx, connection, attrs); }; -BpmnRenderer.prototype.getShapePath = function(element) { +/** + * Get shape path. + * + * @param {Shape} shape + * + * @return {string} path + */ +BpmnRenderer.prototype.getShapePath = function(shape) { + + if (isLabel(shape)) { + return getRoundRectPath(shape, EXTERNAL_LABEL_BORDER_RADIUS); + } - if (is(element, 'bpmn:Event')) { - return getCirclePath(element); + if (is(shape, 'bpmn:Event')) { + return getCirclePath(shape); } - if (is(element, 'bpmn:Activity')) { - return getRoundRectPath(element, TASK_BORDER_RADIUS); + if (is(shape, 'bpmn:Activity')) { + return getRoundRectPath(shape, TASK_BORDER_RADIUS); } - if (is(element, 'bpmn:Gateway')) { - return getDiamondPath(element); + if (is(shape, 'bpmn:Gateway')) { + return getDiamondPath(shape); } - return getRectPath(element); + return getRectPath(shape); }; + +/** + * Pick attributes if they exist. + * + * @param {Object} attrs + * @param {string[]} keys + * + * @returns {Object} + */ +function pickAttrs(attrs, keys = []) { + return keys.reduce((pickedAttrs, key) => { + if (attrs[ key ]) { + pickedAttrs[ key ] = attrs[ key ]; + } + + return pickedAttrs; + }, {}); +} \ No newline at end of file diff --git a/lib/draw/PathMap.js b/lib/draw/PathMap.js index a55e788a5c..0ceb72ab12 100644 --- a/lib/draw/PathMap.js +++ b/lib/draw/PathMap.js @@ -1,7 +1,6 @@ /** - * Map containing SVG paths needed by BpmnRenderer. + * Map containing SVG paths needed by BpmnRenderer */ - export default function PathMap() { /** @@ -40,22 +39,22 @@ export default function PathMap() { d: 'm {mx},{my} l 0,{e.y1} l {e.x1},0 l 0,-{e.y1} z l {e.x0},{e.y0} l {e.x0},-{e.y0}', height: 36, width: 36, - heightElements: [6, 14], - widthElements: [10.5, 21] + heightElements: [ 6, 14 ], + widthElements: [ 10.5, 21 ] }, 'EVENT_SIGNAL': { d: 'M {mx},{my} l {e.x0},{e.y0} l -{e.x1},0 Z', height: 36, width: 36, - heightElements: [18], - widthElements: [10, 20] + heightElements: [ 18 ], + widthElements: [ 10, 20 ] }, 'EVENT_ESCALATION': { d: 'M {mx},{my} l {e.x0},{e.y0} l -{e.x0},-{e.y1} l -{e.x0},{e.y1} Z', height: 36, width: 36, - heightElements: [20, 7], - widthElements: [8] + heightElements: [ 20, 7 ], + widthElements: [ 8 ] }, 'EVENT_CONDITIONAL': { d: 'M {e.x0},{e.y0} l {e.x1},0 l 0,{e.y2} l -{e.x1},0 Z ' + @@ -67,67 +66,67 @@ export default function PathMap() { 'M {e.x2},{e.y8} l {e.x0},0 ', height: 36, width: 36, - heightElements: [8.5, 14.5, 18, 11.5, 14.5, 17.5, 20.5, 23.5, 26.5], - widthElements: [10.5, 14.5, 12.5] + heightElements: [ 8.5, 14.5, 18, 11.5, 14.5, 17.5, 20.5, 23.5, 26.5 ], + widthElements: [ 10.5, 14.5, 12.5 ] }, 'EVENT_LINK': { d: 'm {mx},{my} 0,{e.y0} -{e.x1},0 0,{e.y1} {e.x1},0 0,{e.y0} {e.x0},-{e.y2} -{e.x0},-{e.y2} z', height: 36, width: 36, - heightElements: [4.4375, 6.75, 7.8125], - widthElements: [9.84375, 13.5] + heightElements: [ 4.4375, 6.75, 7.8125 ], + widthElements: [ 9.84375, 13.5 ] }, 'EVENT_ERROR': { d: 'm {mx},{my} {e.x0},-{e.y0} {e.x1},-{e.y1} {e.x2},{e.y2} {e.x3},-{e.y3} -{e.x4},{e.y4} -{e.x5},-{e.y5} z', height: 36, width: 36, - heightElements: [0.023, 8.737, 8.151, 16.564, 10.591, 8.714], - widthElements: [0.085, 6.672, 6.97, 4.273, 5.337, 6.636] + heightElements: [ 0.023, 8.737, 8.151, 16.564, 10.591, 8.714 ], + widthElements: [ 0.085, 6.672, 6.97, 4.273, 5.337, 6.636 ] }, 'EVENT_CANCEL_45': { d: 'm {mx},{my} -{e.x1},0 0,{e.x0} {e.x1},0 0,{e.y1} {e.x0},0 ' + '0,-{e.y1} {e.x1},0 0,-{e.y0} -{e.x1},0 0,-{e.y1} -{e.x0},0 z', height: 36, width: 36, - heightElements: [4.75, 8.5], - widthElements: [4.75, 8.5] + heightElements: [ 4.75, 8.5 ], + widthElements: [ 4.75, 8.5 ] }, 'EVENT_COMPENSATION': { d: 'm {mx},{my} {e.x0},-{e.y0} 0,{e.y1} z m {e.x1},-{e.y2} {e.x2},-{e.y3} 0,{e.y1} -{e.x2},-{e.y3} z', height: 36, width: 36, - heightElements: [6.5, 13, 0.4, 6.1], - widthElements: [9, 9.3, 8.7] + heightElements: [ 6.5, 13, 0.4, 6.1 ], + widthElements: [ 9, 9.3, 8.7 ] }, 'EVENT_TIMER_WH': { d: 'M {mx},{my} l {e.x0},-{e.y0} m -{e.x0},{e.y0} l {e.x1},{e.y1} ', height: 36, width: 36, - heightElements: [10, 2], - widthElements: [3, 7] + heightElements: [ 10, 2 ], + widthElements: [ 3, 7 ] }, 'EVENT_TIMER_LINE': { d: 'M {mx},{my} ' + 'm {e.x0},{e.y0} l -{e.x1},{e.y1} ', height: 36, width: 36, - heightElements: [10, 3], - widthElements: [0, 0] + heightElements: [ 10, 3 ], + widthElements: [ 0, 0 ] }, 'EVENT_MULTIPLE': { d:'m {mx},{my} {e.x1},-{e.y0} {e.x1},{e.y0} -{e.x0},{e.y1} -{e.x2},0 z', height: 36, width: 36, - heightElements: [6.28099, 12.56199], - widthElements: [3.1405, 9.42149, 12.56198] + heightElements: [ 6.28099, 12.56199 ], + widthElements: [ 3.1405, 9.42149, 12.56198 ] }, 'EVENT_PARALLEL_MULTIPLE': { d:'m {mx},{my} {e.x0},0 0,{e.y1} {e.x1},0 0,{e.y0} -{e.x1},0 0,{e.y1} ' + '-{e.x0},0 0,-{e.y1} -{e.x1},0 0,-{e.y0} {e.x1},0 z', height: 36, width: 36, - heightElements: [2.56228, 7.68683], - widthElements: [2.56228, 7.68683] + heightElements: [ 2.56228, 7.68683 ], + widthElements: [ 2.56228, 7.68683 ] }, 'GATEWAY_EXCLUSIVE': { d:'m {mx},{my} {e.x0},{e.y0} {e.x1},{e.y0} {e.x2},0 {e.x4},{e.y2} ' + @@ -135,23 +134,23 @@ export default function PathMap() { '{e.x3},0 {e.x5},{e.y1} {e.x5},{e.y2} {e.x3},0 z', height: 17.5, width: 17.5, - heightElements: [8.5, 6.5312, -6.5312, -8.5], - widthElements: [6.5, -6.5, 3, -3, 5, -5] + heightElements: [ 8.5, 6.5312, -6.5312, -8.5 ], + widthElements: [ 6.5, -6.5, 3, -3, 5, -5 ] }, 'GATEWAY_PARALLEL': { d:'m {mx},{my} 0,{e.y1} -{e.x1},0 0,{e.y0} {e.x1},0 0,{e.y1} {e.x0},0 ' + '0,-{e.y1} {e.x1},0 0,-{e.y0} -{e.x1},0 0,-{e.y1} -{e.x0},0 z', height: 30, width: 30, - heightElements: [5, 12.5], - widthElements: [5, 12.5] + heightElements: [ 5, 12.5 ], + widthElements: [ 5, 12.5 ] }, 'GATEWAY_EVENT_BASED': { d:'m {mx},{my} {e.x0},{e.y0} {e.x0},{e.y1} {e.x1},{e.y2} {e.x2},0 z', height: 11, width: 11, - heightElements: [-6, 6, 12, -12], - widthElements: [9, -3, -12] + heightElements: [ -6, 6, 12, -12 ], + widthElements: [ 9, -3, -12 ] }, 'GATEWAY_COMPLEX': { d:'m {mx},{my} 0,{e.y0} -{e.x0},-{e.y1} -{e.x1},{e.y2} {e.x0},{e.y1} -{e.x2},0 0,{e.y3} ' + @@ -160,15 +159,15 @@ export default function PathMap() { '-{e.x0},{e.y1} 0,-{e.y0} -{e.x3},0 z', height: 17.125, width: 17.125, - heightElements: [4.875, 3.4375, 2.125, 3], - widthElements: [3.4375, 2.125, 4.875, 3] + heightElements: [ 4.875, 3.4375, 2.125, 3 ], + widthElements: [ 3.4375, 2.125, 4.875, 3 ] }, 'DATA_OBJECT_PATH': { d:'m 0,0 {e.x1},0 {e.x0},{e.y0} 0,{e.y1} -{e.x2},0 0,-{e.y2} {e.x1},0 0,{e.y0} {e.x0},0', height: 61, width: 51, - heightElements: [10, 50, 60], - widthElements: [10, 40, 50, 60] + heightElements: [ 10, 50, 60 ], + widthElements: [ 10, 40, 50, 60 ] }, 'DATA_OBJECT_COLLECTION_PATH': { d: 'm{mx},{my} m 3,2 l 0,10 m 3,-10 l 0,10 m 3,-10 l 0,10', @@ -197,15 +196,15 @@ export default function PathMap() { 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0', height: 61, width: 61, - heightElements: [7, 10, 45], - widthElements: [2, 58, 60] + heightElements: [ 7, 10, 45 ], + widthElements: [ 2, 58, 60 ] }, 'TEXT_ANNOTATION': { d: 'm {mx}, {my} m 10,0 l -10,0 l 0,{e.y0} l 10,0', height: 30, width: 10, - heightElements: [30], - widthElements: [10] + heightElements: [ 30 ], + widthElements: [ 10 ] }, 'MARKER_SUB_PROCESS': { d: 'm{mx},{my} m 7,2 l 0,10 m -5,-5 l 10,0', @@ -260,8 +259,8 @@ export default function PathMap() { d: 'm {mx},{my} l 0,{e.y1} l {e.x1},0 l 0,-{e.y1} z l {e.x0},{e.y0} l {e.x0},-{e.y0}', height: 14, width: 21, - heightElements: [6, 14], - widthElements: [10.5, 21] + heightElements: [ 6, 14 ], + widthElements: [ 10.5, 21 ] }, 'TASK_TYPE_SCRIPT': { d: 'm {mx},{my} c 9.966553,-6.27276 -8.000926,-7.91932 2.968968,-14.938 l -8.802728,0 ' + @@ -272,8 +271,8 @@ export default function PathMap() { 'm -4,3 l 5,0', height: 15, width: 12.6, - heightElements: [6, 14], - widthElements: [10.5, 21] + heightElements: [ 6, 14 ], + widthElements: [ 10.5, 21 ] }, 'TASK_TYPE_USER_1': { d: 'm {mx},{my} c 0.909,-0.845 1.594,-2.049 1.594,-3.385 0,-2.554 -1.805,-4.62199999 ' + @@ -343,6 +342,13 @@ export default function PathMap() { } }; + /** + * Return raw path for the given ID. + * + * @param {string} pathId + * + * @return {string} raw path + */ this.getRawPath = function getRawPath(pathId) { return this.pathMap[pathId].d; }; @@ -395,6 +401,7 @@ export default function PathMap() { * *

* + * @return {string} scaled path */ this.getScaledPath = function getScaledPath(pathId, param) { var rawPath = this.pathMap[pathId]; diff --git a/lib/draw/TextRenderer.js b/lib/draw/TextRenderer.js index e70b80967c..e87c37d51f 100644 --- a/lib/draw/TextRenderer.js +++ b/lib/draw/TextRenderer.js @@ -2,12 +2,38 @@ import { assign } from 'min-dash'; import TextUtil from 'diagram-js/lib/util/Text'; +import { DEFAULT_LABEL_SIZE } from '../util/LabelUtil'; +import { TEXT_ANNOTATION_PADDING } from '../util/AnnotationUtil'; + var DEFAULT_FONT_SIZE = 12; var LINE_HEIGHT_RATIO = 1.2; -var MIN_TEXT_ANNOTATION_HEIGHT = 30; - - +var MIN_TEXT_ANNOTATION_HEIGHT = 40; + +/** + * @typedef { { + * fontFamily: string; + * fontSize: number; + * fontWeight: string; + * lineHeight: number; + * } } TextRendererStyle + * + * @typedef { { + * defaultStyle?: Partial; + * externalStyle?: Partial; + * } } TextRendererConfig + * + * @typedef { import('diagram-js/lib/util/Text').TextLayoutConfig } TextLayoutConfig + * + * @typedef { import('diagram-js/lib/util/Types').Rect } Rect + */ + + +/** + * Renders text and computes text bounding boxes. + * + * @param {TextRendererConfig} [config] + */ export default function TextRenderer(config) { var defaultStyle = assign({ @@ -31,29 +57,27 @@ export default function TextRenderer(config) { * Get the new bounds of an externally rendered, * layouted label. * - * @param {Bounds} bounds - * @param {string} text + * @param {Rect} bounds + * @param {string} text * - * @return {Bounds} + * @return {Rect} */ this.getExternalLabelBounds = function(bounds, text) { - var layoutedDimensions = textUtil.getDimensions(text, { - box: { - width: 90, - height: 30, - x: bounds.width / 2 + bounds.x, - y: bounds.height / 2 + bounds.y - }, + var box = { + width: Math.max(bounds.width, DEFAULT_LABEL_SIZE.width), + height: 30 + }; + + var dimensions = getTextboxDimensions(text, box, { style: externalStyle }); - // resize label shape to fit label text return { - x: Math.round(bounds.x + bounds.width / 2 - layoutedDimensions.width / 2), - y: Math.round(bounds.y), - width: Math.ceil(layoutedDimensions.width), - height: Math.ceil(layoutedDimensions.height) + x: Math.round(bounds.x + bounds.width / 2 - dimensions.width / 2), + y: bounds.y, + width: Math.ceil(dimensions.width), + height: Math.ceil(dimensions.height) }; }; @@ -61,33 +85,57 @@ export default function TextRenderer(config) { /** * Get the new bounds of text annotation. * - * @param {Bounds} bounds - * @param {string} text + * @param {Rect} bounds + * @param {string} text * - * @return {Bounds} + * @return {Rect} */ this.getTextAnnotationBounds = function(bounds, text) { - var layoutedDimensions = textUtil.getDimensions(text, { - box: bounds, + var dimensions = getTextboxDimensions(text, bounds, { style: defaultStyle, align: 'left-top', - padding: 5 + padding: TEXT_ANNOTATION_PADDING }); return { x: bounds.x, y: bounds.y, width: bounds.width, - height: Math.max(MIN_TEXT_ANNOTATION_HEIGHT, Math.round(layoutedDimensions.height)) + height: Math.max(MIN_TEXT_ANNOTATION_HEIGHT, Math.round(dimensions.height)) }; }; + /** + * Get the dimensions of a text element. + * + * @param {string} text + * @param {TextLayoutConfig} [options] + * + * @return {import('diagram-js/lib/util/Types').Dimensions} + */ + this.getDimensions = function(text, options) { + return textUtil.getDimensions(text, options || {}); + }; + + /** + * Compute dimension of text fitted inside a box. + * + * @param {string} text + * @param {Rect} box + * @param {TextLayoutConfig} layoutOptions + * + * @return {import('diagram-js/lib/util/Types').Dimensions} + */ + function getTextboxDimensions(text, box, layoutOptions) { + return textUtil.getDimensions(text, assign({ box: box }, layoutOptions)); + } + /** * Create a layouted text element. * * @param {string} text - * @param {Object} [options] + * @param {TextLayoutConfig} [options] * * @return {SVGElement} rendered text */ diff --git a/lib/draw/TextRenderer.spec.ts b/lib/draw/TextRenderer.spec.ts new file mode 100644 index 0000000000..f5e7a5d670 --- /dev/null +++ b/lib/draw/TextRenderer.spec.ts @@ -0,0 +1,34 @@ +import TextRenderer from './TextRenderer'; + +new TextRenderer({ + defaultStyle: { + fontFamily: 'foo' + } +}); + +const textRenderer = new TextRenderer(); + +const externalLabelBounds = textRenderer.getExternalLabelBounds({ + x: 100, + y: 100, + width: 100, + height: 100 +}, 'FOO\nBAR\n\BAZ'); + +const textAnnotationBounds = textRenderer.getTextAnnotationBounds({ + x: 100, + y: 100, + width: 100, + height: 100 +}, 'FOO\nBAR\n\BAZ'); + +let text = textRenderer.createText('foo'); + +text = textRenderer.createText('foo', { + align: 'center-top', + padding: 10 +}); + +const defaultStyle = textRenderer.getDefaultStyle(); + +const externalStyle = textRenderer.getExternalStyle(); \ No newline at end of file diff --git a/lib/features/align-elements/AlignElementsContextPadProvider.js b/lib/features/align-elements/AlignElementsContextPadProvider.js new file mode 100644 index 0000000000..ea4b320522 --- /dev/null +++ b/lib/features/align-elements/AlignElementsContextPadProvider.js @@ -0,0 +1,105 @@ +import { + assign +} from 'min-dash'; + +import ICONS from './AlignElementsIcons'; + +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/features/context-pad/ContextPad').default} ContextPad + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').default} PopupMenu + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('diagram-js/lib/features/context-pad/ContextPad').ContextPadEntries} ContextPadEntries + * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').default} ContextPadProvider + */ + +var LOW_PRIORITY = 900; + +/** + * A provider for the `Align elements` context pad entry. + * + * @implements {ContextPadProvider} + * + * @param {ContextPad} contextPad + * @param {PopupMenu} popupMenu + * @param {Translate} translate + * @param {Canvas} canvas + */ +export default function AlignElementsContextPadProvider(contextPad, popupMenu, translate, canvas) { + + contextPad.registerProvider(LOW_PRIORITY, this); + + this._contextPad = contextPad; + this._popupMenu = popupMenu; + this._translate = translate; + this._canvas = canvas; +} + +AlignElementsContextPadProvider.$inject = [ + 'contextPad', + 'popupMenu', + 'translate', + 'canvas' +]; + +/** + * @param {Element[]} elements + * + * @return {ContextPadEntries} + */ +AlignElementsContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) { + var actions = {}; + + if (this._isAllowed(elements)) { + assign(actions, this._getEntries(elements)); + } + + return actions; +}; + +AlignElementsContextPadProvider.prototype._isAllowed = function(elements) { + return !this._popupMenu.isEmpty(elements, 'align-elements'); +}; + +AlignElementsContextPadProvider.prototype._getEntries = function() { + var self = this; + + return { + 'align-elements': { + group: 'align-elements', + title: self._translate('Align elements'), + html: `
${ICONS['align']}
`, + action: { + click: function(event, target) { + var position = self._getMenuPosition(target); + + assign(position, { + cursor: { + x: event.x, + y: event.y + } + }); + + self._popupMenu.open(target, 'align-elements', position); + } + } + } + }; +}; + +AlignElementsContextPadProvider.prototype._getMenuPosition = function(elements) { + var Y_OFFSET = 5; + + var pad = this._contextPad.getPad(elements).html; + + var padRect = pad.getBoundingClientRect(); + + var pos = { + x: padRect.left, + y: padRect.bottom + Y_OFFSET + }; + + return pos; +}; diff --git a/lib/features/align-elements/AlignElementsIcons.js b/lib/features/align-elements/AlignElementsIcons.js new file mode 100644 index 0000000000..ac5c38a006 --- /dev/null +++ b/lib/features/align-elements/AlignElementsIcons.js @@ -0,0 +1,43 @@ +/** + * To change the icons, modify the SVGs in `./resources`, execute `npx svgo -f resources --datauri enc -o dist`, + * and then replace respective icons with the optimized data URIs in `./dist`. + */ +var icons = { + align: ` + + + + `, + bottom: ` + + + + `, + center: ` + + + + `, + left: ` + + + + `, + right: ` + + + + `, + top: ` + + + + `, + middle: ` + + + + ` +}; + +export default icons; diff --git a/lib/features/align-elements/AlignElementsMenuProvider.js b/lib/features/align-elements/AlignElementsMenuProvider.js new file mode 100644 index 0000000000..20415bd10a --- /dev/null +++ b/lib/features/align-elements/AlignElementsMenuProvider.js @@ -0,0 +1,100 @@ +import ICONS from './AlignElementsIcons'; + +import { + assign, + forEach, +} from 'min-dash'; + +/** + * @typedef {import('diagram-js/lib/features/align-elements/AlignElements').default} AlignElements + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').default} PopupMenu + * @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + * + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').PopupMenuEntries} PopupMenuEntries + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenuProvider').default} PopupMenuProvider + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').PopupMenuTarget} PopupMenuTarget + */ + +var ALIGNMENT_OPTIONS = [ + 'left', + 'center', + 'right', + 'top', + 'middle', + 'bottom' +]; + +/** + * A provider for the `Align elements` popup menu. + * + * @implements {PopupMenuProvider} + * + * @param {PopupMenu} popupMenu + * @param {AlignElements} alignElements + * @param {Translate} translate + * @param {Rules} rules + */ +export default function AlignElementsMenuProvider(popupMenu, alignElements, translate, rules) { + + this._alignElements = alignElements; + this._translate = translate; + this._popupMenu = popupMenu; + this._rules = rules; + + popupMenu.registerProvider('align-elements', this); +} + +AlignElementsMenuProvider.$inject = [ + 'popupMenu', + 'alignElements', + 'translate', + 'rules' +]; + +/** + * @param {PopupMenuTarget} target + * + * @return {PopupMenuEntries} + */ +AlignElementsMenuProvider.prototype.getPopupMenuEntries = function(target) { + var entries = {}; + + if (this._isAllowed(target)) { + assign(entries, this._getEntries(target)); + } + + return entries; +}; + +AlignElementsMenuProvider.prototype._isAllowed = function(target) { + return this._rules.allowed('elements.align', { elements: target }); +}; + +/** + * @param {PopupMenuTarget} target + * + * @return {PopupMenuEntries} + */ +AlignElementsMenuProvider.prototype._getEntries = function(target) { + var alignElements = this._alignElements, + translate = this._translate, + popupMenu = this._popupMenu; + + var entries = {}; + + forEach(ALIGNMENT_OPTIONS, function(alignment) { + entries[ 'align-elements-' + alignment ] = { + group: 'align', + title: translate('Align elements ' + alignment), + className: 'bjs-align-elements-menu-entry', + imageHtml: ICONS[ alignment ], + action: function() { + alignElements.trigger(target, alignment); + popupMenu.close(); + } + }; + }); + + return entries; +}; diff --git a/lib/features/align-elements/BpmnAlignElements.js b/lib/features/align-elements/BpmnAlignElements.js new file mode 100644 index 0000000000..45f451d37a --- /dev/null +++ b/lib/features/align-elements/BpmnAlignElements.js @@ -0,0 +1,45 @@ +import inherits from 'inherits-browser'; + +import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; +import { getParents } from 'diagram-js/lib/util/Elements'; + +import { + filter +} from 'min-dash'; + +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + */ + +/** + * Rule provider for aligning BPMN elements. + * + * @param {EventBus} eventBus + */ +export default function BpmnAlignElements(eventBus) { + RuleProvider.call(this, eventBus); +} + +BpmnAlignElements.$inject = [ 'eventBus' ]; + +inherits(BpmnAlignElements, RuleProvider); + +BpmnAlignElements.prototype.init = function() { + this.addRule('elements.align', function(context) { + var elements = context.elements; + + // filter out elements which cannot be aligned + var filteredElements = filter(elements, function(element) { + return !(element.waypoints || element.host || element.labelTarget); + }); + + // filter out elements which are children of any of the selected elements + filteredElements = getParents(filteredElements); + + if (filteredElements.length < 2) { + return false; + } + + return filteredElements; + }); +}; diff --git a/lib/features/align-elements/index.js b/lib/features/align-elements/index.js new file mode 100644 index 0000000000..397e461b7b --- /dev/null +++ b/lib/features/align-elements/index.js @@ -0,0 +1,23 @@ +import AlignElementsModule from 'diagram-js/lib/features/align-elements'; +import ContextPadModule from 'diagram-js/lib/features/context-pad'; +import PopupMenuModule from 'diagram-js/lib/features/popup-menu'; + +import AlignElementsContextPadProvider from './AlignElementsContextPadProvider'; +import AlignElementsMenuProvider from './AlignElementsMenuProvider'; +import BpmnAlignElements from './BpmnAlignElements'; + +export default { + __depends__: [ + AlignElementsModule, + ContextPadModule, + PopupMenuModule + ], + __init__: [ + 'alignElementsContextPadProvider', + 'alignElementsMenuProvider', + 'bpmnAlignElements' + ], + alignElementsContextPadProvider: [ 'type', AlignElementsContextPadProvider ], + alignElementsMenuProvider: [ 'type', AlignElementsMenuProvider ], + bpmnAlignElements: [ 'type', BpmnAlignElements ] +}; diff --git a/lib/features/append-preview/AppendPreview.js b/lib/features/append-preview/AppendPreview.js new file mode 100644 index 0000000000..e14122152a --- /dev/null +++ b/lib/features/append-preview/AppendPreview.js @@ -0,0 +1,109 @@ +import { + assign, + isNil +} from 'min-dash'; + +const round = Math.round; + +/** + * @typedef {import('diagram-js/lib/features/complex-preview/ComplexPreview').default} ComplexPreview + * @typedef {import('diagram-js/lib/layout/ConnectionDocking').default} ConnectionDocking + * @typedef {import('../modeling/ElementFactory').default} ElementFactory + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../modeling/BpmnLayouter').default} BpmnLayouter + * @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules + * + * @typedef {import('../../model/Types').Shape} Shape + */ + +/** + * A preview for appending. + * + * @param {ComplexPreview} complexPreview + * @param {ConnectionDocking} connectionDocking + * @param {ElementFactory} elementFactory + * @param {EventBus} eventBus + * @param {BpmnLayouter} layouter + * @param {Rules} rules + */ +export default function AppendPreview(complexPreview, connectionDocking, elementFactory, eventBus, layouter, rules) { + this._complexPreview = complexPreview; + this._connectionDocking = connectionDocking; + this._elementFactory = elementFactory; + this._eventBus = eventBus; + this._layouter = layouter; + this._rules = rules; +} + +/** + * Create a preview of appending a shape of the given type to the given source. + * + * @param {Shape} source + * @param {string} type + * @param {Partial} options + */ +AppendPreview.prototype.create = function(source, type, options) { + const complexPreview = this._complexPreview, + connectionDocking = this._connectionDocking, + elementFactory = this._elementFactory, + eventBus = this._eventBus, + layouter = this._layouter, + rules = this._rules; + + const shape = elementFactory.createShape(assign({ type }, options)); + + const position = eventBus.fire('autoPlace', { + source, + shape + }); + + if (!position) { + return; + } + + assign(shape, { + x: position.x - round(shape.width / 2), + y: position.y - round(shape.height / 2) + }); + + const connectionCreateAllowed = rules.allowed('connection.create', { + source, + target: shape, + hints: { + targetParent: source.parent + } + }); + + let connection = null; + + if (connectionCreateAllowed) { + connection = elementFactory.createConnection(connectionCreateAllowed); + + connection.waypoints = layouter.layoutConnection(connection, { + source, + target: shape + }); + + connection.waypoints = connectionDocking.getCroppedWaypoints(connection, source, shape); + } + + complexPreview.create({ + created: [ + shape, + connection + ].filter((element) => !isNil(element)) + }); +}; + +AppendPreview.prototype.cleanUp = function() { + this._complexPreview.cleanUp(); +}; + +AppendPreview.$inject = [ + 'complexPreview', + 'connectionDocking', + 'elementFactory', + 'eventBus', + 'layouter', + 'rules' +]; \ No newline at end of file diff --git a/lib/features/append-preview/index.js b/lib/features/append-preview/index.js new file mode 100644 index 0000000000..eb9efe1a67 --- /dev/null +++ b/lib/features/append-preview/index.js @@ -0,0 +1,15 @@ +import AutoPlaceModule from '../auto-place'; +import ComplexPreviewModule from 'diagram-js/lib/features/complex-preview'; +import ModelingModule from '../modeling'; + +import AppendPreview from './AppendPreview'; + +export default { + __depends__: [ + AutoPlaceModule, + ComplexPreviewModule, + ModelingModule + ], + __init__: [ 'appendPreview' ], + appendPreview: [ 'type', AppendPreview ] +}; diff --git a/lib/features/auto-place/BpmnAutoPlace.js b/lib/features/auto-place/BpmnAutoPlace.js index cd2e117110..f25533d0df 100644 --- a/lib/features/auto-place/BpmnAutoPlace.js +++ b/lib/features/auto-place/BpmnAutoPlace.js @@ -1,18 +1,23 @@ import { getNewShapePosition } from './BpmnAutoPlaceUtil'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + */ /** * BPMN auto-place behavior. * * @param {EventBus} eventBus + * @param {ElementRegistry} elementRegistry */ -export default function AutoPlace(eventBus) { +export default function AutoPlace(eventBus, elementRegistry) { eventBus.on('autoPlace', function(context) { var shape = context.shape, source = context.source; - return getNewShapePosition(source, shape); + return getNewShapePosition(source, shape, elementRegistry); }); } -AutoPlace.$inject = [ 'eventBus' ]; \ No newline at end of file +AutoPlace.$inject = [ 'eventBus', 'elementRegistry' ]; \ No newline at end of file diff --git a/lib/features/auto-place/BpmnAutoPlaceUtil.js b/lib/features/auto-place/BpmnAutoPlaceUtil.js index bb7343ecad..89fdb01d3f 100644 --- a/lib/features/auto-place/BpmnAutoPlaceUtil.js +++ b/lib/features/auto-place/BpmnAutoPlaceUtil.js @@ -1,5 +1,9 @@ import { is } from '../../util/ModelUtil'; -import { isAny } from '../modeling/util/ModelingUtil'; + +import { + isAny, + isDirectionHorizontal +} from '../modeling/util/ModelingUtil'; import { getMid, @@ -13,78 +17,126 @@ import { getConnectedDistance } from 'diagram-js/lib/features/auto-place/AutoPlaceUtil'; +import { isConnection } from 'diagram-js/lib/util/ModelUtil'; + +/** + * @typedef {import('../../model/Types').Shape} Shape + * + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/util/Types').Point} Point + * @typedef {import('diagram-js/lib/util/Types').DirectionTRBL} DirectionTRBL + */ /** - * Find the new position for the target element to - * connect to source. + * Get the position for given new target relative to the source it will be + * connected to. * - * @param {djs.model.Shape} source - * @param {djs.model.Shape} element + * @param {Shape} source + * @param {Shape} element + * @param {ElementRegistry} elementRegistry * * @return {Point} */ -export function getNewShapePosition(source, element) { +export function getNewShapePosition(source, element, elementRegistry) { + + var placeHorizontally = isDirectionHorizontal(source, elementRegistry); if (is(element, 'bpmn:TextAnnotation')) { - return getTextAnnotationPosition(source, element); + return getTextAnnotationPosition(source, element, placeHorizontally); } if (isAny(element, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) { - return getDataElementPosition(source, element); + return getDataElementPosition(source, element, placeHorizontally); } if (is(element, 'bpmn:FlowNode')) { - return getFlowNodePosition(source, element); + return getFlowNodePosition(source, element, placeHorizontally); } } /** - * Always try to place element right of source; - * compute actual distance from previous nodes in flow. + * Get the position for given new flow node. Try placing the flow node right/bottom of + * the source. + * + * @param {Shape} source + * @param {Shape} element + * @param {boolean} placeHorizontally Whether to place the new element horizontally + * + * @return {Point} */ -export function getFlowNodePosition(source, element) { +export function getFlowNodePosition(source, element, placeHorizontally) { var sourceTrbl = asTRBL(source); var sourceMid = getMid(source); - var horizontalDistance = getConnectedDistance(source, { + var placement = placeHorizontally ? { + directionHint: 'e', + minDistance: 80, + baseOrientation: 'left', + boundaryOrientation: 'top', + start: 'top', + end: 'bottom' + } : { + directionHint: 's', + minDistance: 90, + baseOrientation: 'top', + boundaryOrientation: 'left', + start: 'left', + end: 'right' + }; + + var connectedDistance = getConnectedDistance(source, { filter: function(connection) { return is(connection, 'bpmn:SequenceFlow'); - } + }, + direction: placement.directionHint }); var margin = 30, - minDistance = 80, - orientation = 'left'; + minDistance = placement.minDistance, + orientation = placement.baseOrientation; if (is(source, 'bpmn:BoundaryEvent')) { orientation = getOrientation(source, source.host, -25); - if (orientation.indexOf('top') !== -1) { + if (orientation.indexOf(placement.boundaryOrientation) !== -1) { margin *= -1; } } - var position = { - x: sourceTrbl.right + horizontalDistance + element.width / 2, - y: sourceMid.y + getVerticalDistance(orientation, minDistance) + var position = placeHorizontally ? { + x: sourceTrbl.right + connectedDistance + element.width / 2, + y: sourceMid.y + getDistance(orientation, minDistance, placement) + } : { + x: sourceMid.x + getDistance(orientation, minDistance, placement), + y: sourceTrbl.bottom + connectedDistance + element.height / 2 }; - var nextPositionDirection = { - y: { - margin: margin, - minDistance: minDistance - } + var nextPosition = { + margin: margin, + minDistance: minDistance + }; + + var nextPositionDirection = placeHorizontally ? { + y: nextPosition + } : { + x: nextPosition }; return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection)); } - -function getVerticalDistance(orientation, minDistance) { - if (orientation.indexOf('top') != -1) { +/** + * @param {DirectionTRBL} orientation + * @param {number} minDistance + * @param {{ start: DirectionTRBL, end: DirectionTRBL }} placement + * + * @return {number} + */ +function getDistance(orientation, minDistance, placement) { + if (orientation.includes(placement.start)) { return -1 * minDistance; - } else if (orientation.indexOf('bottom') != -1) { + } else if (orientation.includes(placement.end)) { return minDistance; } else { return 0; @@ -93,22 +145,46 @@ function getVerticalDistance(orientation, minDistance) { /** - * Always try to place text annotations top right of source. + * Get the position for given text annotation. Try placing the text annotation + * top-right of the source (bottom-right in vertical layouts). + * + * @param {Shape} source + * @param {Shape} element + * @param {boolean} placeHorizontally Whether to place the new element horizontally + * + * @return {Point} */ -export function getTextAnnotationPosition(source, element) { +export function getTextAnnotationPosition(source, element, placeHorizontally) { var sourceTrbl = asTRBL(source); - var position = { + var position = placeHorizontally ? { x: sourceTrbl.right + element.width / 2, y: sourceTrbl.top - 50 - element.height / 2 + } : { + x: sourceTrbl.right + 50 + element.width / 2, + y: sourceTrbl.bottom + element.height / 2 }; - var nextPositionDirection = { - y: { - margin: -30, - minDistance: 20 + if (isConnection(source)) { + position = getMid(source); + if (placeHorizontally) { + position.x += 100; + position.y -= 50; + } else { + position.x += 100; + position.y += 50; } + } + + var nextPosition = { + margin: placeHorizontally ? -30 : 30, + minDistance: 20 + }; + var nextPositionDirection = placeHorizontally ? { + y: nextPosition + } : { + x: nextPosition }; return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection)); @@ -116,22 +192,35 @@ export function getTextAnnotationPosition(source, element) { /** - * Always put element bottom right of source. + * Get the position for given new data element. Try placing the data element + * bottom-right of the source (bottom-left in vertical layouts). + * + * @param {Shape} source + * @param {Shape} element + * @param {boolean} placeHorizontally Whether to place the new element horizontally + * + * @return {Point} */ -export function getDataElementPosition(source, element) { +export function getDataElementPosition(source, element, placeHorizontally) { var sourceTrbl = asTRBL(source); - var position = { + var position = placeHorizontally ? { x: sourceTrbl.right - 10 + element.width / 2, y: sourceTrbl.bottom + 40 + element.width / 2 + } : { + x: sourceTrbl.left - 40 - element.width / 2, + y: sourceTrbl.bottom - 10 + element.height / 2 }; - var nextPositionDirection = { - x: { - margin: 30, - minDistance: 30 - } + var nextPosition = { + margin: 30, + minDistance: 30 + }; + var nextPositionDirection = placeHorizontally ? { + x: nextPosition + } : { + y: nextPosition }; return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection)); diff --git a/lib/features/auto-resize/BpmnAutoResize.js b/lib/features/auto-resize/BpmnAutoResize.js index e518728e5e..05711e9e9d 100644 --- a/lib/features/auto-resize/BpmnAutoResize.js +++ b/lib/features/auto-resize/BpmnAutoResize.js @@ -1,13 +1,21 @@ import AutoResize from 'diagram-js/lib/features/auto-resize/AutoResize'; -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import { is } from '../../util/ModelUtil'; +/** + * @typedef {import('didi').Injector} Injector + * + * @typedef {import('../../model/Types').Shape} Shape + * + * @typedef {import('diagram-js/lib/util/Types').Rect} Rect + */ /** - * Sub class of the AutoResize module which implements a BPMN - * specific resize function. + * BPMN-specific resize behavior. + * + * @param {Injector} injector */ export default function BpmnAutoResize(injector) { @@ -20,13 +28,13 @@ BpmnAutoResize.$inject = [ inherits(BpmnAutoResize, AutoResize); - /** - * Resize shapes and lanes. + * Perform BPMN-specific resizing of participants. * - * @param {djs.model.Shape} target - * @param {Bounds} newBounds - * @param {Object} hints + * @param {Shape} target + * @param {Rect} newBounds + * @param {Object} [hints] + * @param {string} [hints.autoResize] */ BpmnAutoResize.prototype.resize = function(target, newBounds, hints) { diff --git a/lib/features/auto-resize/BpmnAutoResizeProvider.js b/lib/features/auto-resize/BpmnAutoResizeProvider.js index 721b08101a..6803b5f4eb 100644 --- a/lib/features/auto-resize/BpmnAutoResizeProvider.js +++ b/lib/features/auto-resize/BpmnAutoResizeProvider.js @@ -1,14 +1,25 @@ import { is } from '../../util/ModelUtil'; -import inherits from 'inherits'; +import { isLabel } from '../../util/LabelUtil'; + +import inherits from 'inherits-browser'; import { forEach } from 'min-dash'; import AutoResizeProvider from 'diagram-js/lib/features/auto-resize/AutoResizeProvider'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../modeling/Modeling').default} Modeling + * + * @typedef {import('../../model/Types').Shape} Shape + */ /** - * This module is a provider for automatically resizing parent BPMN elements + * BPMN-specific provider for automatic resizung. + * + * @param {EventBus} eventBus + * @param {Modeling} modeling */ export default function BpmnAutoResizeProvider(eventBus, modeling) { AutoResizeProvider.call(this, eventBus); @@ -25,9 +36,10 @@ BpmnAutoResizeProvider.$inject = [ /** - * Check if the given target can be expanded + * BPMN-specific check whether given elements can be resized. * - * @param {djs.model.Shape} target + * @param {Shape[]} elements + * @param {Shape} target * * @return {boolean} */ @@ -47,7 +59,7 @@ BpmnAutoResizeProvider.prototype.canResize = function(elements, target) { forEach(elements, function(element) { - if (is(element, 'bpmn:Lane') || element.labelTarget) { + if (is(element, 'bpmn:Lane') || isLabel(element)) { canResize = false; return; } diff --git a/lib/features/context-pad/ContextPadProvider.js b/lib/features/context-pad/ContextPadProvider.js index 80b0491320..18c18b507f 100644 --- a/lib/features/context-pad/ContextPadProvider.js +++ b/lib/features/context-pad/ContextPadProvider.js @@ -1,7 +1,8 @@ import { assign, forEach, - isArray + isArray, + every } from 'min-dash'; import { @@ -10,6 +11,7 @@ import { import { isExpanded, + isHorizontal, isEventSubProcess } from '../../util/DiUtil'; @@ -25,15 +27,54 @@ import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse'; +/** + * @typedef {import('didi').Injector} Injector + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/context-pad/ContextPad').default} ContextPad + * @typedef {import('../modeling/Modeling').default} Modeling + * @typedef {import('../modeling/ElementFactory').default} ElementFactory + * @typedef {import('../append-preview/AppendPreview').default} AppendPreview + * @typedef {import('diagram-js/lib/features/connect/Connect').default} Connect + * @typedef {import('diagram-js/lib/features/create/Create').default} Create + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').default} PopupMenu + * @typedef {import('diagram-js/lib/features/canvas/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').ModdleElement} ModdleElement + * + * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').default} BaseContextPadProvider + * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').ContextPadEntries} ContextPadEntries + * @typedef {import('diagram-js/lib/features/context-pad/ContextPadProvider').ContextPadEntry} ContextPadEntry + * + * @typedef { { autoPlace?: boolean; } } ContextPadConfig + */ /** - * A provider for BPMN 2.0 elements context pad + * BPMN-specific context pad provider. + * + * @implements {BaseContextPadProvider} + * + * @param {ContextPadConfig} config + * @param {Injector} injector + * @param {EventBus} eventBus + * @param {ContextPad} contextPad + * @param {Modeling} modeling + * @param {ElementFactory} elementFactory + * @param {Connect} connect + * @param {Create} create + * @param {PopupMenu} popupMenu + * @param {Canvas} canvas + * @param {Rules} rules + * @param {Translate} translate + * @param {AppendPreview} appendPreview */ export default function ContextPadProvider( config, injector, eventBus, contextPad, modeling, elementFactory, connect, create, popupMenu, - canvas, rules, translate) { + canvas, rules, translate, appendPreview) { config = config || {}; @@ -50,6 +91,8 @@ export default function ContextPadProvider( this._canvas = canvas; this._rules = rules; this._translate = translate; + this._eventBus = eventBus; + this._appendPreview = appendPreview; if (config.autoPlace !== false) { this._autoPlace = injector.get('autoPlace', false); @@ -69,6 +112,10 @@ export default function ContextPadProvider( entries.replace.action.click(event, shape); } }); + + eventBus.on('contextPad.close', function() { + appendPreview.cleanUp(); + }); } ContextPadProvider.$inject = [ @@ -83,27 +130,79 @@ ContextPadProvider.$inject = [ 'popupMenu', 'canvas', 'rules', - 'translate' + 'translate', + 'appendPreview' ]; +/** + * @param {Element[]} elements + * + * @return {ContextPadEntries} + */ +ContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) { + var modeling = this._modeling; + + var actions = {}; -ContextPadProvider.prototype.getContextPadEntries = function(element) { + if (this._isDeleteAllowed(elements)) { + assign(actions, { + 'delete': { + group: 'edit', + className: 'bpmn-icon-trash', + title: this._translate('Delete'), + action: { + click: function(event, elements) { + modeling.removeElements(elements.slice()); + } + } + } + }); + } + + return actions; +}; + +/** + * @param {Element[]} elements + * + * @return {boolean} + */ +ContextPadProvider.prototype._isDeleteAllowed = function(elements) { + + var baseAllowed = this._rules.allowed('elements.delete', { + elements: elements + }); + + if (isArray(baseAllowed)) { + return every(elements, el => baseAllowed.includes(el)); + } + return baseAllowed; +}; + +/** + * @param {Element} element + * + * @return {ContextPadEntries} + */ +ContextPadProvider.prototype.getContextPadEntries = function(element) { var contextPad = this._contextPad, modeling = this._modeling, - elementFactory = this._elementFactory, connect = this._connect, create = this._create, popupMenu = this._popupMenu, - canvas = this._canvas, - rules = this._rules, autoPlace = this._autoPlace, - translate = this._translate; + translate = this._translate, + appendPreview = this._appendPreview; var actions = {}; if (element.type === 'label') { + if (this._isDeleteAllowed([ element ])) { + assign(actions, deleteAction()); + } + return actions; } @@ -113,64 +212,77 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { connect.start(event, element); } - function removeElement(e) { + function removeElement(e, element) { modeling.removeElements([ element ]); } + function deleteAction() { + return { + 'delete': { + group: 'edit', + className: 'bpmn-icon-trash', + title: translate('Delete'), + action: { + click: removeElement + } + } + }; + } + function getReplaceMenuPosition(element) { var Y_OFFSET = 5; - var diagramContainer = canvas.getContainer(), - pad = contextPad.getPad(element).html; - - var diagramRect = diagramContainer.getBoundingClientRect(), - padRect = pad.getBoundingClientRect(); + var pad = contextPad.getPad(element).html; - var top = padRect.top - diagramRect.top; - var left = padRect.left - diagramRect.left; + var padRect = pad.getBoundingClientRect(); var pos = { - x: left, - y: top + padRect.height + Y_OFFSET + x: padRect.left, + y: padRect.bottom + Y_OFFSET }; return pos; } - /** - * Create an append action + * Create an append action. * * @param {string} type * @param {string} className - * @param {string} [title] + * @param {string} title * @param {Object} [options] * - * @return {Object} descriptor + * @return {ContextPadEntry} */ function appendAction(type, className, title, options) { - if (typeof title !== 'string') { - options = title; - title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') }); - } - function appendStart(event, element) { var shape = elementFactory.createShape(assign({ type: type }, options)); + create.start(event, shape, { source: element }); } - - var append = autoPlace ? function(event, element) { + var append = autoPlace ? function(_, element) { var shape = elementFactory.createShape(assign({ type: type }, options)); autoPlace.append(element, shape); } : appendStart; + var previewAppend = autoPlace ? function(_, element) { + + // mouseover + appendPreview.create(element, type, options); + + return () => { + + // mouseout + appendPreview.cleanUp(); + }; + } : null; return { group: 'model', @@ -178,14 +290,15 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { title: title, action: { dragstart: appendStart, - click: append + click: append, + hover: previewAppend } }; } function splitLaneHandler(count) { - return function(event, element) { + return function(_, element) { // actual split modeling.splitLane(element, count); @@ -205,7 +318,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'lane-insert-above': { group: 'lane-insert-above', className: 'bpmn-icon-lane-insert-above', - title: translate('Add Lane above'), + title: translate('Add lane above'), action: { click: function(event, element) { modeling.addLane(element, 'top'); @@ -216,12 +329,12 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { if (childLanes.length < 2) { - if (element.height >= 120) { + if (isHorizontal(element) ? element.height >= 120 : element.width >= 120) { assign(actions, { 'lane-divide-two': { group: 'lane-divide', className: 'bpmn-icon-lane-divide-two', - title: translate('Divide into two Lanes'), + title: translate('Divide into two lanes'), action: { click: splitLaneHandler(2) } @@ -229,12 +342,12 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { }); } - if (element.height >= 180) { + if (isHorizontal(element) ? element.height >= 180 : element.width >= 180) { assign(actions, { 'lane-divide-three': { group: 'lane-divide', className: 'bpmn-icon-lane-divide-three', - title: translate('Divide into three Lanes'), + title: translate('Divide into three lanes'), action: { click: splitLaneHandler(3) } @@ -247,7 +360,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'lane-insert-below': { group: 'lane-insert-below', className: 'bpmn-icon-lane-insert-below', - title: translate('Add Lane below'), + title: translate('Add lane below'), action: { click: function(event, element) { modeling.addLane(element, 'bottom'); @@ -266,36 +379,34 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'append.receive-task': appendAction( 'bpmn:ReceiveTask', 'bpmn-icon-receive-task', - translate('Append ReceiveTask') + translate('Append receive task') ), 'append.message-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-message', - translate('Append MessageIntermediateCatchEvent'), + translate('Append message intermediate catch event'), { eventDefinitionType: 'bpmn:MessageEventDefinition' } ), 'append.timer-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-timer', - translate('Append TimerIntermediateCatchEvent'), + translate('Append timer intermediate catch event'), { eventDefinitionType: 'bpmn:TimerEventDefinition' } ), 'append.condition-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-condition', - translate('Append ConditionIntermediateCatchEvent'), + translate('Append conditional intermediate catch event'), { eventDefinitionType: 'bpmn:ConditionalEventDefinition' } ), 'append.signal-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-signal', - translate('Append SignalIntermediateCatchEvent'), + translate('Append signal intermediate catch event'), { eventDefinitionType: 'bpmn:SignalEventDefinition' } ) }); - } else - - if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) { + } else if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) { assign(actions, { 'append.compensation-activity': @@ -308,9 +419,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { } ) }); - } else - - if (!is(businessObject, 'bpmn:EndEvent') && + } else if (!is(businessObject, 'bpmn:EndEvent') && !businessObject.isForCompensation && !isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') && !isEventSubProcess(businessObject)) { @@ -319,22 +428,22 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'append.end-event': appendAction( 'bpmn:EndEvent', 'bpmn-icon-end-event-none', - translate('Append EndEvent') + translate('Append end event') ), 'append.gateway': appendAction( 'bpmn:ExclusiveGateway', 'bpmn-icon-gateway-none', - translate('Append Gateway') + translate('Append gateway') ), 'append.append-task': appendAction( 'bpmn:Task', 'bpmn-icon-task', - translate('Append Task') + translate('Append task') ), 'append.intermediate-event': appendAction( 'bpmn:IntermediateThrowEvent', 'bpmn-icon-intermediate-event-none', - translate('Append Intermediate/Boundary Event') + translate('Append intermediate/boundary event') ) }); } @@ -347,7 +456,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'replace': { group: 'edit', className: 'bpmn-icon-screw-wrench', - title: translate('Change type'), + title: translate('Change element'), action: { click: function(event, element) { @@ -355,13 +464,37 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { cursor: { x: event.x, y: event.y } }); - popupMenu.open(element, 'bpmn-replace', position); + popupMenu.open(element, 'bpmn-replace', position, { + title: translate('Change element'), + width: 300, + search: true + }); } } } }); } + if (is(businessObject, 'bpmn:SequenceFlow')) { + assign(actions, { + 'append.text-annotation': appendAction( + 'bpmn:TextAnnotation', + 'bpmn-icon-text-annotation', + translate('Add text annotation') + ) + }); + } + + if (is(businessObject, 'bpmn:MessageFlow')) { + assign(actions, { + 'append.text-annotation': appendAction( + 'bpmn:TextAnnotation', + 'bpmn-icon-text-annotation', + translate('Add text annotation') + ) + }); + } + if ( isAny(businessObject, [ 'bpmn:FlowNode', @@ -373,19 +506,13 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { assign(actions, { 'append.text-annotation': appendAction( 'bpmn:TextAnnotation', - 'bpmn-icon-text-annotation' + 'bpmn-icon-text-annotation', + translate('Add text annotation') ), - 'connect': { group: 'connect', className: 'bpmn-icon-connection-multi', - title: translate( - 'Connect using ' + - (businessObject.isForCompensation - ? '' - : 'Sequence/MessageFlow or ') + - 'Association' - ), + title: translate('Connect to other element'), action: { click: startConnect, dragstart: startConnect, @@ -399,7 +526,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'connect': { group: 'connect', className: 'bpmn-icon-connection-multi', - title: translate('Connect using Association'), + title: translate('Connect using association'), action: { click: startConnect, dragstart: startConnect, @@ -413,7 +540,7 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { 'connect': { group: 'connect', className: 'bpmn-icon-connection-multi', - title: translate('Connect using DataInputAssociation'), + title: translate('Connect using data input association'), action: { click: startConnect, dragstart: startConnect @@ -424,30 +551,16 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { if (is(businessObject, 'bpmn:Group')) { assign(actions, { - 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation') + 'append.text-annotation': appendAction( + 'bpmn:TextAnnotation', + 'bpmn-icon-text-annotation', + translate('Add text annotation') + ) }); } - // delete element entry, only show if allowed by rules - var deleteAllowed = rules.allowed('elements.delete', { elements: [ element ] }); - - if (isArray(deleteAllowed)) { - - // was the element returned as a deletion candidate? - deleteAllowed = deleteAllowed[0] === element; - } - - if (deleteAllowed) { - assign(actions, { - 'delete': { - group: 'edit', - className: 'bpmn-icon-trash', - title: translate('Remove'), - action: { - click: removeElement - } - } - }); + if (this._isDeleteAllowed([ element ])) { + assign(actions, deleteAction()); } return actions; @@ -456,14 +569,21 @@ ContextPadProvider.prototype.getContextPadEntries = function(element) { // helpers ///////// -function isEventType(eventBo, type, definition) { +/** + * @param {ModdleElement} businessObject + * @param {string} type + * @param {string} eventDefinitionType + * + * @return {boolean} + */ +function isEventType(businessObject, type, eventDefinitionType) { - var isType = eventBo.$instanceOf(type); + var isType = businessObject.$instanceOf(type); var isDefinition = false; - var definitions = eventBo.eventDefinitions || []; + var definitions = businessObject.eventDefinitions || []; forEach(definitions, function(def) { - if (def.$type === definition) { + if (def.$type === eventDefinitionType) { isDefinition = true; } }); diff --git a/lib/features/context-pad/index.js b/lib/features/context-pad/index.js index 44752b4d72..2efab5ce06 100644 --- a/lib/features/context-pad/index.js +++ b/lib/features/context-pad/index.js @@ -1,3 +1,4 @@ +import AppendPreviewModule from '../append-preview'; import DirectEditingModule from 'diagram-js-direct-editing'; import ContextPadModule from 'diagram-js/lib/features/context-pad'; import SelectionModule from 'diagram-js/lib/features/selection'; @@ -9,6 +10,7 @@ import ContextPadProvider from './ContextPadProvider'; export default { __depends__: [ + AppendPreviewModule, DirectEditingModule, ContextPadModule, SelectionModule, diff --git a/lib/features/copy-paste/BpmnCopyPaste.js b/lib/features/copy-paste/BpmnCopyPaste.js index 99c0730193..6b3d7d24d5 100644 --- a/lib/features/copy-paste/BpmnCopyPaste.js +++ b/lib/features/copy-paste/BpmnCopyPaste.js @@ -4,6 +4,8 @@ import { is } from '../../util/ModelUtil'; +import { collectElementsAnnotations } from '../../util/AnnotationUtil'; + import { forEach, isArray, @@ -12,6 +14,14 @@ import { reduce } from 'min-dash'; +import { isLabel } from '../../util/LabelUtil'; + +/** + * @typedef {import('../modeling/BpmnFactory').default} BpmnFactory + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('./ModdleCopy').default} ModdleCopy + */ + function copyProperties(source, target, properties) { if (!isArray(properties)) { properties = [ properties ]; @@ -24,67 +34,58 @@ function copyProperties(source, target, properties) { }); } -function removeProperties(element, properties) { - if (!isArray(properties)) { - properties = [ properties ]; - } - - forEach(properties, function(property) { - if (element[property]) { - delete element[property]; - } - }); -} - var LOW_PRIORITY = 750; - +/** + * BPMN-specific copy & paste. + * + * @param {BpmnFactory} bpmnFactory + * @param {EventBus} eventBus + * @param {ModdleCopy} moddleCopy + */ export default function BpmnCopyPaste(bpmnFactory, eventBus, moddleCopy) { - eventBus.on('copyPaste.copyElement', LOW_PRIORITY, function(context) { - var descriptor = context.descriptor, - element = context.element; - - var businessObject = descriptor.oldBusinessObject = getBusinessObject(element); - var di = descriptor.oldDi = getDi(element); - - descriptor.type = element.type; + function copy(bo, clone) { + var targetBo = bpmnFactory.create(bo.$type); - copyProperties(businessObject, descriptor, 'name'); + return moddleCopy.copyElement(bo, targetBo, null, clone); + } - copyProperties(di, descriptor, 'isExpanded'); + eventBus.on('copyPaste.copyElement', LOW_PRIORITY, function(context) { + var descriptor = context.descriptor, + element = context.element, + businessObject = getBusinessObject(element); - if (isLabel(descriptor)) { + // do not copy business object + di for labels; + // will be pulled from the referenced label target + if (isLabel(element)) { return descriptor; } + var businessObjectCopy = descriptor.businessObject = copy(businessObject, true); + var diCopy = descriptor.di = copy(getDi(element), true); + diCopy.bpmnElement = businessObjectCopy; + + copyProperties(businessObjectCopy, descriptor, 'name'); + copyProperties(diCopy, descriptor, 'isExpanded'); + // default sequence flow if (businessObject.default) { descriptor.default = businessObject.default.id; } }); - eventBus.on('moddleCopy.canCopyProperty', function(context) { - var parent = context.parent, - property = context.property, - propertyName = context.propertyName, - bpmnProcess; - - if ( - propertyName === 'processRef' && - is(parent, 'bpmn:Participant') && - is(property, 'bpmn:Process') - ) { - bpmnProcess = bpmnFactory.create('bpmn:Process'); - - // return copy of process - return moddleCopy.copyElement(property, bpmnProcess); - } - }); + var referencesKey = '-bpmn-js-refs'; + + function getReferences(cache) { + return (cache[referencesKey] = cache[referencesKey] || {}); + } - var references; + function setReferences(cache, references) { + cache[referencesKey] = references; + } - function resolveReferences(descriptor, cache) { + function resolveReferences(descriptor, cache, references) { var businessObject = getBusinessObject(descriptor); // default sequence flows @@ -104,12 +105,12 @@ export default function BpmnCopyPaste(bpmnFactory, eventBus, moddleCopy) { getBusinessObject(descriptor).attachedToRef = getBusinessObject(cache[ descriptor.host ]); } - references = omit(references, reduce(references, function(array, reference, key) { + return omit(references, reduce(references, function(array, reference, key) { var element = reference.element, property = reference.property; if (key === descriptor.id) { - element[ property ] = businessObject; + element.set(property, businessObject); array.push(descriptor.id); } @@ -118,18 +119,13 @@ export default function BpmnCopyPaste(bpmnFactory, eventBus, moddleCopy) { }, [])); } - eventBus.on('copyPaste.pasteElements', function() { - references = {}; - }); - eventBus.on('copyPaste.pasteElement', function(context) { var cache = context.cache, descriptor = context.descriptor, - oldBusinessObject = descriptor.oldBusinessObject, - oldDi = descriptor.oldDi, - newBusinessObject, newDi; + businessObject = descriptor.businessObject, + di = descriptor.di; - // do NOT copy business object if external label + // wire existing di + businessObject for external label if (isLabel(descriptor)) { descriptor.businessObject = getBusinessObject(cache[ descriptor.labelTarget ]); descriptor.di = getDi(cache[ descriptor.labelTarget ]); @@ -137,30 +133,71 @@ export default function BpmnCopyPaste(bpmnFactory, eventBus, moddleCopy) { return; } - newBusinessObject = bpmnFactory.create(oldBusinessObject.$type); + businessObject = descriptor.businessObject = copy(businessObject); - descriptor.businessObject = moddleCopy.copyElement( - oldBusinessObject, - newBusinessObject - ); + di = descriptor.di = copy(di); + di.bpmnElement = businessObject; - newDi = bpmnFactory.create(oldDi.$type); - newDi.bpmnElement = newBusinessObject; - - descriptor.di = moddleCopy.copyElement( - oldDi, - newDi - ); - - // resolve references e.g. default sequence flow - resolveReferences(descriptor, cache); - - copyProperties(descriptor, newBusinessObject, [ + copyProperties(descriptor, businessObject, [ 'isExpanded', 'name' ]); - removeProperties(descriptor, 'oldBusinessObject'); + descriptor.type = businessObject.$type; + }); + + // copy + paste processRef with participant + + eventBus.on('copyPaste.copyElement', LOW_PRIORITY, function(context) { + var descriptor = context.descriptor, + element = context.element; + + if (!is(element, 'bpmn:Participant')) { + return; + } + + var participantBo = getBusinessObject(element); + + if (participantBo.processRef) { + descriptor.processRef = copy(participantBo.processRef, true); + } + }); + + eventBus.on('copyPaste.pasteElement', function(context) { + var descriptor = context.descriptor, + processRef = descriptor.processRef; + + if (processRef) { + descriptor.processRef = copy(processRef); + } + }); + + eventBus.on('copyPaste.createTree', function(context) { + var element = context.element, + children = context.children; + + if (!is(element, 'bpmn:SubProcess')) { + return; + } + + // add TextAnnotations to copy the closure, + // since by default they are children of a global process, not subprocess + forEach(collectElementsAnnotations(children), (entry) => { + children.push(entry.annotation); + }); + }); + + // resolve references + + eventBus.on('copyPaste.pasteElement', LOW_PRIORITY, function(context) { + var cache = context.cache, + descriptor = context.descriptor; + + // resolve references e.g. default sequence flow + setReferences( + cache, + resolveReferences(descriptor, cache, getReferences(cache)) + ); }); } @@ -170,10 +207,4 @@ BpmnCopyPaste.$inject = [ 'bpmnFactory', 'eventBus', 'moddleCopy' -]; - -// helpers ////////// - -function isLabel(element) { - return !!element.labelTarget; -} +]; \ No newline at end of file diff --git a/lib/features/copy-paste/ModdleCopy.js b/lib/features/copy-paste/ModdleCopy.js index 0c51ee2247..1343aed38c 100644 --- a/lib/features/copy-paste/ModdleCopy.js +++ b/lib/features/copy-paste/ModdleCopy.js @@ -1,16 +1,18 @@ import { find, forEach, + has, isArray, isDefined, isObject, matchPattern, reduce, - has, sortBy } from 'min-dash'; -var DISALLOWED_PROPERTIES = [ +import { is } from '../../util/ModelUtil'; + +const DISALLOWED_PROPERTIES = [ 'artifacts', 'dataInputAssociations', 'dataOutputAssociations', @@ -18,43 +20,24 @@ var DISALLOWED_PROPERTIES = [ 'flowElements', 'lanes', 'incoming', - 'outgoing' + 'outgoing', + 'categoryValue' ]; -/** - * @typedef {Function} listener - * - * @param {Object} context - * @param {Array} context.propertyNames - * @param {ModdleElement} context.sourceElement - * @param {ModdleElement} context.targetElement - * - * @returns {Array|boolean} - Return properties to be copied or false to disallow - * copying. - */ - -/** - * @typedef {Function} listener - * - * @param {Object} context - * @param {ModdleElement} context.parent - * @param {*} context.property - * @param {string} context.propertyName - * - * @returns {*|boolean} - Return copied property or false to disallow - * copying. - */ +const ALLOWED_REFERENCES = [ + 'errorRef', + 'escalationRef', + 'messageRef', + 'signalRef', + 'dataObjectRef' +]; /** - * @typedef {Function} listener - * - * @param {Object} context - * @param {ModdleElement} context.parent - * @param {*} context.property - * @param {string} context.propertyName + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../modeling/BpmnFactory').default} BpmnFactory + * @typedef {import('../../model/Types').Moddle} Moddle * - * @returns {boolean} - Return false to disallow - * setting copied property. + * @typedef {import('../../model/Types').ModdleElement} ModdleElement */ /** @@ -62,7 +45,7 @@ var DISALLOWED_PROPERTIES = [ * * @param {EventBus} eventBus * @param {BpmnFactory} bpmnFactory - * @param {BpmnModdle} moddle + * @param {Moddle} moddle */ export default function ModdleCopy(eventBus, bpmnFactory, moddle) { this._bpmnFactory = bpmnFactory; @@ -70,25 +53,35 @@ export default function ModdleCopy(eventBus, bpmnFactory, moddle) { this._moddle = moddle; // copy extension elements last - eventBus.on('moddleCopy.canCopyProperties', function(context) { - var propertyNames = context.propertyNames; + eventBus.on('moddleCopy.canCopyProperties', (context) => { + const { propertyNames } = context; if (!propertyNames || !propertyNames.length) { return; } - return sortBy(propertyNames, function(propertyName) { + return sortBy(propertyNames, (propertyName) => { return propertyName === 'extensionElements'; }); }); // default check whether property can be copied - eventBus.on('moddleCopy.canCopyProperty', function(context) { - var parent = context.parent, - parentDescriptor = isObject(parent) && parent.$descriptor, - propertyName = context.propertyName; + eventBus.on('moddleCopy.canCopyProperty', (context) => { + const { + parent, + property, + propertyName + } = context; - if (propertyName && DISALLOWED_PROPERTIES.indexOf(propertyName) !== -1) { + const parentDescriptor = isObject(parent) && parent.$descriptor; + + if (propertyName && ALLOWED_REFERENCES.includes(propertyName)) { + + // allow copying reference + return property; + } + + if (propertyName && DISALLOWED_PROPERTIES.includes(propertyName)) { // disallow copying property return false; @@ -104,8 +97,8 @@ export default function ModdleCopy(eventBus, bpmnFactory, moddle) { }); // do NOT allow to copy empty extension elements - eventBus.on('moddleCopy.canSetCopiedProperty', function(context) { - var property = context.property; + eventBus.on('moddleCopy.canSetCopiedProperty', (context) => { + const { property } = context; if (is(property, 'bpmn:ExtensionElements') && (!property.values || !property.values.length)) { @@ -126,23 +119,23 @@ ModdleCopy.$inject = [ * * @param {ModdleElement} sourceElement * @param {ModdleElement} targetElement - * @param {Array} [propertyNames] + * @param {string[]} [propertyNames] + * @param {boolean} [clone=false] * - * @param {ModdleElement} + * @return {ModdleElement} */ -ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, propertyNames) { - var self = this; - +ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, propertyNames, clone = false) { if (propertyNames && !isArray(propertyNames)) { propertyNames = [ propertyNames ]; } propertyNames = propertyNames || getPropertyNames(sourceElement.$descriptor); - var canCopyProperties = this._eventBus.fire('moddleCopy.canCopyProperties', { + const canCopyProperties = this._eventBus.fire('moddleCopy.canCopyProperties', { propertyNames: propertyNames, sourceElement: sourceElement, - targetElement: targetElement + targetElement: targetElement, + clone: clone }); if (canCopyProperties === false) { @@ -154,16 +147,20 @@ ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, proper } // copy properties - forEach(propertyNames, function(propertyName) { - var sourceProperty; + forEach(propertyNames, (propertyName) => { + let sourceProperty; if (has(sourceElement, propertyName)) { sourceProperty = sourceElement.get(propertyName); } - var copiedProperty = self.copyProperty(sourceProperty, targetElement, propertyName); + const copiedProperty = this.copyProperty(sourceProperty, targetElement, propertyName, clone); + + if (!isDefined(copiedProperty)) { + return; + } - var canSetProperty = self._eventBus.fire('moddleCopy.canSetCopiedProperty', { + const canSetProperty = this._eventBus.fire('moddleCopy.canSetCopiedProperty', { parent: targetElement, property: copiedProperty, propertyName: propertyName @@ -173,9 +170,9 @@ ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, proper return; } - if (isDefined(copiedProperty)) { - targetElement.set(propertyName, copiedProperty); - } + // TODO(nikku): unclaim old IDs if ID property is copied over + // this._moddle.getPropertyDescriptor(parent, propertyName) + targetElement.set(propertyName, copiedProperty); }); return targetElement; @@ -184,20 +181,21 @@ ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, proper /** * Copy model property. * - * @param {*} property + * @param {any} property * @param {ModdleElement} parent * @param {string} propertyName + * @param {boolean} [clone=false] * - * @returns {*} + * @return {any} */ -ModdleCopy.prototype.copyProperty = function(property, parent, propertyName) { - var self = this; +ModdleCopy.prototype.copyProperty = function(property, parent, propertyName, clone = false) { // allow others to copy property - var copiedProperty = this._eventBus.fire('moddleCopy.canCopyProperty', { + let copiedProperty = this._eventBus.fire('moddleCopy.canCopyProperty', { parent: parent, property: property, - propertyName: propertyName + propertyName: propertyName, + clone: clone }); // return if copying is NOT allowed @@ -213,7 +211,7 @@ ModdleCopy.prototype.copyProperty = function(property, parent, propertyName) { return copiedProperty; } - var propertyDescriptor = this._moddle.getPropertyDescriptor(parent, propertyName); + const propertyDescriptor = this._moddle.getPropertyDescriptor(parent, propertyName); // do NOT copy references if (propertyDescriptor.isReference) { @@ -222,20 +220,18 @@ ModdleCopy.prototype.copyProperty = function(property, parent, propertyName) { // copy id if (propertyDescriptor.isId) { - return this._copyId(property, parent); + return property && this._copyId(property, parent, clone); } // copy arrays if (isArray(property)) { - return reduce(property, function(childProperties, childProperty) { + return reduce(property, (childProperties, childProperty) => { // recursion - copiedProperty = self.copyProperty(childProperty, parent, propertyName); + const copiedProperty = this.copyProperty(childProperty, parent, propertyName, clone); // copying might NOT be allowed if (copiedProperty) { - copiedProperty.$parent = parent; - return childProperties.concat(copiedProperty); } @@ -249,12 +245,12 @@ ModdleCopy.prototype.copyProperty = function(property, parent, propertyName) { return; } - copiedProperty = self._bpmnFactory.create(property.$type); + copiedProperty = this._bpmnFactory.create(property.$type); copiedProperty.$parent = parent; // recursion - copiedProperty = self.copyElement(property, copiedProperty); + copiedProperty = this.copyElement(property, copiedProperty, null, clone); return copiedProperty; } @@ -263,7 +259,10 @@ ModdleCopy.prototype.copyProperty = function(property, parent, propertyName) { return property; }; -ModdleCopy.prototype._copyId = function(id, element) { +ModdleCopy.prototype._copyId = function(id, element, clone = false) { + if (clone) { + return id; + } // disallow if already taken if (this._moddle.ids.assigned(id)) { @@ -278,7 +277,7 @@ ModdleCopy.prototype._copyId = function(id, element) { // helpers ////////// export function getPropertyNames(descriptor, keepDefaultProperties) { - return reduce(descriptor.properties, function(properties, property) { + return reduce(descriptor.properties, (properties, property) => { if (keepDefaultProperties && property.default) { return properties; @@ -286,8 +285,4 @@ export function getPropertyNames(descriptor, keepDefaultProperties) { return properties.concat(property.name); }, []); -} - -function is(element, type) { - return element && (typeof element.$instanceOf === 'function') && element.$instanceOf(type); } \ No newline at end of file diff --git a/lib/features/di-ordering/BpmnDiOrdering.js b/lib/features/di-ordering/BpmnDiOrdering.js index 9b345a15f8..26a1db5df1 100644 --- a/lib/features/di-ordering/BpmnDiOrdering.js +++ b/lib/features/di-ordering/BpmnDiOrdering.js @@ -8,9 +8,17 @@ import { import { selfAndAllChildren } from 'diagram-js/lib/util/Elements'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + */ var HIGH_PRIORITY = 2000; +/** + * @param {EventBus} eventBus + * @param {Canvas} canvas + */ export default function BpmnDiOrdering(eventBus, canvas) { eventBus.on('saveXML.start', HIGH_PRIORITY, orderDi); diff --git a/lib/features/distribute-elements/BpmnDistributeElements.js b/lib/features/distribute-elements/BpmnDistributeElements.js index ce63a419a1..5685a8f0d8 100644 --- a/lib/features/distribute-elements/BpmnDistributeElements.js +++ b/lib/features/distribute-elements/BpmnDistributeElements.js @@ -1,3 +1,8 @@ +import inherits from 'inherits-browser'; + +import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; +import { getParents } from 'diagram-js/lib/util/Elements'; + import { filter } from 'min-dash'; @@ -6,15 +11,29 @@ import { isAny } from '../modeling/util/ModelingUtil'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + */ /** - * Registers element exclude filters for elements that - * currently do not support distribution. + * Registers element exclude filters for elements that currently do not support + * distribution. + * + * @param {EventBus} eventBus */ -export default function BpmnDistributeElements(distributeElements) { +export default function BpmnDistributeElements(eventBus) { + RuleProvider.call(this, eventBus); +} + +BpmnDistributeElements.$inject = [ 'eventBus' ]; + +inherits(BpmnDistributeElements, RuleProvider); - distributeElements.registerFilter(function(elements) { - return filter(elements, function(element) { +BpmnDistributeElements.prototype.init = function() { + this.addRule('elements.distribute', function(context) { + var elements = context.elements; + + elements = filter(elements, function(element) { var cannotDistribute = isAny(element, [ 'bpmn:Association', 'bpmn:BoundaryEvent', @@ -22,14 +41,20 @@ export default function BpmnDistributeElements(distributeElements) { 'bpmn:DataOutputAssociation', 'bpmn:Lane', 'bpmn:MessageFlow', - 'bpmn:Participant', 'bpmn:SequenceFlow', 'bpmn:TextAnnotation' ]); return !(element.labelTarget || cannotDistribute); }); - }); -} -BpmnDistributeElements.$inject = [ 'distributeElements' ]; \ No newline at end of file + // filter out elements which are children of any of the selected elements + elements = getParents(elements); + + if (elements.length < 3) { + return false; + } + + return elements; + }); +}; diff --git a/lib/features/distribute-elements/DistributeElementsIcons.js b/lib/features/distribute-elements/DistributeElementsIcons.js new file mode 100644 index 0000000000..dbb09bdfb1 --- /dev/null +++ b/lib/features/distribute-elements/DistributeElementsIcons.js @@ -0,0 +1,18 @@ +/** + * To change the icons, modify the SVGs in `./resources`, execute `npx svgo -f resources --datauri enc -o dist`, + * and then replace respective icons with the optimized data URIs in `./dist`. + */ +var icons = { + horizontal: ` + + + + `, + vertical: ` + + + + ` +}; + +export default icons; diff --git a/lib/features/distribute-elements/DistributeElementsMenuProvider.js b/lib/features/distribute-elements/DistributeElementsMenuProvider.js new file mode 100644 index 0000000000..4ec6258033 --- /dev/null +++ b/lib/features/distribute-elements/DistributeElementsMenuProvider.js @@ -0,0 +1,92 @@ +import ICONS from './DistributeElementsIcons'; + +import { assign } from 'min-dash'; + +/** + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').default} PopupMenu + * @typedef {import('./BpmnDistributeElements').default} DistributeElements + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + * @typedef {import('diagram-js/lib/features/rules/Rules').default} Rules + * + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenuProvider').PopupMenuEntries} PopupMenuEntries + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenuProvider').default} PopupMenuProvider + * @typedef {import('diagram-js/lib/features/popup-menu/PopupMenu').PopupMenuTarget} PopupMenuTarget + */ + +var LOW_PRIORITY = 900; + +/** + * A provider for the distribute elements popup menu. + * + * @implements {PopupMenuProvider} + * + * @param {PopupMenu} popupMenu + * @param {DistributeElements} distributeElements + * @param {Translate} translate + * @param {Rules} rules + */ +export default function DistributeElementsMenuProvider( + popupMenu, distributeElements, translate, rules) { + this._distributeElements = distributeElements; + this._translate = translate; + this._popupMenu = popupMenu; + this._rules = rules; + + popupMenu.registerProvider('align-elements', LOW_PRIORITY, this); +} + +DistributeElementsMenuProvider.$inject = [ + 'popupMenu', + 'distributeElements', + 'translate', + 'rules' +]; + +/** + * @param {PopupMenuTarget} target + * + * @return {PopupMenuEntries} + */ +DistributeElementsMenuProvider.prototype.getPopupMenuEntries = function(target) { + var entries = {}; + + if (this._isAllowed(target)) { + assign(entries, this._getEntries(target)); + } + + return entries; +}; + +DistributeElementsMenuProvider.prototype._isAllowed = function(elements) { + return this._rules.allowed('elements.distribute', { elements: elements }); +}; + +DistributeElementsMenuProvider.prototype._getEntries = function(elements) { + var distributeElements = this._distributeElements, + translate = this._translate, + popupMenu = this._popupMenu; + + var entries = { + 'distribute-elements-horizontal': { + group: 'distribute', + title: translate('Distribute elements horizontally'), + className: 'bjs-align-elements-menu-entry', + imageHtml: ICONS['horizontal'], + action: function(event, entry) { + distributeElements.trigger(elements, 'horizontal'); + popupMenu.close(); + } + }, + 'distribute-elements-vertical': { + group: 'distribute', + title: translate('Distribute elements vertically'), + imageHtml: ICONS['vertical'], + action: function(event, entry) { + distributeElements.trigger(elements, 'vertical'); + popupMenu.close(); + } + }, + }; + + return entries; +}; diff --git a/lib/features/distribute-elements/index.js b/lib/features/distribute-elements/index.js index d6ec22c8dc..15ea2f0ff6 100644 --- a/lib/features/distribute-elements/index.js +++ b/lib/features/distribute-elements/index.js @@ -1,12 +1,19 @@ import DistributeElementsModule from 'diagram-js/lib/features/distribute-elements'; +import PopupMenuModule from 'diagram-js/lib/features/popup-menu'; import BpmnDistributeElements from './BpmnDistributeElements'; +import DistributeElementsMenuProvider from './DistributeElementsMenuProvider'; export default { __depends__: [ + PopupMenuModule, DistributeElementsModule ], - __init__: [ 'bpmnDistributeElements' ], - bpmnDistributeElements: [ 'type', BpmnDistributeElements ] + __init__: [ + 'bpmnDistributeElements', + 'distributeElementsMenuProvider' + ], + bpmnDistributeElements: [ 'type', BpmnDistributeElements ], + distributeElementsMenuProvider: [ 'type', DistributeElementsMenuProvider ] }; diff --git a/lib/features/drilldown/DrilldownBreadcrumbs.js b/lib/features/drilldown/DrilldownBreadcrumbs.js index 930d49a59f..ba01dfa6ec 100644 --- a/lib/features/drilldown/DrilldownBreadcrumbs.js +++ b/lib/features/drilldown/DrilldownBreadcrumbs.js @@ -7,32 +7,40 @@ import { getPlaneIdFromShape } from '../../util/DrilldownUtil'; +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + */ + var OPEN_CLASS = 'bjs-breadcrumbs-shown'; /** * Adds overlays that allow switching planes on collapsed subprocesses. * - * @param {eventBus} eventBus - * @param {elementRegistry} elementRegistry - * @param {overlays} overlays - * @param {canvas} canvas + * @param {EventBus} eventBus + * @param {ElementRegistry} elementRegistry + * @param {Canvas} canvas */ -export default function DrilldownBreadcrumbs(eventBus, elementRegistry, overlays, canvas) { +export default function DrilldownBreadcrumbs(eventBus, elementRegistry, canvas) { var breadcrumbs = domify('
    '); var container = canvas.getContainer(); var containerClasses = classes(container); container.appendChild(breadcrumbs); - var boParents = []; + var businessObjectParents = []; // update breadcrumbs if name or ID of the primary shape changes - eventBus.on('element.changed', function(e) { - var shape = e.element, - bo = getBusinessObject(shape); + eventBus.on('element.changed', function(event) { + var shape = event.element, + businessObject = getBusinessObject(shape); - var isPresent = find(boParents, function(el) { - return el === bo; + var isPresent = find(businessObjectParents, function(element) { + return element === businessObject; }); if (!isPresent) { @@ -46,30 +54,38 @@ export default function DrilldownBreadcrumbs(eventBus, elementRegistry, overlays * Updates the displayed breadcrumbs. If no element is provided, only the * labels are updated. * - * @param {djs.model.Base} [element] + * @param {Element} [element] */ function updateBreadcrumbs(element) { if (element) { - boParents = getBoParentChain(element); + businessObjectParents = getBusinessObjectParentChain(element); } - var path = boParents.map(function(parent) { - var title = escapeHTML(parent.name || parent.id); - var link = domify('
  • ' + title + '
  • '); - - var parentPlane = canvas.findRoot(getPlaneIdFromShape(parent)) || canvas.findRoot(parent.id); + var path = businessObjectParents.flatMap(function(parent) { + var parentPlane = + canvas.findRoot(getPlaneIdFromShape(parent)) || + canvas.findRoot(parent.id); - // when the root is a collaboration, the process does not have a corresponding - // element in the elementRegisty. Instead, we search for the corresponding participant + // when the root is a collaboration, the process does not have a + // corresponding element in the elementRegisty. Instead, we search + // for the corresponding participant if (!parentPlane && is(parent, 'bpmn:Process')) { var participant = elementRegistry.find(function(element) { - var bo = getBusinessObject(element); - return bo && bo.processRef && bo.processRef === parent; + var businessObject = getBusinessObject(element); + + return businessObject && businessObject.get('processRef') === parent; }); - parentPlane = canvas.findRoot(participant.id); + parentPlane = participant && canvas.findRoot(participant.id); + } + + if (!parentPlane) { + return []; } + var title = escapeHTML(parent.name || parent.id); + var link = domify('
  • ' + title + '
  • '); + link.addEventListener('click', function() { canvas.setRootElement(parentPlane); }); @@ -81,10 +97,11 @@ export default function DrilldownBreadcrumbs(eventBus, elementRegistry, overlays // show breadcrumbs and expose state to .djs-container var visible = path.length > 1; + containerClasses.toggle(OPEN_CLASS, visible); - path.forEach(function(el) { - breadcrumbs.appendChild(el); + path.forEach(function(element) { + breadcrumbs.appendChild(element); }); } @@ -94,7 +111,7 @@ export default function DrilldownBreadcrumbs(eventBus, elementRegistry, overlays } -DrilldownBreadcrumbs.$inject = [ 'eventBus', 'elementRegistry', 'overlays', 'canvas' ]; +DrilldownBreadcrumbs.$inject = [ 'eventBus', 'elementRegistry', 'canvas' ]; // helpers ////////// @@ -103,16 +120,16 @@ DrilldownBreadcrumbs.$inject = [ 'eventBus', 'elementRegistry', 'overlays', 'can * Returns the parents for the element using the business object chain, * starting with the root element. * - * @param {djs.model.Shape} child + * @param {Shape} child * - * @returns {Array} parents + * @return {Shape} */ -function getBoParentChain(child) { - var bo = getBusinessObject(child); +function getBusinessObjectParentChain(child) { + var businessObject = getBusinessObject(child); var parents = []; - for (var element = bo; element; element = element.$parent) { + for (var element = businessObject; element; element = element.$parent) { if (is(element, 'bpmn:SubProcess') || is(element, 'bpmn:Process')) { parents.push(element); } diff --git a/lib/features/drilldown/DrilldownCentering.js b/lib/features/drilldown/DrilldownCentering.js index c7f346de7c..86d51c3d1d 100644 --- a/lib/features/drilldown/DrilldownCentering.js +++ b/lib/features/drilldown/DrilldownCentering.js @@ -1,12 +1,17 @@ import { is } from '../../util/ModelUtil'; +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + */ + /** * Move collapsed subprocesses into view when drilling down. * * Zoom and scroll are saved in a session. * - * @param {eventBus} eventBus - * @param {canvas} canvas + * @param {EventBus} eventBus + * @param {Canvas} canvas */ export default function DrilldownCentering(eventBus, canvas) { @@ -26,8 +31,8 @@ export default function DrilldownCentering(eventBus, canvas) { currentRoot = newRoot; - // current root was replaced with a collaboration, we don't update the viewbox - if (is(newRoot, 'bpmn:Collaboration') && !storedViewbox) { + // Keep viewbox when replacing root elements + if (!is(newRoot, 'bpmn:SubProcess') && !storedViewbox) { return; } diff --git a/lib/features/drilldown/DrilldownOverlayBehavior.js b/lib/features/drilldown/DrilldownOverlayBehavior.js index 738e9e21f3..04770a91c4 100644 --- a/lib/features/drilldown/DrilldownOverlayBehavior.js +++ b/lib/features/drilldown/DrilldownOverlayBehavior.js @@ -1,17 +1,36 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; -import { is } from '../../util/ModelUtil'; +import { getBusinessObject, is } from '../../util/ModelUtil'; import { classes, domify } from 'min-dom'; import { getPlaneIdFromShape } from '../../util/DrilldownUtil'; +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/overlays/Overlays').default} Overlays + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Parent} Parent + * @typedef {import('../../model/Types').Shape} Shape + */ + var LOW_PRIORITY = 250; var ARROW_DOWN_SVG = ''; var EMPTY_MARKER = 'bjs-drilldown-empty'; +/** + * @param {Canvas} canvas + * @param {EventBus} eventBus + * @param {ElementRegistry} elementRegistry + * @param {Overlays} overlays + * @param {Translate} translate + */ export default function DrilldownOverlayBehavior( - canvas, eventBus, elementRegistry, overlays + canvas, eventBus, elementRegistry, overlays, translate ) { CommandInterceptor.call(this, eventBus); @@ -19,6 +38,7 @@ export default function DrilldownOverlayBehavior( this._eventBus = eventBus; this._elementRegistry = elementRegistry; this._overlays = overlays; + this._translate = translate; var self = this; @@ -26,10 +46,10 @@ export default function DrilldownOverlayBehavior( var shape = context.shape; // Add overlay to the collapsed shape - if (self.canDrillDown(shape)) { - self.addOverlay(shape); + if (self._canDrillDown(shape)) { + self._addOverlay(shape); } else { - self.removeOverlay(shape); + self._removeOverlay(shape); } }, true); @@ -38,53 +58,53 @@ export default function DrilldownOverlayBehavior( var shape = context.shape; // Add overlay to the collapsed shape - if (self.canDrillDown(shape)) { - self.addOverlay(shape); + if (self._canDrillDown(shape)) { + self._addOverlay(shape); } else { - self.removeOverlay(shape); + self._removeOverlay(shape); } }, true); - this.executed(['shape.create', 'shape.move', 'shape.delete'], LOW_PRIORITY, + this.executed([ 'shape.create', 'shape.move', 'shape.delete' ], LOW_PRIORITY, function(context) { var oldParent = context.oldParent, newParent = context.newParent || context.parent, shape = context.shape; // Add overlay to the collapsed shape - if (self.canDrillDown(shape)) { - self.addOverlay(shape); + if (self._canDrillDown(shape)) { + self._addOverlay(shape); } - self.updateDrilldownOverlay(oldParent); - self.updateDrilldownOverlay(newParent); - self.updateDrilldownOverlay(shape); + self._updateDrilldownOverlay(oldParent); + self._updateDrilldownOverlay(newParent); + self._updateDrilldownOverlay(shape); }, true); - this.reverted(['shape.create', 'shape.move', 'shape.delete'], LOW_PRIORITY, + this.reverted([ 'shape.create', 'shape.move', 'shape.delete' ], LOW_PRIORITY, function(context) { var oldParent = context.oldParent, newParent = context.newParent || context.parent, shape = context.shape; // Add overlay to the collapsed shape - if (self.canDrillDown(shape)) { - self.addOverlay(shape); + if (self._canDrillDown(shape)) { + self._addOverlay(shape); } - self.updateDrilldownOverlay(oldParent); - self.updateDrilldownOverlay(newParent); - self.updateDrilldownOverlay(shape); + self._updateDrilldownOverlay(oldParent); + self._updateDrilldownOverlay(newParent); + self._updateDrilldownOverlay(shape); }, true); eventBus.on('import.render.complete', function() { elementRegistry.filter(function(e) { - return self.canDrillDown(e); + return self._canDrillDown(e); }).map(function(el) { - self.addOverlay(el); + self._addOverlay(el); }); }); @@ -92,7 +112,10 @@ export default function DrilldownOverlayBehavior( inherits(DrilldownOverlayBehavior, CommandInterceptor); -DrilldownOverlayBehavior.prototype.updateDrilldownOverlay = function(shape) { +/** + * @param {Shape} shape + */ +DrilldownOverlayBehavior.prototype._updateDrilldownOverlay = function(shape) { var canvas = this._canvas; if (!shape) { @@ -100,54 +123,68 @@ DrilldownOverlayBehavior.prototype.updateDrilldownOverlay = function(shape) { } var root = canvas.findRoot(shape); + if (root) { - this.updateOverlayVisibility(root); + this._updateOverlayVisibility(root); } }; - -DrilldownOverlayBehavior.prototype.canDrillDown = function(element) { +/** + * @param {Element} element + * + * @return {boolean} + */ +DrilldownOverlayBehavior.prototype._canDrillDown = function(element) { var canvas = this._canvas; + return is(element, 'bpmn:SubProcess') && canvas.findRoot(getPlaneIdFromShape(element)); }; /** - * Updates visibility of the drilldown overlay. If the plane has no elements, - * the drilldown will be only shown when the element is selected. + * Update the visibility of the drilldown overlay. If the plane has no elements, + * the drilldown will only be shown when the element is selected. * - * @param {djs.model.Shape|djs.model.Root} element collapsed shape or root element + * @param {Parent} element The collapsed root or shape. */ -DrilldownOverlayBehavior.prototype.updateOverlayVisibility = function(element) { +DrilldownOverlayBehavior.prototype._updateOverlayVisibility = function(element) { var overlays = this._overlays; - var bo = element.businessObject; + var businessObject = getBusinessObject(element); - var overlay = overlays.get({ element: bo.id, type: 'drilldown' })[0]; + var overlay = overlays.get({ element: businessObject.id, type: 'drilldown' })[0]; if (!overlay) { return; } - var hasContent = bo && bo.flowElements && bo.flowElements.length; - classes(overlay.html).toggle(EMPTY_MARKER, !hasContent); + var hasFlowElements = businessObject + && businessObject.get('flowElements') + && businessObject.get('flowElements').length; + + classes(overlay.html).toggle(EMPTY_MARKER, !hasFlowElements); }; /** - * Attaches a drilldown button to the given element. We assume that the plane has - * the same id as the element. + * Add a drilldown button to the given element assuming the plane has the same + * ID as the element. * - * @param {djs.model.Shape} element collapsed shape + * @param {Shape} element The collapsed shape. */ -DrilldownOverlayBehavior.prototype.addOverlay = function(element) { - var canvas = this._canvas; - var overlays = this._overlays; +DrilldownOverlayBehavior.prototype._addOverlay = function(element) { + var canvas = this._canvas, + overlays = this._overlays, + bo = getBusinessObject(element); var existingOverlays = overlays.get({ element: element, type: 'drilldown' }); + if (existingOverlays.length) { - this.removeOverlay(element); + this._removeOverlay(element); } - var button = domify(''); + var button = domify(''), + elementName = bo.get('name') || bo.get('id'), + title = this._translate('Open {element}', { element: elementName }); + button.setAttribute('title', title); button.addEventListener('click', function() { canvas.setRootElement(canvas.findRoot(getPlaneIdFromShape(element))); @@ -161,10 +198,10 @@ DrilldownOverlayBehavior.prototype.addOverlay = function(element) { html: button }); - this.updateOverlayVisibility(element); + this._updateOverlayVisibility(element); }; -DrilldownOverlayBehavior.prototype.removeOverlay = function(element) { +DrilldownOverlayBehavior.prototype._removeOverlay = function(element) { var overlays = this._overlays; overlays.remove({ @@ -177,5 +214,6 @@ DrilldownOverlayBehavior.$inject = [ 'canvas', 'eventBus', 'elementRegistry', - 'overlays' + 'overlays', + 'translate' ]; \ No newline at end of file diff --git a/lib/features/drilldown/SubprocessCompatibility.js b/lib/features/drilldown/SubprocessCompatibility.js index 2bd76c135c..c190a0e0e9 100644 --- a/lib/features/drilldown/SubprocessCompatibility.js +++ b/lib/features/drilldown/SubprocessCompatibility.js @@ -2,6 +2,18 @@ import { asBounds, asTRBL } from 'diagram-js/lib/layout/LayoutUtil'; import { is, isAny } from '../../util/ModelUtil'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../../model/Types').Moddle} Moddle + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + * + * @typedef {import('diagram-js/lib/core/Canvas').CanvasPlane} CanvasPlane + * + * @typedef {import('diagram-js/lib/util/Types').Rect} Rect + */ + var DEFAULT_POSITION = { x: 180, y: 160 @@ -9,10 +21,10 @@ var DEFAULT_POSITION = { /** * Hook into `import.render.start` and create new planes for diagrams with - * collapsed subprocesses and all dis on the same plane. + * collapsed subprocesses and all DI elements on the same plane. * - * @param {eventBus} eventBus - * @param {moddle} moddle + * @param {EventBus} eventBus + * @param {Moddle} moddle */ export default function SubprocessCompatibility(eventBus, moddle) { this._eventBus = eventBus; @@ -21,11 +33,14 @@ export default function SubprocessCompatibility(eventBus, moddle) { var self = this; eventBus.on('import.render.start', 1500, function(e, context) { - self.handleImport(context.definitions); + self._handleImport(context.definitions); }); } -SubprocessCompatibility.prototype.handleImport = function(definitions) { +/** + * @param {ModdleElement} definitions + */ +SubprocessCompatibility.prototype._handleImport = function(definitions) { if (!definitions.diagrams) { return; } @@ -42,14 +57,12 @@ SubprocessCompatibility.prototype.handleImport = function(definitions) { self._processToDiagramMap[diagram.plane.bpmnElement.id] = diagram; }); - var newDiagrams = []; - definitions.diagrams.forEach(function(diagram) { - var createdDiagrams = self.createNewDiagrams(diagram.plane); - Array.prototype.push.apply(newDiagrams, createdDiagrams); - }); + var newDiagrams = definitions.diagrams + .filter(diagram => diagram.plane) + .flatMap(diagram => self._createNewDiagrams(diagram.plane)); newDiagrams.forEach(function(diagram) { - self.movePlaneElementsToOrigin(diagram.plane); + self._movePlaneElementsToOrigin(diagram.plane); }); }; @@ -57,29 +70,30 @@ SubprocessCompatibility.prototype.handleImport = function(definitions) { /** * Moves all DI elements from collapsed subprocesses to a new plane. * - * @param {Object} plane - * @return {Array} new diagrams created for the collapsed subprocesses + * @param {CanvasPlane} plane + * + * @return {ModdleElement[]} new diagrams created for the collapsed subprocesses */ -SubprocessCompatibility.prototype.createNewDiagrams = function(plane) { +SubprocessCompatibility.prototype._createNewDiagrams = function(plane) { var self = this; var collapsedElements = []; var elementsToMove = []; plane.get('planeElement').forEach(function(diElement) { - var bo = diElement.bpmnElement; + var businessObject = diElement.bpmnElement; - if (!bo) { + if (!businessObject) { return; } - var parent = bo.$parent; + var parent = businessObject.$parent; - if (is(bo, 'bpmn:SubProcess') && !diElement.isExpanded) { - collapsedElements.push(bo); + if (is(businessObject, 'bpmn:SubProcess') && !diElement.isExpanded) { + collapsedElements.push(businessObject); } - if (shouldMoveToPlane(bo, plane)) { + if (shouldMoveToPlane(businessObject, plane)) { // don't change the array while we iterate over it elementsToMove.push({ diElement: diElement, parent: parent }); @@ -90,9 +104,11 @@ SubprocessCompatibility.prototype.createNewDiagrams = function(plane) { // create new planes for all collapsed subprocesses, even when they are empty collapsedElements.forEach(function(element) { - if (!self._processToDiagramMap[element.id]) { - var diagram = self.createDiagram(element); + if (!self._processToDiagramMap[ element.id ]) { + var diagram = self._createDiagram(element); + self._processToDiagramMap[element.id] = diagram; + newDiagrams.push(diagram); } }); @@ -111,14 +127,18 @@ SubprocessCompatibility.prototype.createNewDiagrams = function(plane) { return; } - var diagram = self._processToDiagramMap[parent.id]; - self.moveToDiPlane(diElement, diagram.plane); + var diagram = self._processToDiagramMap[ parent.id ]; + + self._moveToDiPlane(diElement, diagram.plane); }); return newDiagrams; }; -SubprocessCompatibility.prototype.movePlaneElementsToOrigin = function(plane) { +/** + * @param {CanvasPlane} plane + */ +SubprocessCompatibility.prototype._movePlaneElementsToOrigin = function(plane) { var elements = plane.get('planeElement'); // get bounding box of all elements @@ -142,33 +162,50 @@ SubprocessCompatibility.prototype.movePlaneElementsToOrigin = function(plane) { }); }; - -SubprocessCompatibility.prototype.moveToDiPlane = function(diElement, newPlane) { +/** + * @param {ModdleElement} diElement + * @param {CanvasPlane} newPlane + */ +SubprocessCompatibility.prototype._moveToDiPlane = function(diElement, newPlane) { var containingDiagram = findRootDiagram(diElement); // remove DI from old Plane and add it to the new one var parentPlaneElement = containingDiagram.plane.get('planeElement'); + parentPlaneElement.splice(parentPlaneElement.indexOf(diElement), 1); + newPlane.get('planeElement').push(diElement); }; +/** + * @param {ModdleElement} businessObject + * + * @return {ModdleElement} + */ +SubprocessCompatibility.prototype._createDiagram = function(businessObject) { + var plane = this._moddle.create('bpmndi:BPMNPlane', { + bpmnElement: businessObject + }); -SubprocessCompatibility.prototype.createDiagram = function(bo) { - var plane = this._moddle.create('bpmndi:BPMNPlane', { bpmnElement: bo }); var diagram = this._moddle.create('bpmndi:BPMNDiagram', { plane: plane }); + plane.$parent = diagram; - plane.bpmnElement = bo; + + plane.bpmnElement = businessObject; + diagram.$parent = this._definitions; + this._definitions.diagrams.push(diagram); + return diagram; }; SubprocessCompatibility.$inject = [ 'eventBus', 'moddle' ]; -// helpers ////////////////////////// +// helpers ////////// function findRootDiagram(element) { if (is(element, 'bpmndi:BPMNDiagram')) { @@ -178,6 +215,11 @@ function findRootDiagram(element) { } } +/** + * @param {CanvasPlane} plane + * + * @return {Rect} + */ function getPlaneBounds(plane) { var planeTrbl = { top: Infinity, @@ -200,8 +242,14 @@ function getPlaneBounds(plane) { return asBounds(planeTrbl); } -function shouldMoveToPlane(bo, plane) { - var parent = bo.$parent; +/** + * @param {ModdleElement} businessObject + * @param {CanvasPlane} plane + * + * @return {boolean} + */ +function shouldMoveToPlane(businessObject, plane) { + var parent = businessObject.$parent; // don't move elements that are already on the plane if (!is(parent, 'bpmn:SubProcess') || parent === plane.bpmnElement) { @@ -210,7 +258,7 @@ function shouldMoveToPlane(bo, plane) { // dataAssociations are children of the subprocess but rendered on process level // cf. https://github.com/bpmn-io/bpmn-js/issues/1619 - if (isAny(bo, ['bpmn:DataInputAssociation', 'bpmn:DataOutputAssociation'])) { + if (isAny(businessObject, [ 'bpmn:DataInputAssociation', 'bpmn:DataOutputAssociation' ])) { return false; } diff --git a/lib/features/drilldown/index.js b/lib/features/drilldown/index.js index affd9add5b..fe32796a50 100644 --- a/lib/features/drilldown/index.js +++ b/lib/features/drilldown/index.js @@ -9,7 +9,7 @@ import DrilldownOverlayBehavior from './DrilldownOverlayBehavior'; export default { __depends__: [ OverlaysModule, ChangeSupportModule, RootElementsModule ], - __init__: [ 'drilldownBreadcrumbs', 'drilldownOverlayBehavior', 'drilldownCentering', 'subprocessCompatibility'], + __init__: [ 'drilldownBreadcrumbs', 'drilldownOverlayBehavior', 'drilldownCentering', 'subprocessCompatibility' ], drilldownBreadcrumbs: [ 'type', DrilldownBreadcrumbs ], drilldownCentering: [ 'type', DrilldownCentering ], drilldownOverlayBehavior: [ 'type', DrilldownOverlayBehavior ], diff --git a/lib/features/editor-actions/BpmnEditorActions.js b/lib/features/editor-actions/BpmnEditorActions.js index b558588473..0af5410851 100644 --- a/lib/features/editor-actions/BpmnEditorActions.js +++ b/lib/features/editor-actions/BpmnEditorActions.js @@ -1,4 +1,4 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import EditorActions from 'diagram-js/lib/features/editor-actions/EditorActions'; @@ -10,6 +10,9 @@ import { getBBox } from 'diagram-js/lib/util/Elements'; +/** + * @typedef {import('didi').Injector} Injector + */ /** * Registers and executes BPMN specific editor actions. @@ -51,6 +54,7 @@ BpmnEditorActions.prototype._registerDefaultActions = function(injector) { var directEditing = injector.get('directEditing', false); var searchPad = injector.get('searchPad', false); var modeling = injector.get('modeling', false); + var contextPad = injector.get('contextPad', false); // (2) check components and register actions @@ -174,4 +178,10 @@ BpmnEditorActions.prototype._registerDefaultActions = function(injector) { }); } -}; \ No newline at end of file + if (selection && contextPad) { + this._registerAction('replaceElement', function(event) { + contextPad.triggerEntry('replace', 'click', event); + }); + } + +}; diff --git a/lib/features/grid-snapping/BpmnGridSnapping.js b/lib/features/grid-snapping/BpmnGridSnapping.js index f18146d280..140d73f9f0 100644 --- a/lib/features/grid-snapping/BpmnGridSnapping.js +++ b/lib/features/grid-snapping/BpmnGridSnapping.js @@ -1,5 +1,12 @@ import { isAny } from '../modeling/util/ModelingUtil'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + */ + +/** + * @param {EventBus} eventBus + */ export default function BpmnGridSnapping(eventBus) { eventBus.on([ 'create.init', diff --git a/lib/features/grid-snapping/behavior/AutoPlaceBehavior.js b/lib/features/grid-snapping/behavior/GridSnappingAutoPlaceBehavior.js similarity index 59% rename from lib/features/grid-snapping/behavior/AutoPlaceBehavior.js rename to lib/features/grid-snapping/behavior/GridSnappingAutoPlaceBehavior.js index 63c9e5d83b..7e88f52056 100644 --- a/lib/features/grid-snapping/behavior/AutoPlaceBehavior.js +++ b/lib/features/grid-snapping/behavior/GridSnappingAutoPlaceBehavior.js @@ -3,16 +3,28 @@ import { getNewShapePosition } from '../../auto-place/BpmnAutoPlaceUtil'; import { getMid } from 'diagram-js/lib/layout/LayoutUtil'; import { is } from '../../../util/ModelUtil'; -var HIGH_PRIORITY = 2000; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/features/grid-snapping/GridSnapping').default} GridSnapping + * + * @typedef {import('diagram-js/lib/util/Types').Axis} Axis + */ +var HIGH_PRIORITY = 2000; -export default function AutoPlaceBehavior(eventBus, gridSnapping) { +/** + * @param {EventBus} eventBus + * @param {GridSnapping} gridSnapping + * @param {ElementRegistry} elementRegistry + */ +export default function GridSnappingAutoPlaceBehavior(eventBus, gridSnapping, elementRegistry) { eventBus.on('autoPlace', HIGH_PRIORITY, function(context) { var source = context.source, sourceMid = getMid(source), shape = context.shape; - var position = getNewShapePosition(source, shape); + var position = getNewShapePosition(source, shape, elementRegistry); [ 'x', 'y' ].forEach(function(axis) { var options = {}; @@ -47,13 +59,19 @@ export default function AutoPlaceBehavior(eventBus, gridSnapping) { }); } -AutoPlaceBehavior.$inject = [ +GridSnappingAutoPlaceBehavior.$inject = [ 'eventBus', - 'gridSnapping' + 'gridSnapping', + 'elementRegistry' ]; // helpers ////////// +/** + * @param {Axis} axis + * + * @return {boolean} + */ function isHorizontal(axis) { return axis === 'x'; } \ No newline at end of file diff --git a/lib/features/grid-snapping/behavior/LayoutConnectionBehavior.js b/lib/features/grid-snapping/behavior/GridSnappingLayoutConnectionBehavior.js similarity index 71% rename from lib/features/grid-snapping/behavior/LayoutConnectionBehavior.js rename to lib/features/grid-snapping/behavior/GridSnappingLayoutConnectionBehavior.js index 828ddb3022..89352f68d8 100644 --- a/lib/features/grid-snapping/behavior/LayoutConnectionBehavior.js +++ b/lib/features/grid-snapping/behavior/GridSnappingLayoutConnectionBehavior.js @@ -1,4 +1,4 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; @@ -8,13 +8,25 @@ import { assign } from 'min-dash'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/grid-snapping/GridSnapping').default} GridSnapping + * @typedef {import('../../modeling/Modeling').default} Modeling + * + * @typedef {import('diagram-js/lib/util/Types').Point} Point + */ + var HIGH_PRIORITY = 3000; /** * Snaps connections with Manhattan layout. + * + * @param {EventBus} eventBus + * @param {GridSnapping} gridSnapping + * @param {Modeling} modeling */ -export default function LayoutConnectionBehavior(eventBus, gridSnapping, modeling) { +export default function GridSnappingLayoutConnectionBehavior(eventBus, gridSnapping, modeling) { CommandInterceptor.call(this, eventBus); this._gridSnapping = gridSnapping; @@ -42,22 +54,22 @@ export default function LayoutConnectionBehavior(eventBus, gridSnapping, modelin }); } -LayoutConnectionBehavior.$inject = [ +GridSnappingLayoutConnectionBehavior.$inject = [ 'eventBus', 'gridSnapping', 'modeling' ]; -inherits(LayoutConnectionBehavior, CommandInterceptor); +inherits(GridSnappingLayoutConnectionBehavior, CommandInterceptor); /** * Snap middle segments of a given connection. * - * @param {Array} waypoints + * @param {Point[]} waypoints * - * @returns {Array} + * @return {Point[]} */ -LayoutConnectionBehavior.prototype.snapMiddleSegments = function(waypoints) { +GridSnappingLayoutConnectionBehavior.prototype.snapMiddleSegments = function(waypoints) { var gridSnapping = this._gridSnapping, snapped; @@ -80,9 +92,9 @@ LayoutConnectionBehavior.prototype.snapMiddleSegments = function(waypoints) { /** * Check whether a connection has a middle segments. * - * @param {Array} waypoints + * @param {Point[]} waypoints * - * @returns {boolean} + * @return {boolean} */ function hasMiddleSegments(waypoints) { return waypoints.length > 3; @@ -93,7 +105,7 @@ function hasMiddleSegments(waypoints) { * * @param {string} aligned * - * @returns {boolean} + * @return {boolean} */ function horizontallyAligned(aligned) { return aligned === 'h'; @@ -104,7 +116,7 @@ function horizontallyAligned(aligned) { * * @param {string} aligned * - * @returns {boolean} + * @return {boolean} */ function verticallyAligned(aligned) { return aligned === 'v'; @@ -113,9 +125,9 @@ function verticallyAligned(aligned) { /** * Get middle segments from a given connection. * - * @param {Array} waypoints + * @param {Point[]} waypoints * - * @returns {Array} + * @return {Point[]} */ function snapSegment(gridSnapping, segmentStart, segmentEnd) { diff --git a/lib/features/grid-snapping/behavior/CreateParticipantBehavior.js b/lib/features/grid-snapping/behavior/GridSnappingParticipantBehavior.js similarity index 58% rename from lib/features/grid-snapping/behavior/CreateParticipantBehavior.js rename to lib/features/grid-snapping/behavior/GridSnappingParticipantBehavior.js index b20ee0a140..98852fa0a5 100644 --- a/lib/features/grid-snapping/behavior/CreateParticipantBehavior.js +++ b/lib/features/grid-snapping/behavior/GridSnappingParticipantBehavior.js @@ -1,9 +1,19 @@ import { is } from '../../../util/ModelUtil'; -var HIGHER_PRIORITY = 1750; +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/grid-snapping/GridSnapping').default} GridSnapping + */ +var HIGHER_PRIORITY = 1750; -export default function CreateParticipantBehavior(canvas, eventBus, gridSnapping) { +/** + * @param {Canvas} canvas + * @param {EventBus} eventBus + * @param {GridSnapping} gridSnapping + */ +export default function GridSnappingParticipantBehavior(canvas, eventBus, gridSnapping) { eventBus.on([ 'create.start', 'shape.move.start' @@ -29,7 +39,7 @@ export default function CreateParticipantBehavior(canvas, eventBus, gridSnapping }); } -CreateParticipantBehavior.$inject = [ +GridSnappingParticipantBehavior.$inject = [ 'canvas', 'eventBus', 'gridSnapping' diff --git a/lib/features/grid-snapping/behavior/index.js b/lib/features/grid-snapping/behavior/index.js index cd1800c514..68038c1302 100644 --- a/lib/features/grid-snapping/behavior/index.js +++ b/lib/features/grid-snapping/behavior/index.js @@ -1,14 +1,14 @@ -import AutoPlaceBehavior from './AutoPlaceBehavior'; -import CreateParticipantBehavior from './CreateParticipantBehavior'; -import LayoutConnectionBehavior from './LayoutConnectionBehavior'; +import GridSnappingAutoPlaceBehavior from './GridSnappingAutoPlaceBehavior'; +import GridSnappingParticipantBehavior from './GridSnappingParticipantBehavior'; +import GridSnappingLayoutConnectionBehavior from './GridSnappingLayoutConnectionBehavior'; export default { __init__: [ 'gridSnappingAutoPlaceBehavior', - 'gridSnappingCreateParticipantBehavior', + 'gridSnappingParticipantBehavior', 'gridSnappingLayoutConnectionBehavior', ], - gridSnappingAutoPlaceBehavior: [ 'type', AutoPlaceBehavior ], - gridSnappingCreateParticipantBehavior: [ 'type', CreateParticipantBehavior ], - gridSnappingLayoutConnectionBehavior: [ 'type', LayoutConnectionBehavior ] + gridSnappingAutoPlaceBehavior: [ 'type', GridSnappingAutoPlaceBehavior ], + gridSnappingParticipantBehavior: [ 'type', GridSnappingParticipantBehavior ], + gridSnappingLayoutConnectionBehavior: [ 'type', GridSnappingLayoutConnectionBehavior ] }; \ No newline at end of file diff --git a/lib/features/interaction-events/BpmnInteractionEvents.js b/lib/features/interaction-events/BpmnInteractionEvents.js index ada086e8e8..44b9a55be6 100644 --- a/lib/features/interaction-events/BpmnInteractionEvents.js +++ b/lib/features/interaction-events/BpmnInteractionEvents.js @@ -1,6 +1,17 @@ import { is } from '../../util/ModelUtil'; -import { isExpanded } from '../../util/DiUtil'; +import { + isExpanded, + isHorizontal +} from '../../util/DiUtil'; + +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/interaction-events/InteractionEvents').default} InteractionEvents + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + */ var LABEL_WIDTH = 30, LABEL_HEIGHT = 30; @@ -26,22 +37,18 @@ export default function BpmnInteractionEvents(eventBus, interactionEvents) { gfx = context.gfx; if (is(element, 'bpmn:Lane')) { - return self.createParticipantHit(element, gfx); - } else - - if (is(element, 'bpmn:Participant')) { + return self._createParticipantHit(element, gfx); + } else if (is(element, 'bpmn:Participant')) { if (isExpanded(element)) { - return self.createParticipantHit(element, gfx); + return self._createParticipantHit(element, gfx); } else { - return self.createDefaultHit(element, gfx); + return self._createDefaultHit(element, gfx); } - } else - - if (is(element, 'bpmn:SubProcess')) { + } else if (is(element, 'bpmn:SubProcess')) { if (isExpanded(element)) { - return self.createSubProcessHit(element, gfx); + return self._createSubProcessHit(element, gfx); } else { - return self.createDefaultHit(element, gfx); + return self._createDefaultHit(element, gfx); } } }); @@ -53,8 +60,13 @@ BpmnInteractionEvents.$inject = [ 'interactionEvents' ]; - -BpmnInteractionEvents.prototype.createDefaultHit = function(element, gfx) { +/** + * @param {Element} element + * @param {SVGElement} gfx + * + * @return {boolean} + */ +BpmnInteractionEvents.prototype._createDefaultHit = function(element, gfx) { this._interactionEvents.removeHits(gfx); this._interactionEvents.createDefaultHit(element, gfx); @@ -63,11 +75,23 @@ BpmnInteractionEvents.prototype.createDefaultHit = function(element, gfx) { return true; }; -BpmnInteractionEvents.prototype.createParticipantHit = function(element, gfx) { +/** + * @param {Shape} element + * @param {SVGElement} gfx + * + * @return {boolean} + */ +BpmnInteractionEvents.prototype._createParticipantHit = function(element, gfx) { // remove existing hits this._interactionEvents.removeHits(gfx); + // add body hit + this._interactionEvents.createBoxHit(gfx, 'no-move', { + width: element.width, + height: element.height + }); + // add outline hit this._interactionEvents.createBoxHit(gfx, 'click-stroke', { width: element.width, @@ -75,20 +99,37 @@ BpmnInteractionEvents.prototype.createParticipantHit = function(element, gfx) { }); // add label hit - this._interactionEvents.createBoxHit(gfx, 'all', { + var box = isHorizontal(element) ? { width: LABEL_WIDTH, height: element.height - }); + } : { + width: element.width, + height: LABEL_HEIGHT + }; + + this._interactionEvents.createBoxHit(gfx, 'all', box); // indicate that we created a hit return true; }; -BpmnInteractionEvents.prototype.createSubProcessHit = function(element, gfx) { +/** + * @param {Shape} element + * @param {SVGElement} gfx + * + * @return {boolean} + */ +BpmnInteractionEvents.prototype._createSubProcessHit = function(element, gfx) { // remove existing hits this._interactionEvents.removeHits(gfx); + // add body hit + this._interactionEvents.createBoxHit(gfx, 'no-move', { + width: element.width, + height: element.height + }); + // add outline hit this._interactionEvents.createBoxHit(gfx, 'click-stroke', { width: element.width, diff --git a/lib/features/keyboard/BpmnKeyboardBindings.js b/lib/features/keyboard/BpmnKeyboardBindings.js index fc940ff764..60236de7aa 100644 --- a/lib/features/keyboard/BpmnKeyboardBindings.js +++ b/lib/features/keyboard/BpmnKeyboardBindings.js @@ -1,7 +1,12 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import KeyboardBindings from 'diagram-js/lib/features/keyboard/KeyboardBindings'; +/** + * @typedef {import('didi').Injector} Injector + * @typedef {import('diagram-js/lib/features/editor-actions/EditorActions').default} EditorActions + * @typedef {import('diagram-js/lib/features/keyboard/Keyboard').default} Keyboard + */ /** * BPMN 2.0 specific keyboard bindings. @@ -50,7 +55,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio var event = context.keyEvent; - if (keyboard.isKey(['a', 'A'], event) && keyboard.isCmd(event)) { + if (keyboard.isKey([ 'a', 'A' ], event) && keyboard.isCmd(event)) { editorActions.trigger('selectElements'); return true; @@ -63,7 +68,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio var event = context.keyEvent; - if (keyboard.isKey(['f', 'F'], event) && keyboard.isCmd(event)) { + if (keyboard.isKey([ 'f', 'F' ], event) && keyboard.isCmd(event)) { editorActions.trigger('find'); return true; @@ -80,7 +85,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio return; } - if (keyboard.isKey(['s', 'S'], event)) { + if (keyboard.isKey([ 's', 'S' ], event)) { editorActions.trigger('spaceTool'); return true; @@ -97,7 +102,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio return; } - if (keyboard.isKey(['l', 'L'], event)) { + if (keyboard.isKey([ 'l', 'L' ], event)) { editorActions.trigger('lassoTool'); return true; @@ -114,7 +119,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio return; } - if (keyboard.isKey(['h', 'H'], event)) { + if (keyboard.isKey([ 'h', 'H' ], event)) { editorActions.trigger('handTool'); return true; @@ -131,7 +136,7 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio return; } - if (keyboard.isKey(['c', 'C'], event)) { + if (keyboard.isKey([ 'c', 'C' ], event)) { editorActions.trigger('globalConnectTool'); return true; @@ -148,11 +153,28 @@ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActio return; } - if (keyboard.isKey(['e', 'E'], event)) { + if (keyboard.isKey([ 'e', 'E' ], event)) { editorActions.trigger('directEditing'); return true; } }); + // activate replace element + // R + addListener('replaceElement', function(context) { + + var event = context.keyEvent; + + if (keyboard.hasModifier(event)) { + return; + } + + if (keyboard.isKey([ 'r', 'R' ], event)) { + editorActions.trigger('replaceElement', event); + + return true; + } + }); + }; \ No newline at end of file diff --git a/lib/features/label-editing/LabelEditingPreview.js b/lib/features/label-editing/LabelEditingPreview.js index 4ce2cf87cc..843677686d 100644 --- a/lib/features/label-editing/LabelEditingPreview.js +++ b/lib/features/label-editing/LabelEditingPreview.js @@ -14,13 +14,21 @@ import { translate } from 'diagram-js/lib/util/SvgTransformUtil'; +/** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../../draw/PathMap').default} PathMap + */ + var MARKER_HIDDEN = 'djs-element-hidden', MARKER_LABEL_HIDDEN = 'djs-label-hidden'; - -export default function LabelEditingPreview( - eventBus, canvas, elementRegistry, - pathMap) { +/** + * @param {EventBus} eventBus + * @param {Canvas} canvas + * @param {PathMap} pathMap + */ +export default function LabelEditingPreview(eventBus, canvas, pathMap) { var self = this; @@ -71,7 +79,8 @@ export default function LabelEditingPreview( } else if (is(element, 'bpmn:Task') || is(element, 'bpmn:CallActivity') || is(element, 'bpmn:SubProcess') || - is(element, 'bpmn:Participant')) { + is(element, 'bpmn:Participant') || + is(element, 'bpmn:Lane')) { canvas.addMarker(element, MARKER_LABEL_HIDDEN); } }); @@ -124,12 +133,11 @@ export default function LabelEditingPreview( LabelEditingPreview.$inject = [ 'eventBus', 'canvas', - 'elementRegistry', 'pathMap' ]; -// helpers /////////////////// +// helpers ////////// function getStrokeColor(element, defaultColor) { var di = getDi(element); diff --git a/lib/features/label-editing/LabelEditingProvider.js b/lib/features/label-editing/LabelEditingProvider.js index 54c31cd503..c27a4bd885 100644 --- a/lib/features/label-editing/LabelEditingProvider.js +++ b/lib/features/label-editing/LabelEditingProvider.js @@ -4,28 +4,66 @@ import { import { getLabel -} from './LabelUtil'; +} from '../../util/LabelUtil'; import { - getBusinessObject, is } from '../../util/ModelUtil'; -import { - createCategoryValue -} from '../modeling/behavior/util/CategoryUtil'; - import { isAny } from '../modeling/util/ModelingUtil'; -import { isExpanded } from '../../util/DiUtil'; + +import { + isExpanded, + isHorizontal +} from '../../util/DiUtil'; import { getExternalLabelMid, isLabelExternal, hasExternalLabel, - isLabel + isLabel, + DEFAULT_LABEL_SIZE } from '../../util/LabelUtil'; +import { + TEXT_ANNOTATION_PADDING +} from '../../util/AnnotationUtil'; +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../modeling/BpmnFactory').default} BpmnFactory + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js-direct-editing/lib/DirectEditing').default} DirectEditing + * @typedef {import('../modeling/Modeling').default} Modeling + * @typedef {import('diagram-js/lib/features/resize/ResizeHandles').default} ResizeHandles + * @typedef {import('../../draw/TextRenderer').default} TextRenderer + * + * @typedef {import('../../model/Types').Element} Element + * + * @typedef { { + * bounds: { + * x: number; + * y: number; + * width: number; + * height: number; + * minWidth?: number; + * minHeight?: number; + * }; + * style: Object; + * } } DirectEditingContext + */ + +var HIGH_PRIORITY = 2000; + +/** + * @param {EventBus} eventBus + * @param {BpmnFactory} bpmnFactory + * @param {Canvas} canvas + * @param {DirectEditing} directEditing + * @param {Modeling} modeling + * @param {ResizeHandles} resizeHandles + * @param {TextRenderer} textRenderer + */ export default function LabelEditingProvider( eventBus, bpmnFactory, canvas, directEditing, modeling, resizeHandles, textRenderer) { @@ -49,14 +87,24 @@ export default function LabelEditingProvider( 'drag.init', 'element.mousedown', 'popupMenu.open', - 'root.set' - ], function(event) { - + 'root.set', + 'selection.changed' + ], function() { if (directEditing.isActive()) { directEditing.complete(); } }); + eventBus.on([ + 'shape.remove', + 'connection.remove' + ], HIGH_PRIORITY, function(event) { + + if (directEditing.isActive(event.element)) { + directEditing.cancel(); + } + }); + // cancel on command stack changes eventBus.on([ 'commandStack.changed' ], function(e) { if (directEditing.isActive()) { @@ -81,7 +129,7 @@ export default function LabelEditingProvider( // break for desworkflowediting on mobile devices // as it breaks the user interaction workflow - // TODO(nre): we should temporarily focus the edited element + // TODO(nikku): we should temporarily focus the edited element // here and release the focused viewport after the direct edit // operation is finished if (isTouch) { @@ -106,8 +154,7 @@ export default function LabelEditingProvider( function activateDirectEdit(element, force) { if (force || - isAny(element, [ 'bpmn:Task', 'bpmn:TextAnnotation' ]) || - isCollapsedSubProcess(element)) { + isAny(element, [ 'bpmn:Activity', 'bpmn:Event', 'bpmn:TextAnnotation', 'bpmn:Participant' ])) { directEditing.activate(element); } @@ -129,9 +176,16 @@ LabelEditingProvider.$inject = [ /** * Activate direct editing for activities and text annotations. * - * @param {djs.model.Base} element + * @param {Element} element * - * @return {Object} an object with properties bounds (position and size), text and options + * @return { { + * text: string; + * options?: { + * autoResize?: boolean; + * centerVertically?: boolean; + * resizable?: boolean; + * } + * } & DirectEditingContext } */ LabelEditingProvider.prototype.activate = function(element) { @@ -152,6 +206,13 @@ LabelEditingProvider.prototype.activate = function(element) { assign(context, bounds); var options = {}; + var style = context.style || {}; + + // Remove background and border + assign(style, { + backgroundColor: null, + border: null + }); // tasks if ( @@ -171,7 +232,14 @@ LabelEditingProvider.prototype.activate = function(element) { // external labels if (isLabelExternal(element)) { assign(options, { - autoResize: true + resizable: true, + autoResize: true, + }); + + // keep background and border for external labels + assign(style, { + backgroundColor: '#ffffff', + border: '1px solid #ccc' }); } @@ -181,10 +249,17 @@ LabelEditingProvider.prototype.activate = function(element) { resizable: true, autoResize: true }); + + // keep background and border for text annotations + assign(style, { + backgroundColor: '#ffffff', + border: '1px solid #ccc' + }); } assign(context, { - options: options + options: options, + style: style }); return context; @@ -192,12 +267,11 @@ LabelEditingProvider.prototype.activate = function(element) { /** - * Get the editing bounding box based on the element's size and position + * Get the editing bounding box based on the element's size and position. * - * @param {djs.model.Base} element + * @param {Element} element * - * @return {Object} an object containing information about position - * and size (fixed or minimum and/or maximum) + * @return {DirectEditingContext} */ LabelEditingProvider.prototype.getEditingBBox = function(element) { var canvas = this._canvas; @@ -232,13 +306,47 @@ LabelEditingProvider.prototype.getEditingBBox = function(element) { // adjust for expanded pools AND lanes if (is(element, 'bpmn:Lane') || isExpandedPool(element)) { + var isHorizontalLane = isHorizontal(element); - assign(bounds, { + var laneBounds = isHorizontalLane ? { width: bbox.height, height: 30 * zoom, x: bbox.x - bbox.height / 2 + (15 * zoom), y: mid.y - (30 * zoom) / 2 + } : { + width: bbox.width, + height: 30 * zoom + }; + + assign(bounds, laneBounds); + + assign(style, { + fontSize: defaultFontSize + 'px', + lineHeight: defaultLineHeight, + paddingTop: (7 * zoom) + 'px', + paddingBottom: (7 * zoom) + 'px', + paddingLeft: (5 * zoom) + 'px', + paddingRight: (5 * zoom) + 'px', + transform: isHorizontalLane ? 'rotate(-90deg)' : null }); + } + + + // internal labels for collapsed participants + if (isCollapsedPool(element)) { + var isHorizontalPool = isHorizontal(element); + + var poolBounds = isHorizontalPool ? { + width: bbox.width, + height: bbox.height + } : { + width: bbox.height, + height: bbox.width, + x: mid.x - bbox.height / 2, + y: mid.y - bbox.width / 2 + }; + + assign(bounds, poolBounds); assign(style, { fontSize: defaultFontSize + 'px', @@ -247,15 +355,14 @@ LabelEditingProvider.prototype.getEditingBBox = function(element) { paddingBottom: (7 * zoom) + 'px', paddingLeft: (5 * zoom) + 'px', paddingRight: (5 * zoom) + 'px', - transform: 'rotate(-90deg)' + transform: isHorizontalPool ? null : 'rotate(-90deg)' }); } - // internal labels for tasks and collapsed call activities, - // sub processes and participants - if (isAny(element, [ 'bpmn:Task', 'bpmn:CallActivity']) || - isCollapsedPool(element) || + // internal labels for tasks and collapsed call activities + // and sub processes + if (isAny(element, [ 'bpmn:Task', 'bpmn:CallActivity' ]) || isCollapsedSubProcess(element)) { assign(bounds, { @@ -291,24 +398,23 @@ LabelEditingProvider.prototype.getEditingBBox = function(element) { }); } - var width = 90 * zoom, - paddingTop = 7 * zoom, - paddingBottom = 4 * zoom; + // making sure that editing box is correct + var BORDER_WIDTH = 1; + + var width = bbox.width + 2 * BORDER_WIDTH; // external labels for events, data elements, gateways, groups and connections if (target.labelTarget) { assign(bounds, { width: width, - height: bbox.height + paddingTop + paddingBottom, - x: mid.x - width / 2, - y: bbox.y - paddingTop + height: bbox.height + 2 * BORDER_WIDTH, + x: bbox.x - BORDER_WIDTH, + y: bbox.y - BORDER_WIDTH }); assign(style, { fontSize: externalFontSize + 'px', - lineHeight: externalLineHeight, - paddingTop: paddingTop + 'px', - paddingBottom: paddingBottom + 'px' + lineHeight: externalLineHeight }); } @@ -326,38 +432,39 @@ LabelEditingProvider.prototype.getEditingBBox = function(element) { height: 0 }); - var height = externalFontSize + paddingTop + paddingBottom; + var height = externalFontSize; + var newLabelWidth = DEFAULT_LABEL_SIZE.width * zoom + 2 * BORDER_WIDTH; assign(bounds, { - width: width, - height: height, - x: absoluteBBox.x - width / 2, - y: absoluteBBox.y - height / 2 + width: newLabelWidth, + height: height + 2 * BORDER_WIDTH, + x: absoluteBBox.x - newLabelWidth / 2, + y: absoluteBBox.y - height / 2 - BORDER_WIDTH }); assign(style, { fontSize: externalFontSize + 'px', - lineHeight: externalLineHeight, - paddingTop: paddingTop + 'px', - paddingBottom: paddingBottom + 'px' + lineHeight: externalLineHeight }); } // text annotations if (is(element, 'bpmn:TextAnnotation')) { assign(bounds, { - width: bbox.width, - height: bbox.height, + width: bbox.width + 2 * BORDER_WIDTH, + height: bbox.height + 2 * BORDER_WIDTH, + x: bbox.x - BORDER_WIDTH, + y: bbox.y - BORDER_WIDTH, minWidth: 30 * zoom, minHeight: 10 * zoom }); assign(style, { textAlign: 'left', - paddingTop: (5 * zoom) + 'px', - paddingBottom: (7 * zoom) + 'px', - paddingLeft: (7 * zoom) + 'px', - paddingRight: (5 * zoom) + 'px', + paddingTop: (TEXT_ANNOTATION_PADDING * zoom) + 'px', + paddingBottom: (TEXT_ANNOTATION_PADDING * zoom) + 'px', + paddingLeft: (TEXT_ANNOTATION_PADDING * zoom) + 'px', + paddingRight: (TEXT_ANNOTATION_PADDING * zoom) + 'px', fontSize: defaultFontSize + 'px', lineHeight: defaultLineHeight }); @@ -386,23 +493,6 @@ LabelEditingProvider.prototype.update = function( }; } - if (is(element, 'bpmn:Group')) { - - var businessObject = getBusinessObject(element); - - // initialize categoryValue if not existing - if (!businessObject.categoryValueRef) { - - var rootElement = this._canvas.getRootElement(), - definitions = getBusinessObject(rootElement).$parent; - - var categoryValue = createCategoryValue(definitions, this._bpmnFactory); - - getBusinessObject(element).categoryValueRef = categoryValue; - } - - } - if (isEmptyText(newLabel)) { newLabel = null; } @@ -412,7 +502,7 @@ LabelEditingProvider.prototype.update = function( -// helpers ////////////////////// +// helpers ////////// function isCollapsedSubProcess(element) { return is(element, 'bpmn:SubProcess') && !isExpanded(element); diff --git a/lib/features/label-editing/LabelUtil.js b/lib/features/label-editing/LabelUtil.js index 25f75bd599..b146b6ee4e 100644 --- a/lib/features/label-editing/LabelUtil.js +++ b/lib/features/label-editing/LabelUtil.js @@ -1,67 +1,4 @@ -import { is } from '../../util/ModelUtil'; - -function getLabelAttr(semantic) { - if ( - is(semantic, 'bpmn:FlowElement') || - is(semantic, 'bpmn:Participant') || - is(semantic, 'bpmn:Lane') || - is(semantic, 'bpmn:SequenceFlow') || - is(semantic, 'bpmn:MessageFlow') || - is(semantic, 'bpmn:DataInput') || - is(semantic, 'bpmn:DataOutput') - ) { - return 'name'; - } - - if (is(semantic, 'bpmn:TextAnnotation')) { - return 'text'; - } - - if (is(semantic, 'bpmn:Group')) { - return 'categoryValueRef'; - } -} - -function getCategoryValue(semantic) { - var categoryValueRef = semantic['categoryValueRef']; - - if (!categoryValueRef) { - return ''; - } - - - return categoryValueRef.value || ''; -} - -export function getLabel(element) { - var semantic = element.businessObject, - attr = getLabelAttr(semantic); - - if (attr) { - - if (attr === 'categoryValueRef') { - - return getCategoryValue(semantic); - } - - return semantic[attr] || ''; - } -} - - -export function setLabel(element, text, isExternal) { - var semantic = element.businessObject, - attr = getLabelAttr(semantic); - - if (attr) { - - if (attr === 'categoryValueRef') { - semantic['categoryValueRef'].value = text; - } else { - semantic[attr] = text; - } - - } - - return element; -} \ No newline at end of file +export { + getLabel, + setLabel +} from '../../util/LabelUtil'; diff --git a/lib/features/label-editing/cmd/UpdateLabelHandler.js b/lib/features/label-editing/cmd/UpdateLabelHandler.js index 35159c2a18..ed921f497f 100644 --- a/lib/features/label-editing/cmd/UpdateLabelHandler.js +++ b/lib/features/label-editing/cmd/UpdateLabelHandler.js @@ -1,7 +1,7 @@ import { setLabel, getLabel -} from '../LabelUtil'; +} from '../../../util/LabelUtil'; import { getExternalLabelMid, @@ -11,7 +11,6 @@ import { } from '../../../util/LabelUtil'; import { - getDi, is } from '../../../util/ModelUtil'; @@ -20,41 +19,29 @@ var NULL_DIMENSIONS = { height: 0 }; +/** + * @typedef {import('../../modeling/Modeling').default} Modeling + * @typedef {import('../../../draw/TextRenderer').default} TextRenderer + * @typedef {import('../../modeling/BpmnFactory').default} BpmnFactory + * + * @typedef {import('../../../model/Types').Element} Element + */ /** * A handler that updates the text of a BPMN element. + * + * @param {Modeling} modeling + * @param {TextRenderer} textRenderer + * @param {BpmnFactory} bpmnFactory */ export default function UpdateLabelHandler(modeling, textRenderer, bpmnFactory) { - /** - * Creates an empty `diLabel` attribute for embedded labels. - * - * @param {djs.model.Base} element - * @param {string} text - */ - function ensureInternalLabelDi(element, text) { - if (isLabelExternal(element)) { - return; - } - - var di = getDi(element); - - if (text && !di.label) { - di.label = bpmnFactory.create('bpmndi:BPMNLabel'); - } - - if (!text && di.label) { - di.label = null; - } - } - - /** * Set the label and return the changed elements. * * Element parameter can be label itself or connection (i.e. sequence flow). * - * @param {djs.model.Base} element + * @param {Element} element * @param {string} text */ function setText(element, text) { @@ -66,8 +53,6 @@ export default function UpdateLabelHandler(modeling, textRenderer, bpmnFactory) setLabel(label, text, labelTarget !== label); - ensureInternalLabelDi(element, text); - return [ label, labelTarget ]; } @@ -129,7 +114,7 @@ export default function UpdateLabelHandler(modeling, textRenderer, bpmnFactory) return; } - var text = getLabel(label); + var text = getLabel(element); // resize element based on label _or_ pre-defined bounds if (typeof newBounds === 'undefined') { @@ -158,7 +143,7 @@ UpdateLabelHandler.$inject = [ ]; -// helpers /////////////////////// +// helpers ////////// function isEmptyText(label) { return !label || !label.trim(); diff --git a/lib/features/label-link/LabelLink.js b/lib/features/label-link/LabelLink.js new file mode 100644 index 0000000000..4cbbae9623 --- /dev/null +++ b/lib/features/label-link/LabelLink.js @@ -0,0 +1,206 @@ +import { queryAll as domQueryAll } from 'min-dom'; + +import { + append as svgAppend, + attr as svgAttr, + remove as svgRemove, +} from 'tiny-svg'; + +import { createLine, updateLine } from 'diagram-js/lib/util/RenderUtil'; +import { getMid, getElementLineIntersection } from 'diagram-js/lib/layout/LayoutUtil'; +import { getDistancePointPoint } from 'diagram-js/lib/features/bendpoints/GeometricUtil'; +import { isLabel } from 'diagram-js/lib/util/ModelUtil'; + +import { isAny } from '../modeling/util/ModelingUtil'; +import { getRoundRectPath, getCirclePath } from '../../draw/BpmnRenderUtil'; + +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/GraphicsFactory').default} GraphicsFactory + * @typedef {import('../outline/OutlineProvider').default} Outline + * @typedef {import('diagram-js/lib/features/selection').default} Selection + * + * @typedef {import('diagram-js/lib/model/Types').Element} Element + */ + +const ALLOWED_ELEMENTS = [ 'bpmn:Event', 'bpmn:SequenceFlow', 'bpmn:Gateway' ]; + +const LINE_STYLE = { + class: 'bjs-label-link', + stroke: 'var(--element-selected-outline-secondary-stroke-color)', + strokeDasharray: '5, 5', +}; + +const DISTANCE_THRESHOLD = 15; +const PATH_OFFSET = 2; + +/** + * Render a line between an external label and its target element, + * when either is selected. + * + * @param {EventBus} eventBus + * @param {Canvas} canvas + * @param {GraphicsFactory} graphicsFactory + * @param {Outline} outline + */ +export default function LabelLink(eventBus, canvas, graphicsFactory, outline, selection) { + + const layer = canvas.getLayer('overlays'); + + eventBus.on([ 'selection.changed', 'shape.changed' ], function() { + cleanUp(); + }); + + eventBus.on('selection.changed', function({ newSelection }) { + + const allowedElements = newSelection.filter(element => isAny(element, ALLOWED_ELEMENTS)); + + if (allowedElements.length === 1) { + const element = allowedElements[0]; + if (isLabel(element)) { + createLink(element, element.labelTarget, newSelection); + } else if (element.labels?.length) { + createLink(element.labels[0], element, newSelection); + } + } + + // Only allowed when both label and its target are selected + if (allowedElements.length === 2) { + const label = allowedElements.find(isLabel); + const target = allowedElements.find(el => el.labels?.includes(label)); + if (label && target) { + createLink(label, target, newSelection); + } + } + }); + + eventBus.on('shape.changed', function({ element }) { + + if (!isAny(element, ALLOWED_ELEMENTS) || !isElementSelected(element)) { + return; + } + + if (isLabel(element)) { + createLink(element, element.labelTarget, selection.get()); + } else if (element.labels?.length) { + createLink(element.labels[0], element, selection.get()); + } + }); + + /** + * Render a line between an external label and its target. + * + * @param {Element} label + * @param {Element} target + * @param {Element[]} selection + */ + function createLink(label, target, selection = []) { + + // Create an auxiliary line between label and target mid points + const line = createLine( + [ getMid(target), getMid(label) ], + LINE_STYLE + ); + const linePath = line.getAttribute('d'); + + // Calculate the intersection point between line and label + const labelSelected = selection.includes(label); + const labelPath = labelSelected ? getElementOutlinePath(label) : getElementPath(label); + const labelInter = getElementLineIntersection(labelPath, linePath); + + // Label on top of the target + if (!labelInter) { + return; + } + + // Calculate the intersection point between line and label + // If the target is a sequence flow, there is no intersection, + // so we link to the middle of it. + const targetSelected = selection.includes(target); + const targetPath = targetSelected ? getElementOutlinePath(target) : getElementPath(target); + const targetInter = getElementLineIntersection(targetPath, linePath) || getMid(target); + + // Do not draw a link if the points are too close + const distance = getDistancePointPoint(targetInter, labelInter); + if (distance < DISTANCE_THRESHOLD) { + return; + } + + // Connect the actual closest points + updateLine(line, [ targetInter, labelInter ]); + svgAppend(layer, line); + } + + /** + * Remove all existing label links. + */ + function cleanUp() { + domQueryAll(`.${LINE_STYLE.class}`, layer).forEach(svgRemove); + } + + /** + * Get element's slightly expanded outline path. + * + * @param {Element} element + * @returns {string} svg path + */ + function getElementOutlinePath(element) { + const outlineShape = outline.getOutline(element); + const outlineOffset = outline.offset; + + if (!outlineShape) { + return getElementPath(element); + } + + if (outlineShape.x) { + const shape = { + x: element.x + parseSvgNumAttr(outlineShape, 'x') - PATH_OFFSET, + y: element.y + parseSvgNumAttr(outlineShape, 'y') - PATH_OFFSET, + width: parseSvgNumAttr(outlineShape, 'width') + PATH_OFFSET * 2, + height: parseSvgNumAttr(outlineShape, 'height') + PATH_OFFSET * 2 + }; + + return getRoundRectPath(shape, parseSvgNumAttr(outlineShape, 'rx')); + } + + if (outlineShape.cx) { + const shape = { + x: element.x - outlineOffset, + y: element.y - outlineOffset, + width: parseSvgNumAttr(outlineShape, 'r') * 2, + height: parseSvgNumAttr(outlineShape, 'r') * 2, + }; + + return getCirclePath(shape); + } + } + + function getElementPath(element) { + return graphicsFactory.getShapePath(element); + } + + function isElementSelected(element) { + return selection.get().includes(element); + } +} + +LabelLink.$inject = [ + 'eventBus', + 'canvas', + 'graphicsFactory', + 'outline', + 'selection' +]; + +/** + * Get numeric attribute from SVG element + * or 0 if not present. + * + * @param {SVGElement} node + * @param {string} attr + * @returns {number} + */ +function parseSvgNumAttr(node, attr) { + return parseFloat(svgAttr(node, attr) || 0); +} diff --git a/lib/features/label-link/index.js b/lib/features/label-link/index.js new file mode 100644 index 0000000000..32123bdeba --- /dev/null +++ b/lib/features/label-link/index.js @@ -0,0 +1,15 @@ +import SelectionModule from 'diagram-js/lib/features/selection'; +import OutlineModule from 'diagram-js/lib/features/outline'; + +import LabelLink from './LabelLink'; + +export default { + __depends__: [ + SelectionModule, + OutlineModule + ], + __init__: [ + 'labelLink' + ], + labelLink: [ 'type', LabelLink ] +}; diff --git a/lib/features/modeling-feedback/ModelingFeedback.js b/lib/features/modeling-feedback/ModelingFeedback.js new file mode 100644 index 0000000000..c51b414173 --- /dev/null +++ b/lib/features/modeling-feedback/ModelingFeedback.js @@ -0,0 +1,51 @@ +import { is } from '../../util/ModelUtil'; + +/** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/tooltips/Tooltips').default} Tooltips + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + */ + +var COLLAB_ERR_MSG = 'flow elements must be children of pools/participants'; +var DATA_OBJECT_ERR_MSG = 'Data object must be placed within a pool/participant.'; + +/** + * @param {EventBus} eventBus + * @param {Tooltips} tooltips + * @param {Translate} translate + */ +export default function ModelingFeedback(eventBus, tooltips, translate) { + + function showError(position, message, timeout) { + tooltips.add({ + position: { + x: position.x + 5, + y: position.y + 5 + }, + type: 'error', + timeout: timeout || 2000, + html: '
    ' + message + '
    ' + }); + } + + eventBus.on([ 'shape.move.rejected', 'create.rejected' ], function(event) { + var context = event.context, + shape = context.shape, + target = context.target; + + if (is(target, 'bpmn:Collaboration')) { + if (is(shape, 'bpmn:FlowNode')) { + showError(event, translate(COLLAB_ERR_MSG)); + } else if (is(shape, 'bpmn:DataObjectReference')) { + showError(event, translate(DATA_OBJECT_ERR_MSG)); + } + } + }); + +} + +ModelingFeedback.$inject = [ + 'eventBus', + 'tooltips', + 'translate' +]; diff --git a/lib/features/modeling-feedback/index.js b/lib/features/modeling-feedback/index.js new file mode 100644 index 0000000000..a3fae1638f --- /dev/null +++ b/lib/features/modeling-feedback/index.js @@ -0,0 +1,13 @@ +import TooltipsModule from 'diagram-js/lib/features/tooltips'; + +import ModelingFeedback from './ModelingFeedback'; + +export default { + __depends__: [ + TooltipsModule + ], + __init__: [ + 'modelingFeedback' + ], + modelingFeedback: [ 'type', ModelingFeedback ] +}; \ No newline at end of file diff --git a/lib/features/modeling/BpmnFactory.js b/lib/features/modeling/BpmnFactory.js index 2cb1257200..40583c6efd 100644 --- a/lib/features/modeling/BpmnFactory.js +++ b/lib/features/modeling/BpmnFactory.js @@ -12,14 +12,29 @@ import { is } from '../../util/ModelUtil'; - +/** + * @typedef {import('../../model/Types').Moddle} Moddle + * @typedef {import('../../model/Types').ModdleElement} ModdleElement + * + * @typedef {import('diagram-js/lib/util/Types').Point} Point + */ + +/** + * A factory for BPMN elements. + * + * @param {Moddle} moddle + */ export default function BpmnFactory(moddle) { this._model = moddle; } BpmnFactory.$inject = [ 'moddle' ]; - +/** + * @param {ModdleElement} element + * + * @return {boolean} + */ BpmnFactory.prototype._needsId = function(element) { return isAny(element, [ 'bpmn:RootElement', @@ -41,6 +56,9 @@ BpmnFactory.prototype._needsId = function(element) { ]); }; +/** + * @param {ModdleElement} element + */ BpmnFactory.prototype._ensureId = function(element) { if (element.id) { this._model.ids.claim(element.id, element); @@ -70,7 +88,14 @@ BpmnFactory.prototype._ensureId = function(element) { } }; - +/** + * Create BPMN element. + * + * @param {string} type + * @param {Object} [attrs] + * + * @return {ModdleElement} + */ BpmnFactory.prototype.create = function(type, attrs) { var element = this._model.create(type, attrs || {}); @@ -79,14 +104,20 @@ BpmnFactory.prototype.create = function(type, attrs) { return element; }; - +/** + * @return {ModdleElement} + */ BpmnFactory.prototype.createDiLabel = function() { return this.create('bpmndi:BPMNLabel', { bounds: this.createDiBounds() }); }; - +/** + * @param {ModdleElement} semantic + * @param {Object} [attrs] + * @return {ModdleElement} + */ BpmnFactory.prototype.createDiShape = function(semantic, attrs) { return this.create('bpmndi:BPMNShape', assign({ bpmnElement: semantic, @@ -94,12 +125,18 @@ BpmnFactory.prototype.createDiShape = function(semantic, attrs) { }, attrs)); }; - +/** + * @return {ModdleElement} + */ BpmnFactory.prototype.createDiBounds = function(bounds) { return this.create('dc:Bounds', bounds); }; - +/** + * @param {Point[]} waypoints + * + * @return {ModdleElement[]} + */ BpmnFactory.prototype.createDiWaypoints = function(waypoints) { var self = this; @@ -108,11 +145,21 @@ BpmnFactory.prototype.createDiWaypoints = function(waypoints) { }); }; +/** + * @param {Point} point + * + * @return {ModdleElement} + */ BpmnFactory.prototype.createDiWaypoint = function(point) { return this.create('dc:Point', pick(point, [ 'x', 'y' ])); }; - +/** + * @param {ModdleElement} semantic + * @param {Object} [attrs] + * + * @return {ModdleElement} + */ BpmnFactory.prototype.createDiEdge = function(semantic, attrs) { return this.create('bpmndi:BPMNEdge', assign({ bpmnElement: semantic, @@ -120,8 +167,14 @@ BpmnFactory.prototype.createDiEdge = function(semantic, attrs) { }, attrs)); }; +/** + * @param {ModdleElement} semantic + * @param {Object} [attrs] + * + * @return {ModdleElement} + */ BpmnFactory.prototype.createDiPlane = function(semantic, attrs) { return this.create('bpmndi:BPMNPlane', assign({ bpmnElement: semantic }, attrs)); -}; \ No newline at end of file +}; diff --git a/lib/features/modeling/BpmnLayouter.js b/lib/features/modeling/BpmnLayouter.js index 9cbbb78c23..c7998fcc19 100644 --- a/lib/features/modeling/BpmnLayouter.js +++ b/lib/features/modeling/BpmnLayouter.js @@ -1,4 +1,4 @@ -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import { assign @@ -22,9 +22,75 @@ import { import { is } from '../../util/ModelUtil'; +import { isDirectionHorizontal } from './util/ModelingUtil'; + +/** + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * + * @typedef {import('diagram-js/lib/util/Types').Point} Point + * + * @typedef {import('../../model/Types').Connection} Connection + * @typedef {import('../../model/Types').Element} Element + * + * @typedef {import('diagram-js/lib/layout/BaseLayouter').LayoutConnectionHints} LayoutConnectionHints + * + * @typedef { { + * source?: Element; + * target?: Element; + * waypoints?: Point[]; + * connectionStart?: Point; + * connectionEnd?: Point; + * } & LayoutConnectionHints } BpmnLayoutConnectionHints + */ + var ATTACH_ORIENTATION_PADDING = -10, BOUNDARY_TO_HOST_THRESHOLD = 40; +// layout all connection between flow elements h:h, except for +// (1) outgoing of boundary events -> layout based on attach orientation and target orientation +// (2) incoming/outgoing of gateways -> v:h for outgoing, h:v for incoming +// (3) loops connect sides clockwise +var PREFERRED_LAYOUTS_HORIZONTAL = { + default: [ 'h:h' ], + fromGateway: [ 'v:h' ], + toGateway: [ 'h:v' ], + loop: { + fromTop: [ 't:r' ], + fromRight: [ 'r:b' ], + fromLeft: [ 'l:t' ], + fromBottom: [ 'b:l' ] + }, + boundaryLoop: { + alternateHorizontalSide: 'b', + alternateVerticalSide: 'l', + default: 'v' + }, + messageFlow: [ 'straight', 'v:v' ], + subProcess: [ 'straight', 'h:h' ], + isHorizontal: true +}; + +// for vertical layouts, switch h and v and loop counter-clockwise +var PREFERRED_LAYOUTS_VERTICAL = { + default: [ 'v:v' ], + fromGateway: [ 'h:v' ], + toGateway: [ 'v:h' ], + loop: { + fromTop: [ 't:l' ], + fromRight: [ 'r:t' ], + fromLeft: [ 'l:b' ], + fromBottom: [ 'b:r' ] + }, + boundaryLoop: { + alternateHorizontalSide: 't', + alternateVerticalSide: 'r', + default: 'h' + }, + messageFlow: [ 'straight', 'h:h' ], + subProcess: [ 'straight', 'v:v' ], + isHorizontal: false +}; + var oppositeOrientationMapping = { 'top': 'bottom', 'top-right': 'bottom-left', @@ -43,12 +109,20 @@ var orientationDirectionMapping = { left: 'l' }; - -export default function BpmnLayouter() {} +export default function BpmnLayouter(elementRegistry) { + this._elementRegistry = elementRegistry; +} inherits(BpmnLayouter, BaseLayouter); - +/** + * Returns waypoints of laid out connection. + * + * @param {Connection} connection + * @param {BpmnLayoutConnectionHints} [hints] + * + * @return {Point[]} + */ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { if (!hints) { hints = {}; @@ -58,7 +132,8 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { target = hints.target || connection.target, waypoints = hints.waypoints || connection.waypoints, connectionStart = hints.connectionStart, - connectionEnd = hints.connectionEnd; + connectionEnd = hints.connectionEnd, + elementRegistry = this._elementRegistry; var manhattanOptions, updatedWaypoints; @@ -71,9 +146,6 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { connectionEnd = getConnectionDocking(waypoints && waypoints[ waypoints.length - 1 ], target); } - // TODO(nikku): support vertical modeling - // and invert preferredLayouts accordingly - if (is(connection, 'bpmn:Association') || is(connection, 'bpmn:DataAssociation')) { @@ -82,35 +154,36 @@ BpmnLayouter.prototype.layoutConnection = function(connection, hints) { } } + var layout = isDirectionHorizontal(source, elementRegistry) ? PREFERRED_LAYOUTS_HORIZONTAL : PREFERRED_LAYOUTS_VERTICAL; + if (is(connection, 'bpmn:MessageFlow')) { - manhattanOptions = getMessageFlowManhattanOptions(source, target); + manhattanOptions = getMessageFlowManhattanOptions(source, target, layout); } else if (is(connection, 'bpmn:SequenceFlow') || isCompensationAssociation(source, target)) { - // layout all connection between flow elements h:h, except for - // (1) outgoing of boundary events -> layout based on attach orientation and target orientation - // (2) incoming/outgoing of gateways -> v:h for outgoing, h:v for incoming - // (3) loops if (source === target) { manhattanOptions = { - preferredLayouts: getLoopPreferredLayout(source, connection) + preferredLayouts: getLoopPreferredLayout(source, connection, layout) }; } else if (is(source, 'bpmn:BoundaryEvent')) { manhattanOptions = { - preferredLayouts: getBoundaryEventPreferredLayouts(source, target, connectionEnd) + preferredLayouts: getBoundaryEventPreferredLayouts(source, target, connectionEnd, layout) }; } else if (isExpandedSubProcess(source) || isExpandedSubProcess(target)) { - manhattanOptions = getSubProcessManhattanOptions(source); + manhattanOptions = { + preferredLayouts: layout.subProcess, + preserveDocking: getSubProcessPreserveDocking(source) + }; } else if (is(source, 'bpmn:Gateway')) { manhattanOptions = { - preferredLayouts: [ 'v:h' ] + preferredLayouts: layout.fromGateway }; } else if (is(target, 'bpmn:Gateway')) { manhattanOptions = { - preferredLayouts: [ 'h:v' ] + preferredLayouts: layout.toGateway }; } else { manhattanOptions = { - preferredLayouts: [ 'h:h' ] + preferredLayouts: layout.default }; } } @@ -140,9 +213,9 @@ function getAttachOrientation(attachedElement) { return getOrientation(getMid(attachedElement), hostElement, ATTACH_ORIENTATION_PADDING); } -function getMessageFlowManhattanOptions(source, target) { +function getMessageFlowManhattanOptions(source, target, layout) { return { - preferredLayouts: [ 'straight', 'v:v' ], + preferredLayouts: layout.messageFlow, preserveDocking: getMessageFlowPreserveDocking(source, target) }; } @@ -179,13 +252,6 @@ function getMessageFlowPreserveDocking(source, target) { return null; } -function getSubProcessManhattanOptions(source) { - return { - preferredLayouts: [ 'straight', 'h:h' ], - preserveDocking: getSubProcessPreserveDocking(source) - }; -} - function getSubProcessPreserveDocking(source) { return isExpandedSubProcess(source) ? 'target' : 'source'; } @@ -248,23 +314,23 @@ function isHorizontalOrientation(orientation) { return orientation === 'right' || orientation === 'left'; } -function getLoopPreferredLayout(source, connection) { +function getLoopPreferredLayout(source, connection, layout) { var waypoints = connection.waypoints; var orientation = waypoints && waypoints.length && getOrientation(waypoints[0], source); if (orientation === 'top') { - return [ 't:r' ]; + return layout.loop.fromTop; } else if (orientation === 'right') { - return [ 'r:b' ]; + return layout.loop.fromRight; } else if (orientation === 'left') { - return [ 'l:t' ]; + return layout.loop.fromLeft; } - return [ 'b:l' ]; + return layout.loop.fromBottom; } -function getBoundaryEventPreferredLayouts(source, target, end) { +function getBoundaryEventPreferredLayouts(source, target, end, layout) { var sourceMid = getMid(source), targetMid = getMid(target), attachOrientation = getAttachOrientation(source), @@ -281,31 +347,31 @@ function getBoundaryEventPreferredLayouts(source, target, end) { }); if (isLoop) { - return getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end); + return getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end, layout); } // source layout - sourceLayout = getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide); + sourceLayout = getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide, layout.isHorizontal); // target layout - targetLayout = getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide); + targetLayout = getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide, layout.isHorizontal); return [ sourceLayout + ':' + targetLayout ]; } -function getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end) { - var orientation = attachedToSide ? attachOrientation : getVerticalOrientation(attachOrientation), +function getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end, layout) { + var orientation = attachedToSide ? attachOrientation : layout.isHorizontal ? getVerticalOrientation(attachOrientation) : getHorizontalOrientation(attachOrientation), sourceLayout = orientationDirectionMapping[ orientation ], targetLayout; if (attachedToSide) { if (isHorizontalOrientation(attachOrientation)) { - targetLayout = shouldConnectToSameSide('y', source, target, end) ? 'h' : 'b'; + targetLayout = shouldConnectToSameSide('y', source, target, end) ? 'h' : layout.boundaryLoop.alternateHorizontalSide; } else { - targetLayout = shouldConnectToSameSide('x', source, target, end) ? 'v' : 'l'; + targetLayout = shouldConnectToSameSide('x', source, target, end) ? 'v' : layout.boundaryLoop.alternateVerticalSide; } } else { - targetLayout = 'v'; + targetLayout = layout.boundaryLoop.default; } return [ sourceLayout + ':' + targetLayout ]; @@ -328,7 +394,7 @@ function areCloseOnAxis(axis, a, b, threshold) { return Math.abs(a[ axis ] - b[ axis ]) < threshold; } -function getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide) { +function getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide, isHorizontal) { // attached to either top, right, bottom or left side if (attachedToSide) { @@ -337,20 +403,36 @@ function getBoundaryEventSourceLayout(attachOrientation, targetOrientation, atta // attached to either top-right, top-left, bottom-right or bottom-left corner - // same vertical or opposite horizontal orientation - if (isSame( - getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation) - ) || isOppositeOrientation( - getHorizontalOrientation(attachOrientation), getHorizontalOrientation(targetOrientation) - )) { - return orientationDirectionMapping[ getVerticalOrientation(attachOrientation) ]; + var verticalAttachOrientation = getVerticalOrientation(attachOrientation), + horizontalAttachOrientation = getHorizontalOrientation(attachOrientation), + verticalTargetOrientation = getVerticalOrientation(targetOrientation), + horizontalTargetOrientation = getHorizontalOrientation(targetOrientation); + + if (isHorizontal) { + + // same vertical or opposite horizontal orientation + if ( + isSame(verticalAttachOrientation, verticalTargetOrientation) || + isOppositeOrientation(horizontalAttachOrientation, horizontalTargetOrientation) + ) { + return orientationDirectionMapping[ verticalAttachOrientation ]; + } + } else { + + // same horizontal or opposite vertical orientation + if ( + isSame(horizontalAttachOrientation, horizontalTargetOrientation) || + isOppositeOrientation(verticalAttachOrientation, verticalTargetOrientation) + ) { + return orientationDirectionMapping[ horizontalAttachOrientation ]; + } } // fallback - return orientationDirectionMapping[ getHorizontalOrientation(attachOrientation) ]; + return orientationDirectionMapping[ isHorizontal ? horizontalAttachOrientation : verticalAttachOrientation ]; } -function getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide) { +function getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide, isHorizontal) { // attached to either top, right, bottom or left side if (attachedToSide) { @@ -385,15 +467,39 @@ function getBoundaryEventTargetLayout(attachOrientation, targetOrientation, atta } } - // attached to either top-right, top-left, bottom-right or bottom-left corner + // attached to either top-right, top-left, bottom-right or bottom-left corner, + // or strictly above/below or left/right of the target. In the corner case, + // the orientation is compared on the counter-axis to decide the layout. + + var verticalAttachOrientation = getVerticalOrientation(attachOrientation), + horizontalAttachOrientation = getHorizontalOrientation(attachOrientation), + verticalTargetOrientation = getVerticalOrientation(targetOrientation), + horizontalTargetOrientation = getHorizontalOrientation(targetOrientation); + + // If the target is strictly above/below (no horizontal orientation) + if (verticalTargetOrientation && !horizontalTargetOrientation) { + return 'v'; + } - // orientation is right, left - // or same vertical orientation but also right or left - if (isHorizontalOrientation(targetOrientation) || - (isSame(getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)) && - getHorizontalOrientation(targetOrientation))) { + // If the target is strictly left/right (no vertical orientation) + if (horizontalTargetOrientation && !verticalTargetOrientation) { return 'h'; + } + + + if (isHorizontal) { + if (isSame(verticalAttachOrientation, verticalTargetOrientation)) { + return 'h'; + } else { + return 'v'; + } } else { - return 'v'; + if (isSame(horizontalAttachOrientation, horizontalTargetOrientation)) { + return 'v'; + } else { + return 'h'; + } } } + +BpmnLayouter.$inject = [ 'elementRegistry' ]; diff --git a/lib/features/modeling/BpmnUpdater.js b/lib/features/modeling/BpmnUpdater.js index 65a0e0c4d1..f41e2ca2af 100644 --- a/lib/features/modeling/BpmnUpdater.js +++ b/lib/features/modeling/BpmnUpdater.js @@ -3,50 +3,66 @@ import { forEach } from 'min-dash'; -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import { - remove as collectionRemove, - add as collectionAdd + add as collectionAdd, + remove as collectionRemove } from 'diagram-js/lib/util/Collections'; -import { - Label -} from 'diagram-js/lib/model'; - import { getBusinessObject, getDi, is } from '../../util/ModelUtil'; -import { - isAny -} from './util/ModelingUtil'; +import { isAny } from './util/ModelingUtil'; import { - delta -} from 'diagram-js/lib/util/PositionUtil'; + getLabel, + isLabel, + isLabelExternal +} from '../../util/LabelUtil'; + +import { isPlane } from '../../util/DrilldownUtil'; + +import { delta } from 'diagram-js/lib/util/PositionUtil'; import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; /** - * A handler responsible for updating the underlying BPMN 2.0 XML + DI - * once changes on the diagram happen + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('./BpmnFactory').default} BpmnFactory + * @typedef {import('diagram-js/lib/layout/CroppingConnectionDocking').default} CroppingConnectionDocking + * + * @typedef {import('../../model/Types').Connection} Connection + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + * @typedef {import('../../model/Types').Parent} Parent + * @typedef {import('../../model/Types').ModdleElement} ModdleElement + */ + +/** + * A handler responsible for updating the underlying BPMN 2.0 XML & DI + * once changes on the diagram happen. + * + * @param {EventBus} eventBus + * @param {BpmnFactory} bpmnFactory + * @param {CroppingConnectionDocking} connectionDocking */ export default function BpmnUpdater( - eventBus, bpmnFactory, connectionDocking, - translate) { + eventBus, + bpmnFactory, + connectionDocking +) { CommandInterceptor.call(this, eventBus); this._bpmnFactory = bpmnFactory; - this._translate = translate; var self = this; - // connection cropping ////////////////////// // crop connection ends during create/update @@ -272,6 +288,28 @@ export default function BpmnUpdater( this.executed([ 'element.updateAttachment' ], ifBpmn(updateAttachment)); this.reverted([ 'element.updateAttachment' ], ifBpmn(updateAttachment)); + + + // update BPMNLabel + this.executed('element.updateLabel', ifBpmn(updateBPMNLabel)); + this.reverted('element.updateLabel', ifBpmn(updateBPMNLabel)); + + function updateBPMNLabel(event) { + const { element } = event.context, + label = getLabel(element); + const di = getDi(element), + diLabel = di && di.get('label'); + + if (isLabelExternal(element) || isPlane(element)) { + return; + } + + if (label && !diLabel) { + di.set('label', bpmnFactory.create('bpmndi:BPMNLabel')); + } else if (!label && diLabel) { + di.set('label', undefined); + } + } } inherits(BpmnUpdater, CommandInterceptor); @@ -279,13 +317,18 @@ inherits(BpmnUpdater, CommandInterceptor); BpmnUpdater.$inject = [ 'eventBus', 'bpmnFactory', - 'connectionDocking', - 'translate' + 'connectionDocking' ]; // implementation ////////////////////// +/** + * @param { { + * shape: Shape; + * host: Shape; + * } } context + */ BpmnUpdater.prototype.updateAttachment = function(context) { var shape = context.shape, @@ -295,10 +338,14 @@ BpmnUpdater.prototype.updateAttachment = function(context) { businessObject.attachedToRef = host && host.businessObject; }; +/** + * @param {Element} element + * @param {Parent} oldParent + */ BpmnUpdater.prototype.updateParent = function(element, oldParent) { // do not update BPMN 2.0 label parent - if (element instanceof Label) { + if (isLabel(element)) { return; } @@ -345,7 +392,9 @@ BpmnUpdater.prototype.updateParent = function(element, oldParent) { this.updateDiParent(di, parentDi); }; - +/** + * @param {Shape} shape + */ BpmnUpdater.prototype.updateBounds = function(shape) { var di = getDi(shape), @@ -361,7 +410,7 @@ BpmnUpdater.prototype.updateBounds = function(shape) { }); } - var target = (shape instanceof Label) ? this._getLabel(di) : di; + var target = isLabel(shape) ? this._getLabel(di) : di; var bounds = target.bounds; @@ -378,6 +427,11 @@ BpmnUpdater.prototype.updateBounds = function(shape) { }); }; +/** + * @param {ModdleElement} businessObject + * @param {ModdleElement} newContainment + * @param {ModdleElement} oldContainment + */ BpmnUpdater.prototype.updateFlowNodeRefs = function(businessObject, newContainment, oldContainment) { if (oldContainment === newContainment) { @@ -397,8 +451,11 @@ BpmnUpdater.prototype.updateFlowNodeRefs = function(businessObject, newContainme } }; - -// update existing sourceElement and targetElement di information +/** + * @param {Connection} connection + * @param {Element} newSource + * @param {Element} newTarget + */ BpmnUpdater.prototype.updateDiConnection = function(connection, newSource, newTarget) { var connectionDi = getDi(connection), newSourceDi = getDi(newSource), @@ -414,7 +471,10 @@ BpmnUpdater.prototype.updateDiConnection = function(connection, newSource, newTa }; - +/** + * @param {ModdleElement} di + * @param {ModdleElement} parentDi + */ BpmnUpdater.prototype.updateDiParent = function(di, parentDi) { if (parentDi && !is(parentDi, 'bpmndi:BPMNPlane')) { @@ -436,6 +496,11 @@ BpmnUpdater.prototype.updateDiParent = function(di, parentDi) { } }; +/** + * @param {ModdleElement} element + * + * @return {ModdleElement} + */ function getDefinitions(element) { while (element && !is(element, 'bpmn:Definitions')) { element = element.$parent; @@ -444,6 +509,11 @@ function getDefinitions(element) { return element; } +/** + * @param {ModdleElement} container + * + * @return {ModdleElement} + */ BpmnUpdater.prototype.getLaneSet = function(container) { var laneSet, laneSets; @@ -479,10 +549,14 @@ BpmnUpdater.prototype.getLaneSet = function(container) { return laneSet; }; +/** + * @param {ModdleElement} businessObject + * @param {ModdleElement} newParent + * @param {ModdleElement} visualParent + */ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, visualParent) { - var containment, - translate = this._translate; + var containment; if (businessObject.$parent === newParent) { return; @@ -507,17 +581,13 @@ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, } containment = 'lanes'; - } else - - if (is(businessObject, 'bpmn:FlowElement')) { + } else if (is(businessObject, 'bpmn:FlowElement')) { if (newParent) { if (is(newParent, 'bpmn:Participant')) { newParent = newParent.processRef; - } else - - if (is(newParent, 'bpmn:Lane')) { + } else if (is(newParent, 'bpmn:Lane')) { do { // unwrap Lane -> LaneSet -> (Lane | FlowElementsContainer) @@ -529,9 +599,7 @@ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, containment = 'flowElements'; - } else - - if (is(businessObject, 'bpmn:Artifact')) { + } else if (is(businessObject, 'bpmn:Artifact')) { while (newParent && !is(newParent, 'bpmn:Process') && @@ -547,14 +615,9 @@ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, } containment = 'artifacts'; - } else - - if (is(businessObject, 'bpmn:MessageFlow')) { + } else if (is(businessObject, 'bpmn:MessageFlow')) { containment = 'messageFlows'; - - } else - - if (is(businessObject, 'bpmn:Participant')) { + } else if (is(businessObject, 'bpmn:Participant')) { containment = 'participants'; // make sure the participants process is properly attached / detached @@ -576,24 +639,14 @@ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, process.$parent = definitions; } } - } else - - if (is(businessObject, 'bpmn:DataOutputAssociation')) { + } else if (is(businessObject, 'bpmn:DataOutputAssociation')) { containment = 'dataOutputAssociations'; - } else - - if (is(businessObject, 'bpmn:DataInputAssociation')) { + } else if (is(businessObject, 'bpmn:DataInputAssociation')) { containment = 'dataInputAssociations'; } if (!containment) { - throw new Error(translate( - 'no parent for {element} in {parent}', - { - element: businessObject.id, - parent: newParent.id - } - )); + throw new Error(`no parent for <${ businessObject.id }> in <${ newParent.id }>`); } var children; @@ -632,14 +685,22 @@ BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, } }; - +/** + * @param {Connection} connection + */ BpmnUpdater.prototype.updateConnectionWaypoints = function(connection) { var di = getDi(connection); di.set('waypoint', this._bpmnFactory.createDiWaypoints(connection.waypoints)); }; - +/** + * @param { { + * connection: Connection; + * parent: Parent; + * newParent: Parent; + * } } context + */ BpmnUpdater.prototype.updateConnection = function(context) { var connection = context.connection, businessObject = getBusinessObject(connection), @@ -676,9 +737,7 @@ BpmnUpdater.prototype.updateConnection = function(context) { businessObject.targetRef = newTargetBo; } - } else - - if (is(businessObject, 'bpmn:DataInputAssociation')) { + } else if (is(businessObject, 'bpmn:DataInputAssociation')) { // handle obnoxious isMsome sourceRef businessObject.get('sourceRef')[0] = newSourceBo; @@ -686,9 +745,7 @@ BpmnUpdater.prototype.updateConnection = function(context) { visualParent = context.parent || context.newParent || newTargetBo; this.updateSemanticParent(businessObject, newTargetBo, visualParent); - } else - - if (is(businessObject, 'bpmn:DataOutputAssociation')) { + } else if (is(businessObject, 'bpmn:DataOutputAssociation')) { visualParent = context.parent || context.newParent || newSourceBo; this.updateSemanticParent(businessObject, newSourceBo, visualParent); @@ -715,18 +772,18 @@ BpmnUpdater.prototype._getLabel = function(di) { /** - * Make sure the event listener is only called - * if the touched element is a BPMN element. + * Call function if shape or connection is BPMN element. * * @param {Function} fn - * @return {Function} guarded function + * + * @return {Function} */ function ifBpmn(fn) { return function(event) { var context = event.context, - element = context.shape || context.connection; + element = context.shape || context.connection || context.element; if (is(element, 'bpmn:BaseElement')) { fn(event); @@ -737,9 +794,9 @@ function ifBpmn(fn) { /** * Return dc:Bounds of bpmndi:BPMNLabel if exists. * - * @param {djs.model.shape} shape + * @param {Shape} shape * - * @returns {Object|undefined} + * @return {ModdleElement|undefined} */ function getEmbeddedLabelBounds(shape) { if (!is(shape, 'bpmn:Activity')) { diff --git a/lib/features/modeling/ElementFactory.js b/lib/features/modeling/ElementFactory.js index 8f4c9ff4e5..7be779fb38 100644 --- a/lib/features/modeling/ElementFactory.js +++ b/lib/features/modeling/ElementFactory.js @@ -1,10 +1,13 @@ import { assign, forEach, - isObject + has, + isDefined, + isObject, + omit } from 'min-dash'; -import inherits from 'inherits'; +import inherits from 'inherits-browser'; import { getBusinessObject, @@ -26,32 +29,84 @@ import { DEFAULT_LABEL_SIZE } from '../../util/LabelUtil'; -import { - ensureCompatDiRef -} from '../../util/CompatibilityUtil'; - +/** + * @typedef {import('diagram-js/lib/util/Types').Dimensions} Dimensions + * + * @typedef {import('./BpmnFactory').default} BpmnFactory + * + * @typedef {import('../../model/Types').BpmnAttributes} BpmnAttributes + * @typedef {import('../../model/Types').Connection} Connection + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Label} Label + * @typedef {import('../../model/Types').Root} Root + * @typedef {import('../../model/Types').Shape} Shape + * @typedef {import('../../model/Types').Moddle} Moddle + * @typedef {import('../../model/Types').ModdleElement} ModdleElement + */ /** - * A bpmn-aware factory for diagram-js shapes + * A BPMN-specific element factory. + * + * @template {Connection} [T=Connection] + * @template {Label} [U=Label] + * @template {Root} [V=Root] + * @template {Shape} [W=Shape] + * + * @extends {BaseElementFactory} + * + * @param {BpmnFactory} bpmnFactory + * @param {Moddle} moddle */ -export default function ElementFactory(bpmnFactory, moddle, translate) { +export default function ElementFactory(bpmnFactory, moddle) { BaseElementFactory.call(this); this._bpmnFactory = bpmnFactory; this._moddle = moddle; - this._translate = translate; } inherits(ElementFactory, BaseElementFactory); ElementFactory.$inject = [ 'bpmnFactory', - 'moddle', - 'translate' + 'moddle' ]; -ElementFactory.prototype.baseCreate = BaseElementFactory.prototype.create; +ElementFactory.prototype._baseCreate = BaseElementFactory.prototype.create; + +/** + * Create a root element. + * + * @overlord + * @param {'root'} elementType + * @param {Partial & Partial} [attrs] + * @return {V} + */ +/** + * Create a shape. + * + * @overlord + * @param {'shape'} elementType + * @param {Partial & Partial} [attrs] + * @return {W} + */ + +/** + * Create a connection. + * + * @overlord + * @param {'connection'} elementType + * @param {Partial & Partial} [attrs] + * @return {T} + */ + +/** + * Create a label. + * + * @param {'label'} elementType + * @param {Partial