├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc.js ├── .travis.yml ├── .vscode └── settings.json ├── .yarnrc ├── CODEOWNERS ├── LICENSE ├── README.md ├── THIRD_PARTY_LICENSES ├── babel.config.js ├── browserslist ├── commitlint.config.js ├── cypress.json ├── docs ├── CONTRIBUTING.md ├── DEVELOPING.md └── README.md ├── i18n ├── bn-IN.properties ├── da-DK.properties ├── de-DE.properties ├── en-AU.properties ├── en-CA.properties ├── en-GB.properties ├── en-US.properties ├── en-x-pseudo.properties ├── es-419.properties ├── es-ES.properties ├── fi-FI.properties ├── fr-CA.properties ├── fr-FR.properties ├── hi-IN.properties ├── it-IT.properties ├── ja-JP.properties ├── ko-KR.properties ├── nb-NO.properties ├── nl-NL.properties ├── pl-PL.properties ├── pt-BR.properties ├── ru-RU.properties ├── sv-SE.properties ├── tr-TR.properties ├── zh-CN.properties └── zh-TW.properties ├── index.js ├── jest.config.js ├── lint-staged.config.js ├── package.json ├── postcss.config.js ├── scripts ├── build_locale.js ├── current_version.sh ├── cypress.js ├── jest │ ├── envWindow.js │ ├── enzyme-adapter.js │ ├── fileMock.js │ ├── i18nMock.js │ ├── moduleMock.js │ ├── popperMock.js │ ├── react-intl-mock.js │ └── styleMock.js ├── license.js ├── prepush.sh ├── prod.js ├── publish.sh ├── release.sh ├── webpack.common.config.js └── webpack.config.js ├── src ├── @types │ ├── api.ts │ ├── events.ts │ ├── global.ts │ ├── i18n.ts │ ├── index.ts │ ├── model.ts │ ├── new.ts │ └── users.ts ├── BoxAnnotations.ts ├── __tests__ │ └── BoxAnnotations-test.js ├── api │ ├── APIFactory.ts │ ├── __mocks__ │ │ └── APIFactory.ts │ ├── index.ts │ └── types.ts ├── common │ ├── BaseAnnotator.scss │ ├── BaseAnnotator.ts │ ├── BaseManager.ts │ ├── DeselectListener.tsx │ ├── DeselectManager.tsx │ ├── EventEmitter.ts │ ├── EventManager.ts │ ├── __mocks__ │ │ ├── EventManager.ts │ │ ├── events.ts │ │ ├── useIsListInteractive.ts │ │ ├── useMountId.ts │ │ ├── useOutsideEvent.ts │ │ └── withProviders.tsx │ ├── __tests__ │ │ ├── BaseAnnotator-test.ts │ │ ├── BaseManager-test.ts │ │ ├── DeselectListener-test.tsx │ │ ├── DeselectManager-test.tsx │ │ ├── EventEmitter-test.ts │ │ ├── EventManager-test.ts │ │ ├── useIsListInteractive-test.tsx │ │ ├── useMountId-test.tsx │ │ └── useOutsideEvent-test.tsx │ ├── useAutoScroll │ │ ├── __tests__ │ │ │ ├── useAutoScroll-test.tsx │ │ │ └── util-test.ts │ │ ├── index.ts │ │ ├── useAutoScroll.tsx │ │ └── util.ts │ ├── useIsListInteractive.ts │ ├── useMountId.ts │ ├── useOutsideEvent.ts │ └── withProviders.tsx ├── components │ ├── ItemList │ │ ├── ItemList.scss │ │ ├── ItemList.tsx │ │ ├── ItemRow.scss │ │ ├── ItemRow.tsx │ │ ├── __mocks__ │ │ │ └── ItemRow.tsx │ │ ├── __tests__ │ │ │ ├── ItemList-test.tsx │ │ │ └── ItemRow-test.tsx │ │ └── index.ts │ ├── PointerCapture │ │ ├── PointerCapture.tsx │ │ ├── __tests__ │ │ │ └── PointerCapture-test.tsx │ │ └── index.ts │ ├── Popups │ │ ├── Popper.ts │ │ ├── PopupArrow.scss │ │ ├── PopupBase.scss │ │ ├── PopupBase.tsx │ │ ├── PopupCursor.scss │ │ ├── PopupCursor.tsx │ │ ├── PopupDrawingToolbar.scss │ │ ├── PopupDrawingToolbar.tsx │ │ ├── PopupHighlight.scss │ │ ├── PopupHighlight.tsx │ │ ├── PopupHighlightError.scss │ │ ├── PopupHighlightError.tsx │ │ ├── PopupList.scss │ │ ├── PopupList.tsx │ │ ├── PopupReply.scss │ │ ├── PopupReply.tsx │ │ ├── __mocks__ │ │ │ ├── PopupBase.tsx │ │ │ ├── PopupHighlight.tsx │ │ │ ├── PopupHighlightError.tsx │ │ │ └── PopupReply.tsx │ │ ├── __tests__ │ │ │ ├── Popper-test.ts │ │ │ ├── PopupBase-test.tsx │ │ │ ├── PopupCursor-test.tsx │ │ │ ├── PopupDrawingToolbar-test.tsx │ │ │ ├── PopupHighlight-test.tsx │ │ │ ├── PopupHighlightError-test.tsx │ │ │ ├── PopupList-test.tsx │ │ │ └── PopupReply-test.tsx │ │ └── messages.ts │ ├── ReplyField │ │ ├── MentionItem.scss │ │ ├── MentionItem.tsx │ │ ├── ReplyField.scss │ │ ├── ReplyField.tsx │ │ ├── ReplyFieldContainer.tsx │ │ ├── __tests__ │ │ │ ├── MentionItem-test.tsx │ │ │ ├── ReplyField-test.tsx │ │ │ ├── ReplyFieldContainer-test.tsx │ │ │ └── withMentionDecorator-test.tsx │ │ ├── index.ts │ │ └── withMentionDecorator.tsx │ └── ReplyForm │ │ ├── ReplyButton.tsx │ │ ├── ReplyForm.scss │ │ ├── ReplyForm.tsx │ │ ├── ReplyFormContainer.tsx │ │ ├── __mocks__ │ │ ├── ReplyForm.tsx │ │ ├── ReplyFormContainer.tsx │ │ └── index.ts │ │ ├── __tests__ │ │ ├── ReplyButton-test.tsx │ │ ├── ReplyForm-test.tsx │ │ └── ReplyFormContainer-test.tsx │ │ ├── index.ts │ │ └── types.ts ├── constants.js ├── document │ ├── DocumentAnnotator.scss │ ├── DocumentAnnotator.ts │ ├── __tests__ │ │ ├── DocumentAnnotator-test.ts │ │ └── docUtil-test.ts │ └── docUtil.ts ├── drawing │ ├── DecoratedDrawingPath.scss │ ├── DecoratedDrawingPath.tsx │ ├── DrawingAnnotations.scss │ ├── DrawingAnnotations.tsx │ ├── DrawingAnnotationsContainer.tsx │ ├── DrawingCreator.scss │ ├── DrawingCreator.tsx │ ├── DrawingCursor.ts │ ├── DrawingList.tsx │ ├── DrawingManager.tsx │ ├── DrawingPath.tsx │ ├── DrawingPathGroup.tsx │ ├── DrawingSVG.tsx │ ├── DrawingSVGGroup.tsx │ ├── DrawingTarget.scss │ ├── DrawingTarget.tsx │ ├── SVGFilterContext.js │ ├── __mocks__ │ │ ├── DrawingList.tsx │ │ └── drawingData.ts │ ├── __tests__ │ │ ├── DecoratedDrawingPath-test.tsx │ │ ├── DrawingAnnotations-test.tsx │ │ ├── DrawingAnnotationsContainer-test.tsx │ │ ├── DrawingCreator-test.tsx │ │ ├── DrawingList-test.tsx │ │ ├── DrawingManager-test.ts │ │ ├── DrawingPath-test.tsx │ │ ├── DrawingPathGroup-test.tsx │ │ ├── DrawingSVG-test.tsx │ │ ├── DrawingSVGGroup-test.tsx │ │ ├── DrawingTarget-test.tsx │ │ └── drawingUtil-test.ts │ ├── actions.ts │ ├── drawingUtil.ts │ └── index.ts ├── highlight │ ├── HighlightAnnotations.scss │ ├── HighlightAnnotations.tsx │ ├── HighlightCanvas.scss │ ├── HighlightCanvas.tsx │ ├── HighlightContainer.tsx │ ├── HighlightCreator.scss │ ├── HighlightCreatorManager.ts │ ├── HighlightList.scss │ ├── HighlightList.tsx │ ├── HighlightListener.ts │ ├── HighlightManager.tsx │ ├── HighlightSvg.scss │ ├── HighlightSvg.tsx │ ├── HighlightTarget.scss │ ├── HighlightTarget.tsx │ ├── __mocks__ │ │ ├── HighlightAnnotations.tsx │ │ ├── HighlightCanvas.tsx │ │ ├── HighlightList.tsx │ │ ├── HighlightTarget.tsx │ │ └── data.ts │ ├── __tests__ │ │ ├── HighlightAnnotations-test.tsx │ │ ├── HighlightCanvas-test.tsx │ │ ├── HighlightContainer-test.tsx │ │ ├── HighlightCreatorManager-test.ts │ │ ├── HighlightList-test.tsx │ │ ├── HighlightListener-test.ts │ │ ├── HighlightManager-test.ts │ │ ├── HighlightSvg-test.tsx │ │ ├── HighlightTarget-test.tsx │ │ ├── actions-test.ts │ │ └── highlightUtil-test.ts │ ├── actions.ts │ ├── highlightUtil.ts │ └── index.ts ├── image │ ├── ImageAnnotator.scss │ ├── ImageAnnotator.ts │ └── __tests__ │ │ └── ImageAnnotator-test.ts ├── messages.js ├── polyfill.ts ├── popup │ ├── PopupContainer.tsx │ ├── PopupLayer.scss │ ├── PopupLayer.tsx │ ├── PopupManager.tsx │ ├── __mocks__ │ │ └── PopupLayer.tsx │ └── __tests__ │ │ ├── PopupContainer-test.tsx │ │ ├── PopupLayer-test.tsx │ │ └── PopupManager-test.tsx ├── region │ ├── RegionAnnotation.scss │ ├── RegionAnnotation.tsx │ ├── RegionAnnotations.scss │ ├── RegionAnnotations.tsx │ ├── RegionAnnotationsContainer.tsx │ ├── RegionCreation.scss │ ├── RegionCreation.tsx │ ├── RegionCreationContainer.tsx │ ├── RegionCreationManager.tsx │ ├── RegionCreator.scss │ ├── RegionCreator.tsx │ ├── RegionList.tsx │ ├── RegionManager.tsx │ ├── RegionRect.scss │ ├── RegionRect.tsx │ ├── __mocks__ │ │ ├── RegionRect.tsx │ │ └── data.ts │ ├── __tests__ │ │ ├── RegionAnnotation-test.tsx │ │ ├── RegionAnnotations-test.tsx │ │ ├── RegionAnnotationsContainer-test.tsx │ │ ├── RegionCreation-test.tsx │ │ ├── RegionCreationContainer-test.tsx │ │ ├── RegionCreationManager-test.ts │ │ ├── RegionCreator-test.tsx │ │ ├── RegionList-test.tsx │ │ ├── RegionManager-test.ts │ │ ├── RegionRect-test.tsx │ │ ├── actions-test.ts │ │ ├── regionUtil-test.ts │ │ └── transformUtils-test.ts │ ├── actions.ts │ ├── index.ts │ ├── regionUtil.ts │ └── transformUtil.ts ├── store │ ├── __mocks__ │ │ ├── createStore.ts │ │ └── index.ts │ ├── annotations │ │ ├── __mocks__ │ │ │ └── annotationsState.ts │ │ ├── __tests__ │ │ │ ├── actions-test.ts │ │ │ ├── reducer-test.ts │ │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── common │ │ ├── __mocks__ │ │ │ └── commonState.ts │ │ ├── __tests__ │ │ │ ├── reducer-test.ts │ │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── createRootReducer.ts │ ├── createStore.ts │ ├── creator │ │ ├── __mocks__ │ │ │ └── creatorState.ts │ │ ├── __tests__ │ │ │ ├── reducer-test.ts │ │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── drawing │ │ ├── __mocks__ │ │ │ └── drawingState.ts │ │ ├── __tests__ │ │ │ ├── actions-test.ts │ │ │ ├── reducer-test.ts │ │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── eventing │ │ ├── __tests__ │ │ │ ├── active-test.ts │ │ │ ├── create-test.ts │ │ │ ├── fetch-test.ts │ │ │ ├── init-test.ts │ │ │ ├── middleware-test.ts │ │ │ ├── mode-test.ts │ │ │ ├── staged-test.ts │ │ │ └── status-test.ts │ │ ├── active.ts │ │ ├── create.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── init.ts │ │ ├── middleware.ts │ │ ├── mode.ts │ │ ├── staged.ts │ │ ├── status.ts │ │ └── types.ts │ ├── highlight │ │ ├── __mocks__ │ │ │ ├── data.ts │ │ │ └── highlightState.ts │ │ ├── __tests__ │ │ │ ├── actions-test.ts │ │ │ ├── reducer-test.ts │ │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── index.ts │ ├── options │ │ ├── __tests__ │ │ │ ├── reducer-test.ts │ │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts │ ├── types.ts │ └── users │ │ ├── __mocks__ │ │ └── usersState.ts │ │ ├── __tests__ │ │ ├── reducer-test.ts │ │ └── selectors-test.ts │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── selectors.ts │ │ └── types.ts └── utils │ ├── __tests__ │ ├── resin-test.ts │ ├── rotate-test.ts │ └── scroll-test.ts │ ├── i18n.ts │ ├── resin.ts │ ├── rotate.ts │ ├── scroll.ts │ └── util.ts ├── stylelint.config.js ├── test ├── .eslintrc.json ├── fixtures │ └── index.json ├── index.html ├── integration │ ├── Discoverability.e2e.test.js │ ├── Drawing.e2e.test.js │ ├── Highlight.e2e.test.js │ ├── Region.e2e.test.js │ └── Sanity.e2e.test.js ├── plugins │ └── index.js ├── styles.css └── support │ ├── commands.js │ ├── constants.js │ ├── defaults.js │ └── index.js ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | docs/* 3 | i18n/** 4 | index.js 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | require.resolve('@box/frontend/eslint/base'), 4 | require.resolve('@box/frontend/eslint/react'), 5 | require.resolve('@box/frontend/eslint/typescript'), 6 | ], 7 | rules: { 8 | 'class-methods-use-this': 0, // fixme 9 | 'flowtype/no-types-missing-file-annotation': 'off', // Allows types in TS files 10 | 'import/no-extraneous-dependencies': 'off', // All dependencies are included in dist bundle 11 | 'import/no-unresolved': 'off', // Allows JS files to import TS files 12 | 'import/prefer-default-export': 'off', 13 | 'prefer-destructuring': ['error', { object: true, array: false }], 14 | }, 15 | overrides: [ 16 | { 17 | files: ['*.ts', '*.tsx'], 18 | rules: { 19 | '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], 20 | '@typescript-eslint/no-explicit-any': ['error', { ignoreRestArgs: true }], 21 | '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_' }], 22 | }, 23 | }, 24 | { 25 | files: ['**/__mocks__/*', '**/__tests__/*'], 26 | rules: { 27 | '@typescript-eslint/naming-convention': 'off', 28 | '@typescript-eslint/no-non-null-assertion': 'off', 29 | 'import/no-import-module-exports': 'off' 30 | }, 31 | }, 32 | { 33 | files: ['*.e2e.test.js'], 34 | rules: { 35 | 'spaced-comment': 'off', // Allow JS files to use TS Triple-Slash Directives 36 | }, 37 | }, 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug/Issue report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | Please fill out the following template so we can reproduce and fix your issue as quickly as possible! 8 | 9 | **Note:** if your issue includes a potential security vulnerability, please do not file it here. Instead, email the issue to security@box.com for support. 10 | 11 | **Environment:** 12 | - Preview and/or Annotations version: 13 | 14 | **Desktop (please complete the following information):** 15 | - OS: [e.g. iOS] 16 | - Browser [e.g. chrome, safari] 17 | - Version [e.g. 22] 18 | 19 | **Smartphone (please complete the following information):** 20 | - Device: [e.g. iPhone6] 21 | - OS: [e.g. iOS8.1] 22 | - Browser [e.g. stock browser, safari] 23 | - Version [e.g. 22] 24 | 25 | 26 | **Steps to reproduce the problem:** 27 | 1. 28 | 2. 29 | 3. 30 | 31 | **What is the expected behavior? (Screenshots can be helpful here)** 32 | 33 | **What went wrong? (Screenshots can be helpful here)** 34 | 35 | **Link to application or sample code:** 36 | 37 | **If relevant, link to file (or attach file here):** 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Describe the solution you'd like** 8 | A clear and concise description of what you want to happen. 9 | 10 | **Describe alternatives you've considered** 11 | A clear and concise description of any alternative solutions or features you've considered. 12 | 13 | **Additional context** 14 | If you're already a Box customer, please send an email preview-team@box.com with your admin's information/enterprise ID. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | dist 4 | functional-tests/output 5 | i18n/*.js 6 | i18n/json 7 | node_modules 8 | npm-debug.log 9 | npm-error.log 10 | package-lock.json 11 | reports 12 | scripts/rsync.json 13 | test/screenshots 14 | test/videos 15 | yarn-debug.log 16 | yarn-error.log 17 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | - status-success=Travis CI - Pull Request 5 | 6 | pull_request_rules: 7 | - name: Automatic strict merge 8 | conditions: 9 | - base=master 10 | - "#approved-reviews-by>=1" 11 | - "#changes-requested-reviews-by=0" 12 | - "#review-requested=0" 13 | - status-success=Travis CI - Pull Request 14 | - label=ready-to-merge 15 | - label!=do-not-merge 16 | actions: 17 | queue: 18 | method: squash 19 | name: default 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !dist 3 | !i18n 4 | !index.js 5 | !LICENSE 6 | !package.json 7 | !README.md 8 | !src/ 9 | !THIRD_PARTY_LICENSES 10 | __mocks__ 11 | __tests__ 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/iron 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@box/frontend/prettier/prettierrc.js'); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # @desktop @mobile @enabled 2 | language: node_js 3 | dist: 'focal' 4 | node_js: 5 | - '20' 6 | addons: 7 | apt: 8 | packages: 9 | - libgconf-2-4 10 | cache: 11 | yarn: true 12 | directories: 13 | - node_modules 14 | - ~/.cache/Cypress 15 | before_install: 16 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.5 17 | - export HUSKY_SKIP_HOOKS=1 18 | - export NODE_OPTIONS=--openssl-legacy-provider 19 | - export PATH=$HOME/.yarn/bin:$PATH 20 | - export TZ=America/Los_Angeles 21 | jobs: 22 | include: 23 | - name: 'Build' 24 | script: yarn build 25 | - name: 'Code Lint' 26 | script: yarn lint 27 | - name: 'Unit Tests' 28 | script: yarn test 29 | - name: 'E2E Tests' 30 | if: fork = false # Note: We can only run E2E tests on canonical due to security concerns 31 | script: travis_wait 30 yarn test:e2e 32 | notifications: 33 | email: 34 | recipients: 35 | - preview-dev@box.com 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "prettier.printWidth": 120, 4 | "prettier.singleQuote": true, 5 | "prettier.tabWidth": 4, 6 | "typescript.format.enable": true, 7 | "typescript.validate.enable": true 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.yarnpkg.com" 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Root 2 | * @box/preview 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache(() => process.env.NODE_ENV); 3 | 4 | return { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | { 9 | modules: false, 10 | }, 11 | ], 12 | '@babel/preset-react', 13 | '@babel/preset-typescript', 14 | ], 15 | plugins: [ 16 | '@babel/plugin-proposal-class-properties', 17 | '@babel/plugin-proposal-object-rest-spread', 18 | '@babel/plugin-transform-flow-strip-types', // Required for jest coverage, for some reason 19 | '@babel/plugin-transform-object-assign', 20 | '@babel/plugin-transform-runtime', 21 | [ 22 | 'react-intl', 23 | { 24 | messagesDir: './i18n/json', 25 | }, 26 | ], 27 | ], 28 | env: { 29 | test: { 30 | plugins: ['@babel/plugin-transform-modules-commonjs'], 31 | }, 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # Browser support list - used by CSS autoprefixer 2 | last 2 Chrome versions 3 | last 2 Firefox versions 4 | last 2 Safari versions 5 | last 2 Edge versions 6 | last 2 iOS versions 7 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | module.exports = require('@box/frontend/commitlint/commitlint.config.js'); 3 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8001/#", 3 | "defaultCommandTimeout": 8000, 4 | "fileServerFolder": "test", 5 | "fixturesFolder": "test/fixtures", 6 | "integrationFolder": "test/integration", 7 | "pluginsFile": "test/plugins/index.js", 8 | "screenshotsFolder": "test/screenshots", 9 | "supportFile": "test/support/index.js", 10 | "video": false, 11 | "videosFolder": "test/videos", 12 | "viewportHeight": 1260, 13 | "viewportWidth": 1600 14 | } 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## High-level Documentation 4 | 5 | General explanations of the available functionality and examples of how to use Box Annotations are available by topic: 6 | 7 | - [Contributing to Box Annotations](CONTRIBUTING.md) 8 | - [Developer Setup](docs/DEVELOPING.md) 9 | - [Using BoxAnnotations](usage.md) 10 | - [Tokens + Scopes](auth.md) 11 | - [Enabling/Disabling Annotation Types](enabling-types.md) 12 | - [Annotator](annotator.md) 13 | - [Annotation Thread](thread.md) 14 | - [Annotation Dialog](dialog.md) 15 | - [Creating a new Annotation Type](add-annotation-type.md) 16 | -------------------------------------------------------------------------------- /i18n/bn-IN.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = বন্ধ করুন 3 | # Error message when creating 4 | ba.annotationsCreateError = আমরা দুঃখিত, অ্যানোটেশন তৈরী করা যায়নি। 5 | # Error message when loading 6 | ba.annotationsLoadError = আমরা দুঃখিত, এই ফাইলের জন্য অ্যানোটেশন লোড করা যায়নি। 7 | # Label for the post button 8 | ba.annotationsPost = পোস্ট করুন 9 | # Label for the save button 10 | ba.annotationsSave = সংরক্ষণ করুন 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = বাতিল করুন 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = মন্তব্য যোগ করুন 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = মুছুন 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = রিডু 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = বাতিল করুন 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = মন্তব্য করতে একটি বক্স আঁকুন 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = হাইলাইট এবং মন্তব্য করুন 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = মন্তব্যগুলি কেবলমাত্র একটি পৃষ্ঠায় সীমাবদ্ধ রয়েছে 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = কাউকে জানাতে তাকে উল্লেখ করুন 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = পোস্ট করুন 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = একটি মন্তব্য টাইপ করুন... 33 | -------------------------------------------------------------------------------- /i18n/da-DK.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Luk 3 | # Error message when creating 4 | ba.annotationsCreateError = Vi beklager, anmærkningen kunne ikke oprettes. 5 | # Error message when loading 6 | ba.annotationsLoadError = Vi beklager, anmærkningerne for denne fil kunne ikke indlæses. 7 | # Label for the post button 8 | ba.annotationsPost = Opslå 9 | # Label for the save button 10 | ba.annotationsSave = Gem 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Annuller 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Tilføj kommentar 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Slet 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Annuller fortryd 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Fortryd 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Tegn et felt for at kommentere 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Fremhæv og kommenter 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Kommentarer er begrænset til enkelt side 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Nævn en person for at give vedkommende besked 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Slå op 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Skriv en kommentar ... 33 | -------------------------------------------------------------------------------- /i18n/de-DE.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Schließen 3 | # Error message when creating 4 | ba.annotationsCreateError = Die Anmerkung konnte leider nicht erstellt werden. 5 | # Error message when loading 6 | ba.annotationsLoadError = Die Anmerkungen für diese Datei konnten leider nicht geladen werden. 7 | # Label for the post button 8 | ba.annotationsPost = Posten 9 | # Label for the save button 10 | ba.annotationsSave = Speichern 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Abbrechen 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Kommentar hinzufügen 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Löschen 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Wiederholen 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Rückgängig machen 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Zeichnen Sie ein Feld zum Kommentieren 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Markieren und kommentieren 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Kommentare sind auf eine Seite beschränkt 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Jemanden erwähnen, der benachrichtigt werden soll 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Posten 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Kommentar eingeben ... 33 | -------------------------------------------------------------------------------- /i18n/en-AU.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Close 3 | # Error message when creating 4 | ba.annotationsCreateError = We're sorry, but the annotation could not be created. 5 | # Error message when loading 6 | ba.annotationsLoadError = We're sorry, the annotations failed to load for this file. 7 | # Label for the post button 8 | ba.annotationsPost = Post 9 | # Label for the save button 10 | ba.annotationsSave = Save 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Cancel 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Add a comment 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Delete 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Redo 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Undo 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Draw a box to comment 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Highlight and comment 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Comments restricted to single page 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mention someone to notify them 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Post 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Type a comment… 33 | -------------------------------------------------------------------------------- /i18n/en-CA.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Close 3 | # Error message when creating 4 | ba.annotationsCreateError = We’re sorry, the annotation could not be created. 5 | # Error message when loading 6 | ba.annotationsLoadError = We’re sorry, the annotations failed to load for this file. 7 | # Label for the post button 8 | ba.annotationsPost = Post 9 | # Label for the save button 10 | ba.annotationsSave = Save 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Cancel 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Add Comment 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Delete 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Redo 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Undo 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Draw a box to comment 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Highlight and Comment 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Comments restricted to single page 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mention someone to notify them 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Post 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Type a comment... 33 | -------------------------------------------------------------------------------- /i18n/en-GB.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Close 3 | # Error message when creating 4 | ba.annotationsCreateError = We're sorry, but the annotation could not be created. 5 | # Error message when loading 6 | ba.annotationsLoadError = We're sorry, the annotations failed to load for this file. 7 | # Label for the post button 8 | ba.annotationsPost = Post 9 | # Label for the save button 10 | ba.annotationsSave = Save 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Cancel 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Add a comment 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Delete 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Redo 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Undo 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Draw a box to comment 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Highlight and comment 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Comments restricted to single page 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mention someone to notify them 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Post 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Type a comment… 33 | -------------------------------------------------------------------------------- /i18n/en-US.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Close 3 | # Error message when creating 4 | ba.annotationsCreateError = We’re sorry, the annotation could not be created. 5 | # Error message when loading 6 | ba.annotationsLoadError = We’re sorry, the annotations failed to load for this file. 7 | # Label for the post button 8 | ba.annotationsPost = Post 9 | # Label for the save button 10 | ba.annotationsSave = Save 11 | # Aria label description for reply popup 12 | ba.popup.reply.comment = Comment 13 | # Aria label description for annotation comment field 14 | ba.popup.reply.field = Type a comment 15 | # Button label for cancelling the creation of a description, comment, or reply 16 | ba.popups.cancel = Cancel 17 | # Button label for adding a comment in the drawing toolbar 18 | ba.popups.drawing.addComment = Add Comment 19 | # Button title for deleting a staged drawing 20 | ba.popups.drawing.delete = Delete 21 | # Button title for redoing a staged drawing 22 | ba.popups.drawing.redo = Redo 23 | # Button title for undoing a staged drawing 24 | ba.popups.drawing.undo = Undo 25 | # Prompt message following cursor in region annotations mode 26 | ba.popups.popupCursor.regionPrompt = Draw a box to comment 27 | # Popup message for highlight promoter 28 | ba.popups.popupHighlight.promoter = Highlight and Comment 29 | # Prompt message when selection crosses multiple pages 30 | ba.popups.popupHighlight.restrictedPrompt = Comments restricted to single page 31 | # Prompt message for empty popup list 32 | ba.popups.popupList.prompt = Mention someone to notify them 33 | # Button label for creating a description, comment, or reply 34 | ba.popups.post = Post 35 | # Placeholder for reply field editor 36 | ba.popups.replyField.placeholder = Type a comment... 37 | -------------------------------------------------------------------------------- /i18n/es-419.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Cerrar 3 | # Error message when creating 4 | ba.annotationsCreateError = Lo sentimos, no se ha podido crear la anotación. 5 | # Error message when loading 6 | ba.annotationsLoadError = Lo sentimos, no se han podido cargar las anotaciones de este archivo. 7 | # Label for the post button 8 | ba.annotationsPost = Publicar 9 | # Label for the save button 10 | ba.annotationsSave = Guardar 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Cancelar 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Agregar comentario 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Eliminar 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Repetir 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Deshacer 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Dibuje un cuadro para comentar 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Resaltar y comentar 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Los comentarios están restringidos a una sola página. 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mencione a alguien para que reciba una notificación 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Publicar 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Escriba un comentario... 33 | -------------------------------------------------------------------------------- /i18n/es-ES.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Cerrar 3 | # Error message when creating 4 | ba.annotationsCreateError = Lo sentimos, no se ha podido crear la anotación. 5 | # Error message when loading 6 | ba.annotationsLoadError = Lo sentimos, no se han podido cargar las anotaciones de este archivo. 7 | # Label for the post button 8 | ba.annotationsPost = Publicar 9 | # Label for the save button 10 | ba.annotationsSave = Guardar 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Cancelar 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Agregar comentario 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Eliminar 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Repetir 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Deshacer 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Dibuje un cuadro para comentar 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Resaltar y comentar 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Los comentarios están restringidos a una sola página. 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mencione a alguien para que reciba una notificación 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Publicar 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Escriba un comentario... 33 | -------------------------------------------------------------------------------- /i18n/fi-FI.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Sulje 3 | # Error message when creating 4 | ba.annotationsCreateError = Huomautusta ei voitu luoda. 5 | # Error message when loading 6 | ba.annotationsLoadError = Tämän tiedoston huomautuksia ei voitu ladata. 7 | # Label for the post button 8 | ba.annotationsPost = Lähetä 9 | # Label for the save button 10 | ba.annotationsSave = Tallenna 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Peruuta 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Lisää kommentti 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Poista 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Tee uudelleen 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Kumoa 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Kommentoi piirtämällä laatikko 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Korosta ja kommentoi 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Kommentit on rajoitettu yhteen sivuun 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mainitse käyttäjä, jotta hän saa ilmoituksen 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Lähetä 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Kirjoita kommentti... 33 | -------------------------------------------------------------------------------- /i18n/fr-CA.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Fermer 3 | # Error message when creating 4 | ba.annotationsCreateError = Nous sommes désolés, l'annotation n'a pas pu être créée. 5 | # Error message when loading 6 | ba.annotationsLoadError = Nous sommes désolés, nous n'avons pas pu charger les annotations pour ce fichier. 7 | # Label for the post button 8 | ba.annotationsPost = Publier 9 | # Label for the save button 10 | ba.annotationsSave = Enregistrer 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Annuler 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Ajouter un commentaire 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Supprimer 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Rétablir 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Annuler 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Dessinez un cadre pour commenter 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Surligner et commenter 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Commentaires limités à une seule page 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mentionnez un utilisateur pour l’avertir 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Publier 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Saisissez un commentaire... 33 | -------------------------------------------------------------------------------- /i18n/fr-FR.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Fermer 3 | # Error message when creating 4 | ba.annotationsCreateError = Nous sommes désolés, l'annotation n'a pas pu être créée. 5 | # Error message when loading 6 | ba.annotationsLoadError = Nous sommes désolés, nous n'avons pas pu charger les annotations pour ce fichier. 7 | # Label for the post button 8 | ba.annotationsPost = Publier 9 | # Label for the save button 10 | ba.annotationsSave = Enregistrer 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Annuler 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Ajouter un commentaire 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Supprimer 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Rétablir 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Annuler 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Dessinez un cadre pour commenter 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Surligner et commenter 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Commentaires limités à une seule page 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mentionnez un utilisateur pour l’avertir 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Publier 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Saisissez un commentaire... 33 | -------------------------------------------------------------------------------- /i18n/hi-IN.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = बंद करें 3 | # Error message when creating 4 | ba.annotationsCreateError = हमें खेद है, टिप्पणी बनाई नहीं जा सकी. 5 | # Error message when loading 6 | ba.annotationsLoadError = हमें खेद है, इस फ़ाइल के लिए टिप्पणी लोड होने में विफल रहीं. 7 | # Label for the post button 8 | ba.annotationsPost = पोस्ट 9 | # Label for the save button 10 | ba.annotationsSave = सहेजें 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = रद्द करें 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = टिप्पणी जोड़ें 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = हटाएँ 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = फिर से करें 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = पहले जैसा करें 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = टिप्पणी के लिए एक बॉक्स रेखांकित करें 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = हाइलाइट करें और टिप्पणी दें 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = टिप्पणियाँ एक पेज तक सीमित की गईं हैं 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = किसी व्यक्ति को सूचित करने के लिए उनका उल्लेख करें 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = पोस्ट 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = एक टिप्पणी लिखें... 33 | -------------------------------------------------------------------------------- /i18n/it-IT.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Chiudi 3 | # Error message when creating 4 | ba.annotationsCreateError = Impossibile creare la nota. 5 | # Error message when loading 6 | ba.annotationsLoadError = Caricamento delle note di questo file non riuscito. 7 | # Label for the post button 8 | ba.annotationsPost = Pubblica 9 | # Label for the save button 10 | ba.annotationsSave = Salva 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Annulla 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Aggiungi commento 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Elimina 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Ripeti 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Annulla 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Disegna una casella per il commento 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Evidenzia e commenta 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = I commenti sono limitati a una sola pagina 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Menziona un utente per inviargli una notifica 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Pubblica 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Scrivi un commento... 33 | -------------------------------------------------------------------------------- /i18n/ja-JP.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = 閉じる 3 | # Error message when creating 4 | ba.annotationsCreateError = 注釈を作成できませんでした。 5 | # Error message when loading 6 | ba.annotationsLoadError = このファイルの注釈を読み込めませんでした。 7 | # Label for the post button 8 | ba.annotationsPost = 投稿 9 | # Label for the save button 10 | ba.annotationsSave = 保存 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = キャンセル 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = コメントを追加 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = 削除 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = やり直す 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = 元に戻す 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = 四角を描いてコメント 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = ハイライトとコメント 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = コメントは1つのページに制限されています 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = 他のユーザーにコメントして通知してください 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = 投稿 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = コメントを入力... 33 | -------------------------------------------------------------------------------- /i18n/ko-KR.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = 닫기 3 | # Error message when creating 4 | ba.annotationsCreateError = 죄송합니다. 주석을 만들 수 없습니다. 5 | # Error message when loading 6 | ba.annotationsLoadError = 죄송합니다. 이 파일에 대한 주석을 로드하지 못했습니다. 7 | # Label for the post button 8 | ba.annotationsPost = 게시 9 | # Label for the save button 10 | ba.annotationsSave = 저장 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = 취소 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = 코멘트 추가 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = 삭제 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = 다시 실행 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = 실행 취소 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = 코멘트를 입력할 상자 그리기 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = 강조 표시 및 코멘트 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = 단일 페이지로 제한된 코멘트 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = 다른 사용자에게 알리기 위해 멘션 달기 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = 게시 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = 코멘트 입력... 33 | -------------------------------------------------------------------------------- /i18n/nb-NO.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Lukk 3 | # Error message when creating 4 | ba.annotationsCreateError = Kan ikke opprette merknaden. 5 | # Error message when loading 6 | ba.annotationsLoadError = Kan ikke laste inn merknadene for denne filen. 7 | # Label for the post button 8 | ba.annotationsPost = Legg inn 9 | # Label for the save button 10 | ba.annotationsSave = Lagre 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Avbryt 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Legg til kommentar 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Slett 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Gjør om 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Angre 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Tegn en boks for å kommentere 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Merk og Kommenter 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Kommentarer er begrenset til én side 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Nevn noen for å varsle vedkommende 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Legg inn 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Skriv en kommentar … 33 | -------------------------------------------------------------------------------- /i18n/nl-NL.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Sluiten 3 | # Error message when creating 4 | ba.annotationsCreateError = De aantekening kan niet worden gemaakt. 5 | # Error message when loading 6 | ba.annotationsLoadError = De annotaties voor dit bestand kunnen niet worden geladen. 7 | # Label for the post button 8 | ba.annotationsPost = Plaatsen 9 | # Label for the save button 10 | ba.annotationsSave = Opslaan 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Annuleren 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Opmerking toevoegen 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Verwijderen 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Opnieuw 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Ongedaan maken 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Teken een vak om een opmerking te plaatsen 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Markeringen en opmerkingen plaatsen 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Opmerkingen beperkt tot enkele pagina 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Noem iemand om die persoon een melding te sturen 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Plaatsen 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Typ een opmerking... 33 | -------------------------------------------------------------------------------- /i18n/pl-PL.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Zamknij 3 | # Error message when creating 4 | ba.annotationsCreateError = Przepraszamy, nie można utworzyć uwagi. 5 | # Error message when loading 6 | ba.annotationsLoadError = Przepraszamy, nie można wczytać uwag dla tego pliku. 7 | # Label for the post button 8 | ba.annotationsPost = Opublikuj 9 | # Label for the save button 10 | ba.annotationsSave = Zapisz 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Anuluj 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Dodaj komentarz 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Usuń 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Ponów 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Cofnij 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Narysuj pole, aby skomentować 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Wyróżnij i skomentuj 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Komentarze ograniczone do pojedynczej strony 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Wspomnij kogoś, aby go powiadomić 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Opublikuj 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Wpisz komentarz... 33 | -------------------------------------------------------------------------------- /i18n/pt-BR.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Fechar 3 | # Error message when creating 4 | ba.annotationsCreateError = Não foi possível criar a anotação. 5 | # Error message when loading 6 | ba.annotationsLoadError = Não foi possível carregar as anotações deste arquivo. 7 | # Label for the post button 8 | ba.annotationsPost = Publicar 9 | # Label for the save button 10 | ba.annotationsSave = Salvar 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Cancelar 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Adicionar comentário 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Excluir 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Refazer 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Desfazer 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Desenhe uma caixa para comentar 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Realçar e comentar 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Comentários restritos a uma única página 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Mencione alguém para notificá-lo 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Publicar 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Digite um comentário... 33 | -------------------------------------------------------------------------------- /i18n/ru-RU.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Закрыть 3 | # Error message when creating 4 | ba.annotationsCreateError = Не удалось создать примечание. 5 | # Error message when loading 6 | ba.annotationsLoadError = Сбой при загрузке примечаний для этого файла. 7 | # Label for the post button 8 | ba.annotationsPost = Отправить 9 | # Label for the save button 10 | ba.annotationsSave = Сохранить 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Отмена 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Добавить комментарий 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Удалить 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Повторить 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Отменить 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Нарисуйте рамку для добавления комментария 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Выделить и прокомментировать 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Комментарии ограничены одной страницей 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Упомяните пользователя, чтобы он получил уведомление 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Отправить 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Введите комментарий... 33 | -------------------------------------------------------------------------------- /i18n/sv-SE.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Stäng 3 | # Error message when creating 4 | ba.annotationsCreateError = Det gick inte att skapa anteckningen. 5 | # Error message when loading 6 | ba.annotationsLoadError = Det gick inte att ladda anteckningar för den här filen. 7 | # Label for the post button 8 | ba.annotationsPost = Publicera 9 | # Label for the save button 10 | ba.annotationsSave = Spara 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = Avbryt 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Lägg till kommentar 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Radera 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Gör om 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Ångra 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Rita en ruta för att kommentera 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Markera och kommentera 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Kommentarer begränsas till en sida 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Nämn någon som ska meddela dem 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Publicera 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Skriv en kommentar ... 33 | -------------------------------------------------------------------------------- /i18n/tr-TR.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = Kapat 3 | # Error message when creating 4 | ba.annotationsCreateError = Üzgünüz, açıklama oluşturulamadı. 5 | # Error message when loading 6 | ba.annotationsLoadError = Üzgünüz, açıklamalar bu dosya için yüklenemedi. 7 | # Label for the post button 8 | ba.annotationsPost = Gönder 9 | # Label for the save button 10 | ba.annotationsSave = Kaydet 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = İptal 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = Yorum Ekle 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = Sil 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = Yinele 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = Geri Al 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = Yorum yapmak için bir kutu çizin 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = Vurgulayın ve Yorum Yapın 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = Yorumlar tek sayfayla sınırlandırıldı 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = Bildirim alması için birinden bahsedin 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = Gönder 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = Bir yorum yazın... 33 | -------------------------------------------------------------------------------- /i18n/zh-CN.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = 关闭 3 | # Error message when creating 4 | ba.annotationsCreateError = 抱歉,无法创建批注。 5 | # Error message when loading 6 | ba.annotationsLoadError = 抱歉,无法为此文件加载批注。 7 | # Label for the post button 8 | ba.annotationsPost = 发布 9 | # Label for the save button 10 | ba.annotationsSave = 保存 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = 取消 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = 添加评论 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = 删除 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = 恢复 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = 撤销 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = 绘制一个框进行评论 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = 突出显示和评论 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = 评论仅限一页 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = 提及某人以通知他们 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = 发布 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = 键入评论... 33 | -------------------------------------------------------------------------------- /i18n/zh-TW.properties: -------------------------------------------------------------------------------- 1 | # Label for the close button 2 | ba.annotationsClose = 關閉 3 | # Error message when creating 4 | ba.annotationsCreateError = 很抱歉,無法建立註解。 5 | # Error message when loading 6 | ba.annotationsLoadError = 很抱歉,無法載入此檔案的註解。 7 | # Label for the post button 8 | ba.annotationsPost = 張貼 9 | # Label for the save button 10 | ba.annotationsSave = 儲存 11 | # Button label for cancelling the creation of a description, comment, or reply 12 | ba.popups.cancel = 取消 13 | # Button label for adding a comment in the drawing toolbar 14 | ba.popups.drawing.addComment = 新增留言 15 | # Button title for deleting a staged drawing 16 | ba.popups.drawing.delete = 刪除 17 | # Button title for redoing a staged drawing 18 | ba.popups.drawing.redo = 重做 19 | # Button title for undoing a staged drawing 20 | ba.popups.drawing.undo = 復原 21 | # Prompt message following cursor in region annotations mode 22 | ba.popups.popupCursor.regionPrompt = 繪圖一個方塊以留言 23 | # Popup message for highlight promoter 24 | ba.popups.popupHighlight.promoter = 醒目提示和留言 25 | # Prompt message when selection crosses multiple pages 26 | ba.popups.popupHighlight.restrictedPrompt = 留言限制在單一頁面 27 | # Prompt message for empty popup list 28 | ba.popups.popupList.prompt = 提及某人來通知該名人員 29 | # Button label for creating a description, comment, or reply 30 | ba.popups.post = 發佈 31 | # Placeholder for reply field editor 32 | ba.popups.replyField.placeholder = 輸入留言... 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import './dist/annotations.css'; 2 | import './dist/annotations.js'; 3 | 4 | export default BoxAnnotations; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | collectCoverage: false, 4 | collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*.d.ts', '!**/messages.{ts,tsx}', '!**/node_modules/**'], 5 | coverageDirectory: '/reports', 6 | globals: { 7 | __NAME__: 'name', 8 | __VERSION__: 'version', 9 | __LANGUAGE__: 'en-US', 10 | }, 11 | moduleNameMapper: { 12 | '\\.(css|scss|less|html)$': '/scripts/jest/styleMock.js', 13 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$': '/scripts/jest/fileMock.js', 14 | '@popperjs/core': '/scripts/jest/popperMock.js', 15 | 'box-annotations-locale-data': '/scripts/jest/i18nMock.js', 16 | 'box-elements-messages': '/scripts/jest/i18nMock.js', 17 | 'react-intl': '/scripts/jest/react-intl-mock.js', 18 | 'react-intl-locale-data': '/node_modules/react-intl/locale-data/en.js', 19 | mousetrap: '/scripts/jest/moduleMock.js', 20 | rangy: '/scripts/jest/moduleMock.js', 21 | }, 22 | modulePathIgnorePatterns: ['__mocks__'], 23 | restoreMocks: true, 24 | roots: ['src'], 25 | setupFiles: ['jest-canvas-mock', '/scripts/jest/envWindow.js'], 26 | setupFilesAfterEnv: ['/scripts/jest/enzyme-adapter.js'], 27 | snapshotSerializers: ['enzyme-to-json/serializer'], 28 | testEnvironment: 'jest-environment-jsdom-sixteen', 29 | transformIgnorePatterns: ['node_modules/(?!(box-ui-elements)/)'], 30 | }; 31 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.js': ['eslint --fix', 'git add'], 3 | '*.json': ['prettier --write --parser=json', 'git add'], 4 | '*.html': ['prettier --write --parser=html', 'git add'], 5 | '*.md': ['prettier --write --parser=markdown', 'git add'], 6 | '*.scss': ['prettier --write --parser=scss', 'stylelint --syntax scss --fix', 'git add'], 7 | '*.ts': ['eslint --ext=.ts --fix', 'git add'], 8 | '*.tsx': ['eslint --ext=.tsx --fix', 'git add'], 9 | }; 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // This is used to auto-prefix CSS, see: https://github.com/postcss/postcss-loader 2 | const autoprefixer = require('autoprefixer'); // eslint-disable-line 3 | 4 | module.exports = { 5 | plugins: [autoprefixer()], 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/build_locale.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | /** 4 | * Build a single locale 5 | * 6 | * @param {string} locale - locale to build 7 | * @param {*} callback - callback from worker-farm master process 8 | * @return {void} 9 | */ 10 | module.exports = (locale = 'en-US', callback) => { 11 | try { 12 | console.log(`Building ${locale}`); // eslint-disable-line 13 | // build assets for a single locale 14 | execSync(`time LANGUAGE=${locale} yarn build:prod:dist`); 15 | callback(); 16 | } catch (error) { 17 | console.error(`Error: Failed to build ${locale}`); // eslint-disable-line 18 | callback(true); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /scripts/current_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | node -pe 'JSON.parse(process.argv[1]).version' "$(cat package.json)" 4 | -------------------------------------------------------------------------------- /scripts/jest/envWindow.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty( 2 | window.navigator, 3 | 'userAgent', 4 | (value => ({ 5 | get() { 6 | return value; 7 | }, 8 | set(v) { 9 | value = v; 10 | }, 11 | }))(window.navigator.userAgent), 12 | ); 13 | -------------------------------------------------------------------------------- /scripts/jest/enzyme-adapter.js: -------------------------------------------------------------------------------- 1 | import 'core-js/es/map'; 2 | import 'core-js/es/set'; 3 | import Enzyme, { mount, shallow } from 'enzyme'; 4 | import Adapter from '@cfaester/enzyme-adapter-react-18'; 5 | 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | 8 | // make Enzyme functions available in all test files without importing 9 | global.shallow = shallow; 10 | global.mount = mount; 11 | -------------------------------------------------------------------------------- /scripts/jest/fileMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/fileMock.js 2 | 3 | module.exports = 'test-file-stub'; 4 | -------------------------------------------------------------------------------- /scripts/jest/i18nMock.js: -------------------------------------------------------------------------------- 1 | const messages = {}; 2 | export default messages; 3 | -------------------------------------------------------------------------------- /scripts/jest/moduleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /scripts/jest/popperMock.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const createPopper = jest.fn(() => ({ 3 | destroy: jest.fn(), 4 | forceUpdate: jest.fn(), 5 | setOptions: jest.fn(), 6 | update: jest.fn(), 7 | })); 8 | 9 | module.exports = { 10 | createPopper, 11 | popperGenerator: jest.fn(() => createPopper), 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/jest/react-intl-mock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const intlMock = { 4 | formatMessage: message => message.defaultMessage || message.message, 5 | formatDate: date => date, 6 | }; 7 | 8 | export const FormattedDate = () =>
; 9 | FormattedDate.displayName = 'FormattedDate'; 10 | 11 | export const FormattedTime = () =>
; 12 | FormattedTime.displayName = 'FormattedTime'; 13 | 14 | export const FormattedMessage = () =>
; 15 | FormattedMessage.displayName = 'FormattedMessage'; 16 | 17 | export const RawIntlProvider = () =>
; 18 | 19 | RawIntlProvider.displayName = 'RawIntlProvider'; 20 | export const createIntl = () => intlMock; 21 | 22 | export const defineMessages = messages => messages; 23 | 24 | export const createIntlCache = () => {}; 25 | 26 | export const injectIntl = Component => { 27 | const WrapperComponent = props => { 28 | const injectedProps = { ...props, intl: intlMock }; 29 | return ; 30 | }; 31 | WrapperComponent.displayName = Component.displayName || Component.name || 'Component'; 32 | return WrapperComponent; 33 | }; 34 | 35 | export const useIntl = () => intlMock; 36 | -------------------------------------------------------------------------------- /scripts/jest/styleMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/styleMock.js 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /scripts/license.js: -------------------------------------------------------------------------------- 1 | module.exports = `Box Annotations 2 | 3 | Copyright 2016-present Box, Inc. All Rights Reserved. 4 | 5 | This source code is licensed under the Box Software License Agreement found 6 | in the LICENSE file in the root directory of this source tree. Additional 7 | third party license disclosures can be found in the THIRD_PARTY_LICENSES 8 | file in the same directory.`; 9 | -------------------------------------------------------------------------------- /scripts/prod.js: -------------------------------------------------------------------------------- 1 | const workerFarm = require('worker-farm'); 2 | const locales = require('@box/languages'); 3 | const numCPUs = require('os').cpus().length; 4 | const { execSync } = require('child_process'); 5 | const path = require('path'); 6 | 7 | const filename = path.basename(__filename); 8 | const bundleCount = locales.length * 2; // One with react, and one without 9 | 10 | let counter = 0; 11 | const workers = workerFarm( 12 | { 13 | maxConcurrentWorkers: numCPUs - 2, 14 | maxRetries: 0, 15 | }, 16 | require.resolve('./build_locale.js'), 17 | ); 18 | 19 | /* eslint-disable */ 20 | locales.forEach(locale => { 21 | workers(locale, error => { 22 | if (++counter === bundleCount || error) { 23 | // terminate after all locales have been processed 24 | workerFarm.end(workers); 25 | } 26 | 27 | if (error) { 28 | // kill the node process that spawns the workers as well as all processes been spawned 29 | execSync(`ps ax | grep "${filename}" | cut -b1-06 | xargs -t kill`); 30 | } 31 | }); 32 | }); 33 | /* eslint-enable */ 34 | -------------------------------------------------------------------------------- /src/@types/api.ts: -------------------------------------------------------------------------------- 1 | export enum PERMISSIONS { 2 | CAN_CREATE_ANNOTATIONS = 'can_create_annotations', 3 | CAN_VIEW_ANNOTATIONS = 'can_view_annotations', 4 | } 5 | 6 | export interface Permissions { 7 | [index: string]: boolean | undefined; 8 | [PERMISSIONS.CAN_CREATE_ANNOTATIONS]?: boolean; 9 | [PERMISSIONS.CAN_VIEW_ANNOTATIONS]?: boolean; 10 | } 11 | 12 | export type TokenLiteral = null | undefined | string | { read: string; write?: string }; 13 | export type TokenResolver = () => TokenLiteral | Promise; 14 | export type Token = TokenLiteral | TokenResolver; 15 | -------------------------------------------------------------------------------- /src/@types/events.ts: -------------------------------------------------------------------------------- 1 | enum Event { 2 | ACTIVE_CHANGE = 'annotations_active_change', 3 | ACTIVE_SET = 'annotations_active_set', 4 | CREATOR_STAGED_CHANGE = 'creator_staged_change', 5 | CREATOR_STATUS_CHANGE = 'creator_status_change', 6 | ANNOTATION_CREATE = 'annotations_create', 7 | ANNOTATION_FETCH_ERROR = 'annotations_fetch_error', 8 | ANNOTATION_REMOVE = 'annotations_remove', 9 | ANNOTATIONS_INITIALIZED = 'annotations_initialized', 10 | ANNOTATIONS_MODE_CHANGE = 'annotations_mode_change', 11 | COLOR_SET = 'annotations_color_set', 12 | VISIBLE_SET = 'annotations_visible_set', 13 | } 14 | 15 | // Existing legacy events, don't rename 16 | enum LegacyEvent { 17 | ANNOTATOR = 'annotatorevent', 18 | ERROR = 'annotationerror', 19 | SCALE = 'scaleannotations', 20 | } 21 | 22 | export { Event, LegacyEvent }; 23 | -------------------------------------------------------------------------------- /src/@types/global.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-namespace */ 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | declare namespace NodeJS { 4 | interface Global { 5 | window: any; 6 | BoxAnnotations: any; 7 | } 8 | } 9 | 10 | declare module 'box-annotations-locale-data'; 11 | declare module 'box-elements-messages'; 12 | declare module 'box-ui-elements/es/*'; // TODO: Figure out why types don't register properly 13 | -------------------------------------------------------------------------------- /src/@types/i18n.ts: -------------------------------------------------------------------------------- 1 | export type IntlOptions = { 2 | messages?: Record; 3 | language?: string; 4 | locale?: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/@types/index.ts: -------------------------------------------------------------------------------- 1 | import './global'; 2 | 3 | export * from '@reduxjs/toolkit'; 4 | export * from './api'; 5 | export * from './events'; 6 | export * from './i18n'; 7 | export * from './model'; 8 | export * from './new'; 9 | export * from './users'; 10 | -------------------------------------------------------------------------------- /src/@types/new.ts: -------------------------------------------------------------------------------- 1 | import { Target } from './model'; 2 | 3 | export interface NewAnnotation { 4 | description?: NewReply; 5 | file_version: { 6 | id: string | null; 7 | }; 8 | target: Target; 9 | } 10 | 11 | export interface NewReply { 12 | message: string; 13 | type: 'reply'; 14 | } 15 | -------------------------------------------------------------------------------- /src/@types/users.ts: -------------------------------------------------------------------------------- 1 | export type Collaborator = { 2 | id: string; 3 | item?: UserMini | GroupMini; 4 | name: string; 5 | }; 6 | 7 | export type UserMini = { 8 | avatar_url?: string; 9 | email?: string; 10 | id: string; 11 | login?: string; 12 | name: string; 13 | type: 'user'; 14 | }; 15 | 16 | export type GroupMini = { 17 | id: string; 18 | name: string; 19 | type: 'group'; 20 | }; 21 | -------------------------------------------------------------------------------- /src/api/APIFactory.ts: -------------------------------------------------------------------------------- 1 | import Annotations from 'box-ui-elements/es/api/Annotations'; 2 | import FileCollaborators from 'box-ui-elements/es/api/FileCollaborators'; 3 | import { DEFAULT_HOSTNAME_API } from 'box-ui-elements/es/constants'; 4 | import { AnnotationsAPI, CollaboratorsAPI, APIOptions } from './types'; 5 | 6 | export default class APIFactory { 7 | options: APIOptions; 8 | 9 | constructor(apiOptions: APIOptions) { 10 | this.options = { 11 | apiHost: DEFAULT_HOSTNAME_API, 12 | clientName: 'box-annotations', 13 | ...apiOptions, 14 | }; 15 | } 16 | 17 | getAnnotationsAPI(): AnnotationsAPI { 18 | return new Annotations(this.options); 19 | } 20 | 21 | getCollaboratorsAPI(): CollaboratorsAPI { 22 | return new FileCollaborators(this.options); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/__mocks__/APIFactory.ts: -------------------------------------------------------------------------------- 1 | export const annotations = [ 2 | { id: 'anno_1', target: { type: 'region' }, type: 'annotation' }, 3 | { id: 'anno_2', target: { type: 'region' }, type: 'annotation' }, 4 | { id: 'anno_3', target: { type: 'region' }, type: 'annotation' }, 5 | ]; 6 | 7 | export default jest.fn(() => ({ 8 | getAnnotationsAPI: jest.fn(() => ({ 9 | createAnnotation: jest.fn((fileId, fileVersionId, payload, permissions, resolve) => resolve(annotations[0])), 10 | getAnnotations: jest.fn((fileId, fileVersionId, permissions, resolve) => 11 | resolve({ entries: annotations, limit: 1000, next_marker: null }), 12 | ), 13 | destroy: jest.fn(), 14 | })), 15 | })); 16 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './APIFactory'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/common/BaseAnnotator.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/elements/common/fonts'; 2 | @import '~box-ui-elements/es/styles/variables'; 3 | 4 | .ba { 5 | @import '~box-ui-elements/es/styles/common/links'; 6 | @import '~box-ui-elements/es/styles/common/forms'; 7 | @import '~box-ui-elements/es/styles/common/buttons'; 8 | 9 | &.is-hidden { 10 | .ba-Layer { 11 | display: none; 12 | } 13 | } 14 | } 15 | 16 | .ba-Layer { 17 | @include box-sizing; 18 | @include common-typography; 19 | } 20 | 21 | .ba-Layer--drawing, 22 | .ba-Layer--highlight, 23 | .ba-Layer--region { 24 | z-index: 3; 25 | } 26 | 27 | .ba-Layer--popup { 28 | z-index: 4; 29 | } 30 | -------------------------------------------------------------------------------- /src/common/DeselectListener.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { setActiveAnnotationIdAction } from '../store/annotations'; 4 | 5 | export default function DeselectListener(): null { 6 | const dispatch = useDispatch(); 7 | 8 | React.useEffect(() => { 9 | const handleMouseDown = (): void => { 10 | dispatch(setActiveAnnotationIdAction(null)); 11 | }; 12 | 13 | document.addEventListener('mousedown', handleMouseDown); 14 | 15 | return () => { 16 | document.removeEventListener('mousedown', handleMouseDown); 17 | }; 18 | }); 19 | 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /src/common/DeselectManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { Store } from 'redux'; 5 | import BaseManager from './BaseManager'; 6 | import DeselectListener from './DeselectListener'; 7 | 8 | export type Options = { 9 | referenceEl: HTMLElement; 10 | }; 11 | 12 | export type Props = { 13 | store: Store; 14 | }; 15 | 16 | export default class DeselectManager extends BaseManager { 17 | decorate(): void { 18 | this.reactEl.classList.add('ba-Layer--deselect'); 19 | this.reactEl.dataset.testid = 'ba-Layer--deselect'; 20 | } 21 | 22 | render({ store }: Props): void { 23 | if (!this.root) { 24 | this.root = ReactDOM.createRoot(this.reactEl); 25 | } 26 | 27 | this.root.render( 28 | 29 | 30 | , 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import eventManager from './EventManager'; 2 | 3 | export default class EventEmitter { 4 | addListener(event: string | symbol, listener: (...args: any[]) => void): void { 5 | eventManager.addListener(event, listener); 6 | } 7 | 8 | emit(event: string | symbol, ...args: any[]): void { 9 | eventManager.emit(event, ...args); 10 | } 11 | 12 | removeAllListeners(): void { 13 | eventManager.removeAllListeners(); 14 | } 15 | 16 | removeListener(event: string | symbol, listener: (...args: any[]) => void): void { 17 | eventManager.removeListener(event, listener); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/EventManager.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { LegacyEvent } from '../@types'; 3 | 4 | class EventManager extends EventEmitter { 5 | emit(event: string | symbol, ...args: unknown[]): boolean { 6 | const [data] = args; 7 | super.emit(event, data); 8 | super.emit(LegacyEvent.ANNOTATOR, { 9 | event, 10 | data, 11 | }); 12 | 13 | return true; 14 | } 15 | } 16 | 17 | export default new EventManager(); 18 | -------------------------------------------------------------------------------- /src/common/__mocks__/EventManager.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | addListener: jest.fn(), 3 | emit: jest.fn(), 4 | removeListener: jest.fn(), 5 | removeAllListeners: jest.fn(), 6 | }; 7 | -------------------------------------------------------------------------------- /src/common/__mocks__/events.ts: -------------------------------------------------------------------------------- 1 | export const mockEvent = { 2 | preventDefault: jest.fn(), 3 | stopPropagation: jest.fn(), 4 | }; 5 | -------------------------------------------------------------------------------- /src/common/__mocks__/useIsListInteractive.ts: -------------------------------------------------------------------------------- 1 | export default jest.fn().mockReturnValue(true); 2 | -------------------------------------------------------------------------------- /src/common/__mocks__/useMountId.ts: -------------------------------------------------------------------------------- 1 | export default function useMountId(callback?: (uuid: string) => void): string { 2 | if (callback) { 3 | callback('123'); 4 | } 5 | 6 | return '123'; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/__mocks__/useOutsideEvent.ts: -------------------------------------------------------------------------------- 1 | export default jest.fn((_type: string, _ref?: React.RefObject, callback?: () => void): void => { 2 | if (callback) { 3 | callback(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /src/common/__mocks__/withProviders.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function withProviders(Component: React.ComponentType) { 4 | return function RootProvider(props = {}): JSX.Element { 5 | return ; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/__tests__/DeselectListener-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import DeselectListener from '../DeselectListener'; 4 | import { setActiveAnnotationIdAction } from '../../store/annotations/actions'; 5 | 6 | jest.mock('react-redux', () => ({ 7 | useDispatch: () => jest.fn(), 8 | })); 9 | jest.mock('../../store/annotations/actions'); 10 | 11 | describe('DeselectListener', () => { 12 | const getWrapper = (): ShallowWrapper => shallow(); 13 | 14 | beforeEach(() => { 15 | jest.spyOn(React, 'useEffect').mockImplementation(f => f()); 16 | jest.spyOn(document, 'addEventListener'); 17 | }); 18 | 19 | test('mousedown', () => { 20 | const wrapper = getWrapper(); 21 | 22 | document.dispatchEvent(new MouseEvent('mousedown')); 23 | 24 | expect(wrapper.isEmptyRender()).toBe(true); 25 | expect(document.addEventListener).toHaveBeenCalledWith('mousedown', expect.any(Function)); 26 | expect(setActiveAnnotationIdAction).toHaveBeenCalledWith(null); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/common/__tests__/DeselectManager-test.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import DeselectManager from '../DeselectManager'; 3 | import { createStore } from '../../store'; 4 | import { Options } from '../BaseManager'; 5 | 6 | jest.mock('react-dom/client', () => ({ 7 | createRoot: jest.fn().mockReturnValue({ 8 | render: jest.fn(), 9 | unmount: jest.fn(), 10 | }), 11 | })); 12 | 13 | describe('DeselectManager', () => { 14 | const rootEl = document.createElement('div'); 15 | const getOptions = (options: Partial = {}): Options => ({ 16 | referenceEl: rootEl.querySelector('.reference') as HTMLElement, 17 | ...options, 18 | }); 19 | const getWrapper = (options?: Partial): DeselectManager => new DeselectManager(getOptions(options)); 20 | 21 | beforeEach(() => { 22 | rootEl.classList.add('root'); 23 | rootEl.innerHTML = '
'; // referenceEl 24 | }); 25 | 26 | describe('decorate()', () => { 27 | test('should add class and testid', () => { 28 | const wrapper = getWrapper(); 29 | wrapper.decorate(); 30 | 31 | expect(wrapper.reactEl.classList.contains('ba-Layer--deselect')).toBe(true); 32 | expect(wrapper.reactEl.dataset.testid).toEqual('ba-Layer--deselect'); 33 | }); 34 | }); 35 | 36 | describe('render()', () => { 37 | test('should format the props and pass them to the underlying components', () => { 38 | const wrapper = getWrapper(); 39 | const root = ReactDOM.createRoot(rootEl); 40 | wrapper.render({ store: createStore() }); 41 | 42 | expect(root.render).toHaveBeenCalled(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/common/__tests__/EventEmitter-test.ts: -------------------------------------------------------------------------------- 1 | import noop from 'lodash/noop'; 2 | import EventEmitter from '../EventEmitter'; 3 | import eventManager from '../EventManager'; 4 | 5 | jest.mock('../EventManager'); 6 | 7 | describe('EventEmitter', () => { 8 | const emitter = new EventEmitter(); 9 | 10 | describe('addListener()', () => { 11 | test('should proxy addListener to eventManager', () => { 12 | emitter.addListener('foo', noop); 13 | expect(eventManager.addListener).toHaveBeenCalledWith('foo', noop); 14 | }); 15 | }); 16 | 17 | describe('emit()', () => { 18 | test('should proxy emit to eventManager', () => { 19 | const data = { hello: 'world' }; 20 | emitter.emit('foo', data); 21 | expect(eventManager.emit).toHaveBeenCalledWith('foo', data); 22 | }); 23 | }); 24 | 25 | describe('removeAllListeners()', () => { 26 | test('should proxy removeAllListeners to eventManager', () => { 27 | emitter.removeAllListeners(); 28 | expect(eventManager.removeAllListeners).toHaveBeenCalled(); 29 | }); 30 | }); 31 | 32 | describe('removeListener()', () => { 33 | test('should proxy removeListener to eventManager', () => { 34 | emitter.removeListener('foo', noop); 35 | expect(eventManager.removeListener).toHaveBeenCalledWith('foo', noop); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/common/__tests__/EventManager-test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { LegacyEvent } from '../../@types'; 3 | import eventManager from '../EventManager'; 4 | 5 | describe('EventManager', () => { 6 | describe('emit()', () => { 7 | test('should proxy its event to its base class', () => { 8 | const emitSpy = jest.spyOn(EventEmitter.prototype, 'emit'); 9 | const mockData = { test: 'data' }; 10 | const result = eventManager.emit('test', mockData); 11 | 12 | expect(emitSpy).toHaveBeenCalledWith('test', mockData); 13 | expect(emitSpy).toHaveBeenCalledWith(LegacyEvent.ANNOTATOR, { event: 'test', data: mockData }); 14 | expect(result).toBe(true); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/common/__tests__/useIsListInteractive-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount, ReactWrapper } from 'enzyme'; 3 | import { Provider } from 'react-redux'; 4 | import useIsListInteractive from '../useIsListInteractive'; 5 | import { createStore, CreatorStatus } from '../../store'; 6 | 7 | describe('useIsListInteractive', () => { 8 | function TestComponent(): JSX.Element { 9 | const isListening = useIsListInteractive(); 10 | 11 | return
; 12 | } 13 | 14 | const getWrapper = (store = createStore()): ReactWrapper => 15 | mount( 16 | 17 | 18 | , 19 | ); 20 | 21 | test('should be listening by default', () => { 22 | const wrapper = getWrapper(); 23 | 24 | expect(wrapper.find('[data-testid="islistening"]').prop('data-islistening')).toEqual(true); 25 | }); 26 | 27 | test('should not be listening if CreatorStatus is not init', () => { 28 | const store = createStore({ creator: { status: CreatorStatus.started } }); 29 | const wrapper = getWrapper(store); 30 | 31 | expect(wrapper.find('[data-testid="islistening"]').prop('data-islistening')).toEqual(false); 32 | }); 33 | 34 | test('should not be listening if isSelecting is true', () => { 35 | const store = createStore({ highlight: { isSelecting: true } }); 36 | const wrapper = getWrapper(store); 37 | 38 | expect(wrapper.find('[data-testid="islistening"]').prop('data-islistening')).toEqual(false); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/common/__tests__/useMountId-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount, ReactWrapper } from 'enzyme'; 3 | import useMountId from '../useMountId'; 4 | 5 | jest.mock('uuid', () => ({ 6 | v4: () => '123', 7 | })); 8 | 9 | describe('useMountId', () => { 10 | function TestComponent({ onMount }: { onMount: (uuid: string) => void }): JSX.Element { 11 | const uuid = useMountId(onMount); 12 | 13 | return
; 14 | } 15 | 16 | const getWrapper = (onMount: (uuid: string) => void): ReactWrapper => mount(); 17 | 18 | test('should render the component with a generated uuid and call the callback with the generated uuid', () => { 19 | const onMount = jest.fn(); 20 | const wrapper = getWrapper(onMount); 21 | 22 | expect(wrapper.find('[data-testid="uuid"]').prop('data-uuid')).toEqual('123'); 23 | expect(onMount).toHaveBeenCalledWith('123'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/common/__tests__/useOutsideEvent-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { mount, ReactWrapper } from 'enzyme'; 3 | import useOutsideEvent from '../useOutsideEvent'; 4 | 5 | describe('useOutsideClick', () => { 6 | let callback: jest.Mock; 7 | 8 | function TestComponent(): JSX.Element { 9 | const ref = React.createRef(); 10 | 11 | useOutsideEvent('click', ref, callback); 12 | 13 | return ( 14 |
15 | 18 |
19 | ); 20 | } 21 | 22 | const getWrapper = (): ReactWrapper => 23 | mount( 24 |
25 | 26 |
, 27 | { attachTo: document.getElementById('test') }, 28 | ); 29 | 30 | beforeEach(() => { 31 | document.body.innerHTML = '
'; 32 | callback = jest.fn(); 33 | }); 34 | 35 | test.each` 36 | elementId | isCalled 37 | ${'container'} | ${true} 38 | ${'test-button'} | ${false} 39 | ${'test-span'} | ${false} 40 | `('should callback be called if click target is $elementId? $isCalled', ({ elementId, isCalled }) => { 41 | getWrapper(); 42 | 43 | const element: HTMLElement | null = document.getElementById(elementId); 44 | if (element) { 45 | element.click(); 46 | } 47 | 48 | expect(element).toBeTruthy(); 49 | expect(callback.mock.calls.length).toBe(isCalled ? 1 : 0); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/common/useAutoScroll/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './useAutoScroll'; 2 | export * from './util'; 3 | -------------------------------------------------------------------------------- /src/common/useAutoScroll/util.ts: -------------------------------------------------------------------------------- 1 | const OVERFLOW_STYLE = /(auto|scroll)/; 2 | const OVERFLOW_PROPS = ['overflow', 'overflow-x', 'overflow-y']; 3 | 4 | export const isScrollable = (node: Node): boolean => { 5 | if (node.nodeType !== Node.ELEMENT_NODE) { 6 | return false; 7 | } 8 | 9 | const style = getComputedStyle(node as Element, null); 10 | return OVERFLOW_PROPS.some(prop => OVERFLOW_STYLE.test(style.getPropertyValue(prop))); 11 | }; 12 | 13 | export const getScrollParent = (el: Element | null): Element => { 14 | if (!el || el === document.body) { 15 | return document.body; 16 | } 17 | 18 | // Use Element.parentNode to support accessing SVG element parents in IE11 19 | return isScrollable(el) ? el : getScrollParent(el.parentNode as Element); 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/useIsListInteractive.ts: -------------------------------------------------------------------------------- 1 | import * as ReactRedux from 'react-redux'; 2 | import { getCreatorStatus, getIsSelecting, CreatorStatus } from '../store'; 3 | 4 | // Returns whether rendered annotations should be interactive 5 | export default function useIsListInteractive(): boolean { 6 | const status = ReactRedux.useSelector(getCreatorStatus); 7 | const isSelecting = ReactRedux.useSelector(getIsSelecting); 8 | 9 | return status === CreatorStatus.init && !isSelecting; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/useMountId.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as uuid from 'uuid'; 3 | 4 | // Returns a generated uuid, then calls the provided callback via an effect 5 | export default function useMountId(callback: (uuid: string) => void): string { 6 | const uuidRef = React.useRef(uuid.v4()); 7 | 8 | // The callback is only invoked after the parent component has rendered 9 | React.useEffect(() => { 10 | callback(uuidRef.current); 11 | }, [callback]); 12 | 13 | return uuidRef.current; 14 | } 15 | -------------------------------------------------------------------------------- /src/common/useOutsideEvent.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // Utilizes the useEffect hook to handle when an event is fired on the document, but outside of the specified element 4 | export default function useOutsideEvent( 5 | type: string, 6 | ref?: React.RefObject | Element | null, 7 | callback?: EventListener, 8 | ): void { 9 | React.useEffect(() => { 10 | function handleEvent(event: T): void { 11 | const element = ref instanceof Element ? ref : ref?.current; 12 | const containsEventTarget = !!element?.contains(event.target as Node); 13 | 14 | if (callback && !containsEventTarget) { 15 | callback(event); 16 | } 17 | } 18 | 19 | // Bind the event listener 20 | document.addEventListener(type, handleEvent); 21 | 22 | return () => { 23 | // Unbind the event listener on clean up 24 | document.removeEventListener(type, handleEvent); 25 | }; 26 | }, [callback, ref, type]); 27 | } 28 | -------------------------------------------------------------------------------- /src/common/withProviders.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IntlShape, RawIntlProvider } from 'react-intl'; 3 | import { Provider as StoreProvider } from 'react-redux'; 4 | import { Store } from 'redux'; 5 | 6 | type WrapperProps = { 7 | intl: IntlShape; 8 | store: Store; 9 | }; 10 | 11 | type WrapperReturn

= React.FC

; 12 | 13 | export default function withProviders

(WrappedComponent: React.ComponentType

): WrapperReturn

{ 14 | return function RootProvider({ intl, store, ...rest }: P & WrapperProps): JSX.Element { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ItemList/ItemList.scss: -------------------------------------------------------------------------------- 1 | .ba { 2 | .ba-ItemList { 3 | max-height: 300px; 4 | margin: 0; 5 | padding: 0; 6 | overflow-y: auto; 7 | text-align: left; 8 | list-style: none; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ItemList/ItemRow.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | 3 | // increase specificity to override .bp styles 4 | .ba { 5 | .ba-ItemRow { 6 | padding: 5px 30px 5px 15px; 7 | 8 | &.is-active { 9 | background-color: fade-out($bdl-gray, .95); 10 | cursor: pointer; 11 | } 12 | } 13 | 14 | .ba-ItemRow-name { 15 | line-height: 15px; 16 | } 17 | 18 | .ba-ItemRow-email { 19 | color: $bdl-gray-62; 20 | font-size: 11px; 21 | line-height: 15px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ItemList/__mocks__/ItemRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { Props } from '../ItemRow'; 4 | 5 | export default React.forwardRef(({ className, isActive, ...rest }: Props, ref: React.Ref) => ( 6 |

7 | )); 8 | -------------------------------------------------------------------------------- /src/components/ItemList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ItemList'; 2 | -------------------------------------------------------------------------------- /src/components/PointerCapture/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PointerCapture'; 2 | export { default } from './PointerCapture'; 3 | -------------------------------------------------------------------------------- /src/components/Popups/Popper.ts: -------------------------------------------------------------------------------- 1 | import * as popper from '@popperjs/core'; 2 | import mergeWith from 'lodash/mergeWith'; 3 | import unionBy from 'lodash/unionBy'; 4 | 5 | export type Instance = popper.Instance; 6 | export type Options = popper.Options; 7 | export type Rect = popper.Rect; 8 | export type State = popper.State; 9 | export type VirtualElement = popper.VirtualElement; 10 | 11 | export type PopupReference = Element | VirtualElement; 12 | 13 | export const defaults = { 14 | modifiers: [], 15 | placement: 'bottom', 16 | }; 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export const merger = (sourceValue: any, newValue: any): any => { 20 | if (Array.isArray(sourceValue) && Array.isArray(newValue)) { 21 | return unionBy(sourceValue, newValue, 'name'); 22 | } 23 | 24 | return undefined; // Default to lodash/merge behavior 25 | }; 26 | 27 | export default function create( 28 | reference: PopupReference, 29 | popup: HTMLElement, 30 | options: Partial = {}, 31 | ): Instance { 32 | return popper.createPopper(reference, popup, mergeWith({}, defaults, options, merger) as Options); 33 | } 34 | 35 | export function clientBoundingRect(height: number, width: number, x: number, y: number): DOMRect { 36 | const rect = { 37 | bottom: y + height, 38 | height, 39 | left: x, 40 | right: x + width, 41 | top: y, 42 | width, 43 | x, 44 | y, 45 | }; 46 | 47 | return { 48 | ...rect, 49 | toJSON: () => ({ ...rect }), 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Popups/PopupBase.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | @import './PopupArrow'; 3 | 4 | .ba { 5 | $popup-content-bg: $white; 6 | $popup-border-color: $bdl-gray-20; 7 | $popup-footer-bg: $bdl-gray-02; 8 | 9 | .ba-Popup { 10 | @include common-typography; 11 | @include ba-PopupArrow(12px, $popup-content-bg, $popup-content-bg, $popup-footer-bg, $popup-content-bg, $popup-border-color); 12 | 13 | z-index: 1; // Make sure popup is showing above Preview controls and left/right arrows 14 | } 15 | 16 | // The following media query is specific to IE10+ 17 | @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { 18 | .ba-Popup { 19 | z-index: 0; // Changing z-index causes page to flicker in IE11 20 | } 21 | 22 | .ba-ItemList { 23 | // stylelint-disable-next-line declaration-no-important 24 | max-height: 140px !important; // Prevent list overflow onto subsequent pages in IE11 25 | } 26 | 27 | .ba-ReplyForm { 28 | opacity: .99; // Creates a new stacking context so that the collaborators list shows on top of the ba-Popup-footer 29 | } 30 | } 31 | 32 | .ba-Popup-content { 33 | overflow: hidden; 34 | background-color: $popup-content-bg; 35 | border: 1px solid $popup-border-color; 36 | border-radius: 6px; 37 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .15); 38 | } 39 | 40 | .ba-Popup-footer { 41 | display: flex; 42 | justify-content: flex-end; 43 | padding: 5px; 44 | background-color: $popup-footer-bg; 45 | border-top: 1px solid $popup-border-color; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Popups/PopupCursor.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | 3 | .ba-PopupCursor { 4 | .ba-Popup-arrow { 5 | display: none; 6 | } 7 | 8 | .ba-Popup-content { 9 | padding: 3px 6px; 10 | color: $white; 11 | font-size: 11px; 12 | line-height: normal; 13 | letter-spacing: normal; 14 | background-color: $bdl-gray; 15 | border: 1px solid $white; 16 | border-radius: 3px; 17 | box-shadow: none; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Popups/PopupHighlight.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | @import './PopupArrow'; 3 | 4 | .ba { 5 | $popup-content-bg: $bdl-gray; 6 | 7 | .ba-PopupHighlight { 8 | @include ba-PopupArrow(4px, $popup-content-bg, $popup-content-bg, $popup-content-bg, $popup-content-bg, $popup-content-bg); 9 | 10 | .ba-Popup-content { 11 | background-color: $popup-content-bg; 12 | border: none; 13 | border-radius: $bdl-border-radius-size; 14 | box-shadow: none; 15 | } 16 | } 17 | 18 | .ba-PopupHighlight-button { 19 | @include common-typography; 20 | 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | padding: 5px 10px; 25 | color: $white; 26 | background: transparent; 27 | border: none; 28 | border-radius: $bdl-border-radius-size; 29 | cursor: pointer; 30 | } 31 | 32 | .ba-PopupHighlight-icon { 33 | margin-right: 8px; 34 | 35 | path { 36 | fill: $white; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Popups/PopupHighlightError.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | @import './PopupArrow'; 3 | 4 | .ba { 5 | $popup-content-bg: $bdl-gray-50; 6 | 7 | .ba-PopupHighlightError { 8 | @include ba-PopupArrow(4px, $popup-content-bg, $popup-content-bg, $popup-content-bg, $popup-content-bg, $popup-content-bg); 9 | @include common-typography; 10 | 11 | .ba-Popup-content { 12 | display: flex; 13 | flex-direction: row; 14 | align-items: center; 15 | padding: 5px 10px; 16 | color: $white; 17 | background-color: $popup-content-bg; 18 | border: none; 19 | border-radius: $bdl-border-radius-size; 20 | box-shadow: none; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Popups/PopupList.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | 3 | .ba-PopupList { 4 | .ba-Popup-arrow { 5 | display: none; 6 | } 7 | 8 | .ba-Popup-content { 9 | border: solid 1px $bdl-gray-30; 10 | border-radius: $bdl-border-radius-size; 11 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 12 | } 13 | 14 | .ba-PopupList-prompt { 15 | padding: 10px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Popups/PopupList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import ItemList from '../ItemList'; 5 | import messages from './messages'; 6 | import PopupBase from './PopupBase'; 7 | import { Options, PopupReference } from './Popper'; 8 | import './PopupList.scss'; 9 | 10 | export type Props = { 11 | activeItemIndex?: number; 12 | autoScroll?: boolean; 13 | items: T[]; 14 | onActivate?: (index: number) => void; 15 | onMouseDown?: (event: React.SyntheticEvent) => void; 16 | onSelect: (index: number, event: React.SyntheticEvent) => void; 17 | reference: PopupReference; 18 | }; 19 | 20 | const options: Partial = { 21 | modifiers: [ 22 | { 23 | name: 'offset', 24 | options: { 25 | offset: [0, 3], 26 | }, 27 | }, 28 | { 29 | name: 'eventListeners', 30 | options: { 31 | scroll: false, 32 | }, 33 | }, 34 | ], 35 | placement: 'bottom-start', 36 | }; 37 | 38 | const PopupList = ({ items, reference, ...rest }: Props): JSX.Element => ( 39 | 40 | {isEmpty(items) ? ( 41 |
42 | 43 |
44 | ) : ( 45 | 46 | )} 47 |
48 | ); 49 | 50 | export default PopupList; 51 | -------------------------------------------------------------------------------- /src/components/Popups/PopupReply.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | 3 | .ba-Popup-footer { 4 | .btn.btn-primary { 5 | &.is-disabled { 6 | background-color: $bdl-gray-50; 7 | border-color: $bdl-gray-50; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Popups/__mocks__/PopupBase.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class PopupBase extends React.Component<{ children: React.ReactNode }> { 4 | name = 'PopupBaseMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Popups/__mocks__/PopupHighlight.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class PopupHighlight extends React.Component<{ children: React.ReactNode }> { 4 | name = 'PopupHighlightMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Popups/__mocks__/PopupHighlightError.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class PopupHighlight extends React.Component<{ children: React.ReactNode }> { 4 | name = 'PopupHighlightErrorMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Popups/__mocks__/PopupReply.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class PopupReply extends React.Component<{ children: React.ReactNode }> { 4 | name = 'PopupReplyMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Popups/__tests__/Popper-test.ts: -------------------------------------------------------------------------------- 1 | import * as popper from '@popperjs/core'; 2 | import createPopper, { defaults } from '../Popper'; 3 | 4 | describe('Popper', () => { 5 | const popupEl = document.createElement('div'); 6 | const referenceEl = document.createElement('div'); 7 | 8 | describe('createPopper()', () => { 9 | test('should return a new popper instance based on the parameters provided', () => { 10 | createPopper(referenceEl, popupEl); 11 | 12 | expect(popper.createPopper).toHaveBeenCalledWith(referenceEl, popupEl, defaults); 13 | }); 14 | 15 | test('should return a new popper with deeply merged options, if provided', () => { 16 | const { modifiers: defaultModifiers, ...rest } = defaults; 17 | const modifiers = [{ name: 'event-handlers' }]; 18 | 19 | createPopper(referenceEl, popupEl, { 20 | modifiers, 21 | placement: 'left', 22 | }); 23 | 24 | expect(popper.createPopper).toHaveBeenCalledWith(referenceEl, popupEl, { 25 | ...rest, 26 | placement: 'left', 27 | modifiers: [...defaultModifiers, ...modifiers], 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/Popups/__tests__/PopupList-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import PopupBase from '../PopupBase'; 4 | import PopupList, { Props } from '../PopupList'; 5 | import { Collaborator } from '../../../@types'; 6 | 7 | describe('PopupList', () => { 8 | const defaults: Props = { 9 | items: [], 10 | onSelect: jest.fn(), 11 | reference: document.createElement('div'), 12 | }; 13 | 14 | const getWrapper = (props = {}): ShallowWrapper => shallow(); 15 | 16 | describe('render()', () => { 17 | test('should render popupBase with correct props', () => { 18 | const wrapper = getWrapper(); 19 | 20 | expect(wrapper.find(PopupBase).props()).toMatchObject({ 21 | className: 'ba-PopupList', 22 | reference: defaults.reference, 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/ReplyField/MentionItem.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | 3 | .ba-MentionItem-link { 4 | color: $bdl-box-blue; 5 | font-weight: bold; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ReplyField/MentionItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ContentState } from 'draft-js'; 3 | import './MentionItem.scss'; 4 | 5 | export type Props = { 6 | children: React.ReactNode; 7 | contentState: ContentState; 8 | entityKey: string; 9 | }; 10 | 11 | const MentionItem = ({ contentState, entityKey, children }: Props): JSX.Element => { 12 | const { id } = contentState.getEntity(entityKey).getData(); 13 | 14 | return id ? ( 15 | 16 | {children} 17 | 18 | ) : ( 19 | 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default MentionItem; 26 | -------------------------------------------------------------------------------- /src/components/ReplyField/ReplyField.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | 3 | .ba-ReplyField { 4 | @import '~draft-js/dist/Draft'; 5 | 6 | .DraftEditor-editorContainer { 7 | border-left: none; 8 | } 9 | 10 | .public-DraftEditorPlaceholder-hasFocus, 11 | .public-DraftEditorPlaceholder-root { 12 | margin: 0; 13 | padding: 12px; 14 | color: $bdl-gray-62; 15 | } 16 | 17 | // Element selector needed to override inherited BUIE styles 18 | div[contentEditable='true'], 19 | div[contentEditable='false'] { 20 | min-height: 80px; 21 | max-height: 200px; 22 | overflow-y: auto; 23 | 24 | &, 25 | &:hover, 26 | &:focus { 27 | width: 300px; 28 | padding: 12px; 29 | color: $bdl-gray; 30 | border: none; 31 | box-shadow: none; 32 | } 33 | } 34 | 35 | div[contentEditable='false'] { 36 | &, 37 | &:hover, 38 | &:focus { 39 | color: $bdl-gray-62; 40 | cursor: default; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ReplyField/ReplyFieldContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { AppState, fetchCollaboratorsAction, getCollaborators, getCreatorCursor, setCursorAction } from '../../store'; 3 | import ReplyField from './ReplyField'; 4 | import { Collaborator } from '../../@types'; 5 | 6 | export type Props = { 7 | collaborators: Collaborator[]; 8 | cursorPosition: number; 9 | }; 10 | 11 | export const mapStateToProps = (state: AppState): Props => ({ 12 | collaborators: getCollaborators(state), 13 | cursorPosition: getCreatorCursor(state), 14 | }); 15 | 16 | export const mapDispatchToProps = { 17 | fetchCollaborators: fetchCollaboratorsAction, 18 | setCursorPosition: setCursorAction, 19 | }; 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(ReplyField); 22 | -------------------------------------------------------------------------------- /src/components/ReplyField/__tests__/MentionItem-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ContentState } from 'draft-js'; 3 | import { shallow, ShallowWrapper } from 'enzyme'; 4 | import MentionItem, { Props } from '../MentionItem'; 5 | 6 | describe('MentionItem', () => { 7 | const defaults: Props = { 8 | children:
, 9 | contentState: ({ 10 | getEntity: () => ({ 11 | getData: () => ({ id: 'testid' }), 12 | }), 13 | } as unknown) as ContentState, 14 | entityKey: 'testEntityKey', 15 | }; 16 | 17 | const getWrapper = (props = {}): ShallowWrapper => shallow(); 18 | 19 | describe('render()', () => { 20 | test('should render link with correct props', () => { 21 | const wrapper = getWrapper(); 22 | 23 | expect(wrapper.find('[data-testid="ba-MentionItem-link"]').props()).toMatchObject({ 24 | className: 'ba-MentionItem-link', 25 | href: '/profile/testid', 26 | }); 27 | }); 28 | 29 | test('should not render link if no id', () => { 30 | const wrapper = getWrapper({ 31 | contentState: ({ 32 | getEntity: () => ({ 33 | getData: () => ({}), 34 | }), 35 | } as unknown) as ContentState, 36 | }); 37 | 38 | expect(wrapper.exists('[data-testid="ba-MentionItem-link"]')).toBeFalsy(); 39 | expect(wrapper.exists('[data-testid="ba-MentionItem-text"]')).toBeTruthy(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/ReplyField/__tests__/ReplyFieldContainer-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { EditorState } from 'draft-js'; 3 | import { mount, ReactWrapper } from 'enzyme'; 4 | import { IntlShape } from 'react-intl'; 5 | import ReplyField from '../ReplyField'; 6 | import ReplyFieldContainer, { Props } from '../ReplyFieldContainer'; 7 | import { createStore } from '../../../store'; 8 | 9 | jest.mock('../ReplyField'); 10 | 11 | describe('ReplyFieldContainer', () => { 12 | const store = createStore({ 13 | creator: { 14 | cursor: 0, 15 | }, 16 | }); 17 | const defaults = { 18 | editorState: EditorState.createEmpty(), 19 | onChange: jest.fn(), 20 | onClick: jest.fn(), 21 | intl: {} as IntlShape, 22 | store, 23 | }; 24 | 25 | const getWrapper = (props = {}): ReactWrapper => mount(); 26 | 27 | describe('render', () => { 28 | test('should connect the underlying component', () => { 29 | const wrapper = getWrapper(); 30 | 31 | expect(wrapper.find(ReplyField).props()).toMatchObject({ 32 | cursorPosition: 0, 33 | onChange: defaults.onChange, 34 | onClick: defaults.onClick, 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/ReplyField/__tests__/withMentionDecorator-test.tsx: -------------------------------------------------------------------------------- 1 | import { EditorState, CompositeDecorator, ContentState } from 'draft-js'; 2 | import withMentionDecorator, { mentionStrategy } from '../withMentionDecorator'; 3 | 4 | describe('ReplyField/withMentionDecorator', () => { 5 | test('should set decorator', () => { 6 | const mockEditorState = EditorState.createEmpty(); 7 | 8 | jest.spyOn(EditorState, 'set'); 9 | 10 | const newEditorState = withMentionDecorator(mockEditorState); 11 | 12 | expect(EditorState.set).toBeCalledWith(mockEditorState, { decorator: expect.any(CompositeDecorator) }); 13 | expect(newEditorState.getDecorator()).not.toBeNull(); 14 | }); 15 | 16 | test('should call findEntityRanges', () => { 17 | const mockContentState = ContentState.createFromText('test'); 18 | const mockContentBlock = mockContentState.getFirstBlock(); 19 | const mockCallback = jest.fn(); 20 | 21 | jest.spyOn(mockContentBlock, 'findEntityRanges'); 22 | 23 | mentionStrategy(mockContentBlock, mockCallback, mockContentState); 24 | 25 | expect(mockContentBlock.findEntityRanges).toBeCalledWith(expect.any(Function), mockCallback); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/ReplyField/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ReplyFieldContainer'; 2 | -------------------------------------------------------------------------------- /src/components/ReplyField/withMentionDecorator.tsx: -------------------------------------------------------------------------------- 1 | import { CompositeDecorator, ContentBlock, ContentState, EditorState } from 'draft-js'; 2 | import MentionItem from './MentionItem'; 3 | 4 | export const mentionStrategy = ( 5 | contentBlock: ContentBlock, 6 | callback: (start: number, end: number) => void, 7 | contentState: ContentState, 8 | ): void => { 9 | contentBlock.findEntityRanges((character: { getEntity: () => string }) => { 10 | const entityKey = character.getEntity(); 11 | return entityKey !== null && contentState.getEntity(entityKey).getType() === 'MENTION'; 12 | }, callback); 13 | }; 14 | 15 | export default function withMentionDecorator(editorState: EditorState): EditorState { 16 | const mentionDecorator = new CompositeDecorator([ 17 | { 18 | strategy: mentionStrategy, 19 | component: MentionItem, 20 | }, 21 | ]); 22 | 23 | return EditorState.set(editorState, { decorator: mentionDecorator }); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ReplyForm/ReplyButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes } from 'react'; 2 | import classNames from 'classnames'; 3 | import noop from 'lodash/noop'; 4 | 5 | export type Props = { 6 | children?: React.ReactNode; 7 | isDisabled?: boolean; 8 | isPrimary?: boolean; 9 | onClick?: (event: React.MouseEvent) => void; 10 | type?: 'button' | 'submit' | 'reset' | undefined; 11 | } & HTMLAttributes; 12 | 13 | export default function ReplyButton({ children, isDisabled, isPrimary, onClick = noop, ...rest }: Props): JSX.Element { 14 | const handleClick = (event: React.MouseEvent): void => { 15 | if (isDisabled) { 16 | event.preventDefault(); 17 | event.stopPropagation(); 18 | return; 19 | } 20 | 21 | onClick(event); 22 | }; 23 | 24 | return ( 25 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/ReplyForm/ReplyForm.scss: -------------------------------------------------------------------------------- 1 | .ba-ReplyForm { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/ReplyForm/__mocks__/ReplyForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class ReplyForm extends React.Component<{ children: React.ReactNode }> { 4 | name = 'ReplyFormMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ReplyForm/__mocks__/ReplyFormContainer.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './ReplyForm'; 2 | -------------------------------------------------------------------------------- /src/components/ReplyForm/__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ReplyForm'; 2 | -------------------------------------------------------------------------------- /src/components/ReplyForm/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ReplyFormContainer'; 2 | -------------------------------------------------------------------------------- /src/components/ReplyForm/types.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'draft-js'; 2 | 3 | export type PropsFromState = { 4 | cursorPosition: number; 5 | }; 6 | 7 | export type ContainerProps = { 8 | isPending: boolean; 9 | onCancel: (text: string) => void; 10 | onChange: (text: string) => void; 11 | onSubmit: (text: string) => void; 12 | value?: string; 13 | } & PropsFromState; 14 | 15 | export type FormErrors = { 16 | [V in keyof FormValues]?: string; 17 | }; 18 | 19 | export type FormValues = { 20 | editorState: EditorState; 21 | }; 22 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const ANNOTATOR_EVENT = { 2 | fetch: 'annotationsfetched', 3 | error: 'annotationerror', 4 | scale: 'scaleannotations', 5 | setVisibility: 'annotationsetvisibility', 6 | }; 7 | 8 | export const MOUSE_PRIMARY = 1; 9 | -------------------------------------------------------------------------------- /src/document/DocumentAnnotator.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | @import '../highlight/HighlightCreator'; 3 | 4 | .bp-doc { 5 | .ba-Layer { 6 | position: absolute; 7 | top: 15px; // Padding on each page as provided by Preview SDK 8 | bottom: 15px; // Padding on each page as provided by Preview SDK 9 | left: 0; 10 | width: 100%; 11 | pointer-events: none; 12 | } 13 | 14 | &.ba-is-create--highlight { 15 | .textLayer { 16 | > span { 17 | @include ba-HighlightCreator-cursor; 18 | } 19 | 20 | ::selection { 21 | background-color: rgba($bdl-yellow, .33); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/drawing/DecoratedDrawingPath.scss: -------------------------------------------------------------------------------- 1 | .ba-DecoratedDrawingPath-decoration { 2 | opacity: 0; 3 | transition: opacity 200ms ease; 4 | 5 | .is-active & { 6 | opacity: 1; 7 | transition-delay: 25ms; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/drawing/DecoratedDrawingPath.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { white } from 'box-ui-elements/es/styles/variables'; 3 | import DrawingPath from './DrawingPath'; 4 | import SVGFilterContext from './SVGFilterContext'; 5 | import { getPathCommands } from './drawingUtil'; 6 | import { Position } from '../@types'; 7 | import './DecoratedDrawingPath.scss'; 8 | 9 | export type Props = { 10 | borderStrokeWidth?: number; 11 | isDecorated?: boolean; 12 | points?: Position[]; 13 | }; 14 | 15 | export default function DecoratedDrawingPath({ 16 | borderStrokeWidth = 0, 17 | isDecorated = false, 18 | points = [], 19 | }: Props): JSX.Element { 20 | const filterID = React.useContext(SVGFilterContext); 21 | const pathCommands = getPathCommands(points); 22 | return ( 23 | 24 | {isDecorated && ( 25 | 26 | 31 | 38 | 39 | )} 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/drawing/DrawingAnnotations.scss: -------------------------------------------------------------------------------- 1 | .ba-DrawingAnnotations-list { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | pointer-events: none; 8 | 9 | &.is-listening { 10 | .ba-DrawingTarget { 11 | pointer-events: auto; // Delegate event control to avoid re-rendering every target on mousedown/up 12 | } 13 | } 14 | } 15 | 16 | .ba-DrawingAnnotations-creator, 17 | .ba-DrawingAnnotations-target { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | .ba-DrawingAnnotations-creator { 26 | pointer-events: auto; 27 | touch-action: none; 28 | } 29 | 30 | .ba-DrawingAnnotations-toolbar { 31 | opacity: 1; 32 | transition: opacity 200ms ease; 33 | pointer-events: auto; 34 | 35 | &.ba-is-drawing { 36 | opacity: 0; 37 | pointer-events: none; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/drawing/DrawingCreator.scss: -------------------------------------------------------------------------------- 1 | .ba-DrawingCreator { 2 | cursor: crosshair; // For legacy browsers like IE11 3 | } 4 | 5 | .ba-DrawingCreator-current { 6 | width: 100%; 7 | height: 100%; 8 | pointer-events: none; 9 | } 10 | -------------------------------------------------------------------------------- /src/drawing/DrawingManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import BaseManager, { Props } from '../common/BaseManager'; 4 | import DrawingAnnotationsContainer from './DrawingAnnotationsContainer'; 5 | 6 | export default class DrawingListManager extends BaseManager { 7 | decorate(): void { 8 | this.reactEl.classList.add('ba-Layer--drawing'); 9 | this.reactEl.dataset.testid = 'ba-Layer--drawing'; 10 | } 11 | 12 | render(props: Props): void { 13 | if (!this.root) { 14 | this.root = ReactDOM.createRoot(this.reactEl); 15 | } 16 | 17 | this.root.render(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/drawing/DrawingPath.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type Props = React.SVGProps & { 4 | pathCommands?: string; 5 | }; 6 | 7 | export type DrawingPathRef = SVGPathElement; 8 | 9 | const DrawingPath = (props: Props, ref: React.Ref): JSX.Element => { 10 | const { pathCommands, ...rest } = props; 11 | 12 | return ; 13 | }; 14 | 15 | export { DrawingPath as DrawingPathBase }; 16 | 17 | export default React.forwardRef(DrawingPath); 18 | -------------------------------------------------------------------------------- /src/drawing/DrawingSVG.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import * as uuid from 'uuid'; 4 | import SVGFilterContext from './SVGFilterContext'; 5 | 6 | export type Props = { 7 | children: React.ReactNode; 8 | className?: string; 9 | }; 10 | 11 | export type DrawingSVGRef = SVGSVGElement; 12 | 13 | export function DrawingSVG({ className, children, ...rest }: Props, ref: React.Ref): JSX.Element { 14 | const { current: filterID } = React.useRef(`ba-DrawingSVG-shadow_${uuid.v4()}`); 15 | 16 | return ( 17 | 24 | 25 | 26 | 27 | 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | 34 | export default React.forwardRef(DrawingSVG); 35 | -------------------------------------------------------------------------------- /src/drawing/DrawingSVGGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import noop from 'lodash/noop'; 3 | import useMountId from '../common/useMountId'; 4 | 5 | export type Props = React.SVGAttributes & { 6 | children?: React.ReactNode; 7 | onMount?: (uuid: string) => void; 8 | }; 9 | 10 | export type DrawingSVGGroup = SVGGElement; 11 | 12 | export function DrawingSVGGroup(props: Props, ref: React.Ref): JSX.Element { 13 | const { children, onMount = noop, ...rest } = props; 14 | const uuid = useMountId(onMount); 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | export default React.forwardRef(DrawingSVGGroup); 24 | -------------------------------------------------------------------------------- /src/drawing/DrawingTarget.scss: -------------------------------------------------------------------------------- 1 | .ba-DrawingTarget { 2 | outline: none; 3 | 4 | &:hover { 5 | cursor: pointer; 6 | 7 | .ba-DecoratedDrawingPath-decoration { 8 | opacity: 1; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/drawing/SVGFilterContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const SVGFilterContext = createContext(''); 4 | 5 | export default SVGFilterContext; 6 | -------------------------------------------------------------------------------- /src/drawing/__mocks__/DrawingList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function DrawingListMock(): JSX.Element { 4 | return
; 5 | } 6 | -------------------------------------------------------------------------------- /src/drawing/__tests__/DrawingPath-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import { DrawingPathBase as DrawingPath, Props } from '../DrawingPath'; 4 | 5 | describe('DrawingPath', () => { 6 | const getDefaults = (): Props => ({ 7 | pathCommands: 'abc', 8 | }); 9 | const getWrapper = (props?: Props): ShallowWrapper => shallow(); 10 | 11 | test('render', () => { 12 | const wrapper = getWrapper({ className: 'foo' }); 13 | 14 | expect(wrapper.props()).toMatchObject({ 15 | className: 'foo', 16 | d: 'abc', 17 | strokeLinecap: 'round', 18 | vectorEffect: 'non-scaling-stroke', 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/drawing/__tests__/DrawingSVG-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import DrawingSVG from '../DrawingSVG'; 4 | 5 | describe('DrawingSVG', () => { 6 | const Component = (): JSX.Element =>
Test
; 7 | const getWrapper = (props = {}): ShallowWrapper => 8 | shallow( 9 | 10 | 11 | , 12 | ); 13 | beforeEach(() => { 14 | jest.spyOn(React, 'useRef').mockImplementation(() => ({ 15 | current: 'ba-DrawingSVG-shadow_123', 16 | })); 17 | }); 18 | 19 | describe('render()', () => { 20 | test('should render svg, filter, and children', () => { 21 | const wrapper = getWrapper(); 22 | 23 | expect(wrapper.hasClass('ba-DrawingSVG')).toBe(true); 24 | expect(wrapper.find('filter').prop('id')).toBe('ba-DrawingSVG-shadow_123'); 25 | expect(wrapper.exists('feGaussianBlur')).toBe(true); 26 | expect(wrapper.exists(Component)).toBe(true); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/drawing/__tests__/DrawingSVGGroup-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import DrawingSVGGroup, { Props } from '../DrawingSVGGroup'; 4 | 5 | jest.mock('../../common/useMountId'); 6 | 7 | describe('DrawingSVGGroup', () => { 8 | const getDefaults = (): Props => ({ 9 | onMount: jest.fn(), 10 | }); 11 | const getWrapper = (props: Partial): ShallowWrapper => 12 | shallow(); 13 | 14 | describe('render', () => { 15 | test('should render with provided props', () => { 16 | const wrapper = getWrapper({ children: , className: 'foo' }); 17 | 18 | expect(wrapper.props()).toMatchObject({ 19 | className: 'foo', 20 | 'data-ba-reference-id': '123', 21 | }); 22 | expect(wrapper.exists('path')).toBe(true); 23 | }); 24 | 25 | test('should call onMount with uuid', () => { 26 | const onMount = jest.fn(); 27 | getWrapper({ onMount }); 28 | 29 | expect(onMount).toHaveBeenCalledWith('123'); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/drawing/actions.ts: -------------------------------------------------------------------------------- 1 | import { AppThunkDispatch, AppState, getCreatorStatus, CreatorStatus } from '../store'; 2 | import { createAnnotationAction } from '../store/annotations'; 3 | import { getFileVersionId } from '../store/options'; 4 | import { PathGroup } from '../@types'; 5 | import { resetDrawingAction, setDrawingLocationAction } from '../store/drawing'; 6 | 7 | export type CreateArg = { 8 | location: number; 9 | message: string; 10 | pathGroups: Array; 11 | }; 12 | 13 | export const createDrawingAction = (arg: CreateArg) => (dispatch: AppThunkDispatch, getState: () => AppState) => { 14 | const { location, message, pathGroups } = arg; 15 | const state = getState(); 16 | const newAnnotation = { 17 | description: { 18 | message, 19 | type: 'reply' as const, 20 | }, 21 | file_version: { 22 | id: getFileVersionId(state), 23 | }, 24 | target: { 25 | location: { 26 | type: 'page' as const, 27 | value: location, 28 | }, 29 | path_groups: pathGroups, 30 | type: 'drawing' as const, 31 | }, 32 | }; 33 | 34 | return dispatch(createAnnotationAction(newAnnotation)); 35 | }; 36 | 37 | export const setupDrawingAction = (location: number) => (dispatch: AppThunkDispatch, getState: () => AppState) => { 38 | const state = getState(); 39 | const creatorStatus = getCreatorStatus(state); 40 | 41 | if (creatorStatus === CreatorStatus.staged) { 42 | dispatch(resetDrawingAction()); 43 | } 44 | 45 | return dispatch(setDrawingLocationAction(location)); 46 | }; 47 | -------------------------------------------------------------------------------- /src/drawing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './drawingUtil'; 2 | export { default as DrawingManager } from './DrawingManager'; 3 | -------------------------------------------------------------------------------- /src/highlight/HighlightAnnotations.scss: -------------------------------------------------------------------------------- 1 | .ba-HighlightAnnotations-creator { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | display: none; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .ba-HighlightAnnotations-popup { 11 | pointer-events: auto; 12 | } 13 | -------------------------------------------------------------------------------- /src/highlight/HighlightCanvas.scss: -------------------------------------------------------------------------------- 1 | .ba-HighlightCanvas { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | mix-blend-mode: multiply; 8 | pointer-events: none; 9 | } 10 | -------------------------------------------------------------------------------- /src/highlight/HighlightList.scss: -------------------------------------------------------------------------------- 1 | .ba-HighlightList { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | 8 | .ba-HighlightSvg.is-listening { 9 | .ba-HighlightTarget-rect { 10 | pointer-events: visible; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/highlight/HighlightListener.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | import { AppStore, getIsSelecting, SelectionArg as Selection, setSelectionAction } from '../store'; 3 | 4 | export type Options = { 5 | getSelection: () => Selection | null; 6 | store: AppStore; 7 | }; 8 | 9 | // Debounce 500ms for keyboard selection 10 | const SELECTION_CHANGE_DEBOUNCE = 500; 11 | 12 | export default class HighlightListener { 13 | getSelection: () => Selection | null; 14 | 15 | store: AppStore; 16 | 17 | constructor({ getSelection, store }: Options) { 18 | this.getSelection = getSelection; 19 | this.store = store; 20 | 21 | document.addEventListener('selectionchange', this.debounceHandleSelectionChange); 22 | } 23 | 24 | destroy(): void { 25 | document.removeEventListener('selectionchange', this.debounceHandleSelectionChange); 26 | } 27 | 28 | handleSelectionChange = (): void => { 29 | if (getIsSelecting(this.store.getState())) { 30 | return; 31 | } 32 | 33 | this.store.dispatch(setSelectionAction(this.getSelection())); 34 | }; 35 | 36 | debounceHandleSelectionChange = debounce(this.handleSelectionChange, SELECTION_CHANGE_DEBOUNCE); 37 | } 38 | -------------------------------------------------------------------------------- /src/highlight/HighlightManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import BaseManager, { Props } from '../common/BaseManager'; 4 | import HighlightContainer from './HighlightContainer'; 5 | 6 | export default class HighlightManager extends BaseManager { 7 | decorate(): void { 8 | this.reactEl.classList.add('ba-Layer--highlight'); 9 | this.reactEl.dataset.testid = 'ba-Layer--highlight'; 10 | } 11 | 12 | render(props: Props): void { 13 | if (!this.root) { 14 | this.root = ReactDOM.createRoot(this.reactEl); 15 | } 16 | 17 | this.root.render(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/highlight/HighlightSvg.scss: -------------------------------------------------------------------------------- 1 | .ba-HighlightSvg { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/highlight/HighlightSvg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import './HighlightSvg.scss'; 4 | 5 | type Props = { 6 | children?: React.ReactNode; 7 | className?: string; 8 | }; 9 | 10 | const HighlightSvg = ({ children, className }: Props, ref: React.Ref): JSX.Element => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export default React.forwardRef(HighlightSvg); 19 | -------------------------------------------------------------------------------- /src/highlight/HighlightTarget.scss: -------------------------------------------------------------------------------- 1 | .ba-HighlightTarget { 2 | &:focus { 3 | outline: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/highlight/__mocks__/HighlightAnnotations.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class HighlightAnnotations extends React.Component<{ children: React.ReactNode }> { 4 | name = 'HighlightAnnotationsMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/highlight/__mocks__/HighlightCanvas.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class HighlightCanvas extends React.Component<{ children: React.ReactNode }> { 4 | name = 'HighlightCanvasMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/highlight/__mocks__/HighlightList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class HighlightList extends React.Component<{ 4 | children: React.ReactNode; 5 | }> { 6 | name = 'HighlightListMock'; 7 | 8 | render(): JSX.Element { 9 | const { children } = this.props; 10 | 11 | return
{children}
; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/highlight/__mocks__/HighlightTarget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class HighlightTarget extends React.Component<{ children: React.ReactNode }> { 4 | name = 'HighlightTargetMock'; 5 | 6 | getBoundingClientRect(): Partial { 7 | return { 8 | height: 10, 9 | width: 10, 10 | top: 10, 11 | left: 10, 12 | }; 13 | } 14 | 15 | render(): JSX.Element { 16 | const { children } = this.props; 17 | 18 | return
{children}
; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/highlight/__tests__/HighlightSvg-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import HighlightSvg from '../HighlightSvg'; 4 | 5 | type Props = { 6 | children?: React.ReactNode; 7 | className?: string; 8 | }; 9 | 10 | describe('HighlightSvg', () => { 11 | const getWrapper = (props: Props): ShallowWrapper => shallow(); 12 | 13 | describe('render', () => { 14 | test('should render svg with classname', () => { 15 | const wrapper = getWrapper({ className: 'foo' }); 16 | const svg = wrapper.find('svg'); 17 | 18 | expect(svg.hasClass('foo')).toBe(true); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/highlight/actions.ts: -------------------------------------------------------------------------------- 1 | import { AppThunkDispatch, AppState } from '../store'; 2 | import { createAnnotationAction } from '../store/annotations'; 3 | import { getFileVersionId } from '../store/options'; 4 | import { Rect } from '../@types'; 5 | 6 | export type CreateArg = { 7 | location: number; 8 | message: string; 9 | shapes: Rect[]; 10 | text?: string; 11 | }; 12 | 13 | export const createHighlightAction = (arg: CreateArg) => (dispatch: AppThunkDispatch, getState: () => AppState) => { 14 | const { location, message, shapes } = arg; 15 | const state = getState(); 16 | const newAnnotation = { 17 | description: { 18 | message, 19 | type: 'reply' as const, 20 | }, 21 | file_version: { 22 | id: getFileVersionId(state), 23 | }, 24 | target: { 25 | location: { 26 | type: 'page' as const, 27 | value: location, 28 | }, 29 | shapes, 30 | type: 'highlight' as const, 31 | }, 32 | }; 33 | 34 | return dispatch(createAnnotationAction(newAnnotation)); 35 | }; 36 | -------------------------------------------------------------------------------- /src/highlight/index.ts: -------------------------------------------------------------------------------- 1 | export * from './highlightUtil'; 2 | export { default as HighlightCreatorManager } from './HighlightCreatorManager'; 3 | export { default as HighlightListener } from './HighlightListener'; 4 | export { default as HighlightManager } from './HighlightManager'; 5 | -------------------------------------------------------------------------------- /src/image/ImageAnnotator.scss: -------------------------------------------------------------------------------- 1 | .bp-image { 2 | .ba-Layer { 3 | position: absolute; 4 | white-space: initial; // Overrides white-space: no-wrap inherited .bp-image 5 | text-align: initial; // Overrides text-align: center inherited .bp-image 6 | pointer-events: none; 7 | } 8 | 9 | // Applied to img element, which is managed by Preview SDK 10 | .ba-is-drawing { 11 | user-select: none; // Prevent visible selection highlight 12 | } 13 | 14 | // Raise the rendering order to override the stacking context created by the 15 | // transform applied to each of the managers' react mount element to account for 16 | // rotation 17 | .ba-Layer--popup { 18 | z-index: 4; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl'; 2 | 3 | export default defineMessages({ 4 | annotationsClose: { 5 | id: 'ba.annotationsClose', 6 | description: 'Label for the close button', 7 | defaultMessage: 'Close', 8 | }, 9 | annotationsSave: { 10 | id: 'ba.annotationsSave', 11 | description: 'Label for the save button', 12 | defaultMessage: 'Save', 13 | }, 14 | annotationsPost: { 15 | id: 'ba.annotationsPost', 16 | description: 'Label for the post button', 17 | defaultMessage: 'Post', 18 | }, 19 | annotationsCreateError: { 20 | id: 'ba.annotationsCreateError', 21 | description: 'Error message when creating', 22 | defaultMessage: 'We’re sorry, the annotation could not be created.', 23 | }, 24 | annotationsLoadError: { 25 | id: 'ba.annotationsLoadError', 26 | description: 'Error message when loading', 27 | defaultMessage: 'We’re sorry, the annotations failed to load for this file.', 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | // Add support for SVGElement.contains in IE11 2 | if (!SVGElement.prototype.contains) { 3 | Object.defineProperty(SVGElement.prototype, 'contains', { 4 | configurable: true, 5 | enumerable: false, 6 | writable: true, 7 | value: function contains(node: Node) { 8 | let n: Node | null = node; 9 | do { 10 | if (this === n) { 11 | return true; 12 | } 13 | n = n && n.parentNode; 14 | } while (n); 15 | 16 | return false; 17 | }, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/popup/PopupContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import PopupLayer from './PopupLayer'; 3 | import withProviders from '../common/withProviders'; 4 | import { 5 | AppState, 6 | CreatorItem, 7 | CreatorStatus, 8 | getAnnotationMode, 9 | getCreatorMessage, 10 | getCreatorStagedForLocation, 11 | getCreatorStatus, 12 | getIsPromoting, 13 | Mode, 14 | resetCreatorAction, 15 | setMessageAction, 16 | getCreatorReferenceId, 17 | } from '../store'; 18 | import { createDrawingAction } from '../drawing/actions'; 19 | import { createHighlightAction } from '../highlight/actions'; 20 | import { createRegionAction } from '../region'; 21 | 22 | export type Props = { 23 | isPromoting: boolean; 24 | message: string; 25 | mode: Mode; 26 | referenceId: string | null; 27 | staged: CreatorItem | null; 28 | status: CreatorStatus; 29 | }; 30 | 31 | export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => { 32 | return { 33 | isPromoting: getIsPromoting(state), 34 | message: getCreatorMessage(state), 35 | mode: getAnnotationMode(state), 36 | referenceId: getCreatorReferenceId(state), 37 | staged: getCreatorStagedForLocation(state, location), 38 | status: getCreatorStatus(state), 39 | }; 40 | }; 41 | 42 | export const mapDispatchToProps = { 43 | createDrawing: createDrawingAction, 44 | createHighlight: createHighlightAction, 45 | createRegion: createRegionAction, 46 | resetCreator: resetCreatorAction, 47 | setMessage: setMessageAction, 48 | }; 49 | 50 | export default connect(mapStateToProps, mapDispatchToProps)(withProviders(PopupLayer)); 51 | -------------------------------------------------------------------------------- /src/popup/PopupLayer.scss: -------------------------------------------------------------------------------- 1 | .ba-PopupLayer-popup { 2 | pointer-events: auto; 3 | } 4 | -------------------------------------------------------------------------------- /src/popup/PopupManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import BaseManager, { Props } from '../common/BaseManager'; 4 | import PopupContainer from './PopupContainer'; 5 | 6 | export default class PopupManager extends BaseManager { 7 | decorate(): void { 8 | this.reactEl.classList.add('ba-Layer--popup'); 9 | this.reactEl.dataset.testid = 'ba-Layer--popup'; 10 | } 11 | 12 | render(props: Props): void { 13 | if (!this.root) { 14 | this.root = ReactDOM.createRoot(this.reactEl); 15 | } 16 | 17 | this.root.render(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/popup/__mocks__/PopupLayer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default class PopupLayer extends React.Component<{ children: React.ReactNode }> { 4 | name = 'PopupLayerMock'; 5 | 6 | render(): JSX.Element { 7 | const { children } = this.props; 8 | 9 | return
{children}
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/popup/__tests__/PopupContainer-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IntlShape } from 'react-intl'; 3 | import { mount, ReactWrapper } from 'enzyme'; 4 | import PopupLayer from '../PopupLayer'; 5 | import PopupContainer, { Props } from '../PopupContainer'; 6 | import { createStore, CreatorStatus, Mode } from '../../store'; 7 | 8 | jest.mock('../PopupLayer'); 9 | jest.mock('../../common/withProviders'); 10 | 11 | describe('PopupContainer', () => { 12 | const defaults = { 13 | intl: {} as IntlShape, 14 | location: 1, 15 | store: createStore(), 16 | }; 17 | const getWrapper = (props = {}): ReactWrapper => mount(); 18 | 19 | describe('render', () => { 20 | test('should connect the underlying component and wrap it with a root provider', () => { 21 | const wrapper = getWrapper(); 22 | 23 | expect(wrapper.exists('RootProvider')).toBe(true); 24 | expect(wrapper.find(PopupLayer).props()).toMatchObject({ 25 | createDrawing: expect.any(Function), 26 | createHighlight: expect.any(Function), 27 | createRegion: expect.any(Function), 28 | isPromoting: false, 29 | mode: Mode.NONE, 30 | referenceId: null, 31 | resetCreator: expect.any(Function), 32 | setMessage: expect.any(Function), 33 | staged: null, 34 | status: CreatorStatus.init, 35 | store: defaults.store, 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/region/RegionAnnotation.scss: -------------------------------------------------------------------------------- 1 | @import '~box-ui-elements/es/styles/variables'; 2 | @import './RegionRect'; 3 | 4 | .ba-RegionAnnotation { 5 | @include ba-RegionRect; 6 | 7 | background: none; 8 | border: none; 9 | box-shadow: none; 10 | 11 | &:hover { 12 | cursor: pointer; 13 | } 14 | 15 | &:focus { 16 | outline: none; 17 | } 18 | 19 | &.is-active { 20 | @include ba-RegionRect-callout; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/region/RegionAnnotations.scss: -------------------------------------------------------------------------------- 1 | .ba-RegionAnnotations-list { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | pointer-events: none; 8 | 9 | &.is-listening { 10 | .ba-RegionAnnotation { 11 | pointer-events: auto; // Delegate event control to avoid re-rendering every target on mousedown/up 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/region/RegionAnnotations.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import RegionList from './RegionList'; 3 | import { AnnotationRegion } from '../@types'; 4 | import './RegionAnnotations.scss'; 5 | 6 | type Props = { 7 | activeAnnotationId: string | null; 8 | annotations: AnnotationRegion[]; 9 | setActiveAnnotationId: (annotationId: string | null) => void; 10 | }; 11 | 12 | const RegionAnnotations = (props: Props): JSX.Element => { 13 | const { activeAnnotationId, annotations, setActiveAnnotationId } = props; 14 | 15 | const handleAnnotationActive = (annotationId: string | null): void => { 16 | setActiveAnnotationId(annotationId); 17 | }; 18 | 19 | return ( 20 | 26 | ); 27 | }; 28 | 29 | export default RegionAnnotations; 30 | -------------------------------------------------------------------------------- /src/region/RegionAnnotationsContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import RegionAnnotations from './RegionAnnotations'; 3 | import withProviders from '../common/withProviders'; 4 | import { AnnotationRegion } from '../@types'; 5 | import { AppState, getActiveAnnotationId, getAnnotationsForLocation, setActiveAnnotationIdAction } from '../store'; 6 | import { isRegion } from './regionUtil'; 7 | 8 | export type Props = { 9 | activeAnnotationId: string | null; 10 | annotations: AnnotationRegion[]; 11 | }; 12 | 13 | export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => { 14 | return { 15 | activeAnnotationId: getActiveAnnotationId(state), 16 | annotations: getAnnotationsForLocation(state, location).filter(isRegion), 17 | }; 18 | }; 19 | 20 | export const mapDispatchToProps = { 21 | setActiveAnnotationId: setActiveAnnotationIdAction, 22 | }; 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(withProviders(RegionAnnotations)); 25 | -------------------------------------------------------------------------------- /src/region/RegionCreation.scss: -------------------------------------------------------------------------------- 1 | .ba-RegionCreation-creator, 2 | .ba-RegionCreation-target { 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .ba-RegionCreation-creator { 11 | pointer-events: auto; 12 | touch-action: none; 13 | } 14 | 15 | .ba-RegionCreation-target { 16 | pointer-events: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/region/RegionCreationContainer.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | AppState, 4 | CreatorItemRegion, 5 | CreatorStatus, 6 | getAnnotationMode, 7 | getCreatorStagedForLocation, 8 | getCreatorStatus, 9 | getRotation, 10 | isCreatorStagedRegion, 11 | Mode, 12 | resetCreatorAction, 13 | setReferenceIdAction, 14 | setStagedAction, 15 | setStatusAction, 16 | } from '../store'; 17 | import RegionCreation from './RegionCreation'; 18 | import withProviders from '../common/withProviders'; 19 | 20 | export type Props = { 21 | isCreating: boolean; 22 | isRotated: boolean; 23 | staged: CreatorItemRegion | null; 24 | }; 25 | 26 | export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => { 27 | const staged = getCreatorStagedForLocation(state, location); 28 | 29 | return { 30 | isCreating: getAnnotationMode(state) === Mode.REGION && getCreatorStatus(state) !== CreatorStatus.pending, 31 | isRotated: !!getRotation(state), 32 | staged: isCreatorStagedRegion(staged) ? staged : null, 33 | }; 34 | }; 35 | 36 | export const mapDispatchToProps = { 37 | resetCreator: resetCreatorAction, 38 | setReferenceId: setReferenceIdAction, 39 | setStaged: setStagedAction, 40 | setStatus: setStatusAction, 41 | }; 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(withProviders(RegionCreation)); 44 | -------------------------------------------------------------------------------- /src/region/RegionCreationManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import BaseManager, { Props } from '../common/BaseManager'; 4 | import RegionCreationContainer from './RegionCreationContainer'; 5 | 6 | export default class RegionManager extends BaseManager { 7 | decorate(): void { 8 | this.reactEl.classList.add('ba-Layer--regionCreation'); 9 | this.reactEl.dataset.testid = 'ba-Layer--regionCreation'; 10 | } 11 | 12 | render(props: Props): void { 13 | if (!this.root) { 14 | this.root = ReactDOM.createRoot(this.reactEl); 15 | } 16 | 17 | this.root.render(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/region/RegionManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | import BaseManager, { Props } from '../common/BaseManager'; 4 | import RegionAnnotationsContainer from './RegionAnnotationsContainer'; 5 | 6 | export default class RegionListManager extends BaseManager { 7 | decorate(): void { 8 | this.reactEl.classList.add('ba-Layer--region'); 9 | this.reactEl.dataset.testid = 'ba-Layer--region'; 10 | } 11 | 12 | render(props: Props): void { 13 | if (!this.root) { 14 | this.root = ReactDOM.createRoot(this.reactEl); 15 | } 16 | 17 | this.root.render(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/region/RegionRect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import noop from 'lodash/noop'; 3 | import classNames from 'classnames'; 4 | import useMountId from '../common/useMountId'; 5 | import { Shape } from '../@types'; 6 | import { styleShape } from './regionUtil'; 7 | import './RegionRect.scss'; 8 | 9 | type Props = { 10 | className?: string; 11 | isActive?: boolean; 12 | onMount?: (uuid: string) => void; 13 | shape?: Shape; 14 | }; 15 | 16 | export type RegionRectRef = HTMLDivElement; 17 | 18 | export function RegionRect(props: Props, ref: React.Ref): JSX.Element { 19 | const { className, isActive, onMount = noop, shape } = props; 20 | const uuid = useMountId(onMount); 21 | 22 | return ( 23 |
29 | ); 30 | } 31 | 32 | export default React.forwardRef(RegionRect); 33 | -------------------------------------------------------------------------------- /src/region/__mocks__/RegionRect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.forwardRef((props, ref: React.Ref) =>
); 4 | -------------------------------------------------------------------------------- /src/region/__tests__/RegionAnnotations-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import RegionAnnotations from '../RegionAnnotations'; 4 | import RegionList from '../RegionList'; 5 | import { annotations } from '../__mocks__/data'; 6 | 7 | jest.mock('../RegionList'); 8 | 9 | describe('RegionAnnotations', () => { 10 | const defaults = { 11 | activeAnnotationId: null, 12 | annotations: [], 13 | setActiveAnnotationId: jest.fn(), 14 | }; 15 | const getWrapper = (props = {}): ShallowWrapper => shallow(); 16 | 17 | describe('event handlers', () => { 18 | describe('handleAnnotationActive()', () => { 19 | test('should call setActiveAnnotationId with annotation id', () => { 20 | getWrapper() 21 | .find(RegionList) 22 | .prop('onSelect')!('123'); 23 | 24 | expect(defaults.setActiveAnnotationId).toHaveBeenCalledWith('123'); 25 | }); 26 | }); 27 | }); 28 | 29 | describe('render()', () => { 30 | test('should render one RegionAnnotation per annotation', () => { 31 | const wrapper = getWrapper({ annotations }); 32 | const list = wrapper.find(RegionList); 33 | 34 | expect(list.hasClass('ba-RegionAnnotations-list')).toBe(true); 35 | expect(list.prop('annotations').length).toBe(annotations.length); 36 | }); 37 | 38 | test('should pass activeId to the region list', () => { 39 | const wrapper = getWrapper({ activeAnnotationId: '123' }); 40 | 41 | expect(wrapper.find(RegionList).prop('activeId')).toBe('123'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/region/__tests__/RegionAnnotationsContainer-test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IntlShape } from 'react-intl'; 3 | import { mount, ReactWrapper } from 'enzyme'; 4 | import RegionAnnotations from '../RegionAnnotations'; 5 | import RegionAnnotationsContainer, { Props } from '../RegionAnnotationsContainer'; 6 | import { createStore } from '../../store'; 7 | 8 | jest.mock('../../common/useIsListInteractive'); 9 | jest.mock('../../common/withProviders'); 10 | 11 | describe('RegionAnnotationsContainer', () => { 12 | const defaults = { 13 | intl: {} as IntlShape, 14 | location: 1, 15 | store: createStore(), 16 | }; 17 | const getWrapper = (props = {}): ReactWrapper => 18 | mount(); 19 | 20 | describe('render', () => { 21 | test('should connect the underlying component and wrap it with a root provider', () => { 22 | const wrapper = getWrapper(); 23 | 24 | expect(wrapper.exists('RootProvider')).toBe(true); 25 | expect(wrapper.find(RegionAnnotations).props()).toMatchObject({ 26 | activeAnnotationId: null, 27 | annotations: [], 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/region/__tests__/RegionRect-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, ShallowWrapper } from 'enzyme'; 3 | import RegionRect from '../RegionRect'; 4 | import { styleShape } from '../regionUtil'; 5 | 6 | jest.mock('../regionUtil', () => ({ 7 | styleShape: jest.fn(value => value), 8 | })); 9 | 10 | jest.mock('../../common/useMountId'); 11 | 12 | describe('RegionRect', () => { 13 | const getWrapper = (props = {}): ShallowWrapper => shallow(); 14 | 15 | describe('render', () => { 16 | test('should call styleShape with the provided shape prop value', () => { 17 | const shape = { height: 10, width: 10, x: 10, y: 10 }; 18 | const wrapper = getWrapper({ shape }); 19 | 20 | expect(styleShape).toHaveBeenCalledWith(shape); 21 | expect(wrapper.find('div').prop('style')).toEqual(shape); 22 | }); 23 | 24 | test.each([true, false])('should render classNames correctly when isActive is %s', isActive => { 25 | const wrapper = getWrapper({ isActive }); 26 | const divEl = wrapper.find('div'); 27 | 28 | expect(divEl.hasClass('ba-RegionRect')).toBe(true); 29 | expect(divEl.hasClass('is-active')).toBe(isActive); 30 | }); 31 | }); 32 | 33 | describe('onMount()', () => { 34 | test('should call onMount with generated uuid', () => { 35 | const handleMount = jest.fn(); 36 | getWrapper({ onMount: handleMount }); 37 | 38 | expect(handleMount).toHaveBeenCalledWith('123'); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/region/__tests__/actions-test.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from '../../@types'; 2 | import { createAnnotationAction } from '../../store/annotations'; 3 | import { createRegionAction } from '../actions'; 4 | 5 | jest.mock('../../store/annotations'); 6 | jest.mock('../../store/options', () => ({ 7 | getFileVersionId: jest.fn().mockReturnValue('123'), 8 | })); 9 | 10 | describe('region/actions', () => { 11 | describe('createRegionAction', () => { 12 | const arg = { 13 | location: 5, 14 | message: 'message', 15 | shape: { 16 | height: 50.25, 17 | width: 50.25, 18 | x: 10.75, 19 | y: 10.75, 20 | } as Rect, 21 | }; 22 | const dispatch = jest.fn(); 23 | const getState = jest.fn(); 24 | 25 | test('should format its argument and dispatch', async () => { 26 | await createRegionAction(arg)(dispatch, getState); 27 | 28 | expect(dispatch).toHaveBeenCalled(); 29 | expect(getState).toHaveBeenCalled(); 30 | expect(createAnnotationAction).toHaveBeenCalledWith({ 31 | description: { 32 | message: 'message', 33 | type: 'reply', 34 | }, 35 | file_version: { 36 | id: '123', 37 | }, 38 | target: { 39 | location: { 40 | type: 'page', 41 | value: 5, 42 | }, 43 | shape: arg.shape, 44 | type: 'region', 45 | }, 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/region/actions.ts: -------------------------------------------------------------------------------- 1 | import { AppThunkDispatch, AppState } from '../store'; 2 | import { createAnnotationAction } from '../store/annotations'; 3 | import { getFileVersionId } from '../store/options'; 4 | import { Rect } from '../@types'; 5 | 6 | export type CreateArg = { 7 | location: number; 8 | message: string; 9 | shape: Rect; 10 | }; 11 | 12 | export const createRegionAction = (arg: CreateArg) => (dispatch: AppThunkDispatch, getState: () => AppState) => { 13 | const { location, message, shape } = arg; 14 | const state = getState(); 15 | const newAnnotation = { 16 | description: { 17 | message, 18 | type: 'reply' as const, 19 | }, 20 | file_version: { 21 | id: getFileVersionId(state), 22 | }, 23 | target: { 24 | location: { 25 | type: 'page' as const, 26 | value: location, 27 | }, 28 | shape, 29 | type: 'region' as const, 30 | }, 31 | }; 32 | 33 | return dispatch(createAnnotationAction(newAnnotation)); 34 | }; 35 | -------------------------------------------------------------------------------- /src/region/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './regionUtil'; 3 | export { default as RegionCreationManager } from './RegionCreationManager'; 4 | export { default as RegionManager } from './RegionManager'; 5 | -------------------------------------------------------------------------------- /src/region/regionUtil.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Annotation, AnnotationRegion, Position, Shape } from '../@types'; 3 | 4 | export const EMPTY_STYLE = { display: 'none' }; 5 | 6 | export const centerShape = (shape: Shape): Position => { 7 | const { height, width } = shape; 8 | 9 | return { 10 | x: width / 2, 11 | y: height / 2, 12 | }; 13 | }; 14 | 15 | export const centerRegion = (shape: Shape): Position => { 16 | const { x: shapeX, y: shapeY } = shape; 17 | const { x: centerX, y: centerY } = centerShape(shape); 18 | 19 | return { 20 | x: centerX + shapeX, 21 | y: centerY + shapeY, 22 | }; 23 | }; 24 | 25 | export function isRegion(annotation: Annotation): annotation is AnnotationRegion { 26 | return annotation?.target?.type === 'region'; 27 | } 28 | 29 | export function styleShape(shape?: Shape): React.CSSProperties { 30 | if (!shape) { 31 | return EMPTY_STYLE; 32 | } 33 | 34 | const { height, width, x, y } = shape; 35 | 36 | return { 37 | display: 'block', // Override inline "display: none" from EMPTY_STYLE 38 | height: `${height}%`, 39 | left: `${x}%`, 40 | top: `${y}%`, 41 | width: `${width}%`, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/region/transformUtil.ts: -------------------------------------------------------------------------------- 1 | export type Point = { 2 | x: number; 3 | y: number; 4 | }; 5 | 6 | export const invertYCoordinate = ({ x, y }: Point, height: number): Point => ({ 7 | x, 8 | y: height > 0 ? height - y : y, 9 | }); 10 | 11 | export const rotatePoint = ({ x, y }: Point, rotationInDegrees: number): Point => { 12 | const radians = (rotationInDegrees * Math.PI) / 180; 13 | const cosine = Math.cos(radians); 14 | const sine = Math.sin(radians); 15 | 16 | // Formula to apply a rotation to a point is: 17 | // x' = x * cos(θ) - y * sin(θ) 18 | // y' = x * sin(θ) + y * cos(θ) 19 | return { 20 | x: x * cosine - y * sine, 21 | y: x * sine + y * cosine, 22 | }; 23 | }; 24 | 25 | export const translatePoint = ({ x, y }: Point, { dx = 0, dy = 0 }: { dx?: number; dy?: number }): Point => ({ 26 | x: x + dx, 27 | y: y + dy, 28 | }); 29 | -------------------------------------------------------------------------------- /src/store/__mocks__/createStore.ts: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => ({ 2 | dispatch: jest.fn(), 3 | getState: jest.fn(), 4 | subscribe: jest.fn(() => jest.fn()), 5 | })); 6 | -------------------------------------------------------------------------------- /src/store/__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | import createStore from './createStore'; 2 | import { isCreatorStagedHighlight, isCreatorStagedRegion } from '../creator/selectors'; 3 | import { Mode } from '../common/types'; 4 | 5 | module.exports = { 6 | createStore, 7 | getActiveAnnotationId: jest.fn(), 8 | getAnnotationMode: jest.fn(), 9 | getAnnotationsForLocation: jest.fn().mockReturnValue([]), 10 | getCreatorCursor: jest.fn().mockReturnValue(1), 11 | getCreatorMessage: jest.fn(), 12 | getCreatorStagedForLocation: jest.fn(), 13 | getCreatorStatus: jest.fn(), 14 | getFileId: jest.fn().mockReturnValue('0'), 15 | getIsCurrentFileVersion: jest.fn().mockReturnValue(true), 16 | getIsInitialized: jest.fn().mockReturnValue(false), 17 | getIsPromoting: jest.fn().mockReturnValue(false), 18 | getIsSelecting: jest.fn().mockReturnValue(false), 19 | getSelectionForLocation: jest.fn(), 20 | isCreatorStagedHighlight, 21 | isCreatorStagedRegion, 22 | Mode, 23 | setIsSelectingAction: jest.fn(), 24 | setSelectionAction: jest.fn(), 25 | }; 26 | -------------------------------------------------------------------------------- /src/store/annotations/__mocks__/annotationsState.ts: -------------------------------------------------------------------------------- 1 | import { Annotation } from '../../../@types'; 2 | import { AnnotationsState } from '../types'; 3 | 4 | const annotationState: AnnotationsState = { 5 | activeId: null, 6 | allIds: ['test1', 'test2', 'test3'], 7 | byId: { 8 | test1: { id: 'test1', target: { location: { value: 1 } } } as Annotation, 9 | test2: { id: 'test2', target: { location: { value: 1 } } } as Annotation, 10 | test3: { id: 'test3', target: { location: { value: 2 } } } as Annotation, 11 | }, 12 | isInitialized: false, 13 | }; 14 | 15 | export default annotationState; 16 | -------------------------------------------------------------------------------- /src/store/annotations/index.ts: -------------------------------------------------------------------------------- 1 | export { default as annotationsReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/annotations/selectors.ts: -------------------------------------------------------------------------------- 1 | import getProp from 'lodash/get'; 2 | import { Annotation } from '../../@types'; 3 | import { AppState } from '../types'; 4 | 5 | type State = Pick; 6 | 7 | export const getActiveAnnotationId = ({ annotations }: State): string | null => annotations.activeId; 8 | export const getAnnotation = ({ annotations }: State, id: string): Annotation | undefined => annotations.byId[id]; 9 | export const getAnnotations = ({ annotations }: State): Annotation[] => [...Object.values(annotations.byId)]; 10 | export const getAnnotationsForLocation = (state: State, location: number): Annotation[] => 11 | getAnnotations(state).filter(annotation => getProp(annotation, 'target.location.value') === location); 12 | export const getIsInitialized = ({ annotations }: State): boolean => annotations.isInitialized; 13 | -------------------------------------------------------------------------------- /src/store/annotations/types.ts: -------------------------------------------------------------------------------- 1 | import { Annotation } from '../../@types'; 2 | 3 | export type AnnotationsState = { 4 | activeId: string | null; 5 | allIds: string[]; 6 | byId: Record; 7 | isInitialized: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /src/store/common/__mocks__/commonState.ts: -------------------------------------------------------------------------------- 1 | import { Mode } from '../types'; 2 | 3 | export default { 4 | color: '#000', 5 | mode: Mode.NONE, 6 | }; 7 | -------------------------------------------------------------------------------- /src/store/common/__tests__/reducer-test.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../reducer'; 2 | import state from '../__mocks__/commonState'; 3 | import { Mode } from '../types'; 4 | import { setColorAction, toggleAnnotationModeAction } from '../actions'; 5 | 6 | describe('store/common/reducer', () => { 7 | describe('setColorAction', () => { 8 | test('should set the color in state', () => { 9 | const newState = reducer(state, setColorAction('#111')); 10 | 11 | expect(newState.color).toEqual('#111'); 12 | }); 13 | }); 14 | 15 | describe('toggleAnnotationModeAction', () => { 16 | const { NONE, REGION } = Mode; 17 | test.each` 18 | payloadMode | currentMode | expectedMode 19 | ${REGION} | ${NONE} | ${REGION} 20 | ${REGION} | ${REGION} | ${REGION} 21 | `('should toggle the current mode appropriately', ({ payloadMode, currentMode, expectedMode }) => { 22 | const newState = reducer({ ...state, mode: currentMode }, toggleAnnotationModeAction(payloadMode)); 23 | expect(newState.mode).toEqual(expectedMode); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/store/common/__tests__/selectors-test.ts: -------------------------------------------------------------------------------- 1 | import commonState from '../__mocks__/commonState'; 2 | import { getAnnotationMode, getColor } from '../selectors'; 3 | 4 | describe('store/common/selectors', () => { 5 | const state = { common: commonState }; 6 | 7 | describe('getAnnotationMode', () => { 8 | test('should return annotation mode', () => { 9 | expect(getAnnotationMode(state)).toBe('none'); 10 | }); 11 | }); 12 | 13 | describe('getColor', () => { 14 | test('should return the current creator color', () => { 15 | expect(getColor(state)).toBe('#000'); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/store/common/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | import { Mode } from './types'; 3 | 4 | export const setColorAction = createAction('SET_COLOR'); 5 | export const toggleAnnotationModeAction = createAction('TOGGLE_ANNOTATION_MODE'); 6 | -------------------------------------------------------------------------------- /src/store/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as commonReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/common/reducer.ts: -------------------------------------------------------------------------------- 1 | import { bdlBoxBlue } from 'box-ui-elements/es/styles/variables'; 2 | import { createReducer } from '@reduxjs/toolkit'; 3 | import { CommonState, Mode } from './types'; 4 | import { setColorAction, toggleAnnotationModeAction } from './actions'; 5 | 6 | export const initialState = { 7 | color: bdlBoxBlue, 8 | mode: Mode.NONE, 9 | }; 10 | 11 | export default createReducer(initialState, builder => 12 | builder 13 | .addCase(setColorAction, (state, { payload }) => { 14 | state.color = payload; 15 | }) 16 | .addCase(toggleAnnotationModeAction, (state, { payload }) => { 17 | state.mode = payload; 18 | }), 19 | ); 20 | -------------------------------------------------------------------------------- /src/store/common/selectors.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../types'; 2 | import { Mode } from './types'; 3 | 4 | type State = Pick; 5 | 6 | export const getAnnotationMode = ({ common }: State): Mode => common.mode; 7 | export const getColor = (state: State): string => state.common.color; 8 | -------------------------------------------------------------------------------- /src/store/common/types.ts: -------------------------------------------------------------------------------- 1 | export enum Mode { 2 | DRAWING = 'drawing', 3 | HIGHLIGHT = 'highlight', 4 | NONE = 'none', 5 | REGION = 'region', 6 | } 7 | 8 | export interface CommonState { 9 | color: string; 10 | mode: Mode; 11 | } 12 | export interface ModeState { 13 | current: Mode; 14 | } 15 | -------------------------------------------------------------------------------- /src/store/createRootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Reducer } from 'redux'; 2 | import { annotationsReducer } from './annotations'; 3 | import { commonReducer } from './common'; 4 | import { creatorReducer } from './creator'; 5 | import { drawingReducer } from './drawing'; 6 | import { highlightReducer } from './highlight'; 7 | import { optionsReducer } from './options'; 8 | import { usersReducer } from './users'; 9 | 10 | const createRootReducer = (): Reducer => 11 | combineReducers({ 12 | annotations: annotationsReducer, 13 | common: commonReducer, 14 | creator: creatorReducer, 15 | drawing: drawingReducer, 16 | highlight: highlightReducer, 17 | options: optionsReducer, 18 | users: usersReducer, 19 | }); 20 | 21 | export default createRootReducer; 22 | -------------------------------------------------------------------------------- /src/store/createStore.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; 2 | import API from '../api'; 3 | import createRootReducer from './createRootReducer'; 4 | import getEventingMiddleware from './eventing'; 5 | import { AppState, AppStore } from './types'; 6 | 7 | export type Options = { 8 | api?: API; 9 | }; 10 | 11 | type RecursivePartial = { 12 | [P in keyof T]?: RecursivePartial; 13 | }; 14 | 15 | export default function createStore(preloadedState?: RecursivePartial, { api }: Options = {}): AppStore { 16 | const thunkOptions = { 17 | extraArgument: { api }, 18 | }; 19 | 20 | return configureStore({ 21 | devTools: process.env.NODE_ENV === 'dev', 22 | middleware: [...getDefaultMiddleware({ thunk: thunkOptions }), getEventingMiddleware()], 23 | preloadedState, 24 | reducer: createRootReducer(), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/store/creator/__mocks__/creatorState.ts: -------------------------------------------------------------------------------- 1 | import { CreatorStatus } from '../types'; 2 | 3 | export default { 4 | cursor: 0, 5 | error: null, 6 | message: 'test', 7 | referenceId: '100001', 8 | staged: { 9 | location: 1, 10 | shape: { 11 | height: 100, 12 | width: 100, 13 | type: 'rect' as const, 14 | x: 10, 15 | y: 10, 16 | }, 17 | type: 'region' as const, 18 | }, 19 | status: CreatorStatus.init, 20 | }; 21 | -------------------------------------------------------------------------------- /src/store/creator/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | import { CreatorItem, CreatorStatus } from './types'; 3 | 4 | export const resetCreatorAction = createAction('RESET_CREATOR'); 5 | export const setCursorAction = createAction('SET_CREATOR_CURSOR'); 6 | export const setMessageAction = createAction('SET_CREATOR_MESSAGE'); 7 | export const setReferenceIdAction = createAction('SET_CREATOR_REFERENCE_ID'); 8 | export const setStagedAction = createAction('SET_CREATOR_STAGED'); 9 | export const setStatusAction = createAction('SET_CREATOR_STATUS'); 10 | -------------------------------------------------------------------------------- /src/store/creator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as creatorReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/creator/selectors.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../types'; 2 | import { CreatorItem, CreatorItemDrawing, CreatorItemHighlight, CreatorItemRegion, CreatorStatus } from './types'; 3 | 4 | type State = Pick; 5 | 6 | export const getCreatorCursor = (state: State): number => state.creator.cursor; 7 | export const getCreatorMessage = (state: State): string => state.creator.message; 8 | export const getCreatorReferenceId = (state: State): string | null => state.creator.referenceId; 9 | export const getCreatorStaged = (state: State): CreatorItem | null => state.creator.staged; 10 | export const getCreatorStagedForLocation = (state: State, location: number): CreatorItem | null => { 11 | const staged = getCreatorStaged(state); 12 | return staged && staged.location === location ? staged : null; 13 | }; 14 | export const getCreatorStatus = (state: State): CreatorStatus => state.creator.status; 15 | 16 | export const isCreatorStagedDrawing = (staged: CreatorItem | null): staged is CreatorItemDrawing => 17 | (staged as CreatorItemDrawing)?.pathGroups !== undefined; 18 | export const isCreatorStagedHighlight = (staged: CreatorItem | null): staged is CreatorItemHighlight => 19 | (staged as CreatorItemHighlight)?.shapes !== undefined; 20 | export const isCreatorStagedRegion = (staged: CreatorItem | null): staged is CreatorItemRegion => 21 | (staged as CreatorItemRegion)?.shape !== undefined; 22 | -------------------------------------------------------------------------------- /src/store/creator/types.ts: -------------------------------------------------------------------------------- 1 | import { PathGroup, Rect, SerializedError } from '../../@types'; 2 | 3 | export enum CreatorStatus { 4 | init = 'init', 5 | pending = 'pending', 6 | rejected = 'rejected', 7 | staged = 'staged', 8 | started = 'started', 9 | } 10 | 11 | export type CreatorItemBase = { 12 | location: number; 13 | }; 14 | 15 | export type CreatorItemRegion = CreatorItemBase & { 16 | shape: Rect; 17 | }; 18 | 19 | export type CreatorItemHighlight = CreatorItemBase & { 20 | shapes: Rect[]; 21 | }; 22 | 23 | export type CreatorItemDrawing = CreatorItemBase & { 24 | pathGroups: Array; 25 | }; 26 | 27 | export type CreatorItem = CreatorItemRegion | CreatorItemHighlight | CreatorItemDrawing | null; 28 | 29 | export type CreatorState = { 30 | cursor: number; 31 | error: SerializedError | null; 32 | message: string; 33 | referenceId: string | null; 34 | staged: CreatorItem; 35 | status: CreatorStatus; 36 | }; 37 | -------------------------------------------------------------------------------- /src/store/drawing/__mocks__/drawingState.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | drawnPathGroups: [], 3 | location: 0, 4 | stashedPathGroups: [], 5 | }; 6 | -------------------------------------------------------------------------------- /src/store/drawing/__tests__/actions-test.ts: -------------------------------------------------------------------------------- 1 | import { addDrawingPathGroupAction } from '../actions'; 2 | import { pathGroups } from '../../../drawing/__mocks__/drawingData'; 3 | 4 | describe('store/drawing/actions', () => { 5 | describe('addDrawingPathGroupAction()', () => { 6 | test('should apply client ids to the provided pathGroup', () => { 7 | expect(addDrawingPathGroupAction(pathGroups[0])).toMatchObject({ 8 | type: 'ADD_DRAWING_PATH_GROUP', 9 | payload: { 10 | clientId: expect.any(String), 11 | paths: [ 12 | { 13 | clientId: expect.any(String), 14 | }, 15 | ], 16 | }, 17 | }); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/store/drawing/__tests__/selectors-test.ts: -------------------------------------------------------------------------------- 1 | import drawingState from '../__mocks__/drawingState'; 2 | import { annotations } from '../../../drawing/__mocks__/drawingData'; 3 | import { getDrawingDrawnPathGroupsForLocation } from '../selectors'; 4 | 5 | const { 6 | target: { path_groups: pathGroups }, 7 | } = annotations[0]; 8 | 9 | describe('store/drawing/selectors', () => { 10 | const state = { drawing: { ...drawingState, drawnPathGroups: pathGroups } }; 11 | 12 | describe('getDrawingDrawnPathGroupsForLocation()', () => { 13 | test('should return the current drawn path groups if the location matches', () => { 14 | expect(getDrawingDrawnPathGroupsForLocation(state, 0)).toEqual(pathGroups); 15 | }); 16 | 17 | test('should return an empty array if the location does not match', () => { 18 | expect(getDrawingDrawnPathGroupsForLocation(state, 1)).toEqual([]); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/store/drawing/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | import { addClientIds } from '../../drawing/drawingUtil'; 3 | 4 | export const addDrawingPathGroupAction = createAction('ADD_DRAWING_PATH_GROUP', pathGroup => ({ 5 | payload: addClientIds(pathGroup), 6 | })); 7 | export const redoDrawingPathGroupAction = createAction('REDO_DRAWING_PATH_GROUP'); 8 | export const resetDrawingAction = createAction('RESET_DRAWING'); 9 | export const setDrawingLocationAction = createAction('SET_DRAWING_LOCATION'); 10 | export const undoDrawingPathGroupAction = createAction('UNDO_DRAWING_PATH_GROUP'); 11 | -------------------------------------------------------------------------------- /src/store/drawing/index.ts: -------------------------------------------------------------------------------- 1 | export { default as drawingReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/drawing/selectors.ts: -------------------------------------------------------------------------------- 1 | import { PathGroup } from '../../@types'; 2 | import { AppState } from '../types'; 3 | 4 | type State = Pick; 5 | 6 | export const getDrawingDrawnPathGroupsForLocation = (state: State, location: number): Array => 7 | state.drawing.location === location ? state.drawing.drawnPathGroups : []; 8 | export const getStashedDrawnPathGroupsForLocation = (state: State, location: number): Array => 9 | state.drawing.location === location ? state.drawing.stashedPathGroups : []; 10 | -------------------------------------------------------------------------------- /src/store/drawing/types.ts: -------------------------------------------------------------------------------- 1 | import { PathGroup } from '../../@types'; 2 | 3 | export type DrawingState = { 4 | drawnPathGroups: Array; 5 | location: number; 6 | stashedPathGroups: Array; 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/eventing/__tests__/active-test.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../../common/EventManager'; 2 | import { AppState } from '../../types'; 3 | import { getActiveAnnotationId } from '../../annotations'; 4 | import { getFileVersionId } from '../../options'; 5 | import { handleActiveAnnotationEvents } from '../active'; 6 | 7 | jest.mock('../../../common/EventManager'); 8 | jest.mock('../../annotations'); 9 | jest.mock('../../options'); 10 | 11 | describe('store/eventing/active', () => { 12 | beforeEach(() => { 13 | (getFileVersionId as jest.Mock).mockReturnValue('456'); 14 | }); 15 | 16 | test('should not emit event if prev and next ids are the same', () => { 17 | const state = {} as AppState; 18 | (getActiveAnnotationId as jest.Mock).mockImplementationOnce(() => '123').mockImplementationOnce(() => '123'); 19 | 20 | handleActiveAnnotationEvents(state, state); 21 | 22 | expect(eventManager.emit).not.toBeCalled(); 23 | }); 24 | 25 | test.each` 26 | prevId | nextId 27 | ${null} | ${'123'} 28 | ${'123'} | ${'456'} 29 | ${'456'} | ${null} 30 | `('should emit event when prevId is $prevId, nextId is $nextId', ({ prevId, nextId }) => { 31 | const state = {} as AppState; 32 | (getActiveAnnotationId as jest.Mock).mockImplementationOnce(() => prevId).mockImplementationOnce(() => nextId); 33 | 34 | handleActiveAnnotationEvents(state, state); 35 | 36 | expect(eventManager.emit).toBeCalledWith('annotations_active_change', { 37 | annotationId: nextId, 38 | fileVersionId: '456', 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/store/eventing/__tests__/fetch-test.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../../common/EventManager'; 2 | import { AppState } from '../../types'; 3 | import { AsyncAction } from '../types'; 4 | import { handleFetchErrorEvents } from '../fetch'; 5 | 6 | jest.mock('../../../common/EventManager'); 7 | 8 | describe('store/eventing/fetch', () => { 9 | test('should emit fetch error event', () => { 10 | const error = new Error('fetch'); 11 | handleFetchErrorEvents({} as AppState, {} as AppState, { error } as AsyncAction); 12 | 13 | expect(eventManager.emit).toBeCalledWith('annotations_fetch_error', { error }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/store/eventing/__tests__/init-test.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../../common/EventManager'; 2 | import annotationState from '../../annotations/__mocks__/annotationsState'; 3 | import { AppState } from '../../types'; 4 | import { handleAnnotationsInitialized } from '../init'; 5 | 6 | jest.mock('../../../common/EventManager'); 7 | 8 | describe('store/eventing/init', () => { 9 | test('should emit annotations_initialized event with annotations if isInitialized changes to true', () => { 10 | handleAnnotationsInitialized( 11 | { annotations: { ...annotationState, isInitialized: false } } as AppState, 12 | { annotations: { ...annotationState, isInitialized: true } } as AppState, 13 | ); 14 | 15 | expect(eventManager.emit).toBeCalledWith('annotations_initialized', { 16 | annotations: [annotationState.byId.test1, annotationState.byId.test2, annotationState.byId.test3], 17 | }); 18 | }); 19 | 20 | test('should do nothing if isInitialized changes to false', () => { 21 | handleAnnotationsInitialized( 22 | { annotations: { ...annotationState, isInitialized: true } } as AppState, 23 | { annotations: { ...annotationState, isInitialized: false } } as AppState, 24 | ); 25 | 26 | expect(eventManager.emit).not.toBeCalled(); 27 | }); 28 | 29 | test('should do nothing if isInitialized does not change', () => { 30 | handleAnnotationsInitialized( 31 | { annotations: { ...annotationState, isInitialized: true } } as AppState, 32 | { annotations: { ...annotationState, isInitialized: true } } as AppState, 33 | ); 34 | 35 | expect(eventManager.emit).not.toBeCalled(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/store/eventing/__tests__/middleware-test.ts: -------------------------------------------------------------------------------- 1 | import getEventingMiddleware from '../middleware'; 2 | 3 | describe('store/eventing/middleware', () => { 4 | describe('getEventingMiddleware()', () => { 5 | const mockHandler = jest.fn(); 6 | const customEventHandlers = { 7 | foo: mockHandler, 8 | }; 9 | const middleware = getEventingMiddleware(customEventHandlers); 10 | const next = jest.fn(); 11 | const store = { 12 | dispatch: jest.fn(), 13 | getState: jest.fn(), 14 | }; 15 | 16 | test('should use provided eventHandlers', () => { 17 | middleware(store)(next)({ type: 'foo' }); 18 | 19 | expect(next).toHaveBeenCalled(); 20 | expect(store.getState).toHaveBeenCalledTimes(2); 21 | expect(mockHandler).toHaveBeenCalled(); 22 | }); 23 | 24 | test('should not call handlers if action type does not match', () => { 25 | middleware(store)(next)({ type: 'bar' }); 26 | 27 | expect(next).toHaveBeenCalled(); 28 | expect(store.getState).toHaveBeenCalledTimes(2); 29 | expect(mockHandler).not.toHaveBeenCalled(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/store/eventing/__tests__/mode-test.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../../common/EventManager'; 2 | import { AppState } from '../../types'; 3 | import { handleToggleAnnotationModeAction } from '../mode'; 4 | 5 | jest.mock('../../../common/EventManager'); 6 | 7 | describe('store/eventing/mode', () => { 8 | test('should emit annotations_mode_change with the next mode when changing annotation modes.', () => { 9 | const nextState = { 10 | common: { 11 | mode: 'region', 12 | }, 13 | } as AppState; 14 | 15 | handleToggleAnnotationModeAction({} as AppState, nextState); 16 | 17 | expect(eventManager.emit).toBeCalledWith('annotations_mode_change', { mode: 'region' }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/store/eventing/__tests__/status-test.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../../common/EventManager'; 2 | import { AppState } from '../../types'; 3 | import { handleSetStatusAction } from '../status'; 4 | 5 | jest.mock('../../../common/EventManager'); 6 | 7 | describe('store/eventing/status', () => { 8 | test('should emit creator_status_change with status and type', () => { 9 | handleSetStatusAction( 10 | { 11 | common: { 12 | mode: 'region', 13 | }, 14 | creator: { 15 | status: 'init', 16 | }, 17 | } as AppState, 18 | { 19 | common: { 20 | mode: 'region', 21 | }, 22 | creator: { 23 | status: 'started', 24 | }, 25 | } as AppState, 26 | ); 27 | 28 | expect(eventManager.emit).toBeCalledWith('creator_status_change', { status: 'started', type: 'region' }); 29 | }); 30 | 31 | test('should not emit event if status does not change', () => { 32 | handleSetStatusAction( 33 | { 34 | creator: { 35 | status: 'init', 36 | }, 37 | } as AppState, 38 | { 39 | creator: { 40 | status: 'init', 41 | }, 42 | } as AppState, 43 | ); 44 | 45 | expect(eventManager.emit).not.toBeCalled(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/store/eventing/active.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../common/EventManager'; 2 | import { AppState } from '../types'; 3 | import { Event } from '../../@types'; 4 | import { getActiveAnnotationId } from '../annotations'; 5 | import { getFileVersionId } from '../options'; 6 | 7 | const handleActiveAnnotationEvents = (prevState: AppState, nextState: AppState): void => { 8 | const prevActiveAnnotationId = getActiveAnnotationId(prevState); 9 | const nextActiveAnnotationId = getActiveAnnotationId(nextState); 10 | const fileVersionId = getFileVersionId(nextState); 11 | 12 | if (prevActiveAnnotationId !== nextActiveAnnotationId) { 13 | eventManager.emit(Event.ACTIVE_CHANGE, { annotationId: nextActiveAnnotationId, fileVersionId }); 14 | } 15 | }; 16 | 17 | export { handleActiveAnnotationEvents }; 18 | -------------------------------------------------------------------------------- /src/store/eventing/create.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../common/EventManager'; 2 | import { AsyncAction, Status } from './types'; 3 | import { AppState } from '../types'; 4 | import { Event } from '../../@types'; 5 | 6 | const emitCreateEvent = (action: AsyncAction, status: Status): void => { 7 | const { error, meta: { arg, requestId } = {}, payload } = action; 8 | eventManager.emit(Event.ANNOTATION_CREATE, { 9 | annotation: payload || arg, 10 | error, 11 | meta: { 12 | requestId, 13 | status, 14 | }, 15 | }); 16 | }; 17 | 18 | const createHandler = (status: Status) => (prevState: AppState, nextState: AppState, action: AsyncAction): void => 19 | emitCreateEvent(action, status); 20 | 21 | const handleCreateErrorEvents = createHandler(Status.ERROR); 22 | const handleCreatePendingEvents = createHandler(Status.PENDING); 23 | const handleCreateSuccessEvents = createHandler(Status.SUCCESS); 24 | 25 | export { handleCreateErrorEvents, handleCreatePendingEvents, handleCreateSuccessEvents }; 26 | -------------------------------------------------------------------------------- /src/store/eventing/fetch.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../common/EventManager'; 2 | import { AppState } from '../types'; 3 | import { AsyncAction } from './types'; 4 | import { Event } from '../../@types'; 5 | 6 | export const handleFetchErrorEvents = (prevState: AppState, nextState: AppState, action: AsyncAction): void => { 7 | eventManager.emit(Event.ANNOTATION_FETCH_ERROR, action); 8 | }; 9 | -------------------------------------------------------------------------------- /src/store/eventing/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './middleware'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/store/eventing/init.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../common/EventManager'; 2 | import { AppState } from '../types'; 3 | import { Event } from '../../@types'; 4 | import { getAnnotations, getIsInitialized } from '../annotations'; 5 | 6 | export const handleAnnotationsInitialized = (prevState: AppState, nextState: AppState): void => { 7 | const prevIsInitialized = getIsInitialized(prevState); 8 | const nextIsInitialized = getIsInitialized(nextState); 9 | 10 | // Only emit an event the first time the library is initialized/rendered 11 | if (prevIsInitialized !== nextIsInitialized && nextIsInitialized) { 12 | eventManager.emit(Event.ANNOTATIONS_INITIALIZED, { annotations: getAnnotations(nextState) }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/store/eventing/mode.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../common/EventManager'; 2 | import { AppState } from '../types'; 3 | import { Event } from '../../@types'; 4 | 5 | export const handleToggleAnnotationModeAction = (prevState: AppState, nextState: AppState): void => { 6 | eventManager.emit(Event.ANNOTATIONS_MODE_CHANGE, { mode: nextState.common.mode }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/eventing/status.ts: -------------------------------------------------------------------------------- 1 | import eventManager from '../../common/EventManager'; 2 | import { AppState } from '../types'; 3 | import { Event } from '../../@types'; 4 | import { getAnnotationMode } from '../common'; 5 | import { getCreatorStatus } from '../creator'; 6 | 7 | export const handleSetStatusAction = (prevState: AppState, nextState: AppState): void => { 8 | const prevStatus = getCreatorStatus(prevState); 9 | const nextStatus = getCreatorStatus(nextState); 10 | 11 | if (prevStatus === nextStatus) { 12 | return; 13 | } 14 | 15 | eventManager.emit(Event.CREATOR_STATUS_CHANGE, { 16 | status: nextStatus, 17 | type: getAnnotationMode(nextState), 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/store/eventing/types.ts: -------------------------------------------------------------------------------- 1 | import { Action, SerializedError } from '@reduxjs/toolkit'; 2 | import { AppState } from '../types'; 3 | import { Annotation, NewAnnotation } from '../../@types'; 4 | 5 | export enum Status { 6 | ERROR = 'error', 7 | PENDING = 'pending', 8 | SUCCESS = 'success', 9 | } 10 | 11 | export interface Metadata { 12 | status: Status; 13 | } 14 | export interface ActionEvent { 15 | annotation: Annotation | undefined; 16 | error: Error | undefined; 17 | meta: Metadata; 18 | } 19 | 20 | export interface ThunkMeta { 21 | arg: M; 22 | requestId: string; 23 | } 24 | 25 | export interface AsyncAction extends Action { 26 | error?: SerializedError; 27 | meta?: ThunkMeta; 28 | payload?: Annotation; 29 | } 30 | 31 | export type EventHandler = (prevState: AppState, nextState: AppState, action: Action | AsyncAction) => void; 32 | 33 | export type EventHandlerMap = Record; 34 | -------------------------------------------------------------------------------- /src/store/highlight/__mocks__/data.ts: -------------------------------------------------------------------------------- 1 | export const mockContainerRect: DOMRect = { 2 | bottom: 1000, 3 | height: 1000, 4 | left: 0, 5 | right: 1000, 6 | toJSON: jest.fn(), 7 | top: 0, 8 | width: 1000, 9 | x: 0, 10 | y: 0, 11 | }; 12 | 13 | export const mockDOMRect: DOMRect = { 14 | bottom: 300, 15 | height: 100, 16 | left: 200, 17 | right: 300, 18 | toJSON: jest.fn(), 19 | top: 200, 20 | width: 100, 21 | x: 200, 22 | y: 200, 23 | }; 24 | 25 | const mockTextNode = document.createTextNode('test'); 26 | 27 | export const mockRange: Range = ({ 28 | endContainer: mockTextNode, 29 | getBoundingClientRect: () => mockDOMRect, 30 | getClientRects: () => [mockDOMRect], 31 | startContainer: mockTextNode, 32 | } as unknown) as Range; 33 | -------------------------------------------------------------------------------- /src/store/highlight/__mocks__/highlightState.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | isPromoting: false, 3 | isSelecting: false, 4 | selection: { 5 | containerRect: { 6 | height: 1000, 7 | width: 1000, 8 | x: 0, 9 | y: 0, 10 | }, 11 | location: 1, 12 | rects: [ 13 | { 14 | height: 100, 15 | width: 100, 16 | x: 200, 17 | y: 200, 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/highlight/index.ts: -------------------------------------------------------------------------------- 1 | export { default as highlightReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/highlight/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { createAnnotationAction } from '../annotations'; 3 | import { HighlightState } from './types'; 4 | import { resetCreatorAction, setStatusAction } from '../creator'; 5 | import { setIsPromotingAction, setIsSelectingAction, setSelectionAction } from './actions'; 6 | import { toggleAnnotationModeAction } from '../common'; 7 | 8 | export const initialState = { 9 | isPromoting: false, 10 | isSelecting: false, 11 | selection: null, 12 | }; 13 | 14 | export default createReducer(initialState, builder => 15 | builder 16 | .addCase(setIsPromotingAction, (state, { payload }) => { 17 | state.isPromoting = payload; 18 | }) 19 | .addCase(setIsSelectingAction, (state, { payload }) => { 20 | state.isSelecting = payload; 21 | }) 22 | .addCase(setSelectionAction, (state, { payload }) => { 23 | state.selection = payload; 24 | }) 25 | .addCase(createAnnotationAction.fulfilled, state => { 26 | state.isPromoting = false; 27 | }) 28 | .addCase(resetCreatorAction, state => { 29 | state.isPromoting = false; 30 | }) 31 | .addCase(toggleAnnotationModeAction, state => { 32 | state.isPromoting = false; 33 | }) 34 | .addCase(setStatusAction, state => { 35 | state.selection = null; 36 | }), 37 | ); 38 | -------------------------------------------------------------------------------- /src/store/highlight/selectors.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../types'; 2 | import { SelectionItem } from './types'; 3 | 4 | type State = Pick; 5 | 6 | export const getIsPromoting = (state: State): boolean => state.highlight.isPromoting; 7 | export const getIsSelecting = (state: State): boolean => state.highlight.isSelecting; 8 | export const getSelection = (state: State): SelectionItem | null => state.highlight.selection; 9 | export const getSelectionForLocation = (state: State, location: number): SelectionItem | null => { 10 | const selection = getSelection(state); 11 | return selection && selection.location === location ? selection : null; 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/highlight/types.ts: -------------------------------------------------------------------------------- 1 | import { Shape } from '../../@types'; 2 | 3 | export type HighlightState = { 4 | isPromoting: boolean; 5 | isSelecting: boolean; 6 | selection: SelectionItem | null; 7 | }; 8 | 9 | export type SelectionItem = { 10 | containerRect: Shape; 11 | hasError?: boolean; 12 | location: number; 13 | rects: Array; 14 | }; 15 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createRootReducer } from './createRootReducer'; 2 | export { default as createStore } from './createStore'; 3 | 4 | export * from './annotations'; 5 | export * from './common'; 6 | export * from './creator'; 7 | export * from './highlight'; 8 | export * from './options'; 9 | export * from './types'; 10 | export * from './users'; 11 | -------------------------------------------------------------------------------- /src/store/options/__tests__/reducer-test.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../reducer'; 2 | import { setFileIdAction, setFileVersionIdAction, setPermissionsAction } from '../actions'; 3 | 4 | describe('store/common/reducer', () => { 5 | describe('setFileIdAction', () => { 6 | test('should set the file id', () => { 7 | const newState = reducer(undefined, setFileIdAction('12345')); 8 | expect(newState.fileId).toEqual('12345'); 9 | }); 10 | }); 11 | 12 | describe('setFileVersionIdAction', () => { 13 | test('should set the file version id', () => { 14 | const newState = reducer(undefined, setFileVersionIdAction('12345')); 15 | expect(newState.fileVersionId).toEqual('12345'); 16 | }); 17 | }); 18 | 19 | describe('setPermissionsAction', () => { 20 | test('should set the permissions', () => { 21 | const newState = reducer(undefined, setPermissionsAction({ can_create_annotations: true })); 22 | expect(newState.permissions).toEqual({ can_create_annotations: true }); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/store/options/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@reduxjs/toolkit'; 2 | import { Permissions } from '../../@types'; 3 | 4 | export const setFileIdAction = createAction('SET_FIlE_ID'); 5 | export const setFileVersionIdAction = createAction('SET_FILE_VERSION_ID'); 6 | export const setPermissionsAction = createAction('SET_PERMISSIONS'); 7 | export const setRotationAction = createAction('SET_ROTATION'); 8 | export const setScaleAction = createAction('SET_SCALE'); 9 | -------------------------------------------------------------------------------- /src/store/options/index.ts: -------------------------------------------------------------------------------- 1 | export { default as optionsReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/options/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { OptionsState } from './types'; 3 | import { 4 | setFileIdAction, 5 | setFileVersionIdAction, 6 | setPermissionsAction, 7 | setRotationAction, 8 | setScaleAction, 9 | } from './actions'; 10 | 11 | export const initialState = { 12 | features: {}, 13 | fileId: null, 14 | fileVersionId: null, 15 | isCurrentFileVersion: true, 16 | permissions: {}, 17 | rotation: 0, 18 | scale: 1, 19 | }; 20 | 21 | export default createReducer(initialState, builder => 22 | builder 23 | .addCase(setFileIdAction, (state, { payload }) => { 24 | state.fileId = payload; 25 | }) 26 | .addCase(setFileVersionIdAction, (state, { payload }) => { 27 | state.fileVersionId = payload; 28 | }) 29 | .addCase(setPermissionsAction, (state, { payload }) => { 30 | state.permissions = payload; 31 | }) 32 | .addCase(setRotationAction, (state, { payload }) => { 33 | state.rotation = payload; 34 | }) 35 | .addCase(setScaleAction, (state, { payload }) => { 36 | state.scale = payload; 37 | }), 38 | ); 39 | -------------------------------------------------------------------------------- /src/store/options/selectors.ts: -------------------------------------------------------------------------------- 1 | import getProp from 'lodash/get'; 2 | import { AppState } from '../types'; 3 | import { Permissions } from '../../@types'; 4 | import { Features } from '../../BoxAnnotations'; 5 | 6 | type State = Pick; 7 | 8 | export const getFeatures = (state: State): Features => state.options.features; 9 | export const getFileId = (state: State): string | null => state.options.fileId; 10 | export const getFileVersionId = (state: State): string | null => state.options.fileVersionId; 11 | export const getIsCurrentFileVersion = (state: State): boolean => state.options.isCurrentFileVersion; 12 | export const getPermissions = (state: State): Permissions => state.options.permissions; 13 | export const getRotation = (state: State): number => state.options.rotation; 14 | export const getScale = (state: State): number => state.options.scale; 15 | export const isFeatureEnabled = (state: State, featurename: string): boolean => 16 | getProp(getFeatures(state), featurename, false); 17 | -------------------------------------------------------------------------------- /src/store/options/types.ts: -------------------------------------------------------------------------------- 1 | import { Features } from '../../BoxAnnotations'; 2 | import { Permissions } from '../../@types'; 3 | 4 | export type OptionsState = { 5 | features: Features; 6 | fileId: string | null; 7 | fileVersionId: string | null; 8 | isCurrentFileVersion: boolean; 9 | permissions: Permissions; 10 | rotation: number; 11 | scale: number; 12 | }; 13 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { Action, ThunkDispatch } from '@reduxjs/toolkit'; 2 | import { Store } from 'redux'; 3 | import API from '../api'; 4 | import { AnnotationsState } from './annotations'; 5 | import { CommonState } from './common'; 6 | import { CreatorState } from './creator'; 7 | import { DrawingState } from './drawing'; 8 | import { HighlightState } from './highlight'; 9 | import { OptionsState } from './options'; 10 | import { UsersState } from './users'; 11 | 12 | export type AppState = { 13 | annotations: AnnotationsState; 14 | common: CommonState; 15 | creator: CreatorState; 16 | drawing: DrawingState; 17 | highlight: HighlightState; 18 | options: OptionsState; 19 | users: UsersState; 20 | }; 21 | 22 | export type AppStore = Store; 23 | export type AppThunkDispatch = ThunkDispatch; 24 | 25 | export type AppThunkExtra = { 26 | api: API; 27 | }; 28 | 29 | export type AppThunkAPI = { 30 | dispatch: AppThunkDispatch; 31 | extra: AppThunkExtra; 32 | requestId: string; 33 | signal: AbortSignal; 34 | state: AppState; 35 | }; 36 | -------------------------------------------------------------------------------- /src/store/users/__mocks__/usersState.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | collaborators: [], 3 | }; 4 | -------------------------------------------------------------------------------- /src/store/users/__tests__/reducer-test.ts: -------------------------------------------------------------------------------- 1 | import reducer from '../reducer'; 2 | import state from '../__mocks__/usersState'; 3 | import { fetchCollaboratorsAction } from '../actions'; 4 | 5 | describe('store/users/reducer', () => { 6 | describe('fetchCollaboratorsAction', () => { 7 | test('should set state when fulfilled', () => { 8 | const collaborators = [ 9 | { id: 'testid1', name: 'test1', item: { id: 'testid1', name: 'test1', type: 'user' as const } }, 10 | { id: 'testid2', name: 'test2', item: { id: 'testid2', name: 'test2', type: 'group' as const } }, 11 | ]; 12 | 13 | const newState = reducer( 14 | state, 15 | fetchCollaboratorsAction.fulfilled( 16 | { entries: collaborators, limit: 25, next_marker: null, previous_marker: null }, 17 | 'fulfilled', 18 | 'test', 19 | ), 20 | ); 21 | 22 | expect(newState.collaborators).toEqual(collaborators); 23 | }); 24 | 25 | test('should set state when rejected', () => { 26 | const newState = reducer(state, fetchCollaboratorsAction.rejected); 27 | 28 | expect(newState.collaborators).toEqual([]); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/store/users/__tests__/selectors-test.ts: -------------------------------------------------------------------------------- 1 | import usersState from '../__mocks__/usersState'; 2 | import { getCollaborators } from '../selectors'; 3 | 4 | describe('store/users/selectors', () => { 5 | const state = { users: usersState }; 6 | 7 | describe('getCollaborators', () => { 8 | test('should return the initial collaborators', () => { 9 | expect(getCollaborators(state)).toEqual([]); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/store/users/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { APICollection } from '../../api'; 3 | import { AppThunkAPI } from '../types'; 4 | import { Collaborator } from '../../@types'; 5 | import { getFileId } from '../options'; 6 | 7 | export const fetchCollaboratorsAction = createAsyncThunk, string, AppThunkAPI>( 8 | 'FETCH_COLLABORATORS', 9 | async (searchString = '', { extra, getState, signal }) => { 10 | // Create a new client for each request 11 | const client = extra.api.getCollaboratorsAPI(); 12 | const state = getState(); 13 | const fileId = getFileId(state); 14 | 15 | // Destroy the client if action's abort method is invoked 16 | signal.addEventListener('abort', () => { 17 | client.destroy(); 18 | }); 19 | 20 | // Wrap the client request in a promise to allow it to be returned and cancelled 21 | return new Promise>((resolve, reject) => { 22 | client.getFileCollaborators(fileId, resolve, reject, { 23 | filter_term: searchString, 24 | include_groups: false, 25 | include_uploader_collabs: false, 26 | }); 27 | }); 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /src/store/users/index.ts: -------------------------------------------------------------------------------- 1 | export { default as usersReducer } from './reducer'; 2 | export * from './actions'; 3 | export * from './selectors'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/store/users/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | import { fetchCollaboratorsAction } from './actions'; 3 | import { UsersState } from './types'; 4 | 5 | export const initialState = { 6 | collaborators: [], 7 | }; 8 | 9 | export default createReducer(initialState, builder => 10 | builder 11 | .addCase(fetchCollaboratorsAction.fulfilled, (state, { payload }) => { 12 | state.collaborators = payload.entries; 13 | }) 14 | .addCase(fetchCollaboratorsAction.rejected, state => { 15 | state.collaborators = []; 16 | }), 17 | ); 18 | -------------------------------------------------------------------------------- /src/store/users/selectors.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '../types'; 2 | import { Collaborator } from '../../@types'; 3 | 4 | type State = Pick; 5 | 6 | export const getCollaborators = (state: State): Collaborator[] => state.users.collaborators; 7 | -------------------------------------------------------------------------------- /src/store/users/types.ts: -------------------------------------------------------------------------------- 1 | import { Collaborator } from '../../@types'; 2 | 3 | export type UsersState = { 4 | collaborators: Collaborator[]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import annotationsLocaleData from 'box-annotations-locale-data'; 2 | import boxElementsMessages from 'box-elements-messages'; 3 | import { createIntl, createIntlCache, IntlShape } from 'react-intl'; 4 | import { IntlOptions } from '../@types'; 5 | 6 | declare const __LANGUAGE__: string; // eslint-disable-line no-underscore-dangle 7 | 8 | const getLocale = (language: string = __LANGUAGE__): string => { 9 | return language.substr(0, language.indexOf('-')); 10 | }; 11 | 12 | if (!window.Intl.PluralRules) { 13 | require('@formatjs/intl-pluralrules/polyfill'); // eslint-disable-line 14 | require(`react-intl-pluralrules-locale-data`); // eslint-disable-line 15 | } 16 | 17 | if (!window.Intl.RelativeTimeFormat) { 18 | require('@formatjs/intl-relativetimeformat/polyfill'); // eslint-disable-line 19 | require('react-intl-relativetimeformat-locale-data'); // eslint-disable-line 20 | } 21 | 22 | const annotationsMessages = { ...annotationsLocaleData, ...boxElementsMessages }; 23 | const intlCache = createIntlCache(); 24 | const createIntlProvider = ({ language, locale, messages = annotationsMessages }: IntlOptions = {}): IntlShape => { 25 | return createIntl( 26 | { 27 | messages, 28 | locale: locale || getLocale(language), 29 | }, 30 | intlCache, 31 | ); 32 | }; 33 | 34 | export default { createIntlProvider, getLocale }; 35 | -------------------------------------------------------------------------------- /src/utils/resin.ts: -------------------------------------------------------------------------------- 1 | export function stringify(value: unknown): string { 2 | if (typeof value === 'object' || typeof value === 'function' || typeof value === 'symbol') { 3 | return ''; 4 | } 5 | 6 | return typeof value === 'string' ? value : String(value); 7 | } 8 | 9 | export function applyResinTags(element: HTMLElement, attributes: Record): void { 10 | if (!element || !Object.values(attributes).length) { 11 | return; 12 | } 13 | 14 | Object.entries(attributes).forEach(([key, value]) => { 15 | const attribute = `data-resin-${key.toLowerCase()}`; 16 | const stringValue = stringify(value); 17 | 18 | if (stringValue) { 19 | element.setAttribute(attribute, stringValue); 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/util.ts: -------------------------------------------------------------------------------- 1 | export function checkValue(value: number): boolean { 2 | return value >= 0 && value <= 100; // Values cannot be negative or larger than 100% 3 | } 4 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | const stylelintrc = require.resolve('@box/frontend/stylelint/stylelint.config.js'); 2 | 3 | module.exports = { 4 | extends: [stylelintrc], 5 | rules: { 6 | 'no-descending-specificity': null, // fixme 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "cypress/globals": true 4 | }, 5 | "extends": ["plugin:cypress/recommended"], 6 | "plugins": ["cypress"] 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/index.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/integration/Sanity.e2e.test.js: -------------------------------------------------------------------------------- 1 | /// 2 | describe('Annotations', () => { 3 | beforeEach(() => { 4 | cy.visit('/'); 5 | }); 6 | 7 | it('should load annotations for a document file', () => { 8 | // Show the preview 9 | cy.showPreview(Cypress.env('FILE_ID_DOC_SANITY')); 10 | 11 | // Wait for annotations to load 12 | cy.get('.bp-doc').should('have.class', 'ba-annotations-loaded'); 13 | 14 | // Assert document content is present 15 | cy.contains('Chicken Chicken Chicken: Chicken Chicken'); 16 | 17 | // Assert that at least one annotation is present on the document 18 | cy.get('[data-testid^="ba-AnnotationTarget"]'); 19 | }); 20 | 21 | it('should load annotations for an image file', () => { 22 | // Show the preview 23 | cy.showPreview(Cypress.env('FILE_ID_IMAGE_SANITY')); 24 | 25 | // Wait for annotations to load 26 | cy.get('.bp-image').should('have.class', 'ba-annotations-loaded'); 27 | 28 | // Assert that at least one annotation is present on the image 29 | cy.get('[data-testid^="ba-AnnotationTarget"]'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | /* eslint-disable */ 15 | module.exports = (on, config) => { 16 | // `on` is used to hook into various events Cypress emits 17 | // `config` is the resolved Cypress config 18 | } 19 | /* eslint-enable */ 20 | -------------------------------------------------------------------------------- /test/styles.css: -------------------------------------------------------------------------------- 1 | .setters-container { 2 | display: flex; 3 | font-size: 75%; 4 | justify-content: space-around; 5 | padding: 20px; 6 | } 7 | 8 | .setters-container * { 9 | font-family: sans-serif; 10 | margin: 0; 11 | padding: 0; 12 | box-sizing: border-box; 13 | } 14 | 15 | .setters-container button, 16 | .setters-container input { 17 | padding: 5px; 18 | } 19 | 20 | .setters-container .container { 21 | flex: 1 1 50%; 22 | text-align: center; 23 | } 24 | 25 | .setters-container .container > input { 26 | text-align: center; 27 | } 28 | 29 | #preview-container { 30 | width: 100vw; 31 | height: 75vh; 32 | } 33 | -------------------------------------------------------------------------------- /test/support/constants.js: -------------------------------------------------------------------------------- 1 | Cypress.env({ 2 | FILE_ID_DOC_SANITY: '764564067308', // Read-only 3 | FILE_ID_IMAGE_SANITY: '694517831110', // Read-only 4 | }); 5 | -------------------------------------------------------------------------------- /test/support/defaults.js: -------------------------------------------------------------------------------- 1 | Cypress.Screenshot.defaults({ 2 | screenshotOnRunFailure: false, 3 | }); 4 | -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import './commands'; 17 | import './constants'; 18 | import './defaults'; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@box/frontend/ts/tsconfig", 3 | "include": ["./src/**/*.ts", "./src/**/*.tsx"], 4 | "compilerOptions": { 5 | "lib": ["DOM", "ES2020"], 6 | "jsx": "react-jsx", 7 | "esModuleInterop": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------