├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── checks.yml │ ├── codeql.yml │ ├── shipjs-manual-prepare.yml │ └── shipjs-trigger.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .nojekyll ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintignore ├── .stylelintrc.cjs ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── abstract ├── ActivityBlock.js ├── Block.js ├── CTX.js ├── LocaleManager.js ├── ModalManager.js ├── SecureUploadsManager.js ├── SolutionBlock.js ├── TypedCollection.js ├── TypedData.js ├── UploaderBlock.js ├── UploaderPublicApi.js ├── ValidationManager.js ├── a11y.js ├── buildOutputCollectionState.js ├── defineComponents.js ├── l10nProcessor.js ├── loadFileUploaderFrom.js ├── localeRegistry.js ├── sharedConfigKey.js ├── testModeProcessor.js └── uploadEntrySchema.js ├── babel.config.json ├── blocks ├── ActivityHeader │ ├── ActivityHeader.js │ └── activity-header.css ├── CameraSource │ ├── CameraSource.js │ ├── calcCameraModes.js │ ├── camera-source.css │ └── constants.js ├── CloudImageEditor │ ├── index.css │ ├── index.js │ └── src │ │ ├── CloudImageEditorBlock.js │ │ ├── CropFrame.js │ │ ├── EditorButtonControl.js │ │ ├── EditorCropButtonControl.js │ │ ├── EditorFilterControl.js │ │ ├── EditorImageCropper.js │ │ ├── EditorImageFader.js │ │ ├── EditorOperationControl.js │ │ ├── EditorScroller.js │ │ ├── EditorSlider.js │ │ ├── EditorToolbar.js │ │ ├── crop-utils.js │ │ ├── cropper-constants.js │ │ ├── css │ │ ├── common.css │ │ ├── icons.css │ │ └── index.css │ │ ├── elements │ │ ├── button │ │ │ ├── BtnUi.js │ │ │ └── test.htm │ │ ├── line-loader │ │ │ └── LineLoaderUi.js │ │ ├── presence-toggle │ │ │ └── PresenceToggle.js │ │ └── slider │ │ │ ├── SliderUi.js │ │ │ └── test.htm │ │ ├── icons │ │ ├── brightness.svg │ │ ├── closeMax.svg │ │ ├── contrast.svg │ │ ├── crop.svg │ │ ├── done.svg │ │ ├── edit-file.svg │ │ ├── enhance.svg │ │ ├── exposure.svg │ │ ├── filters.svg │ │ ├── flip.svg │ │ ├── gamma.svg │ │ ├── mirror.svg │ │ ├── original.svg │ │ ├── rotate.svg │ │ ├── sad.svg │ │ ├── saturation.svg │ │ ├── slider.svg │ │ ├── tuning.svg │ │ ├── vibrance.svg │ │ └── warmth.svg │ │ ├── index.js │ │ ├── lib │ │ ├── FocusVisible.js │ │ ├── applyFocusVisiblePolyfill.js │ │ ├── classNames.js │ │ ├── linspace.js │ │ ├── parseCropPreset.js │ │ ├── parseTabs.js │ │ ├── pick.js │ │ └── transformationUtils.js │ │ ├── state.js │ │ ├── svg-sprite.js │ │ ├── template.js │ │ ├── toolbar-constants.js │ │ ├── types.js │ │ └── util.js ├── CloudImageEditorActivity │ ├── CloudImageEditorActivity.js │ ├── index.css │ ├── ref.htm │ └── test.js ├── Config │ ├── Config.js │ ├── assertions.js │ ├── config.css │ ├── initialConfig.js │ ├── normalizeConfigValue.js │ ├── side-effects.js │ └── validatorsType.js ├── Copyright │ ├── Copyright.js │ └── copyright.css ├── DropArea │ ├── DropArea.js │ ├── addDropzone.js │ ├── drop-area.css │ └── getDropItems.js ├── ExternalSource │ ├── ExternalSource.js │ ├── MessageBridge.js │ ├── buildThemeDefinition.js │ ├── external-source.css │ ├── query-string.js │ └── types.js ├── FileItem │ ├── FileItem.js │ ├── FileItemConfig.js │ └── file-item.css ├── FormInput │ └── FormInput.js ├── Icon │ ├── Icon.js │ └── icon.css ├── Img │ ├── Img.js │ ├── ImgBase.js │ ├── ImgConfig.js │ ├── configurations.js │ ├── props-map.js │ ├── test.css │ └── utils │ │ └── parseObjectToString.js ├── Modal │ ├── Modal.js │ └── modal.css ├── ProgressBar │ ├── ProgressBar.js │ └── progress-bar.css ├── ProgressBarCommon │ ├── ProgressBarCommon.js │ └── progress-bar-common.css ├── Range │ ├── Range.js │ └── range.css ├── Select │ ├── Select.js │ └── select.css ├── SimpleBtn │ ├── SimpleBtn.js │ └── simple-btn.css ├── SourceBtn │ ├── SourceBtn.js │ └── source-btn.css ├── SourceList │ └── SourceList.js ├── Spinner │ ├── Spinner.js │ └── spinner.css ├── StartFrom │ ├── StartFrom.js │ └── start-from.css ├── Thumb │ ├── Thumb.js │ └── thumb.css ├── UploadCtxProvider │ ├── EventEmitter.js │ └── UploadCtxProvider.js ├── UploadList │ ├── UploadList.js │ └── upload-list.css ├── UrlSource │ ├── UrlSource.js │ └── url-source.css ├── svg-backgrounds │ └── svg-backgrounds.js ├── themes │ └── uc-basic │ │ ├── common.css │ │ ├── config.css │ │ ├── icons │ │ ├── about.svg │ │ ├── add.svg │ │ ├── arrow-down.svg │ │ ├── back.svg │ │ ├── badge-error.svg │ │ ├── badge-success.svg │ │ ├── box.svg │ │ ├── camera-full.svg │ │ ├── camera.svg │ │ ├── close.svg │ │ ├── collapse.svg │ │ ├── default.svg │ │ ├── dropbox.svg │ │ ├── edit-file.svg │ │ ├── error.svg │ │ ├── evernote.svg │ │ ├── expand.svg │ │ ├── external-source-placeholder.svg │ │ ├── facebook.svg │ │ ├── file.svg │ │ ├── flickr.svg │ │ ├── gdrive.svg │ │ ├── gphotos.svg │ │ ├── huddle.svg │ │ ├── info.svg │ │ ├── local.svg │ │ ├── microphone-mute.svg │ │ ├── microphone.svg │ │ ├── mobile-photo-camera.svg │ │ ├── mobile-video-camera.svg │ │ ├── onedrive.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── remove-file.svg │ │ ├── select.svg │ │ ├── square.svg │ │ ├── upload-error.svg │ │ ├── upload.svg │ │ ├── url.svg │ │ ├── video-camera-full.svg │ │ ├── video-camera.svg │ │ └── vk.svg │ │ ├── index.css │ │ ├── post-reset.css │ │ ├── svg-sprite.js │ │ └── theme.css └── utils │ ├── UploadSource.js │ ├── abilities.js │ ├── comma-separated.js │ ├── debounce.js │ ├── preloadImage.js │ ├── resizeImage.js │ ├── throttle.js │ └── userAgent.js ├── build-items.js ├── build-jsx-types.js ├── build-ssr-stubs.js ├── build-svg-sprite.js ├── build.js ├── demo ├── cloud-image-editor.html ├── custom-icons.html ├── form.html ├── index.html ├── locales.html ├── new-social-sources-test.html ├── preview-proxy │ ├── secure-delivery-proxy-url-resolver.html │ ├── secure-delivery-proxy-url-template.html │ └── secure-delivery-proxy.js ├── raw-inline.html ├── raw-minimal.html ├── raw-regular.html ├── secure-uploads.html ├── test.svg ├── upload-api.html └── validators.html ├── env.js ├── env.template.js ├── index.html ├── index.js ├── locales └── file-uploader │ ├── ar.js │ ├── az.js │ ├── ca.js │ ├── cs.js │ ├── da.js │ ├── de.js │ ├── el.js │ ├── en.js │ ├── es.js │ ├── et.js │ ├── fi.js │ ├── fr.js │ ├── he.js │ ├── hy.js │ ├── is.js │ ├── it.js │ ├── ja.js │ ├── ka.js │ ├── kk.js │ ├── ko.js │ ├── lv.js │ ├── nb.js │ ├── nl.js │ ├── pl.js │ ├── pt.js │ ├── ro.js │ ├── ru.js │ ├── sk.js │ ├── sr.js │ ├── sv.js │ ├── tr.js │ ├── uk.js │ ├── vi.js │ ├── zh-TW.js │ └── zh.js ├── package-lock.json ├── package.json ├── pull_request_template.md ├── ship.config.mjs ├── solutions ├── adaptive-image │ └── index.js ├── cloud-image-editor │ ├── CloudImageEditor.js │ ├── index.css │ └── index.js └── file-uploader │ ├── inline │ ├── FileUploaderInline.js │ ├── index.css │ └── index.js │ ├── minimal │ ├── FileUploaderMinimal.js │ ├── index.css │ └── index.js │ └── regular │ ├── FileUploaderRegular.js │ ├── index.css │ └── index.js ├── stylelint-force-app-name-prefix.cjs ├── test-locales.js ├── tests ├── __screenshots__ │ └── file-uploader-regular.e2e.test.tsx │ │ └── File-uploader-regular-Add-files-to-the-upload-list-from-camera-1.png ├── api.e2e.test.tsx ├── file-uploader-regular.e2e.test.tsx ├── fixtures │ └── test_image.jpeg └── utils │ ├── commands.ts │ └── test-renderer.tsx ├── tsconfig.json ├── tsconfig.types.json ├── types ├── events.d.ts ├── events.js ├── exported.d.ts ├── exported.js ├── global.d.ts ├── https.d.ts ├── index.d.ts ├── index.js ├── jsx.d.ts ├── jsx.js └── test │ ├── public-upload-api.test-d.tsx │ ├── uc-cloud-image-editor.test-d.tsx │ ├── uc-config.test-d.tsx │ ├── uc-form-input.test-d.tsx │ └── uc-upload-ctx-provider.test-d.tsx ├── utils ├── WindowHeightTracker.js ├── browser-info.js ├── browser-info.test.js ├── cdn-utils.js ├── cdn-utils.test.js ├── delay.js ├── fileTypes.js ├── fileTypes.test.js ├── getLocaleDirection.js ├── getPluralForm.js ├── getPluralForm.test.js ├── ifRef.js ├── isSecureTokenExpired.js ├── isSecureTokenExpired.test.js ├── memoize.js ├── memoize.test.js ├── mixinClass.js ├── parseCdnUrl.js ├── parseCdnUrl.test.js ├── parseShrink.js ├── parseShrink.test.js ├── prettyBytes.js ├── prettyBytes.test.js ├── stringToArray.js ├── stringToArray.test.js ├── template-utils.js ├── template-utils.test.js ├── toKebabCase.js ├── toKebabCase.test.js ├── transparentPixelSrc.js ├── uniqueArray.js ├── uniqueArray.test.js ├── validators │ ├── collection │ │ ├── index.js │ │ ├── validateCollectionUploadError.js │ │ └── validateMultiple.js │ └── file │ │ ├── index.js │ │ ├── validateFileType.js │ │ ├── validateIsImage.js │ │ ├── validateMaxSizeLimit.js │ │ └── validateUploadError.js ├── waitForAttribute.js ├── waitForAttribute.test.js ├── warnOnce.js ├── wildcardRegexp.js └── wildcardRegexp.test.js ├── vite.config.js └── vitest.workspace.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | web 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaVersion": 2020 6 | }, 7 | "extends": [ 8 | "eslint-config-prettier", 9 | "plugin:import/recommended" 10 | ], 11 | "plugins": [], 12 | "rules": { 13 | "import/no-cycle": 2, 14 | "import/no-unresolved": "off", 15 | "no-unused-vars": 1, 16 | "no-console": "off", 17 | "max-classes-per-file": "off", 18 | "prefer-const": "off", 19 | "no-param-reassign": "off", 20 | "guard-for-in": "off", 21 | "no-restricted-syntax": "off", 22 | "class-methods-use-this": "off", 23 | "dot-notation": "off", 24 | "no-plusplus": "off", 25 | "no-return-await": "off", 26 | "no-await-in-loop": "off", 27 | "one-var": "off", 28 | "default-case": "warn", 29 | "no-shadow": "warn", 30 | "no-prototype-builtins": "off", 31 | "prefer-template": "off", 32 | "lit/no-invalid-html": "off", 33 | "no-dupe-keys": "error", 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: checks 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 22 14 | cache: 'npm' 15 | - uses: actions/cache@v4 16 | id: playwright-cache 17 | with: 18 | path: | 19 | ~/.cache/ms-playwright 20 | key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }} 21 | - name: Install dependencies 22 | working-directory: ./ 23 | run: npm ci 24 | - name: Install playwright deps 25 | run: npm run playwright:install 26 | if: steps.playwright-cache.outputs.cache-hit != 'true' 27 | - name: Run lint 28 | run: npm run lint 29 | - name: Run test 30 | run: npm run test 31 | - name: Run build 32 | run: npm run build 33 | - name: Run tsc 34 | run: npm run clean:types && npm run tsc 35 | - name: Run build 36 | run: npm run build 37 | - name: Archive artifacts 38 | uses: actions/upload-artifact@v4 39 | if: always() 40 | with: 41 | name: e2e-tests 42 | path: | 43 | tests/__screenshots__/** 44 | tests/__coverage__/** 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main", "dev" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "29 13 * * 1" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/shipjs-manual-prepare.yml: -------------------------------------------------------------------------------- 1 | name: Ship js Prepare 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | prepare: 9 | runs-on: ubuntu-latest 10 | if: ${{ !startsWith(github.event.head_commit.message, format('chore{0} release', ':')) }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | ref: main 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '18' 19 | - run: npm ci 20 | - run: | 21 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 22 | git config --global user.name "github-actions[bot]" 23 | - run: npm run release -- --yes --no-browse 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }} 27 | -------------------------------------------------------------------------------- /.github/workflows/shipjs-trigger.yml: -------------------------------------------------------------------------------- 1 | name: Ship js trigger 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | jobs: 7 | build: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'releases/v') 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | ref: main 16 | - uses: actions/setup-node@v4 17 | with: 18 | registry-url: "https://registry.npmjs.org" 19 | node-version: '18' 20 | - run: npm ci 21 | - run: | 22 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 23 | git config --global user.name "github-actions[bot]" 24 | - run: npx shipjs trigger 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 28 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | TMP 4 | **/*.d.ts 5 | !types/** 6 | **/*.d.ts.map 7 | !global.d.ts 8 | ./index.ssr.* 9 | index.ssr.js 10 | web 11 | tests/__coverage__ 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ./node_modules/.bin/lint-staged 5 | npm run tsc 6 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,cjs}": ["eslint --fix", "prettier --write", "git add"], 3 | "*.css": ["stylelint --fix", "prettier --write --parser css", "git add"], 4 | "*.json": ["prettier --write --parser json", "git add"], 5 | "*.md": ["prettier --write --parser markdown", "git add"] 6 | } 7 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uploadcare/file-uploader/c2197aa84dc8addcc3a33baf8745390e954963d7/.nojekyll -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uploadcare/file-uploader/c2197aa84dc8addcc3a33baf8745390e954963d7/.npmignore -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | web 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "arrowParens": "always", 6 | "printWidth": 120, 7 | "overrides": [ 8 | { 9 | "files": "*.js", 10 | "options": { 11 | "parser": "babel", 12 | "plugins": ["prettier-plugin-jsdoc"] 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | web 2 | node_modules 3 | -------------------------------------------------------------------------------- /.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-standard'], 3 | plugins: ['stylelint-order', 'stylelint-declaration-block-no-ignored-properties'], 4 | rules: { 5 | 'hue-degree-notation': null, 6 | 'alpha-value-notation': null, 7 | 'plugin/declaration-block-no-ignored-properties': true, 8 | 'function-calc-no-unspaced-operator': true, // can cause out of memory in some cases 9 | 'keyframes-name-pattern': null, 10 | 'selector-class-pattern': null, 11 | 'custom-property-pattern': null, 12 | 'declaration-block-no-redundant-longhand-properties': null, 13 | 'custom-property-empty-line-before': null, 14 | 'length-zero-no-unit': null, 15 | 'no-descending-specificity': null, 16 | 'value-keyword-case': [ 17 | 'lower', 18 | { 19 | ignoreKeywords: ['currentColor'], 20 | }, 21 | ], 22 | 'color-function-notation': null, 23 | 'order/order': ['custom-properties', 'declarations'], 24 | 'order/properties-order': ['width', 'height'], 25 | 'rule-empty-line-before': null, 26 | 'at-rule-no-unknown': [ 27 | true, 28 | { 29 | ignoreAtRules: ['container'], 30 | }, 31 | ], 32 | 'property-no-unknown': [ 33 | true, 34 | { 35 | ignoreProperties: ['container-type', 'container-name'], 36 | }, 37 | ], 38 | 'media-feature-range-notation': null, 39 | }, 40 | overrides: [ 41 | { 42 | files: ['blocks/**/*.css', 'solutions/**/*.css'], 43 | ignoreFiles: ['**/test/**/*.css'], 44 | plugins: ['./stylelint-force-app-name-prefix.cjs'], 45 | }, 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for investing your time in contributing to this project! ❤️ 4 | 5 | ## ⚠️ When contributing to this repository, please first discuss the change you wish to make in [issue](https://github.com/uploadcare/blocks/issues) before making a change. 6 | 7 | - [Issue templates](./.github/ISSUE_TEMPLATE) 8 | - [PR template](./pull_request_template.md) 9 | 10 | Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Uploadcare (hello@uploadcare.com). All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /abstract/CTX.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Queue } from '@uploadcare/upload-client'; 3 | 4 | export const blockCtx = () => ({}); 5 | 6 | /** @param {import('./Block').Block} fnCtx */ 7 | export const activityBlockCtx = (fnCtx) => ({ 8 | ...blockCtx(), 9 | '*currentActivity': null, 10 | '*currentActivityParams': {}, 11 | 12 | '*history': [], 13 | '*historyBack': null, 14 | '*closeModal': () => { 15 | fnCtx.modalManager.close(fnCtx.$['*currentActivity']); 16 | 17 | fnCtx.set$({ 18 | '*currentActivity': null, 19 | }); 20 | }, 21 | }); 22 | 23 | /** @param {import('./Block').Block} fnCtx */ 24 | export const uploaderBlockCtx = (fnCtx) => ({ 25 | ...activityBlockCtx(fnCtx), 26 | '*commonProgress': 0, 27 | '*uploadList': [], 28 | '*uploadQueue': new Queue(1), 29 | /** @type {ReturnType[]} */ 30 | '*collectionErrors': [], 31 | /** @type {import('../types').OutputCollectionState | null} */ 32 | '*collectionState': null, 33 | /** @type {import('@uploadcare/upload-client').UploadcareGroup | null} */ 34 | '*groupInfo': null, 35 | /** @type {Set} */ 36 | '*uploadTrigger': new Set(), 37 | /** @type {import('./SecureUploadsManager.js').SecureUploadsManager | null} */ 38 | '*secureUploadsManager': null, 39 | }); 40 | -------------------------------------------------------------------------------- /abstract/SolutionBlock.js: -------------------------------------------------------------------------------- 1 | import svgIconsSprite from '../blocks/themes/uc-basic/svg-sprite.js'; 2 | import { Block } from './Block.js'; 3 | import { uploaderBlockCtx } from './CTX.js'; 4 | 5 | export class SolutionBlock extends Block { 6 | static styleAttrs = ['uc-wgt-common']; 7 | requireCtxName = true; 8 | init$ = uploaderBlockCtx(this); 9 | _template = null; 10 | 11 | initCallback() { 12 | super.initCallback(); 13 | this.a11y?.registerBlock(this); 14 | } 15 | 16 | static set template(value) { 17 | this._template = svgIconsSprite + value + /** HTML */ ``; 18 | } 19 | 20 | static get template() { 21 | return this._template; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /abstract/defineComponents.js: -------------------------------------------------------------------------------- 1 | /** @param {Object} blockExports */ 2 | export function defineComponents(blockExports) { 3 | for (let blockName in blockExports) { 4 | let tagName = [...blockName].reduce((name, char) => { 5 | if (char.toUpperCase() === char) { 6 | char = '-' + char.toLowerCase(); 7 | } 8 | return (name += char); 9 | }, ''); 10 | if (tagName.startsWith('-')) { 11 | tagName = tagName.replace('-', ''); 12 | } 13 | 14 | if (!tagName.startsWith('uc-')) { 15 | tagName = 'uc-' + tagName; 16 | } 17 | if (blockExports[blockName].reg) { 18 | blockExports[blockName].reg(tagName); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /abstract/loadFileUploaderFrom.js: -------------------------------------------------------------------------------- 1 | import { defineComponents } from './defineComponents.js'; 2 | 3 | export const UC_WINDOW_KEY = 'UC'; 4 | 5 | /** 6 | * @param {String} url File Uploader pack url 7 | * @param {Boolean} [register] Register connected package, if it not registered yet 8 | * @returns {Promise} 9 | */ 10 | export async function loadFileUploaderFrom(url, register = false) { 11 | return new Promise((resolve, reject) => { 12 | if (typeof document !== 'object') { 13 | resolve(null); 14 | return; 15 | } 16 | if (typeof window === 'object' && window[UC_WINDOW_KEY]) { 17 | resolve(window[UC_WINDOW_KEY]); 18 | return; 19 | } 20 | let script = document.createElement('script'); 21 | script.async = true; 22 | script.src = url; 23 | script.onerror = () => { 24 | reject(); 25 | }; 26 | script.onload = () => { 27 | /** @type {import('../index.js')} */ 28 | let blocks = window[UC_WINDOW_KEY]; 29 | register && defineComponents(blocks); 30 | resolve(blocks); 31 | }; 32 | document.head.appendChild(script); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /abstract/localeRegistry.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { default as en } from '../locales/file-uploader/en.js'; 4 | 5 | /** @type {Map} */ 6 | const localeRegistry = new Map(); 7 | /** @type {Map} */ 8 | const localeResolvers = new Map(); 9 | 10 | /** @typedef {Record} LocaleDefinition */ 11 | /** @typedef {() => Promise} LocaleDefinitionResolver */ 12 | 13 | /** 14 | * @param {string} localeName 15 | * @param {LocaleDefinition} definition 16 | */ 17 | const defineLocaleSync = (localeName, definition) => { 18 | if (localeRegistry.has(localeName)) { 19 | console.log(`Locale ${localeName} is already defined. Overwriting...`); 20 | } 21 | 22 | localeRegistry.set(localeName, { ...en, ...definition }); 23 | }; 24 | 25 | /** 26 | * @param {string} localeName 27 | * @param {LocaleDefinitionResolver} definitionResolver 28 | */ 29 | const defineLocaleAsync = (localeName, definitionResolver) => { 30 | localeResolvers.set(localeName, definitionResolver); 31 | }; 32 | 33 | /** 34 | * @param {string} localeName 35 | * @param {LocaleDefinition | LocaleDefinitionResolver} definitionOrResolver 36 | */ 37 | export const defineLocale = (localeName, definitionOrResolver) => { 38 | if (typeof definitionOrResolver === 'function') { 39 | defineLocaleAsync(localeName, definitionOrResolver); 40 | } else { 41 | defineLocaleSync(localeName, definitionOrResolver); 42 | } 43 | }; 44 | 45 | /** 46 | * @param {string} localeName 47 | * @returns {Promise} 48 | */ 49 | export const resolveLocaleDefinition = async (localeName) => { 50 | if (!localeRegistry.has(localeName)) { 51 | if (!localeResolvers.has(localeName)) { 52 | throw new Error(`Locale ${localeName} is not defined`); 53 | } 54 | 55 | const definitionResolver = /** @type {LocaleDefinitionResolver} */ (localeResolvers.get(localeName)); 56 | const definition = await definitionResolver(); 57 | defineLocaleSync(localeName, definition); 58 | } 59 | 60 | return /** @type {LocaleDefinition} */ (localeRegistry.get(localeName)); 61 | }; 62 | 63 | defineLocale('en', en); 64 | -------------------------------------------------------------------------------- /abstract/sharedConfigKey.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @template {keyof import('../types').ConfigType} T 5 | * @param {T} key 6 | * @returns {`*cfg/${T}`} 7 | */ 8 | export const sharedConfigKey = (key) => `*cfg/${key}`; 9 | -------------------------------------------------------------------------------- /abstract/testModeProcessor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @template {import('./Block.js').Block} T 5 | * @param {DocumentFragment} fr 6 | * @param {T} fnCtx 7 | */ 8 | export function testModeProcessor(fr, fnCtx) { 9 | const elementsWithTestId = fr.querySelectorAll('[data-testid]'); 10 | if (elementsWithTestId.length === 0) { 11 | return; 12 | } 13 | const valuesPerElement = new WeakMap(); 14 | 15 | for (const el of elementsWithTestId) { 16 | const testIdValue = el.getAttribute('data-testid'); 17 | if (testIdValue) { 18 | valuesPerElement.set(el, testIdValue); 19 | } 20 | } 21 | 22 | fnCtx.subConfigValue('testMode', (testMode) => { 23 | if (!testMode) { 24 | for (const el of elementsWithTestId) { 25 | el.removeAttribute('data-testid'); 26 | } 27 | return; 28 | } 29 | 30 | const testIdPrefix = fnCtx.testId; 31 | for (const el of elementsWithTestId) { 32 | const testIdValue = valuesPerElement.get(el); 33 | if (!testIdValue) { 34 | continue; 35 | } 36 | el.setAttribute(`data-testid`, `${testIdPrefix}--${testIdValue}`); 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "17" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /blocks/ActivityHeader/ActivityHeader.js: -------------------------------------------------------------------------------- 1 | import { ActivityBlock } from '../../abstract/ActivityBlock.js'; 2 | 3 | export class ActivityHeader extends ActivityBlock {} 4 | -------------------------------------------------------------------------------- /blocks/ActivityHeader/activity-header.css: -------------------------------------------------------------------------------- 1 | uc-activity-header { 2 | display: flex; 3 | justify-content: space-between; 4 | gap: var(--uc-padding); 5 | padding: var(--uc-padding); 6 | color: var(--uc-foreground); 7 | font-weight: 500; 8 | font-size: 1em; 9 | } 10 | 11 | uc-activity-header > * { 12 | display: flex; 13 | align-items: center; 14 | } 15 | -------------------------------------------------------------------------------- /blocks/CameraSource/calcCameraModes.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { deserializeCsv } from '../utils/comma-separated.js'; 4 | import { CameraSourceTypes } from './constants.js'; 5 | 6 | export const calcCameraModes = (/** @type {import('../../types').ConfigType} } */ cfg) => { 7 | return { 8 | isVideoRecordingEnabled: deserializeCsv(cfg.cameraModes).includes(CameraSourceTypes.VIDEO), 9 | isPhotoEnabled: deserializeCsv(cfg.cameraModes).includes(CameraSourceTypes.PHOTO), 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /blocks/CameraSource/constants.js: -------------------------------------------------------------------------------- 1 | export const CameraSourceTypes = Object.freeze({ 2 | PHOTO: 'photo', 3 | VIDEO: 'video', 4 | }); 5 | 6 | export const CameraSourceEvents = Object.freeze({ 7 | IDLE: 'idle', 8 | SHOT: 'shot', 9 | 10 | PLAY: 'play', 11 | PAUSE: 'pause', 12 | RESUME: 'resume', 13 | STOP: 'stop', 14 | 15 | RETAKE: 'retake', 16 | ACCEPT: 'accept', 17 | }); 18 | 19 | /** @typedef {(typeof CameraSourceTypes)[keyof typeof CameraSourceTypes]} ModeCameraType */ 20 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/index.css: -------------------------------------------------------------------------------- 1 | @import url('./src/css/index.css'); 2 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/index.js: -------------------------------------------------------------------------------- 1 | export * from './src/index.js'; 2 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/EditorButtonControl.js: -------------------------------------------------------------------------------- 1 | import { classNames } from './lib/classNames.js'; 2 | import { Block } from '../../../abstract/Block.js'; 3 | 4 | export class EditorButtonControl extends Block { 5 | init$ = { 6 | ...this.init$, 7 | active: false, 8 | title: '', 9 | icon: '', 10 | 'on.click': null, 11 | 'title-prop': '', 12 | }; 13 | 14 | initCallback() { 15 | super.initCallback(); 16 | 17 | this._titleEl = this.ref['title-el']; 18 | this._iconEl = this.ref['icon-el']; 19 | 20 | this.sub('title', (title) => { 21 | let titleEl = this._titleEl; 22 | if (titleEl) { 23 | this._titleEl.style.display = title ? 'block' : 'none'; 24 | } 25 | }); 26 | 27 | this.sub('active', (active) => { 28 | this.className = classNames({ 29 | 'uc-active': active, 30 | 'uc-not_active': !active, 31 | }); 32 | }); 33 | 34 | this.sub('on.click', (onClick) => { 35 | this.onclick = onClick; 36 | }); 37 | } 38 | } 39 | 40 | EditorButtonControl.template = /* HTML */ ` 41 | 45 | `; 46 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/EditorCropButtonControl.js: -------------------------------------------------------------------------------- 1 | import { EditorButtonControl } from './EditorButtonControl.js'; 2 | 3 | function nextAngle(prev) { 4 | let angle = prev + 90; 5 | angle = angle >= 360 ? 0 : angle; 6 | return angle; 7 | } 8 | 9 | function nextValue(operation, prev) { 10 | if (operation === 'rotate') { 11 | return nextAngle(prev); 12 | } 13 | if (['mirror', 'flip'].includes(operation)) { 14 | return !prev; 15 | } 16 | return null; 17 | } 18 | 19 | export class EditorCropButtonControl extends EditorButtonControl { 20 | initCallback() { 21 | super.initCallback(); 22 | 23 | this.defineAccessor('operation', (operation) => { 24 | if (!operation) { 25 | return; 26 | } 27 | 28 | /** @private */ 29 | this._operation = operation; 30 | this.$['icon'] = operation; 31 | this.bindL10n('title-prop', () => 32 | this.l10n('a11y-cloud-editor-apply-crop', { 33 | name: this.l10n(operation).toLowerCase(), 34 | }), 35 | ); 36 | }); 37 | 38 | this.$['on.click'] = () => { 39 | let prev = this.$['*cropperEl'].getValue(this._operation); 40 | let next = nextValue(this._operation, prev); 41 | this.$['*cropperEl'].setValue(this._operation, next); 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/EditorOperationControl.js: -------------------------------------------------------------------------------- 1 | import { EditorButtonControl } from './EditorButtonControl.js'; 2 | import { COLOR_OPERATIONS_CONFIG } from './toolbar-constants.js'; 3 | 4 | export class EditorOperationControl extends EditorButtonControl { 5 | /** 6 | * @private 7 | * @type {String} 8 | */ 9 | _operation = ''; 10 | 11 | initCallback() { 12 | super.initCallback(); 13 | 14 | this.$['on.click'] = (e) => { 15 | this.$['*sliderEl'].setOperation(this._operation); 16 | this.$['*showSlider'] = true; 17 | this.$['*currentOperation'] = this._operation; 18 | }; 19 | 20 | this.defineAccessor('operation', (operation) => { 21 | if (operation) { 22 | this._operation = operation; 23 | this.$['icon'] = operation; 24 | this.bindL10n('title-prop', () => 25 | this.l10n('a11y-cloud-editor-apply-tuning', { 26 | name: this.l10n(operation).toLowerCase(), 27 | }), 28 | ); 29 | this.bindL10n('title', () => this.l10n(operation)); 30 | } 31 | }); 32 | 33 | this.sub('*editorTransformations', (editorTransformations) => { 34 | if (!this._operation) { 35 | return; 36 | } 37 | 38 | let { zero } = COLOR_OPERATIONS_CONFIG[this._operation]; 39 | let value = editorTransformations[this._operation]; 40 | let isActive = typeof value !== 'undefined' ? value !== zero : false; 41 | this.$.active = isActive; 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/EditorScroller.js: -------------------------------------------------------------------------------- 1 | import { Block } from '../../../abstract/Block.js'; 2 | 3 | const X_THRESHOLD = 1; 4 | 5 | export class EditorScroller extends Block { 6 | initCallback() { 7 | super.initCallback(); 8 | 9 | this.addEventListener( 10 | 'wheel', 11 | (e) => { 12 | e.preventDefault(); 13 | 14 | let { deltaY, deltaX } = e; 15 | if (Math.abs(deltaX) > X_THRESHOLD) { 16 | this.scrollLeft += deltaX; 17 | } else { 18 | this.scrollLeft += deltaY; 19 | } 20 | }, 21 | { 22 | passive: false, 23 | }, 24 | ); 25 | 26 | // This fixes some strange bug on MacOS - wheel event doesn't fire for physical mouse wheel if no scroll event attached also 27 | this.addEventListener('scroll', () => {}, { 28 | passive: true, 29 | }); 30 | } 31 | } 32 | 33 | EditorScroller.template = /* HTML */ ` `; 34 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/cropper-constants.js: -------------------------------------------------------------------------------- 1 | export const CROP_PADDING = 20; 2 | export const THUMB_CORNER_SIZE = 24; 3 | export const THUMB_SIDE_SIZE = 34; 4 | export const THUMB_STROKE_WIDTH = 3; 5 | export const THUMB_OFFSET = THUMB_STROKE_WIDTH / 2; 6 | 7 | export const GUIDE_STROKE_WIDTH = 1; 8 | export const GUIDE_THIRD = 100 / 3; 9 | export const MIN_CROP_SIZE = 1; 10 | export const MAX_INTERACTION_SIZE = 24; 11 | export const MIN_INTERACTION_SIZE = 6; 12 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/css/icons.css: -------------------------------------------------------------------------------- 1 | :where([uc-cloud-image-editor]) uc-icon { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | :where([uc-cloud-image-editor]) uc-icon svg { 10 | width: calc(var(--uc-button-size) / 2); 11 | height: calc(var(--uc-button-size) / 2); 12 | } 13 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/css/index.css: -------------------------------------------------------------------------------- 1 | @import url('common.css'); 2 | @import url('icons.css'); 3 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/elements/button/test.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ButtonUi test 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/elements/line-loader/LineLoaderUi.js: -------------------------------------------------------------------------------- 1 | import { Block } from '../../../../../abstract/Block.js'; 2 | 3 | export class LineLoaderUi extends Block { 4 | constructor() { 5 | super(); 6 | 7 | this._active = false; 8 | 9 | this._handleTransitionEndRight = () => { 10 | let lineEl = this.ref['line-el']; 11 | lineEl.style.transition = `initial`; 12 | lineEl.style.opacity = '0'; 13 | lineEl.style.transform = `translateX(-101%)`; 14 | this._active && this._start(); 15 | }; 16 | } 17 | 18 | initCallback() { 19 | super.initCallback(); 20 | this.defineAccessor('active', (active) => { 21 | if (typeof active === 'boolean') { 22 | if (active) { 23 | this._start(); 24 | } else { 25 | this._stop(); 26 | } 27 | } 28 | }); 29 | } 30 | 31 | _start() { 32 | this._active = true; 33 | let { width } = this.getBoundingClientRect(); 34 | let lineEl = this.ref['line-el']; 35 | lineEl.style.transition = `transform 1s`; 36 | lineEl.style.opacity = '1'; 37 | lineEl.style.transform = `translateX(${width}px)`; 38 | lineEl.addEventListener('transitionend', this._handleTransitionEndRight, { 39 | once: true, 40 | }); 41 | } 42 | 43 | _stop() { 44 | this._active = false; 45 | } 46 | } 47 | 48 | LineLoaderUi.template = /* HTML */ ` 49 |
50 |
51 |
52 | `; 53 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/elements/presence-toggle/PresenceToggle.js: -------------------------------------------------------------------------------- 1 | import { applyClassNames } from '../../lib/classNames.js'; 2 | import { Block } from '../../../../../abstract/Block.js'; 3 | 4 | /** 5 | * @typedef {Object} Style 6 | * @property {String} [transition] 7 | * @property {String} [visible] 8 | * @property {String} [hidden] 9 | */ 10 | 11 | /** @type {Style} */ 12 | const DEFAULT_STYLE = { 13 | transition: 'uc-transition', 14 | visible: 'uc-visible', 15 | hidden: 'uc-hidden', 16 | }; 17 | 18 | export class PresenceToggle extends Block { 19 | constructor() { 20 | super(); 21 | 22 | this._visible = false; 23 | this._visibleStyle = DEFAULT_STYLE.visible; 24 | this._hiddenStyle = DEFAULT_STYLE.hidden; 25 | this._externalTransitions = false; 26 | 27 | this.defineAccessor('styles', (styles) => { 28 | if (!styles) { 29 | return; 30 | } 31 | this._externalTransitions = true; 32 | this._visibleStyle = styles.visible; 33 | this._hiddenStyle = styles.hidden; 34 | }); 35 | 36 | this.defineAccessor('visible', (visible) => { 37 | if (typeof visible !== 'boolean') { 38 | return; 39 | } 40 | 41 | this._visible = visible; 42 | this._handleVisible(); 43 | }); 44 | } 45 | 46 | _handleVisible() { 47 | this.style.visibility = this._visible ? 'inherit' : 'hidden'; 48 | applyClassNames(this, { 49 | [DEFAULT_STYLE.transition]: !this._externalTransitions, 50 | [this._visibleStyle]: this._visible, 51 | [this._hiddenStyle]: !this._visible, 52 | }); 53 | this.setAttribute('aria-hidden', this._visible ? 'false' : 'true'); 54 | } 55 | 56 | initCallback() { 57 | super.initCallback(); 58 | 59 | this.classList.toggle('uc-initial', true); 60 | 61 | if (!this._externalTransitions) { 62 | this.classList.add(DEFAULT_STYLE.transition); 63 | } 64 | 65 | this._handleVisible(); 66 | setTimeout(() => { 67 | this.classList.toggle('uc-initial', false); 68 | }, 0); 69 | } 70 | } 71 | PresenceToggle.template = /* HTML */ ` `; 72 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/elements/slider/test.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SliderUi 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/brightness.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/closeMax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/contrast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/crop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/done.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/edit-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/enhance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/exposure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/filters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/flip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/gamma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/mirror.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/original.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/rotate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/sad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/saturation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/slider.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/tuning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/icons/warmth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/index.js: -------------------------------------------------------------------------------- 1 | export { CloudImageEditorBlock } from './CloudImageEditorBlock.js'; 2 | export { CropFrame } from './CropFrame.js'; 3 | export { EditorCropButtonControl } from './EditorCropButtonControl.js'; 4 | export { EditorFilterControl } from './EditorFilterControl.js'; 5 | export { EditorOperationControl } from './EditorOperationControl.js'; 6 | export { EditorImageCropper } from './EditorImageCropper.js'; 7 | export { EditorImageFader } from './EditorImageFader.js'; 8 | export { EditorScroller } from './EditorScroller.js'; 9 | export { EditorSlider } from './EditorSlider.js'; 10 | export { EditorToolbar } from './EditorToolbar.js'; 11 | export { BtnUi } from './elements/button/BtnUi.js'; 12 | export { LineLoaderUi } from './elements/line-loader/LineLoaderUi.js'; 13 | export { PresenceToggle } from './elements/presence-toggle/PresenceToggle.js'; 14 | export { SliderUi } from './elements/slider/SliderUi.js'; 15 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/lib/FocusVisible.js: -------------------------------------------------------------------------------- 1 | import { applyFocusVisiblePolyfill } from './applyFocusVisiblePolyfill.js'; 2 | 3 | export class FocusVisible { 4 | /** 5 | * @param {boolean} focusVisible 6 | * @param {HTMLElement} element 7 | */ 8 | static handleFocusVisible(focusVisible, element) { 9 | if (focusVisible) { 10 | let customOutline = element.style.getPropertyValue('--focus-visible-outline'); 11 | element.style.outline = customOutline || '2px solid var(--color-focus-ring)'; 12 | } else { 13 | element.style.outline = 'none'; 14 | } 15 | } 16 | 17 | /** @param {ShadowRoot | Document} scope */ 18 | static register(scope) { 19 | FocusVisible._destructors.set(scope, applyFocusVisiblePolyfill(scope, FocusVisible.handleFocusVisible)); 20 | } 21 | 22 | /** @param {Document | ShadowRoot} scope */ 23 | static unregister(scope) { 24 | if (!FocusVisible._destructors.has(scope)) { 25 | return; 26 | } 27 | let removeFocusVisiblePolyfill = FocusVisible._destructors.get(scope); 28 | removeFocusVisiblePolyfill(); 29 | FocusVisible._destructors.delete(scope); 30 | } 31 | } 32 | 33 | FocusVisible._destructors = new WeakMap(); 34 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/lib/classNames.js: -------------------------------------------------------------------------------- 1 | function normalize(...args) { 2 | return args.reduce((result, arg) => { 3 | if (typeof arg === 'string') { 4 | result[arg] = true; 5 | return result; 6 | } 7 | 8 | for (let token of Object.keys(arg)) { 9 | result[token] = arg[token]; 10 | } 11 | 12 | return result; 13 | }, {}); 14 | } 15 | 16 | export function classNames(...args) { 17 | let mapping = normalize(...args); 18 | return Object.keys(mapping) 19 | .reduce((result, token) => { 20 | if (mapping[token]) { 21 | result.push(token); 22 | } 23 | 24 | return result; 25 | }, []) 26 | .join(' '); 27 | } 28 | 29 | export function applyClassNames(element, ...args) { 30 | let mapping = normalize(...args); 31 | for (let token of Object.keys(mapping)) { 32 | element.classList.toggle(token, mapping[token]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/lib/linspace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} a Start of sample (int) 3 | * @param {Number} b End of sample (int) 4 | * @param {Number} n Number of elements (int) 5 | * @returns {Number[]} 6 | */ 7 | export function linspace(a, b, n) { 8 | let ret = Array(n); 9 | n--; 10 | for (let i = n; i >= 0; i--) { 11 | ret[i] = Math.ceil((i * b + (n - i) * a) / n); 12 | } 13 | return ret; 14 | } 15 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/lib/parseCropPreset.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @param {import('../../../../types/exported.d.ts').ConfigType['cropPreset']} cropPreset */ 4 | export const parseCropPreset = (cropPreset) => { 5 | if (!cropPreset) return []; 6 | const [w, h] = cropPreset.split(':').map(Number); 7 | if (!Number.isFinite(w) || !Number.isFinite(h)) { 8 | console.error(`Invalid crop preset: ${cropPreset}`); 9 | return; 10 | } 11 | /** @type {import('../types.js').CropAspectRatio} */ 12 | const aspectRatio = { type: 'aspect-ratio', width: w, height: h }; 13 | return [aspectRatio]; 14 | }; 15 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/lib/parseTabs.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { deserializeCsv } from '../../../utils/comma-separated.js'; 4 | import { ALL_TABS } from '../toolbar-constants.js'; 5 | 6 | /** @param {string} tabs */ 7 | export const parseTabs = (tabs) => { 8 | if (!tabs) return ALL_TABS; 9 | const tabList = deserializeCsv(tabs).filter((tab) => ALL_TABS.includes(tab)); 10 | if (tabList.length === 0) { 11 | return ALL_TABS; 12 | } 13 | return tabList; 14 | }; 15 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/lib/pick.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {{}} obj 3 | * @param {String[]} keys 4 | * @returns {{}} 5 | */ 6 | export function pick(obj, keys) { 7 | let result = {}; 8 | for (let key of keys) { 9 | let value = obj[key]; 10 | if (obj.hasOwnProperty(key) || value !== undefined) { 11 | result[key] = value; 12 | } 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/template.js: -------------------------------------------------------------------------------- 1 | import { TRANSPARENT_PIXEL_SRC } from '../../../utils/transparentPixelSrc.js'; 2 | import svgIconsSprite from './svg-sprite.js'; 3 | 4 | export const TEMPLATE = /* HTML */ ` 5 | ${svgIconsSprite} 6 |
7 | 8 |
9 |
10 | 11 |
12 |
Network error
13 |
14 | 17 |
18 |
19 |
20 |
{{fileType}}
21 |
22 |
23 | 24 | 25 | 26 |
27 |
{{msg}}
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | `; 37 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mapping of loading resources per operation 3 | * 4 | * @typedef {Map>} LoadingOperations 5 | */ 6 | 7 | /** 8 | * Image size 9 | * 10 | * @typedef {{ width: Number; height: Number }} ImageSize 11 | */ 12 | 13 | /** 14 | * @typedef {Object} Rectangle 15 | * @property {Number} x 16 | * @property {Number} y 17 | * @property {Number} width 18 | * @property {Number} height 19 | */ 20 | 21 | /** 22 | * @typedef {{ 23 | * enhance?: number; 24 | * brightness?: number; 25 | * exposure?: number; 26 | * gamma?: number; 27 | * contrast?: number; 28 | * saturation?: number; 29 | * vibrance?: number; 30 | * warmth?: number; 31 | * rotate?: number; 32 | * mirror?: boolean; 33 | * flip?: boolean; 34 | * filter?: { name: string; amount: number }; 35 | * crop?: { dimensions: [number, number]; coords: [number, number] }; 36 | * }} Transformations 37 | */ 38 | 39 | /** 40 | * @typedef {Object} ApplyResult 41 | * @property {string} originalUrl 42 | * @property {string} cdnUrlModifiers 43 | * @property {string} cdnUrl 44 | * @property {Transformations} transformations 45 | */ 46 | 47 | /** 48 | * @typedef {Object} ChangeResult 49 | * @property {string} originalUrl 50 | * @property {string} cdnUrlModifiers 51 | * @property {string} cdnUrl 52 | * @property {Transformations} transformations 53 | */ 54 | 55 | /** @typedef {{ type: 'aspect-ratio'; width: number; height: number }} CropAspectRatio */ 56 | 57 | /** @typedef {CropAspectRatio[]} CropPresetList */ 58 | 59 | /** 60 | * @typedef {Partial<{ 61 | * [K in Direction]: { 62 | * direction: Direction; 63 | * pathNode: SVGElement; 64 | * interactionNode: SVGElement; 65 | * groupNode: SVGElement; 66 | * }; 67 | * }>} FrameThumbs 68 | */ 69 | 70 | /** @typedef {'' | 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'} Direction */ 71 | 72 | export {}; 73 | -------------------------------------------------------------------------------- /blocks/CloudImageEditor/src/util.js: -------------------------------------------------------------------------------- 1 | import { PACKAGE_NAME, PACKAGE_VERSION } from '../../../env.js'; 2 | import { createCdnUrl, createCdnUrlModifiers } from '../../../utils/cdn-utils.js'; 3 | import { COMMON_OPERATIONS, transformationsToOperations } from './lib/transformationUtils.js'; 4 | 5 | export function viewerImageSrc(originalUrl, width, transformations) { 6 | const MAX_CDN_DIMENSION = 3000; 7 | let dpr = window.devicePixelRatio; 8 | let size = Math.min(Math.ceil(width * dpr), MAX_CDN_DIMENSION); 9 | let quality = dpr >= 2 ? 'lightest' : 'normal'; 10 | 11 | return createCdnUrl( 12 | originalUrl, 13 | createCdnUrlModifiers( 14 | COMMON_OPERATIONS, 15 | transformationsToOperations(transformations), 16 | `quality/${quality}`, 17 | `stretch/off/-/resize/${size}x`, 18 | `@clib/${PACKAGE_NAME}/${PACKAGE_VERSION}/uc-cloud-image-editor/`, 19 | ), 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /blocks/CloudImageEditorActivity/index.css: -------------------------------------------------------------------------------- 1 | uc-cloud-image-editor-activity { 2 | position: relative; 3 | display: flex; 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | background-color: var(--uc-background); 8 | } 9 | 10 | [uc-modal] > dialog:has(uc-cloud-image-editor-activity[active]) { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /blocks/CloudImageEditorActivity/ref.htm: -------------------------------------------------------------------------------- 1 |

Cloud image editor

2 | 3 |

Load image by UUID

4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

Load image by CDN URL

21 | 22 | 23 | 31 | 32 | 33 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /blocks/CloudImageEditorActivity/test.js: -------------------------------------------------------------------------------- 1 | import { ifRef } from '../../utils/ifRef.js'; 2 | import * as blocks from '../../index.js'; 3 | 4 | ifRef(() => { 5 | blocks.defineComponents(blocks); 6 | document.querySelector(blocks.CloudImageEditorBlock.is)?.addEventListener('apply', (e) => { 7 | console.log(e); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /blocks/Config/assertions.js: -------------------------------------------------------------------------------- 1 | import { warnOnce } from '../../utils/warnOnce.js'; 2 | import { debounce } from '../utils/debounce.js'; 3 | 4 | const ASSERTIONS = [ 5 | { 6 | test: (cfg) => cfg.accept && !!cfg.imgOnly, 7 | message: 8 | 'There could be a mistake.\n' + 9 | 'Both `accept` and `imgOnly` parameters are set.\n' + 10 | 'The value of `accept` will be concatenated with the internal image mime types list.', 11 | }, 12 | { 13 | test: (cfg) => cfg.enableVideoRecording !== null, 14 | message: 15 | 'The `enableVideoRecording` parameter is deprecated and will be removed in the next major release.\n' + 16 | 'Please use the `cameraModes` parameter instead.', 17 | }, 18 | { 19 | test: (cfg) => cfg.defaultCameraMode !== null, 20 | message: 21 | 'The `defaultCameraMode` parameter is deprecated and will be removed in the next major release.\n' + 22 | 'Please use the `cameraModes` parameter instead.', 23 | }, 24 | ]; 25 | 26 | /** Runs on every config change and warns about potential issues. */ 27 | export const runAssertions = debounce( 28 | /** @param {import('../../types').ConfigType} cfg */ 29 | (cfg) => { 30 | for (const { test, message } of ASSERTIONS) { 31 | if (test(cfg)) { 32 | warnOnce(message); 33 | } 34 | } 35 | }, 36 | 0, 37 | ); 38 | -------------------------------------------------------------------------------- /blocks/Config/config.css: -------------------------------------------------------------------------------- 1 | uc-config { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /blocks/Config/side-effects.js: -------------------------------------------------------------------------------- 1 | import { deserializeCsv, serializeCsv } from '../utils/comma-separated.js'; 2 | 3 | /** 4 | * @template {keyof import('../../types').ConfigType} T 5 | * @param {{ 6 | * key: T; 7 | * value: import('../../types').ConfigType[T]; 8 | * setValue: (key: T, value: import('../../types').ConfigType[T]) => void; 9 | * getValue: (key: T) => import('../../types').ConfigType[T]; 10 | * }} options 11 | */ 12 | export const runSideEffects = ({ key, value, setValue, getValue }) => { 13 | if (key === 'enableVideoRecording' && value !== null) { 14 | let cameraModes = deserializeCsv(getValue('cameraModes')); 15 | if (value && !cameraModes.includes('video')) { 16 | cameraModes = cameraModes.concat('video'); 17 | } else if (!value) { 18 | cameraModes = cameraModes.filter((mode) => mode !== 'video'); 19 | } 20 | setValue('cameraModes', serializeCsv(cameraModes)); 21 | } 22 | 23 | if (key === 'defaultCameraMode' && value !== null) { 24 | let cameraModes = deserializeCsv(getValue('cameraModes')); 25 | cameraModes = cameraModes.sort((a, b) => { 26 | if (a === value) return -1; 27 | if (b === value) return 1; 28 | return 0; 29 | }); 30 | setValue('cameraModes', serializeCsv(cameraModes)); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /blocks/Copyright/Copyright.js: -------------------------------------------------------------------------------- 1 | import { Block } from '../../abstract/Block.js'; 2 | 3 | export class Copyright extends Block { 4 | initCallback() { 5 | super.initCallback(); 6 | 7 | this.subConfigValue( 8 | 'removeCopyright', 9 | /** @param {number} value */ 10 | (value) => { 11 | this.toggleAttribute('hidden', !!value); 12 | }, 13 | ); 14 | } 15 | 16 | static template = /* HTML */ ` 17 | Powered by Uploadcare 23 | `; 24 | } 25 | -------------------------------------------------------------------------------- /blocks/Copyright/copyright.css: -------------------------------------------------------------------------------- 1 | uc-copyright { 2 | display: flex; 3 | width: 100%; 4 | justify-content: center; 5 | } 6 | 7 | uc-copyright .uc-credits { 8 | all: unset; 9 | position: absolute; 10 | bottom: 12px; 11 | background-color: var(--uc-background); 12 | padding: 2px 5px; 13 | border-radius: 6px; 14 | color: var(--uc-muted-foreground); 15 | font-weight: normal; 16 | font-size: 12px; 17 | opacity: 0.9; 18 | cursor: pointer; 19 | transition: 20 | opacity var(--uc-transition), 21 | background-color var(--uc-transition); 22 | } 23 | 24 | uc-copyright .uc-credits:focus-visible { 25 | outline: 1px auto Highlight; 26 | outline: 1px auto -webkit-focus-ring-color; 27 | } 28 | 29 | uc-copyright .uc-credits:hover { 30 | opacity: 1; 31 | background-color: var(--uc-muted); 32 | } 33 | -------------------------------------------------------------------------------- /blocks/ExternalSource/MessageBridge.js: -------------------------------------------------------------------------------- 1 | /** @type {import('./types').InputMessageType[]} */ 2 | const MESSAGE_TYPE_WHITELIST = ['selected-files-change']; 3 | 4 | /** 5 | * @param {unknown} message 6 | * @returns {message is import("./types").InputMessageMap[import("./types").InputMessageType]} 7 | */ 8 | const isWhitelistedMessage = (message) => { 9 | if (!message) return false; 10 | if (typeof message !== 'object') return false; 11 | return ( 12 | 'type' in message && 13 | MESSAGE_TYPE_WHITELIST.includes(/** @type {import('./types').InputMessageType} */ (message.type)) 14 | ); 15 | }; 16 | 17 | export class MessageBridge { 18 | /** @type {Map>>} */ 19 | _handlerMap = new Map(); 20 | 21 | /** @type {Window} */ 22 | _context; 23 | 24 | /** @param {Window} context */ 25 | constructor(context) { 26 | this._context = context; 27 | 28 | window.addEventListener('message', this._handleMessage); 29 | } 30 | 31 | /** @param {MessageEvent} e */ 32 | _handleMessage = (e) => { 33 | if (e.source !== this._context) { 34 | return; 35 | } 36 | const message = e.data; 37 | if (!isWhitelistedMessage(message)) { 38 | return; 39 | } 40 | 41 | const handlers = this._handlerMap.get(message.type); 42 | if (handlers) { 43 | for (const handler of handlers) { 44 | handler(message); 45 | } 46 | } 47 | }; 48 | 49 | /** 50 | * @template {import('./types').InputMessageType} T 51 | * @param {T} type 52 | * @param {import('./types').InputMessageHandler} handler 53 | */ 54 | on(type, handler) { 55 | const handlers = this._handlerMap.get(type) ?? new Set(); 56 | if (!this._handlerMap.has(type)) { 57 | this._handlerMap.set(type, handlers); 58 | } 59 | 60 | handlers.add(/** @type {import('./types').InputMessageHandler} */ (handler)); 61 | } 62 | 63 | /** @param {import('./types').OutputMessage} message */ 64 | send(message) { 65 | this._context.postMessage(message, '*'); 66 | } 67 | 68 | destroy() { 69 | window.removeEventListener('message', this._handleMessage); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /blocks/ExternalSource/buildThemeDefinition.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {HTMLElement} element 5 | * @param {string} propName 6 | */ 7 | function getCssValue(element, propName) { 8 | let style = window.getComputedStyle(element); 9 | return style.getPropertyValue(propName).trim(); 10 | } 11 | 12 | const ucCustomProperties = /** @type {(keyof import('./types.js').ThemeDefinition)[]} */ ([ 13 | '--uc-font-family', 14 | '--uc-font-size', 15 | '--uc-line-height', 16 | '--uc-button-size', 17 | '--uc-preview-size', 18 | '--uc-input-size', 19 | '--uc-padding', 20 | '--uc-radius', 21 | '--uc-transition', 22 | '--uc-background', 23 | '--uc-foreground', 24 | '--uc-primary', 25 | '--uc-primary-hover', 26 | '--uc-primary-transparent', 27 | '--uc-primary-foreground', 28 | '--uc-secondary', 29 | '--uc-secondary-hover', 30 | '--uc-secondary-foreground', 31 | '--uc-muted', 32 | '--uc-muted-foreground', 33 | '--uc-destructive', 34 | '--uc-destructive-foreground', 35 | '--uc-border', 36 | ]); 37 | 38 | /** @param {HTMLElement} element */ 39 | export function buildThemeDefinition(element) { 40 | return ucCustomProperties.reduce((acc, prop) => { 41 | const value = getCssValue(element, prop); 42 | if (value) { 43 | acc[prop] = value; 44 | } 45 | return acc; 46 | }, /** @type {Record} */ ({})); 47 | } 48 | -------------------------------------------------------------------------------- /blocks/ExternalSource/external-source.css: -------------------------------------------------------------------------------- 1 | uc-external-source { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100%; 6 | background-color: var(--uc-background); 7 | overflow: hidden; 8 | position: relative; 9 | } 10 | 11 | [uc-modal] > dialog:has(uc-external-source[active]) { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | 16 | uc-external-source > .uc-content { 17 | position: relative; 18 | display: grid; 19 | flex: 1; 20 | grid-template-rows: 1fr min-content; 21 | } 22 | 23 | uc-external-source iframe { 24 | display: block; 25 | width: 100%; 26 | height: 100%; 27 | border: none; 28 | } 29 | 30 | uc-external-source .uc-iframe-wrapper { 31 | overflow: hidden; 32 | } 33 | 34 | uc-external-source .uc-toolbar { 35 | display: flex; 36 | width: 100%; 37 | grid-gap: var(--uc-padding); 38 | align-items: center; 39 | justify-content: space-between; 40 | padding: var(--uc-padding); 41 | border-top: 1px solid var(--uc-border); 42 | } 43 | 44 | uc-external-source .uc-back-btn { 45 | padding-left: 0; 46 | } 47 | 48 | uc-external-source .uc-selection-status-box { 49 | color: var(--uc-foreground); 50 | display: flex; 51 | flex-direction: column; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | 56 | uc-external-source .uc-selection-status-box button { 57 | color: var(--uc-primary); 58 | height: auto; 59 | padding: 0; 60 | background: none; 61 | } 62 | 63 | uc-external-source .uc-selection-status-box button:hover { 64 | text-decoration: underline; 65 | } 66 | 67 | uc-external-source uc-activity-header { 68 | position: absolute; 69 | width: 100%; 70 | justify-content: flex-end; 71 | z-index: 1; 72 | left: 0; 73 | top: 0; 74 | right: 0; 75 | pointer-events: none; 76 | } 77 | 78 | uc-external-source uc-activity-header .uc-close-btn { 79 | pointer-events: auto; 80 | } 81 | 82 | uc-external-source .uc-done-btn > span.uc-hidden { 83 | visibility: hidden; 84 | pointer-events: none; 85 | } 86 | 87 | uc-external-source .uc-done-btn > uc-spinner { 88 | position: absolute; 89 | width: 100%; 90 | height: 100%; 91 | display: flex; 92 | align-items: center; 93 | justify-content: center; 94 | } 95 | -------------------------------------------------------------------------------- /blocks/ExternalSource/query-string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {{ [key: string]: string | number | boolean | null | undefined }} params 3 | * @returns {string} 4 | */ 5 | export function queryString(params) { 6 | let list = []; 7 | for (let [key, value] of Object.entries(params)) { 8 | if (value === undefined || value === null || (typeof value === 'string' && value.length === 0)) { 9 | continue; 10 | } 11 | list.push(`${key}=${encodeURIComponent(value)}`); 12 | } 13 | return list.join('&'); 14 | } 15 | -------------------------------------------------------------------------------- /blocks/FileItem/FileItemConfig.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | import { UploaderBlock } from '../../abstract/UploaderBlock.js'; 4 | 5 | export class FileItemConfig extends UploaderBlock { 6 | /** @protected */ 7 | _entrySubs = new Set(); 8 | 9 | /** 10 | * @type {import('../../abstract/uploadEntrySchema.js').UploadEntryTypedData | null} 11 | * @protected 12 | */ 13 | _entry = null; 14 | 15 | /** 16 | * @template {any[]} A 17 | * @template {(entry: import('../../abstract/uploadEntrySchema.js').UploadEntryTypedData, ...args: A) => any} T 18 | * @param {T} fn 19 | * @returns {(...args: A) => ReturnType} 20 | * @protected 21 | */ 22 | _withEntry(fn) { 23 | const wrapperFn = /** @type {(...args: A) => ReturnType} */ ( 24 | (...args) => { 25 | const entry = this._entry; 26 | if (!entry) { 27 | console.warn('No entry found'); 28 | return; 29 | } 30 | return fn(entry, ...args); 31 | } 32 | ); 33 | return wrapperFn; 34 | } 35 | 36 | /** 37 | * @template {import('../../abstract/uploadEntrySchema.js').UploadEntryKeys} K 38 | * @param {K} prop_ 39 | * @param {(value: import('../../abstract/uploadEntrySchema.js').UploadEntryData[K]) => void} handler_ 40 | * @protected 41 | */ 42 | _subEntry = (prop_, handler_) => 43 | this._withEntry( 44 | /** 45 | * @template {import('../../abstract/uploadEntrySchema.js').UploadEntryKeys} K 46 | * @param {import('../../abstract/uploadEntrySchema.js').UploadEntryTypedData} entry 47 | * @param {K} prop 48 | * @param {(value: import('../../abstract/uploadEntrySchema.js').UploadEntryData[K]) => void} handler 49 | */ 50 | (entry, prop, handler) => { 51 | let sub = entry.subscribe(prop, (value) => { 52 | if (this.isConnected) { 53 | handler(value); 54 | } 55 | }); 56 | this._entrySubs.add(sub); 57 | }, 58 | )(prop_, handler_); 59 | 60 | /** @protected */ 61 | _reset() { 62 | for (let sub of this._entrySubs) { 63 | sub.remove(); 64 | } 65 | 66 | this._entrySubs = new Set(); 67 | this._entry = null; 68 | } 69 | 70 | disconnectedCallback() { 71 | super.disconnectedCallback(); 72 | this._entrySubs = new Set(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /blocks/Icon/Icon.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Block } from '../../abstract/Block.js'; 3 | 4 | export class Icon extends Block { 5 | constructor() { 6 | super(); 7 | 8 | this.init$ = { 9 | ...this.init$, 10 | name: '', 11 | href: '', 12 | }; 13 | } 14 | 15 | initCallback() { 16 | super.initCallback(); 17 | this.sub('name', (val) => { 18 | if (!val) { 19 | return; 20 | } 21 | let iconHref = `#uc-icon-${val}`; 22 | this.subConfigValue('iconHrefResolver', (iconHrefResolver) => { 23 | if (iconHrefResolver) { 24 | const customIconHref = iconHrefResolver(val); 25 | iconHref = customIconHref ?? iconHref; 26 | } 27 | this.$.href = iconHref; 28 | }); 29 | }); 30 | 31 | this.setAttribute('aria-hidden', 'true'); 32 | } 33 | } 34 | 35 | Icon.template = /* HTML */ ` 36 | 37 | 38 | 39 | `; 40 | 41 | Icon.bindAttributes({ 42 | name: 'name', 43 | }); 44 | -------------------------------------------------------------------------------- /blocks/Icon/icon.css: -------------------------------------------------------------------------------- 1 | uc-icon { 2 | display: inline-flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: var(--uc-button-size); 6 | height: var(--uc-button-size); 7 | } 8 | 9 | uc-icon svg { 10 | width: calc(var(--uc-button-size) / 2); 11 | height: calc(var(--uc-button-size) / 2); 12 | } 13 | -------------------------------------------------------------------------------- /blocks/Img/Img.js: -------------------------------------------------------------------------------- 1 | import { ImgBase } from './ImgBase.js'; 2 | 3 | export class Img extends ImgBase { 4 | initCallback() { 5 | super.initCallback(); 6 | 7 | this.sub$$('src', () => { 8 | this.init(); 9 | }); 10 | 11 | this.sub$$('uuid', () => { 12 | this.init(); 13 | }); 14 | 15 | this.sub$$('lazy', (val) => { 16 | if (!this.$$('is-background-for') && !this.$$('is-preview-blur')) { 17 | this.img.loading = val ? 'lazy' : 'eager'; 18 | } 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blocks/Img/configurations.js: -------------------------------------------------------------------------------- 1 | export const CSS_PREF = '--uc-img-'; 2 | export const UNRESOLVED_ATTR = 'unresolved'; 3 | export const HI_RES_K = 2; 4 | export const ULTRA_RES_K = 3; 5 | export const DEV_MODE = 6 | !window.location.host.trim() || window.location.host.includes(':') || window.location.hostname.includes('localhost'); 7 | 8 | export const MAX_WIDTH = 3000; 9 | export const MAX_WIDTH_JPG = 5000; 10 | 11 | export const ImgTypeEnum = Object.freeze({ 12 | PREVIEW: 'PREVIEW', 13 | MAIN: 'MAIN', 14 | }); 15 | -------------------------------------------------------------------------------- /blocks/Img/props-map.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_CDN_BASE = 'https://ucarecdn.com'; 2 | 3 | export const PROPS_MAP = Object.freeze({ 4 | 'dev-mode': {}, 5 | pubkey: {}, 6 | uuid: {}, 7 | src: {}, 8 | // alt: {}, 9 | // 'placeholder-src': {}, // available via CSS 10 | lazy: { 11 | default: 1, 12 | }, 13 | intersection: {}, 14 | breakpoints: { 15 | // '200, 300, 400' 16 | }, 17 | 'cdn-cname': { 18 | default: DEFAULT_CDN_BASE, 19 | }, 20 | 'proxy-cname': {}, 21 | 'secure-delivery-proxy': {}, 22 | 'hi-res-support': { 23 | default: 1, 24 | }, 25 | 'ultra-res-support': {}, // ? 26 | format: {}, 27 | 'cdn-operations': {}, 28 | progressive: {}, 29 | quality: {}, 30 | 'is-background-for': {}, 31 | 'is-preview-blur': { 32 | default: 1, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /blocks/Img/test.css: -------------------------------------------------------------------------------- 1 | uc-img { 2 | --uc-img-pubkey: '364c0864158c27472ffe'; 3 | --uc-img-test: 'TEST'; 4 | 5 | display: contents; 6 | } 7 | 8 | uc-img > img { 9 | transition: 1s; 10 | } 11 | 12 | uc-img > img[unresolved] { 13 | transform: scale(0.8); 14 | opacity: 0; 15 | transition: 1s; 16 | } 17 | -------------------------------------------------------------------------------- /blocks/Img/utils/parseObjectToString.js: -------------------------------------------------------------------------------- 1 | export const parseObjectToString = (params) => 2 | Object.entries(params) 3 | .filter(([key, value]) => value !== undefined && value !== '') 4 | .map(([key, value]) => { 5 | if (key === 'cdn-operations') { 6 | return value; 7 | } 8 | if (key === 'analytics') { 9 | return value; 10 | } 11 | 12 | return `${key}/${value}`; 13 | }); 14 | -------------------------------------------------------------------------------- /blocks/Modal/modal.css: -------------------------------------------------------------------------------- 1 | @supports selector(dialog::backdrop) { 2 | :where([uc-modal]) > dialog::backdrop { 3 | /* backdrop don't inherit theme properties */ 4 | background-color: oklch(0 0 0 / 0.1); 5 | } 6 | :where([uc-modal])[strokes] > dialog::backdrop { 7 | /* TODO: it's not working, fix it */ 8 | background-image: var(--modal-backdrop-background-image); 9 | } 10 | } 11 | 12 | :where([uc-modal]) > dialog[open] { 13 | transform: translateY(0px); 14 | visibility: visible; 15 | opacity: 1; 16 | } 17 | 18 | :where([uc-modal]) > dialog:not([open]) { 19 | transform: translateY(20px); 20 | visibility: hidden; 21 | opacity: 0; 22 | } 23 | 24 | :where([uc-modal]) > dialog { 25 | display: flex; 26 | flex-direction: column; 27 | width: min(var(--uc-dialog-width), 100%); 28 | max-width: min(calc(100% - var(--uc-padding) * 2), var(--uc-dialog-max-width)); 29 | min-height: var(--uc-button-size); 30 | max-height: min(calc(100% - var(--uc-padding) * 2), var(--uc-dialog-max-height)); 31 | margin: auto; 32 | padding: 0; 33 | overflow: hidden; 34 | background-color: var(--uc-background); 35 | border: 0; 36 | border-radius: calc(var(--uc-radius) * 1.75); 37 | box-shadow: var(--uc-dialog-shadow); 38 | transition: 39 | transform 0.4s ease, 40 | opacity 0.4s ease; 41 | } 42 | 43 | :where(.uc-contrast) :where([uc-modal]) > dialog { 44 | outline: 1px solid var(--uc-border); 45 | } 46 | -------------------------------------------------------------------------------- /blocks/ProgressBar/ProgressBar.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Block } from '../../abstract/Block.js'; 3 | 4 | export class ProgressBar extends Block { 5 | /** @type {Number} */ 6 | _value = 0; 7 | 8 | /** @type {Number} */ 9 | _prevValue = 0; 10 | 11 | /** @type {Boolean} */ 12 | _visible = true; 13 | 14 | constructor() { 15 | super(); 16 | this.init$ = { 17 | ...this.init$, 18 | width: 0, 19 | opacity: 0, 20 | }; 21 | } 22 | 23 | initCallback() { 24 | super.initCallback(); 25 | const handleFakeProgressAnimation = () => { 26 | const fakeProgressLine = this.ref.fakeProgressLine; 27 | if (!this._visible) { 28 | fakeProgressLine.classList.add('uc-fake-progress--hidden'); 29 | return; 30 | } 31 | if (this._value > 0) { 32 | fakeProgressLine.classList.add('uc-fake-progress--hidden'); 33 | } 34 | }; 35 | 36 | this.ref.fakeProgressLine.addEventListener('animationiteration', handleFakeProgressAnimation); 37 | 38 | this.defineAccessor( 39 | 'value', 40 | /** @param {number} value */ (value) => { 41 | if (value === undefined || value === null) return; 42 | this._prevValue = this._value; 43 | this._value = value; 44 | if (!this._visible) return; 45 | this.style.setProperty('--l-progress-value', this._value.toString()); 46 | }, 47 | ); 48 | 49 | this.defineAccessor( 50 | 'visible', 51 | /** @param {boolean} visible */ (visible) => { 52 | this._visible = visible; 53 | this.classList.toggle('uc-progress-bar--hidden', !visible); 54 | }, 55 | ); 56 | } 57 | } 58 | 59 | ProgressBar.template = /* HTML */ ` 60 |
61 |
62 | `; 63 | -------------------------------------------------------------------------------- /blocks/ProgressBar/progress-bar.css: -------------------------------------------------------------------------------- 1 | uc-progress-bar { 2 | --l-progress-value: 0; 3 | 4 | position: absolute; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | pointer-events: none; 12 | transition: opacity 0.3s; 13 | opacity: 1; 14 | } 15 | 16 | uc-progress-bar.uc-progress-bar--hidden { 17 | opacity: 0; 18 | } 19 | 20 | uc-progress-bar .uc-progress { 21 | position: absolute; 22 | width: calc(var(--l-progress-value) * 1%); 23 | height: 100%; 24 | background-color: var(--uc-primary); 25 | transform: translateX(0); 26 | opacity: 1; 27 | transition: 28 | width 0.6s, 29 | opacity 0.3s; 30 | } 31 | 32 | uc-progress-bar .uc-progress--hidden { 33 | opacity: 0; 34 | } 35 | 36 | uc-progress-bar .uc-fake-progress { 37 | --l-fake-progress-width: 30; 38 | 39 | position: absolute; 40 | width: calc(var(--l-fake-progress-width) * 1%); 41 | height: 100%; 42 | background-color: var(--uc-primary); 43 | animation: fake-progress-animation 1s ease-in-out infinite; 44 | opacity: 1; 45 | transition: opacity 0.3s; 46 | z-index: 1; 47 | } 48 | 49 | uc-progress-bar .uc-fake-progress--hidden { 50 | opacity: 0; 51 | animation: none; 52 | } 53 | 54 | @keyframes fake-progress-animation { 55 | from { 56 | transform: translateX(-100%); 57 | } 58 | 59 | to { 60 | transform: translateX(calc(100 / var(--l-fake-progress-width) * 100 * 1%)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /blocks/ProgressBarCommon/ProgressBarCommon.js: -------------------------------------------------------------------------------- 1 | import { UploaderBlock } from '../../abstract/UploaderBlock.js'; 2 | 3 | export class ProgressBarCommon extends UploaderBlock { 4 | init$ = { 5 | ...this.init$, 6 | visible: false, 7 | value: 0, 8 | 9 | '*commonProgress': 0, 10 | }; 11 | 12 | initCallback() { 13 | super.initCallback(); 14 | /** @private */ 15 | this._unobserveCollection = this.uploadCollection.observeProperties(() => { 16 | let anyUploading = this.uploadCollection.items().some((id) => { 17 | let item = this.uploadCollection.read(id); 18 | return item.getValue('isUploading'); 19 | }); 20 | 21 | this.$.visible = anyUploading; 22 | }); 23 | 24 | this.sub('visible', (visible) => { 25 | if (visible) { 26 | this.setAttribute('active', ''); 27 | } else { 28 | this.removeAttribute('active'); 29 | } 30 | }); 31 | 32 | this.sub('*commonProgress', (progress) => { 33 | this.$.value = progress; 34 | }); 35 | } 36 | 37 | destroyCallback() { 38 | super.destroyCallback(); 39 | 40 | this._unobserveCollection?.(); 41 | } 42 | } 43 | 44 | ProgressBarCommon.template = /* HTML */ ` `; 45 | -------------------------------------------------------------------------------- /blocks/ProgressBarCommon/progress-bar-common.css: -------------------------------------------------------------------------------- 1 | uc-progress-bar-common { 2 | position: fixed; 3 | right: 0; 4 | bottom: 0; 5 | left: 0; 6 | z-index: 10000; 7 | display: block; 8 | height: 10px; 9 | background-color: var(--uc-background); 10 | transition: opacity 0.3s; 11 | } 12 | 13 | uc-progress-bar-common:not([active]) { 14 | opacity: 0; 15 | pointer-events: none; 16 | } 17 | -------------------------------------------------------------------------------- /blocks/Range/Range.js: -------------------------------------------------------------------------------- 1 | import { BaseComponent } from '@symbiotejs/symbiote'; 2 | 3 | export class Range extends BaseComponent { 4 | init$ = { 5 | cssLeft: '50%', 6 | barActive: false, 7 | value: 50, 8 | onChange: (e) => { 9 | e.preventDefault(); 10 | e.stopPropagation(); 11 | this.$.value = parseFloat(this._range.value); 12 | this.dispatchEvent(new Event('change')); 13 | }, 14 | }; 15 | 16 | initCallback() { 17 | super.initCallback(); 18 | /** @type {HTMLInputElement} */ 19 | this._range = this.ref.range; 20 | [...this.attributes].forEach((attr) => { 21 | let exclude = ['style', 'ref']; 22 | if (!exclude.includes(attr.name)) { 23 | this.ref.range.setAttribute(attr.name, attr.value); 24 | } 25 | }); 26 | this.sub('value', (val) => { 27 | let pcnt = (val / 100) * 100; 28 | this.$.cssLeft = `${pcnt}%`; 29 | }); 30 | this.defineAccessor('value', (val) => { 31 | this.$.value = val; 32 | }); 33 | } 34 | } 35 | 36 | Range.template = /* HTML */ ` 37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | `; 45 | -------------------------------------------------------------------------------- /blocks/Range/range.css: -------------------------------------------------------------------------------- 1 | uc-range { 2 | position: relative; 3 | display: inline-flex; 4 | align-items: center; 5 | justify-content: center; 6 | height: var(--uc-button-size); 7 | } 8 | 9 | uc-range datalist { 10 | display: none; 11 | } 12 | 13 | uc-range input { 14 | width: 100%; 15 | height: 100%; 16 | opacity: 0; 17 | } 18 | 19 | uc-range .uc-track-wrapper { 20 | position: absolute; 21 | right: 10px; 22 | left: 10px; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | height: 2px; 27 | user-select: none; 28 | pointer-events: none; 29 | } 30 | 31 | uc-range .uc-track { 32 | position: absolute; 33 | right: 0; 34 | left: 0; 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | height: 2px; 39 | background-color: currentColor; 40 | border-radius: 2px; 41 | opacity: 0.5; 42 | } 43 | 44 | uc-range .uc-slider { 45 | position: absolute; 46 | width: 16px; 47 | height: 16px; 48 | background-color: currentColor; 49 | border-radius: 100%; 50 | transform: translateX(-50%); 51 | } 52 | 53 | uc-range .uc-bar { 54 | position: absolute; 55 | left: 0; 56 | height: 100%; 57 | background-color: currentColor; 58 | border-radius: 2px; 59 | } 60 | 61 | uc-range .uc-caption { 62 | position: absolute; 63 | display: inline-flex; 64 | justify-content: center; 65 | } 66 | -------------------------------------------------------------------------------- /blocks/Select/Select.js: -------------------------------------------------------------------------------- 1 | import { Block } from '../../abstract/Block.js'; 2 | 3 | export class Select extends Block { 4 | init$ = { 5 | ...this.init$, 6 | currentText: '', 7 | options: [], 8 | selectHtml: '', 9 | onSelect: (e) => { 10 | e.preventDefault(); 11 | e.stopPropagation(); 12 | this.value = this.ref.select.value; 13 | this.$.currentText = 14 | this.$.options.find((opt) => { 15 | return opt.value == this.value; 16 | })?.text || ''; 17 | this.dispatchEvent(new Event('change')); 18 | }, 19 | }; 20 | 21 | initCallback() { 22 | super.initCallback(); 23 | 24 | this.sub('options', (/** @type {{ text: String; value: String }[]} */ options) => { 25 | this.$.currentText = options?.[0]?.text || ''; 26 | let html = ''; 27 | options?.forEach((opt) => { 28 | html += /* HTML */ ``; 29 | }); 30 | this.$.selectHtml = html; 31 | }); 32 | } 33 | } 34 | 35 | Select.template = /* HTML */ ` `; 36 | -------------------------------------------------------------------------------- /blocks/Select/select.css: -------------------------------------------------------------------------------- 1 | uc-select { 2 | display: inline-flex; 3 | } 4 | 5 | uc-select select { 6 | position: relative; 7 | display: inline-flex; 8 | align-items: center; 9 | justify-content: center; 10 | height: var(--uc-button-size); 11 | padding: 0 14px; 12 | font-size: 1em; 13 | font-family: inherit; 14 | white-space: nowrap; 15 | border: none; 16 | border-radius: var(--uc-radius); 17 | cursor: pointer; 18 | user-select: none; 19 | transition: background-color var(--uc-transition); 20 | color: var(--uc-secondary-foreground); 21 | background-color: var(--uc-secondary); 22 | } 23 | -------------------------------------------------------------------------------- /blocks/SimpleBtn/SimpleBtn.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { UploaderBlock } from '../../abstract/UploaderBlock.js'; 3 | import { asBoolean } from '../Config/validatorsType.js'; 4 | 5 | export class SimpleBtn extends UploaderBlock { 6 | static styleAttrs = [...super.styleAttrs, 'uc-simple-btn']; 7 | couldBeCtxOwner = true; 8 | constructor() { 9 | super(); 10 | 11 | this.init$ = { 12 | ...this.init$, 13 | withDropZone: true, 14 | onClick: () => { 15 | this.api.initFlow(); 16 | }, 17 | 'button-text': '', 18 | }; 19 | } 20 | 21 | initCallback() { 22 | super.initCallback(); 23 | 24 | this.defineAccessor( 25 | 'dropzone', 26 | /** @param {unknown} val */ 27 | (val) => { 28 | if (typeof val === 'undefined') { 29 | return; 30 | } 31 | this.$.withDropZone = asBoolean(val); 32 | }, 33 | ); 34 | this.subConfigValue('multiple', (val) => { 35 | this.$['button-text'] = val ? 'upload-files' : 'upload-file'; 36 | }); 37 | } 38 | } 39 | 40 | SimpleBtn.template = /* HTML */ ` 41 | 42 | 48 | 49 | `; 50 | 51 | SimpleBtn.bindAttributes({ 52 | // @ts-expect-error TODO: we need to update symbiote types 53 | dropzone: null, 54 | }); 55 | -------------------------------------------------------------------------------- /blocks/SimpleBtn/simple-btn.css: -------------------------------------------------------------------------------- 1 | :where([uc-simple-btn]) { 2 | position: relative; 3 | display: inline-flex; 4 | } 5 | 6 | :where([uc-simple-btn]) button { 7 | height: auto; 8 | gap: 0.5em; 9 | padding: var(--uc-simple-btn-padding); 10 | background-color: var(--uc-simple-btn); 11 | color: var(--uc-simple-btn-foreground); 12 | font-size: var(--uc-simple-btn-font-size); 13 | font-family: var(--uc-simple-btn-font-family); 14 | } 15 | 16 | :where([uc-simple-btn]) button uc-icon { 17 | width: auto; 18 | height: auto; 19 | } 20 | 21 | :where([uc-simple-btn]) button uc-icon svg { 22 | width: 0.9em; 23 | height: 0.9em; 24 | } 25 | 26 | :where([uc-simple-btn]) button:hover { 27 | background-color: var(--uc-simple-btn-hover); 28 | } 29 | 30 | :where([uc-simple-btn]) > uc-drop-area { 31 | display: contents; 32 | } 33 | 34 | :where([uc-simple-btn]) .uc-visual-drop-area { 35 | position: absolute; 36 | top: 0px; 37 | left: 0px; 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | width: 100%; 42 | height: 100%; 43 | padding: var(--uc-simple-btn-padding); 44 | background-color: transparent; 45 | color: transparent; 46 | font-size: var(--uc-simple-btn-font-size); 47 | border: 1px dashed var(--uc-simple-btn-foreground); 48 | border-radius: inherit; 49 | opacity: 0; 50 | transition: opacity var(--uc-transition); 51 | } 52 | 53 | :where([uc-simple-btn]) > uc-drop-area[drag-state='active'] .uc-visual-drop-area { 54 | opacity: 1; 55 | } 56 | :where([uc-simple-btn]) > uc-drop-area[drag-state='inactive'] .uc-visual-drop-area { 57 | opacity: 0; 58 | } 59 | :where([uc-simple-btn]) > uc-drop-area[drag-state='near'] .uc-visual-drop-area { 60 | opacity: 1; 61 | } 62 | :where([uc-simple-btn]) > uc-drop-area[drag-state='over'] .uc-visual-drop-area { 63 | opacity: 1; 64 | } 65 | -------------------------------------------------------------------------------- /blocks/SourceBtn/source-btn.css: -------------------------------------------------------------------------------- 1 | uc-source-btn > button { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 2px; 5 | padding: 2px var(--uc-padding); 6 | color: var(--uc-foreground); 7 | border-radius: var(--uc-radius); 8 | cursor: pointer; 9 | transition: 10 | background-color var(--uc-transition), 11 | color var(--uc-transition); 12 | user-select: none; 13 | width: 100%; 14 | background-color: unset; 15 | height: unset; 16 | } 17 | 18 | uc-source-btn:last-child > button { 19 | margin-bottom: 0; 20 | } 21 | 22 | uc-source-btn > button:hover { 23 | background-color: var(--uc-primary-transparent); 24 | } 25 | 26 | :where(.uc-contrast) uc-source-btn > button:hover { 27 | background-color: var(--uc-secondary); 28 | color: var(--uc-foreground); 29 | } 30 | 31 | uc-source-btn uc-icon { 32 | display: inline-flex; 33 | flex-grow: 1; 34 | justify-content: center; 35 | min-width: var(--uc-button-size); 36 | margin-right: var(--uc-padding); 37 | opacity: 0.8; 38 | } 39 | 40 | :where(.uc-contrast) uc-source-btn uc-icon { 41 | opacity: 1; 42 | } 43 | 44 | uc-source-btn .uc-txt { 45 | display: flex; 46 | align-items: center; 47 | box-sizing: border-box; 48 | width: 100%; 49 | height: var(--uc-button-size); 50 | padding: 0; 51 | white-space: nowrap; 52 | border: none; 53 | } 54 | -------------------------------------------------------------------------------- /blocks/SourceList/SourceList.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Block } from '../../abstract/Block.js'; 3 | import { browserFeatures } from '../../utils/browser-info.js'; 4 | import { stringToArray } from '../../utils/stringToArray.js'; 5 | import { deserializeCsv } from '../utils/comma-separated.js'; 6 | 7 | export class SourceList extends Block { 8 | initCallback() { 9 | super.initCallback(); 10 | 11 | this.subConfigValue('sourceList', (/** @type {String} */ val) => { 12 | let list = stringToArray(val); 13 | let html = ''; 14 | 15 | list.forEach((srcName) => { 16 | if (srcName === 'instagram') { 17 | console.error( 18 | "Instagram source was removed because the Instagram Basic Display API hasn't been available since December 4, 2024. " + 19 | 'Official statement, see here:' + 20 | 'https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/?locale=en_US', 21 | ); 22 | return; 23 | } 24 | 25 | if (srcName === 'camera' && browserFeatures.htmlMediaCapture) { 26 | this.subConfigValue('cameraModes', (/** @type {String} */ val) => { 27 | const cameraModes = deserializeCsv(val); 28 | 29 | cameraModes.forEach((mode) => { 30 | html += /* HTML */ ``; 31 | }); 32 | 33 | if (cameraModes.length === 0) { 34 | html += /* HTML */ ``; 35 | } 36 | }); 37 | 38 | return; 39 | } 40 | 41 | html += /* HTML */ ``; 42 | }); 43 | 44 | if (this.cfg.sourceListWrap) { 45 | this.innerHTML = html; 46 | } else { 47 | this.outerHTML = html; 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /blocks/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import { BaseComponent } from '@symbiotejs/symbiote'; 2 | 3 | export class Spinner extends BaseComponent {} 4 | 5 | Spinner.template = /* HTML */ `
`; 6 | -------------------------------------------------------------------------------- /blocks/Spinner/spinner.css: -------------------------------------------------------------------------------- 1 | @keyframes uc-spinner-keyframes { 2 | from { 3 | transform: rotate(0deg); 4 | } 5 | to { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .uc-spinner { 11 | width: 1em; 12 | height: 1em; 13 | border: solid 2px transparent; 14 | border-top-color: currentColor; 15 | border-left-color: currentColor; 16 | border-radius: 50%; 17 | animation: uc-spinner-keyframes 400ms linear infinite; 18 | } 19 | -------------------------------------------------------------------------------- /blocks/StartFrom/StartFrom.js: -------------------------------------------------------------------------------- 1 | import { ActivityBlock } from '../../abstract/ActivityBlock.js'; 2 | 3 | export class StartFrom extends ActivityBlock { 4 | historyTracked = true; 5 | /** @type {import('../../abstract/ActivityBlock.js').ActivityType} */ 6 | activityType = ActivityBlock.activities.START_FROM; 7 | 8 | initCallback() { 9 | super.initCallback(); 10 | this.registerActivity(this.activityType); 11 | } 12 | } 13 | 14 | StartFrom.template = /* HTML */ `
`; 15 | -------------------------------------------------------------------------------- /blocks/StartFrom/start-from.css: -------------------------------------------------------------------------------- 1 | uc-start-from { 2 | display: block; 3 | overflow-y: auto; 4 | } 5 | 6 | uc-start-from .uc-content { 7 | display: grid; 8 | grid-auto-flow: row; 9 | gap: calc(var(--uc-padding) * 2); 10 | width: 100%; 11 | height: 100%; 12 | padding: calc(var(--uc-padding) * 2); 13 | background-color: var(--uc-background); 14 | } 15 | 16 | [uc-modal] > dialog:has(uc-start-from[active]) { 17 | width: var(--uc-dialog-width); 18 | } 19 | 20 | [uc-modal] uc-start-from uc-drop-area { 21 | border-radius: var(--uc-radius); 22 | } 23 | 24 | @media only screen and (max-width: 430px) { 25 | [uc-modal] uc-start-from uc-drop-area { 26 | display: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /blocks/Thumb/thumb.css: -------------------------------------------------------------------------------- 1 | uc-thumb { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /blocks/UploadCtxProvider/UploadCtxProvider.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { UploaderBlock } from '../../abstract/UploaderBlock.js'; 4 | import { EventType } from './EventEmitter.js'; 5 | class UploadCtxProviderClass extends UploaderBlock { 6 | requireCtxName = true; 7 | 8 | initCallback() { 9 | super.initCallback(); 10 | 11 | this.$['*eventEmitter'].bindTarget(this); 12 | } 13 | 14 | destroyCallback() { 15 | super.destroyCallback(); 16 | 17 | this.$['*eventEmitter'].unbindTarget(this); 18 | } 19 | } 20 | 21 | UploadCtxProviderClass.EventType = EventType; 22 | 23 | /** 24 | * @typedef {import('../../utils/mixinClass.js').MixinClass< 25 | * typeof UploadCtxProviderClass, 26 | * { 27 | * addEventListener< 28 | * T extends (typeof import('./EventEmitter.js').EventType)[keyof typeof import('./EventEmitter.js').EventType], 29 | * >( 30 | * type: T, 31 | * listener: (e: CustomEvent) => void, 32 | * options?: boolean | AddEventListenerOptions, 33 | * ): void; 34 | * removeEventListener< 35 | * T extends (typeof import('./EventEmitter.js').EventType)[keyof typeof import('./EventEmitter.js').EventType], 36 | * >( 37 | * type: T, 38 | * listener: (e: CustomEvent) => void, 39 | * options?: boolean | EventListenerOptions, 40 | * ): void; 41 | * } 42 | * >} UploadCtxProvider 43 | */ 44 | 45 | export const UploadCtxProvider = /** @type {UploadCtxProvider} */ (/** @type {unknown} */ (UploadCtxProviderClass)); 46 | -------------------------------------------------------------------------------- /blocks/UrlSource/url-source.css: -------------------------------------------------------------------------------- 1 | uc-url-source { 2 | display: block; 3 | background-color: var(--uc-background); 4 | } 5 | 6 | uc-url-source > .uc-content { 7 | display: grid; 8 | grid-gap: 4px; 9 | grid-template-columns: 1fr min-content; 10 | padding: var(--uc-padding); 11 | padding-top: 0; 12 | } 13 | 14 | uc-url-source .uc-url-input { 15 | display: flex; 16 | } 17 | -------------------------------------------------------------------------------- /blocks/svg-backgrounds/svg-backgrounds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {String} svg 3 | * @returns {String} 4 | */ 5 | function createSvgBlobUrl(svg) { 6 | let blob = new Blob([svg], { 7 | type: 'image/svg+xml', 8 | }); 9 | return URL.createObjectURL(blob); 10 | } 11 | 12 | /** 13 | * @param {String} [color1] 14 | * @param {String} [color2] 15 | * @returns {String} 16 | */ 17 | export function checkerboardCssBg(color1 = '#fff', color2 = 'rgba(0, 0, 0, .1)') { 18 | return createSvgBlobUrl(/*svg*/ ` 19 | 20 | 21 | 22 | `); 23 | } 24 | 25 | /** 26 | * @param {String} [color] 27 | * @returns {String} 28 | */ 29 | export function strokesCssBg(color = 'rgba(0, 0, 0, .1)') { 30 | return createSvgBlobUrl(/*svg*/ ` 31 | 32 | `); 33 | } 34 | 35 | /** 36 | * @param {String} [color] 37 | * @returns {String} 38 | */ 39 | export function fileCssBg(color = 'hsl(209, 21%, 65%)', width = 32, height = 32) { 40 | return createSvgBlobUrl(/*svg*/ ` 41 | 42 | 43 | 44 | `); 45 | } 46 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/config.css: -------------------------------------------------------------------------------- 1 | :where([uc-wgt-common]) { 2 | --cfg-init-activity: 'start-from'; 3 | --cfg-done-activity: ''; 4 | } 5 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/badge-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/badge-success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/camera-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/collapse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/dropbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/edit-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/external-source-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/flickr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/gdrive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/gphotos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/huddle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/local.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/microphone-mute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/mobile-photo-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/mobile-video-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/onedrive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/remove-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/upload-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/url.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/video-camera-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/video-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/icons/vk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/index.css: -------------------------------------------------------------------------------- 1 | /* BASE CONFIGURATION: */ 2 | @import url('config.css'); 3 | 4 | /* THEME */ 5 | @import url('theme.css'); 6 | 7 | /* COMMON STYLES */ 8 | @import url('common.css'); 9 | 10 | /* UI COMPONENTS: */ 11 | @import url('../../Icon/icon.css'); 12 | @import url('../../Range/range.css'); 13 | 14 | /* BLOCKS: */ 15 | @import url('../../Config/config.css'); 16 | @import url('../../SimpleBtn/simple-btn.css'); 17 | @import url('../../SourceBtn/source-btn.css'); 18 | @import url('../../DropArea/drop-area.css'); 19 | @import url('../../Modal/modal.css'); 20 | @import url('../../UrlSource/url-source.css'); 21 | @import url('../../CameraSource/camera-source.css'); 22 | @import url('../../ExternalSource/external-source.css'); 23 | @import url('../../UploadList/upload-list.css'); 24 | @import url('../../StartFrom/start-from.css'); 25 | @import url('../../FileItem/file-item.css'); 26 | @import url('../../ProgressBarCommon/progress-bar-common.css'); 27 | @import url('../../ProgressBar/progress-bar.css'); 28 | @import url('../../ActivityHeader/activity-header.css'); 29 | @import url('../../Copyright/copyright.css'); 30 | @import url('../../CloudImageEditor/index.css'); 31 | @import url('../../CloudImageEditorActivity/index.css'); 32 | @import url('../../Select/select.css'); 33 | @import url('../../Spinner/spinner.css'); 34 | @import url('../../Thumb/thumb.css'); 35 | 36 | /* POST RESET */ 37 | @import url('post-reset.css'); 38 | -------------------------------------------------------------------------------- /blocks/themes/uc-basic/post-reset.css: -------------------------------------------------------------------------------- 1 | :where([uc-wgt-common]) uc-source-btn[type] { 2 | all: unset; 3 | } 4 | -------------------------------------------------------------------------------- /blocks/utils/UploadSource.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | export const ExternalUploadSource = Object.freeze({ 3 | FACEBOOK: 'facebook', 4 | DROPBOX: 'dropbox', 5 | GDRIVE: 'gdrive', 6 | GPHOTOS: 'gphotos', 7 | FLICKR: 'flickr', 8 | VK: 'vk', 9 | EVERNOTE: 'evernote', 10 | BOX: 'box', 11 | ONEDRIVE: 'onedrive', 12 | HUDDLE: 'huddle', 13 | }); 14 | 15 | export const UploadSourceMobile = Object.freeze({ 16 | MOBILE_VIDEO_CAMERA: 'mobile-video-camera', 17 | MOBILE_PHOTO_CAMERA: 'mobile-photo-camera', 18 | }); 19 | 20 | export const UploadSource = Object.freeze({ 21 | LOCAL: 'local', 22 | DROP_AREA: 'drop-area', 23 | CAMERA: 'camera', 24 | EXTERNAL: 'external', 25 | API: 'js-api', 26 | URL: 'url', 27 | DRAW: 'draw', 28 | 29 | ...UploadSourceMobile, 30 | ...ExternalUploadSource, 31 | }); 32 | 33 | /** @typedef {(typeof UploadSource)[keyof typeof UploadSource]} SourceTypes */ 34 | -------------------------------------------------------------------------------- /blocks/utils/abilities.js: -------------------------------------------------------------------------------- 1 | export const canUsePermissionsApi = () => { 2 | return typeof navigator.permissions !== 'undefined'; 3 | }; 4 | -------------------------------------------------------------------------------- /blocks/utils/comma-separated.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @param {string} value */ 4 | export const deserializeCsv = (value) => { 5 | if (!value) { 6 | return []; 7 | } 8 | 9 | return value 10 | .split(',') 11 | .map((item) => item.trim()) 12 | .filter(Boolean); 13 | }; 14 | 15 | /** @param {unknown[]} value */ 16 | export const serializeCsv = (value) => { 17 | if (!value) { 18 | return ''; 19 | } 20 | 21 | return value.join(','); 22 | }; 23 | -------------------------------------------------------------------------------- /blocks/utils/debounce.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @template {{ (...args: any[]): any }} T 5 | * @param {T} callback 6 | * @param {number} wait 7 | * @returns {T & { cancel: () => void }} } 8 | */ 9 | export function debounce(callback, wait) { 10 | /** @type {NodeJS.Timeout} */ 11 | let timer; 12 | const debounced = 13 | /** @param {...any} args */ 14 | (...args) => { 15 | clearTimeout(timer); 16 | timer = setTimeout(() => callback(...args), wait); 17 | }; 18 | debounced.cancel = () => { 19 | clearTimeout(timer); 20 | }; 21 | return /** @type {T & { cancel: () => void }} } */ (debounced); 22 | } 23 | -------------------------------------------------------------------------------- /blocks/utils/preloadImage.js: -------------------------------------------------------------------------------- 1 | import { TRANSPARENT_PIXEL_SRC } from '../../utils/transparentPixelSrc.js'; 2 | 3 | export function preloadImage(src) { 4 | let image = new Image(); 5 | 6 | let promise = new Promise((resolve, reject) => { 7 | image.src = src; 8 | image.onload = resolve; 9 | image.onerror = reject; 10 | }); 11 | 12 | let cancel = () => { 13 | if (image.naturalWidth === 0) { 14 | image.src = TRANSPARENT_PIXEL_SRC; 15 | } 16 | }; 17 | 18 | return { promise, image, cancel }; 19 | } 20 | 21 | export function batchPreloadImages(list) { 22 | let preloaders = []; 23 | 24 | for (let src of list) { 25 | let preload = preloadImage(src); 26 | preloaders.push(preload); 27 | } 28 | 29 | let images = preloaders.map((preload) => preload.image); 30 | let promise = Promise.allSettled(preloaders.map((preload) => preload.promise)); 31 | let cancel = () => { 32 | preloaders.forEach((preload) => { 33 | preload.cancel(); 34 | }); 35 | }; 36 | 37 | return { promise, images, cancel }; 38 | } 39 | -------------------------------------------------------------------------------- /blocks/utils/resizeImage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {File} imgFile 3 | * @param {Number} [size] 4 | */ 5 | export function generateThumb(imgFile, size = 40) { 6 | if (imgFile.type === 'image/svg+xml') { 7 | return URL.createObjectURL(imgFile); 8 | } 9 | let canvas = document.createElement('canvas'); 10 | let ctx = canvas.getContext('2d'); 11 | let img = new Image(); 12 | let promise = new Promise((resolve, reject) => { 13 | img.onload = () => { 14 | let ratio = img.height / img.width; 15 | if (ratio > 1) { 16 | canvas.width = size; 17 | canvas.height = size * ratio; 18 | } else { 19 | canvas.height = size; 20 | canvas.width = size / ratio; 21 | } 22 | ctx.fillStyle = 'rgb(240, 240, 240)'; 23 | ctx.fillRect(0, 0, canvas.width, canvas.height); 24 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 25 | canvas.toBlob((blob) => { 26 | if (!blob) { 27 | reject(); 28 | return; 29 | } 30 | let url = URL.createObjectURL(blob); 31 | resolve(url); 32 | }); 33 | }; 34 | img.onerror = (err) => { 35 | reject(err); 36 | }; 37 | }); 38 | img.src = URL.createObjectURL(imgFile); 39 | return promise; 40 | } 41 | -------------------------------------------------------------------------------- /blocks/utils/throttle.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @template {{ (...args: any[]): void }} T 5 | * @param {T} fn 6 | * @param {number} wait 7 | * @returns {T & { readonly cancel: () => void }} } 8 | */ 9 | export const throttle = (fn, wait) => { 10 | /** @type {boolean} */ 11 | let inThrottle; 12 | /** @type {ReturnType} */ 13 | let lastFn; 14 | /** @type {number} */ 15 | let lastTime; 16 | /** @param {...any} args */ 17 | const throttled = (...args) => { 18 | if (!inThrottle) { 19 | fn(...args); 20 | lastTime = Date.now(); 21 | inThrottle = true; 22 | } else { 23 | clearTimeout(lastFn); 24 | lastFn = setTimeout( 25 | () => { 26 | if (Date.now() - lastTime >= wait) { 27 | fn(...args); 28 | lastTime = Date.now(); 29 | } 30 | }, 31 | Math.max(wait - (Date.now() - lastTime), 0), 32 | ); 33 | } 34 | }; 35 | Object.defineProperty(throttled, 'cancel', { 36 | configurable: false, 37 | writable: false, 38 | enumerable: false, 39 | value: () => { 40 | clearTimeout(lastFn); 41 | }, 42 | }); 43 | 44 | return /** @type {T & { readonly cancel: () => void }} */ (/** @type {unknown} */ (throttled)); 45 | }; 46 | -------------------------------------------------------------------------------- /blocks/utils/userAgent.js: -------------------------------------------------------------------------------- 1 | import { getUserAgent } from '@uploadcare/upload-client'; 2 | import { PACKAGE_VERSION, PACKAGE_NAME } from '../../env.js'; 3 | 4 | /** 5 | * @param {import('@uploadcare/upload-client').CustomUserAgentOptions} options 6 | * @returns {ReturnType} 7 | */ 8 | export function customUserAgent(options) { 9 | return getUserAgent({ 10 | ...options, 11 | libraryName: PACKAGE_NAME, 12 | libraryVersion: PACKAGE_VERSION, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /build-items.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @typedef {{ in: string; out: string; minify: boolean; minifyHtml?: boolean; iife?: boolean }} BuildItem */ 4 | 5 | /** @type {BuildItem[]} */ 6 | export const buildItems = [ 7 | // uc-blocks 8 | { 9 | in: './index.js', 10 | out: './web/file-uploader.min.js', 11 | minify: true, 12 | minifyHtml: true, 13 | }, 14 | { 15 | in: './index.js', 16 | out: './web/file-uploader.iife.min.js', 17 | minify: true, 18 | minifyHtml: true, 19 | iife: true, 20 | }, 21 | { 22 | in: './blocks/themes/uc-basic/index.css', 23 | out: './web/uc-basic.min.css', 24 | minify: true, 25 | }, 26 | // uc-cloud-image-editor 27 | { 28 | in: './solutions/cloud-image-editor/index.js', 29 | out: './web/uc-cloud-image-editor.min.js', 30 | minify: true, 31 | minifyHtml: true, 32 | }, 33 | { 34 | in: './solutions/cloud-image-editor/index.css', 35 | out: './web/uc-cloud-image-editor.min.css', 36 | minify: true, 37 | }, 38 | 39 | // file-uploader-regular 40 | { 41 | in: './solutions/file-uploader/regular/index.js', 42 | out: './web/uc-file-uploader-regular.min.js', 43 | minify: true, 44 | minifyHtml: true, 45 | }, 46 | { 47 | in: './solutions/file-uploader/regular/index.css', 48 | out: './web/uc-file-uploader-regular.min.css', 49 | minify: true, 50 | }, 51 | 52 | // file-uploader-inline 53 | { 54 | in: './solutions/file-uploader/inline/index.js', 55 | out: './web/uc-file-uploader-inline.min.js', 56 | minify: true, 57 | minifyHtml: true, 58 | }, 59 | { 60 | in: './solutions/file-uploader/inline/index.css', 61 | out: './web/uc-file-uploader-inline.min.css', 62 | minify: true, 63 | }, 64 | 65 | // file-uploader-minimal 66 | { 67 | in: './solutions/file-uploader/minimal/index.js', 68 | out: './web/uc-file-uploader-minimal.min.js', 69 | minify: true, 70 | minifyHtml: true, 71 | }, 72 | { 73 | in: './solutions/file-uploader/minimal/index.css', 74 | out: './web/uc-file-uploader-minimal.min.css', 75 | minify: true, 76 | }, 77 | 78 | // uc-img 79 | { 80 | in: './solutions/adaptive-image/index.js', 81 | out: './web/uc-img.min.js', 82 | minify: true, 83 | minifyHtml: true, 84 | }, 85 | ]; 86 | -------------------------------------------------------------------------------- /build-jsx-types.js: -------------------------------------------------------------------------------- 1 | // TODO: build JSX types 2 | -------------------------------------------------------------------------------- /build-svg-sprite.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import SVGSpriter from 'svg-sprite'; 4 | import url from 'url'; 5 | 6 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const DATA = [ 9 | { 10 | input: path.resolve(__dirname, './blocks/CloudImageEditor/src/icons/'), 11 | output: path.resolve(__dirname, './blocks/CloudImageEditor/src/svg-sprite.js'), 12 | }, 13 | { 14 | input: path.resolve(__dirname, './blocks/themes/uc-basic/icons/'), 15 | output: path.resolve(__dirname, './blocks/themes/uc-basic/svg-sprite.js'), 16 | }, 17 | ]; 18 | 19 | const config = { 20 | mode: { 21 | symbol: { 22 | inline: true, 23 | }, 24 | }, 25 | shape: { 26 | id: { 27 | generator: (name) => `uc-icon-${name.replace(/\.svg$/, '')}`, 28 | }, 29 | transform: [ 30 | { 31 | svgo: { 32 | plugins: [ 33 | { 34 | name: 'preset-default', 35 | }, 36 | { 37 | name: 'prefixIds', 38 | params: { 39 | prefix: 'uc-icon-id', 40 | }, 41 | }, 42 | ], 43 | }, 44 | }, 45 | ], 46 | }, 47 | }; 48 | 49 | console.log('Generating SVG sprite...'); 50 | 51 | DATA.forEach((item) => { 52 | const spriter = new SVGSpriter(config); 53 | 54 | fs.readdir(item.input, (err, files) => { 55 | if (err) { 56 | throw err; 57 | } 58 | 59 | console.log(`Processing ${item.input}...`); 60 | 61 | files.forEach((file) => { 62 | const filePath = path.resolve(item.input, file); 63 | console.log(`Icon processed: ${filePath}`); 64 | spriter.add(filePath, null, fs.readFileSync(filePath, { encoding: 'utf-8' })); 65 | }); 66 | 67 | spriter.compile((error, result) => { 68 | if (error) { 69 | throw error; 70 | } 71 | 72 | const jsTemplate = `export default "${result.symbol.sprite.contents.toString().replace(/\"/g, "'")}";` 73 | .trim() 74 | .concat('\n'); 75 | 76 | fs.writeFileSync(item.output, jsTemplate); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /demo/cloud-image-editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /demo/custom-icons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | 25 | 33 | 34 |
35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /demo/new-social-sources-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 42 | 43 | 44 | 45 | 51 | 52 | 53 |
54 | Options 55 | 59 | 63 | 67 |
68 | -------------------------------------------------------------------------------- /demo/preview-proxy/secure-delivery-proxy-url-resolver.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/preview-proxy/secure-delivery-proxy-url-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /demo/preview-proxy/secure-delivery-proxy.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | const PORT = 3000; 4 | 5 | http 6 | .createServer(function (request, response) { 7 | if (request.method !== 'GET') { 8 | return response.end('Only GET requests are supported'); 9 | } 10 | let url = new URL(request.url, `http://localhost:${PORT}`); 11 | let path = url.pathname.replace(/([^\/])$/, '$1/'); 12 | if (path !== '/preview/') { 13 | return response.end('Only `/preview/` path is supported'); 14 | } 15 | let searchParams = url.searchParams; 16 | let fileUrl = searchParams.get('url'); 17 | let size = searchParams.get('size'); 18 | console.log(`Got request. Url: "${fileUrl}". Size: "${size}"`); 19 | if (!fileUrl) { 20 | return response.end('`url` parameter is required'); 21 | } 22 | response.statusCode = 302; 23 | response.setHeader('Location', fileUrl); //lgtm [js/server-side-unvalidated-url-redirection]; 24 | response.end(); 25 | }) 26 | .listen(PORT); 27 | -------------------------------------------------------------------------------- /demo/raw-inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 18 | 19 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /demo/raw-minimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/raw-regular.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/secure-uploads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/test.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /demo/upload-api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | Please select behaviour: 54 |
55 | 56 | 57 | 58 | 59 | 60 |
61 |
62 | -------------------------------------------------------------------------------- /demo/validators.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 49 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | /** Do not edit this file manually. It's generated during build process. */ 2 | export const PACKAGE_NAME = 'blocks'; 3 | export const PACKAGE_VERSION = '1.16.2'; 4 | -------------------------------------------------------------------------------- /env.template.js: -------------------------------------------------------------------------------- 1 | export const PACKAGE_NAME = 'blocks'; 2 | export const PACKAGE_VERSION = '{{packageVersion}}'; 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ## Checklist 10 | 11 | - [ ] Tests (if applicable) 12 | - [ ] Documentation (if applicable) 13 | - [ ] Changelog stub (or use [conventional commit messages](https://www.conventionalcommits.org/)) 14 | -------------------------------------------------------------------------------- /ship.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export default { 5 | buildCommand: () => 'npm run build', 6 | publishCommand: ({ defaultCommand }) => `${defaultCommand} --access public`, 7 | versionUpdated: ({ version, dir }) => { 8 | function generateEnvFile(variables) { 9 | let template = fs.readFileSync(path.join(dir, './env.template.js')).toString(); 10 | template = template.replaceAll(/{{(.+?)}}/g, (match, p1) => { 11 | return variables[p1]; 12 | }); 13 | template = `/** Do not edit this file manually. It's generated during build process. */\n` + template; 14 | fs.writeFileSync(path.join(dir, './env.js'), template); 15 | } 16 | 17 | generateEnvFile({ packageVersion: version }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /solutions/adaptive-image/index.js: -------------------------------------------------------------------------------- 1 | import { Img } from '../../blocks/Img/Img.js'; 2 | 3 | Img.reg('uc-img'); 4 | 5 | export { Img }; 6 | -------------------------------------------------------------------------------- /solutions/cloud-image-editor/CloudImageEditor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { CloudImageEditorBlock } from '../../blocks/CloudImageEditor/src/CloudImageEditorBlock.js'; 3 | 4 | export class CloudImageEditor extends CloudImageEditorBlock { 5 | static styleAttrs = [...super.styleAttrs, 'uc-wgt-common']; 6 | 7 | initCallback() { 8 | super.initCallback(); 9 | 10 | this.a11y?.registerBlock(this); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /solutions/cloud-image-editor/index.css: -------------------------------------------------------------------------------- 1 | @import url('../../blocks/themes/uc-basic/theme.css'); 2 | @import url('../../blocks/CloudImageEditor/index.css'); 3 | -------------------------------------------------------------------------------- /solutions/cloud-image-editor/index.js: -------------------------------------------------------------------------------- 1 | export * from '../../blocks/CloudImageEditor/index.js'; 2 | export * from './CloudImageEditor.js'; 3 | 4 | /* TODO: We need to make some dependency injection/checking magic 5 | I see it as a declared list of tags on which the block depends 6 | Then we can check whether the dependent tag is registered in the CustomElementRegistry or not. 7 | If not, register it from default ones or just log the warning */ 8 | 9 | export { Icon } from '../../blocks/Icon/Icon.js'; 10 | export { defineComponents } from '../../abstract/defineComponents.js'; 11 | export { Config } from '../../blocks/Config/Config.js'; 12 | -------------------------------------------------------------------------------- /solutions/file-uploader/inline/index.css: -------------------------------------------------------------------------------- 1 | @import url('../../../blocks/themes/uc-basic/index.css'); 2 | 3 | [uc-file-uploader-inline] uc-start-from { 4 | height: 100%; 5 | container-type: inline-size; 6 | } 7 | 8 | [uc-file-uploader-inline] { 9 | --cfg-done-activity: 'start-from'; 10 | --cfg-init-activity: 'start-from'; 11 | 12 | flex: 1; 13 | } 14 | 15 | [uc-file-uploader-inline] uc-activity-header::after { 16 | width: var(--uc-button-size); 17 | height: var(--uc-button-size); 18 | content: ''; 19 | } 20 | 21 | [uc-file-uploader-inline] uc-activity-header .uc-close-btn { 22 | display: none; 23 | } 24 | 25 | [uc-file-uploader-inline] uc-copyright .uc-credits { 26 | position: static; 27 | } 28 | 29 | @container (min-width: 500px) { 30 | [uc-file-uploader-inline] uc-start-from .uc-content { 31 | grid-template-columns: 1fr max-content; 32 | height: 100%; 33 | } 34 | 35 | [uc-file-uploader-inline] uc-start-from uc-copyright { 36 | grid-column: 2; 37 | } 38 | 39 | [uc-file-uploader-inline] uc-start-from uc-drop-area { 40 | grid-row: span 3; 41 | } 42 | 43 | [uc-file-uploader-inline] uc-start-from:has(uc-copyright[hidden]) uc-drop-area { 44 | grid-row: span 2; 45 | } 46 | 47 | [uc-file-uploader-inline] uc-start-from:has(.uc-cancel-btn[hidden]) uc-drop-area { 48 | grid-row: span 2; 49 | } 50 | 51 | [uc-file-uploader-inline] uc-start-from:has(uc-copyright[hidden]):has(.uc-cancel-btn[hidden]) uc-drop-area { 52 | grid-row: span 1; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /solutions/file-uploader/inline/index.js: -------------------------------------------------------------------------------- 1 | export * from '../../../index.js'; 2 | -------------------------------------------------------------------------------- /solutions/file-uploader/minimal/index.js: -------------------------------------------------------------------------------- 1 | export * from '../../../index.js'; 2 | -------------------------------------------------------------------------------- /solutions/file-uploader/regular/FileUploaderRegular.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { SolutionBlock } from '../../../abstract/SolutionBlock.js'; 3 | import { asBoolean } from '../../../blocks/Config/validatorsType.js'; 4 | 5 | export class FileUploaderRegular extends SolutionBlock { 6 | static styleAttrs = [...super.styleAttrs, 'uc-file-uploader-regular']; 7 | 8 | constructor() { 9 | super(); 10 | 11 | this.init$ = { 12 | ...this.init$, 13 | isHidden: false, 14 | }; 15 | } 16 | 17 | initCallback() { 18 | super.initCallback(); 19 | 20 | this.defineAccessor( 21 | 'headless', 22 | /** @param {unknown} value */ (value) => { 23 | this.set$({ isHidden: asBoolean(value) }); 24 | }, 25 | ); 26 | } 27 | } 28 | 29 | FileUploaderRegular.template = /* HTML */ ` 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | `; 61 | 62 | FileUploaderRegular.bindAttributes({ 63 | // @ts-expect-error TODO: fix types inside symbiote 64 | headless: null, 65 | }); 66 | -------------------------------------------------------------------------------- /solutions/file-uploader/regular/index.css: -------------------------------------------------------------------------------- 1 | @import url('../../../blocks/themes/uc-basic/index.css'); 2 | -------------------------------------------------------------------------------- /solutions/file-uploader/regular/index.js: -------------------------------------------------------------------------------- 1 | export * from '../../../index.js'; 2 | -------------------------------------------------------------------------------- /tests/__screenshots__/file-uploader-regular.e2e.test.tsx/File-uploader-regular-Add-files-to-the-upload-list-from-camera-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uploadcare/file-uploader/c2197aa84dc8addcc3a33baf8745390e954963d7/tests/__screenshots__/file-uploader-regular.e2e.test.tsx/File-uploader-regular-Add-files-to-the-upload-list-from-camera-1.png -------------------------------------------------------------------------------- /tests/api.e2e.test.tsx: -------------------------------------------------------------------------------- 1 | import { commands, page, userEvent } from '@vitest/browser/context'; 2 | import { beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest'; 3 | import { renderer } from './utils/test-renderer'; 4 | import '../types/jsx'; 5 | import { EventPayload } from '@/types'; 6 | 7 | beforeAll(async () => { 8 | await import('@/solutions/file-uploader/regular/index.css'); 9 | const UC = await import('@/index.js'); 10 | UC.defineComponents(UC); 11 | }); 12 | 13 | beforeEach(() => { 14 | const ctxName = `test-${Math.random().toString(36).slice(2)}`; 15 | page.render( 16 | <> 17 | 18 | 19 | 20 | , 21 | ); 22 | }); 23 | 24 | describe('API', () => { 25 | it('should somehow work', async () => { 26 | const uploadCtxProvider = page.getByTestId('uc-upload-ctx-provider').query()! as InstanceType; 27 | const api = uploadCtxProvider.api; 28 | 29 | const eventHandler = vi.fn<(e: CustomEvent) => void>(); 30 | 31 | uploadCtxProvider.addEventListener('file-added', eventHandler); 32 | 33 | const url = 34 | 'https://images.unsplash.com/photo-1699102241946-45c5e1937d69?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=prithiviraj-a-fa7Stge3YXs-unsplash.jpg&w=640'; 35 | api.addFileFromUrl(url); 36 | 37 | const eventPayload = await vi.waitFor(() => { 38 | expect(eventHandler).toHaveBeenCalled(); 39 | return eventHandler.mock.calls[0][0].detail; 40 | }); 41 | 42 | expect(eventPayload).toMatchObject(expect.objectContaining({ status: 'idle', externalUrl: url })); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/fixtures/test_image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uploadcare/file-uploader/c2197aa84dc8addcc3a33baf8745390e954963d7/tests/fixtures/test_image.jpeg -------------------------------------------------------------------------------- /tests/utils/commands.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { BrowserCommand } from 'vitest/node'; 3 | 4 | export const waitFileChooserAndUpload: BrowserCommand<[string[]]> = async ({ page, testPath }, relativePaths) => { 5 | if (!testPath) { 6 | throw new Error('Test path is not defined'); 7 | } 8 | const fileChooserPromise = page.waitForEvent('filechooser'); 9 | const fileChooser = await fileChooserPromise; 10 | for (const relativePath of relativePaths) { 11 | const absolutePath = path.join(path.dirname(testPath), relativePath); 12 | await fileChooser.setFiles(absolutePath); 13 | } 14 | }; 15 | 16 | export const commands = { 17 | waitFileChooserAndUpload, 18 | }; 19 | 20 | declare module '@vitest/browser/context' { 21 | interface BrowserCommands { 22 | waitFileChooserAndUpload: (relativePaths: string[]) => Promise; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/utils/test-renderer.tsx: -------------------------------------------------------------------------------- 1 | import { page } from '@vitest/browser/context'; 2 | import { CommonDOMRenderer } from 'render-jsx/dom'; 3 | import { beforeEach } from 'vitest'; 4 | 5 | export const renderer = new CommonDOMRenderer(); 6 | 7 | const containers = new Set(); 8 | 9 | export const render = (jsx: any) => { 10 | const container = document.createElement('div'); 11 | containers.add(container); 12 | renderer.render(jsx).on(container); 13 | document.body.appendChild(container); 14 | }; 15 | 16 | export const cleanup = () => { 17 | containers.forEach((container) => { 18 | container.remove(); 19 | }); 20 | containers.clear(); 21 | }; 22 | 23 | page.extend({ 24 | render, 25 | [Symbol.for('vitest:component-cleanup')]: cleanup, 26 | }); 27 | 28 | beforeEach(async () => { 29 | cleanup(); 30 | }); 31 | 32 | declare module '@vitest/browser/context' { 33 | interface BrowserPage { 34 | render: typeof render; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "pretty": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "lib": ["es2017", "ESNext", "ESNext.Array", "DOM", "DOM.Iterable", "WebWorker"], 8 | "jsx": "react", 9 | "allowJs": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "types": [ 13 | "node", 14 | "chai", 15 | "mocha", 16 | "@total-typescript/ts-reset", 17 | "@vitest/browser/providers/playwright", 18 | "vite/client" 19 | ], 20 | "noEmit": true, 21 | "jsxFactory": "renderer.create", 22 | "jsxFragmentFactory": "renderer.fragment", 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": ["**/*.js", "types", "tests", "tests/utils/test-renderer.tsx"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "declarationMap": true 8 | }, 9 | "include": ["**/*.js", "types/*"], 10 | "exclude": ["node_modules", "test"] 11 | } 12 | -------------------------------------------------------------------------------- /types/events.d.ts: -------------------------------------------------------------------------------- 1 | import type { EventPayload } from '../blocks/UploadCtxProvider/EventEmitter'; 2 | 3 | export type EventMap = { 4 | [T in keyof EventPayload]: CustomEvent; 5 | }; -------------------------------------------------------------------------------- /types/events.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/exported.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import { UC_WINDOW_KEY } from '../abstract/loadFileUploaderFrom.js'; 2 | import * as blocks from '../index.js'; 3 | 4 | declare global { 5 | interface Window { 6 | [UC_WINDOW_KEY]?: typeof blocks; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /types/https.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'https://*'; 2 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./events.js"; 2 | export * from "./exported.js"; 3 | -------------------------------------------------------------------------------- /types/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | export * from './exported.js'; 3 | export * from './events.js'; 4 | -------------------------------------------------------------------------------- /types/jsx.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /types/test/public-upload-api.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { UploadCtxProvider } from '../../index.js'; 2 | 3 | const instance = new UploadCtxProvider(); 4 | const api = instance.getAPI(); 5 | 6 | api.addFileFromUrl('https://example.com/image.png'); 7 | 8 | api.setCurrentActivity('camera'); 9 | api.setCurrentActivity('cloud-image-edit', { internalId: 'id' }); 10 | api.setCurrentActivity('external', { 11 | externalSourceType: 'type', 12 | }); 13 | 14 | // @ts-expect-error - should not allow to set activity without params 15 | api.setCurrentActivity('cloud-image-edit'); 16 | // @ts-expect-error - should not allow to set activity without params 17 | api.setCurrentActivity('external'); 18 | 19 | // @ts-expect-error - should not allow to set activity with invalid params 20 | api.setCurrentActivity('camera', { 21 | invalidParam: 'value', 22 | }); 23 | api.setCurrentActivity('cloud-image-edit', { 24 | // @ts-expect-error - should not allow to set activity with invalid params 25 | invalidParam: 'value', 26 | }); 27 | api.setCurrentActivity('external', { 28 | // @ts-expect-error - should not allow to set activity with invalid params 29 | invalidParam: 'value', 30 | }); 31 | 32 | // should allow to set some custom activity 33 | api.setCurrentActivity('my-custom-activity'); 34 | api.setCurrentActivity('my-custom-activity', { myCustomParam: 'value' }); 35 | -------------------------------------------------------------------------------- /types/test/uc-cloud-image-editor.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { renderer } from '../../tests/utils/test-renderer.js'; 2 | import '../jsx'; 3 | 4 | // @ts-expect-error - no props 5 | () => ; 6 | 7 | // @ts-expect-error - no css-url 8 | () => ; 9 | 10 | // @ts-expect-error - no css-src 11 | () => ; 12 | 13 | () => ; 14 | () => ; 15 | () => ; 16 | -------------------------------------------------------------------------------- /types/test/uc-form-input.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { FormInput } from '../../index.js'; 2 | import { renderer } from '../../tests/utils/test-renderer.js'; 3 | 4 | () => ; 5 | 6 | const formInput = new FormInput(); 7 | -------------------------------------------------------------------------------- /utils/WindowHeightTracker.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { debounce } from '../blocks/utils/debounce.js'; 3 | 4 | const WINDOW_HEIGHT_TRACKER_PROPERTY = '--uploadcare-blocks-window-height'; 5 | 6 | export class WindowHeightTracker { 7 | /** 8 | * @private 9 | * @type {Set} 10 | */ 11 | static clientsRegistry = new Set(); 12 | 13 | /** @private */ 14 | static flush = debounce(() => { 15 | document.documentElement.style.setProperty(WINDOW_HEIGHT_TRACKER_PROPERTY, `${window.innerHeight}px`); 16 | }, 100); 17 | 18 | /** 19 | * @param {unknown} client 20 | * @public 21 | */ 22 | static registerClient(client) { 23 | if (this.clientsRegistry.size === 0) { 24 | this.attachTracker(); 25 | } 26 | this.clientsRegistry.add(client); 27 | } 28 | 29 | /** 30 | * @param {unknown} client 31 | * @public 32 | */ 33 | static unregisterClient(client) { 34 | this.clientsRegistry.delete(client); 35 | if (this.clientsRegistry.size === 0) { 36 | this.detachTracker(); 37 | } 38 | } 39 | 40 | /** @private */ 41 | static attachTracker() { 42 | window.addEventListener('resize', this.flush, { passive: true, capture: true }); 43 | this.flush(); 44 | } 45 | 46 | /** @private */ 47 | static detachTracker() { 48 | window.removeEventListener('resize', this.flush, { capture: true }); 49 | document.documentElement.style.removeProperty(WINDOW_HEIGHT_TRACKER_PROPERTY); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /utils/browser-info.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const calcIsDesktopSafari = () => { 4 | const ua = navigator.userAgent; 5 | return /Macintosh|Windows/.test(ua) && /Version\/[\d\.]+.*Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR/.test(ua); 6 | }; 7 | 8 | const calcHtmlMediaCaptureSupport = () => { 9 | return 'capture' in document.createElement('input'); 10 | }; 11 | 12 | export const calcBrowserInfo = () => ({ 13 | safariDesktop: calcIsDesktopSafari(), 14 | }); 15 | 16 | export const calcBrowserFeatures = () => ({ 17 | htmlMediaCapture: calcHtmlMediaCaptureSupport(), 18 | }); 19 | 20 | export const browserInfo = calcBrowserInfo(); 21 | 22 | export const browserFeatures = calcBrowserFeatures(); 23 | -------------------------------------------------------------------------------- /utils/delay.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @param {number} ms */ 4 | export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 5 | -------------------------------------------------------------------------------- /utils/getLocaleDirection.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @param {string} localeId */ 3 | export const getLocaleDirection = (localeId) => { 4 | /** 5 | * @type {typeof Intl.Locale & { 6 | * textInfo?: { direction: string }; 7 | * getTextInfo?: () => { direction: string }; 8 | * }} 9 | */ 10 | const locale = /** @type {any} */ (new Intl.Locale(localeId)); 11 | let direction = 'ltr'; 12 | if (typeof locale.getTextInfo === 'function' && locale.getTextInfo().direction) { 13 | direction = locale.getTextInfo().direction; 14 | } else if ('textInfo' in locale && locale.textInfo?.direction) { 15 | direction = locale.textInfo.direction; 16 | } 17 | return direction; 18 | }; 19 | -------------------------------------------------------------------------------- /utils/getPluralForm.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @typedef {Intl.LDMLPluralRule} PluralForm */ 4 | 5 | /** 6 | * @param {string} locale 7 | * @param {number} count 8 | * @returns {PluralForm} 9 | */ 10 | export const getPluralForm = (locale, count) => { 11 | const pluralForm = new Intl.PluralRules(locale).select(count); 12 | return pluralForm; 13 | }; 14 | -------------------------------------------------------------------------------- /utils/getPluralForm.test.js: -------------------------------------------------------------------------------- 1 | import { getPluralForm } from './getPluralForm'; 2 | import { expect } from '@esm-bundle/chai'; 3 | 4 | describe('getPluralForm', () => { 5 | it('should return selected form for es-US', () => { 6 | expect(getPluralForm('en-US', 1)).to.equal('one'); 7 | expect(getPluralForm('en-US', 2)).to.equal('other'); 8 | }); 9 | 10 | it('should return selected form for ru-RU', () => { 11 | expect(getPluralForm('ru-RU', 1)).to.equal('one'); 12 | expect(getPluralForm('ru-RU', 2)).to.equal('few'); 13 | expect(getPluralForm('ru-RU', 5)).to.equal('many'); 14 | expect(getPluralForm('ru-RU', 1.5)).to.equal('other'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /utils/ifRef.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This will check if the execution environment is a short code snippet, and not the complete HTML-document 3 | * 4 | * @param {Function} cb 5 | */ 6 | export function ifRef(cb) { 7 | // @ts-ignore 8 | typeof window.__IS_REF__ === 'boolean' && !!window.__IS_REF__ && cb(); 9 | } 10 | -------------------------------------------------------------------------------- /utils/isSecureTokenExpired.js: -------------------------------------------------------------------------------- 1 | /** @param {number} ms */ 2 | const msToUnixTimestamp = (ms) => Math.floor(ms / 1000); 3 | 4 | /** 5 | * Check if secure token is expired. It uses a threshold of 10 seconds by default. i.e. if the token is not expired yet 6 | * but will expire in the next 10 seconds, it will return false. 7 | * 8 | * @param {import('../types').SecureUploadsSignatureAndExpire} secureToken 9 | * @param {{ threshold?: number }} options 10 | */ 11 | export const isSecureTokenExpired = (secureToken, { threshold }) => { 12 | const { secureExpire } = secureToken; 13 | const nowUnix = msToUnixTimestamp(Date.now()); 14 | const expireUnix = Number(secureExpire); 15 | const thresholdUnix = msToUnixTimestamp(threshold); 16 | return nowUnix + thresholdUnix >= expireUnix; 17 | }; 18 | -------------------------------------------------------------------------------- /utils/isSecureTokenExpired.test.js: -------------------------------------------------------------------------------- 1 | import { isSecureTokenExpired } from './isSecureTokenExpired'; 2 | import { expect } from '@esm-bundle/chai'; 3 | import * as sinon from 'sinon'; 4 | 5 | const DATE_NOW = 60 * 1000; 6 | const THRESHOLD = 10 * 1000; 7 | 8 | describe('isSecureTokenExpired', () => { 9 | let clock; 10 | beforeEach(() => { 11 | clock = sinon.useFakeTimers(DATE_NOW); 12 | }); 13 | 14 | afterEach(() => { 15 | clock.restore(); 16 | }); 17 | 18 | it('should return true if the token is expired', () => { 19 | expect(isSecureTokenExpired({ secureExpire: '0', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(true); 20 | expect(isSecureTokenExpired({ secureExpire: '59', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(true); 21 | }); 22 | 23 | it('should return true if the token will expire in the next 10 seconds', () => { 24 | expect(isSecureTokenExpired({ secureExpire: '60', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(true); 25 | expect(isSecureTokenExpired({ secureExpire: '61', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(true); 26 | expect(isSecureTokenExpired({ secureExpire: '70', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(true); 27 | }); 28 | 29 | it("should return false if the token is not expired and won't expire in next 10 seconds", () => { 30 | expect(isSecureTokenExpired({ secureExpire: '71', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(false); 31 | expect(isSecureTokenExpired({ secureExpire: '80', secureSignature: '' }, { threshold: THRESHOLD })).to.equal(false); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /utils/memoize.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @template {any[]} TArgs 5 | * @template {any} TReturn 6 | * @template {(...args: TArgs) => TReturn} T 7 | * @param {T} fn 8 | * @returns {T} 9 | */ 10 | export const memoize = (fn) => { 11 | const cache = new Map(); 12 | return /** @type {T} */ ( 13 | (...args) => { 14 | const key = JSON.stringify(args); 15 | if (cache.has(key)) { 16 | return cache.get(key); 17 | } 18 | const result = fn(...args); 19 | cache.set(key, result); 20 | return result; 21 | } 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /utils/memoize.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { memoize } from './memoize.js'; 3 | import { spy } from 'sinon'; 4 | 5 | describe('memoize', () => { 6 | it('should cache result', () => { 7 | let counter = 0; 8 | const fn = spy(() => counter++); 9 | const memoized = memoize(fn); 10 | memoized(); 11 | memoized(); 12 | memoized(); 13 | expect(fn.callCount).to.equal(1); 14 | }); 15 | 16 | it('should cache result for each set of arguments', () => { 17 | const fn = spy((a, b) => { 18 | return a + b; 19 | }); 20 | const memoized = memoize(fn); 21 | 22 | memoized(1, 2); 23 | memoized(1, 2); 24 | memoized(1, 2); 25 | memoized(2, 3); 26 | memoized(2, 3); 27 | memoized(2, 3); 28 | expect(fn.callCount).to.equal(2); 29 | }); 30 | 31 | it('should return the same result as original function', () => { 32 | const fn = (a, b) => a + b; 33 | const memoized = memoize(fn); 34 | expect(memoized(1, 2)).to.equal(fn(1, 2)); 35 | expect(memoized(2, 3)).to.equal(fn(2, 3)); 36 | expect(memoized(3, 4)).to.equal(fn(3, 4)); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /utils/mixinClass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template T 3 | * @typedef {new (...args: any[]) => T} GConstructor 4 | */ 5 | 6 | /** 7 | * This is a helper to create a class type extended with the provided set of instance properties. It's useful when there 8 | * are some dynamic generated properties or native overrides in the class. We're use it to define dynamic access 9 | * properties and events to subscribe to. 10 | * 11 | * @template {GConstructor} Base 12 | * @template {Record} [InstanceProperties={}] Default is `{}` 13 | * @typedef {{ 14 | * new (...args: ConstructorParameters): InstanceProperties & InstanceType; 15 | * } & Omit} MixinClass 16 | */ 17 | 18 | export {}; 19 | -------------------------------------------------------------------------------- /utils/parseCdnUrl.js: -------------------------------------------------------------------------------- 1 | const uuidRegex = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i; 2 | const cdnUrlRegex = new RegExp(`^/?(${uuidRegex.source})(?:/(-/(?:[^/]+/)+)?([^/]*))?$`, 'i'); 3 | 4 | /** @param {{ url: string; cdnBase: string }} options */ 5 | export const parseCdnUrl = ({ url, cdnBase }) => { 6 | const cdnBaseUrlObj = new URL(cdnBase); 7 | const urlObj = new URL(url); 8 | 9 | if (cdnBaseUrlObj.host !== urlObj.host) { 10 | return null; 11 | } 12 | 13 | const [, uuid, cdnUrlModifiers, filename] = cdnUrlRegex.exec(urlObj.pathname); 14 | 15 | return { 16 | uuid, 17 | cdnUrlModifiers: cdnUrlModifiers || '', 18 | filename: filename || null, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /utils/parseShrink.js: -------------------------------------------------------------------------------- 1 | /** TODO parseShrink move to package @uploadcare/image-shrink */ 2 | 3 | const MAX_SQUARE_SIDE = 16384; 4 | 5 | const regExpShrink = /^([0-9]+)x([0-9]+)(?:\s+(\d{1,2}|100)%)?$/i; 6 | 7 | /** 8 | * @param value 9 | * @returns {{ size: number; quality: number | undefined }} 10 | */ 11 | export const parseShrink = (value) => { 12 | const terms = regExpShrink.exec(value?.toLocaleLowerCase()) || []; 13 | 14 | if (!terms.length) { 15 | return false; 16 | } 17 | 18 | const sizeShrink = terms[1] * terms[2]; 19 | const maxSize = MAX_SQUARE_SIDE * MAX_SQUARE_SIDE; 20 | 21 | if (sizeShrink > maxSize) { 22 | console.warn( 23 | `Shrinked size can not be larger than ${Math.floor(maxSize / 1000 / 1000)}MP. ` + 24 | `You have set ${terms[1]}x${terms[2]} (` + 25 | `${Math.ceil(sizeShrink / 1000 / 100) / 10}MP).`, 26 | ); 27 | return false; 28 | } 29 | 30 | return { 31 | quality: terms[3] ? terms[3] / 100 : undefined, 32 | size: sizeShrink, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /utils/parseShrink.test.js: -------------------------------------------------------------------------------- 1 | import { parseShrink } from './parseShrink.js'; 2 | import { expect } from '@esm-bundle/chai'; 3 | 4 | describe('parseShrink', () => { 5 | it('should be false', () => { 6 | expect(parseShrink()).to.false; 7 | }); 8 | 9 | it('should be right', () => { 10 | const result = expect(parseShrink('1000x1000 100%')); 11 | 12 | result.to.have.property('quality', 1); 13 | result.to.have.property('size', 1000000); 14 | }); 15 | 16 | it('should be right without quality', () => { 17 | const result = expect(parseShrink('1000x1000')); 18 | 19 | result.to.have.property('quality', undefined); 20 | result.to.have.property('size', 1000000); 21 | }); 22 | 23 | it('should be warn, because size shrink more max size', () => { 24 | expect(parseShrink('268435456x268435456 100%')).to.false; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /utils/prettyBytes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { getPluralForm } from './getPluralForm.js'; 3 | 4 | const BASE = 1000; 5 | 6 | export const ByteUnitEnum = Object.freeze({ 7 | AUTO: 'auto', 8 | BYTE: 'byte', 9 | KB: 'kb', 10 | MB: 'mb', 11 | GB: 'gb', 12 | TB: 'tb', 13 | PB: 'pb', 14 | }); 15 | 16 | /** 17 | * Round a specified number to decimal with two places. Round to larger value, because basically we use it for usage and 18 | * we charge customers for 1 GB even he consumed 1 byte. Feature limits are usually specified in exact MB/GB/TB, so they 19 | * will not be rounded. 20 | * 21 | * @param {number} number 22 | * @returns {number} 23 | */ 24 | const round = (number) => Math.ceil(number * 100) / 100; 25 | 26 | /** 27 | * @param {number} bytes 28 | * @param {(typeof ByteUnitEnum)[keyof typeof ByteUnitEnum]} unit 29 | * @returns {string} 30 | */ 31 | export const prettyBytes = (bytes, unit = ByteUnitEnum.AUTO) => { 32 | const isAutoMode = unit === ByteUnitEnum.AUTO; 33 | 34 | if (unit === ByteUnitEnum.BYTE || (isAutoMode && bytes < BASE ** 1)) { 35 | // TODO: handle blocks locale 36 | const pluralForm = /** @type {Extract} */ ( 37 | getPluralForm('en-US', bytes) 38 | ); 39 | const pluralized = { 40 | one: 'byte', 41 | other: 'bytes', 42 | }[pluralForm]; 43 | 44 | return `${bytes} ${pluralized}`; 45 | } 46 | 47 | if (unit === ByteUnitEnum.KB || (isAutoMode && bytes < BASE ** 2)) { 48 | return `${round(bytes / BASE ** 1)} KB`; 49 | } 50 | 51 | if (unit === ByteUnitEnum.MB || (isAutoMode && bytes < BASE ** 3)) { 52 | return `${round(bytes / BASE ** 2)} MB`; 53 | } 54 | 55 | if (unit === ByteUnitEnum.GB || (isAutoMode && bytes < BASE ** 4)) { 56 | return `${round(bytes / BASE ** 3)} GB`; 57 | } 58 | 59 | if (unit === ByteUnitEnum.TB || (isAutoMode && bytes < BASE ** 5)) { 60 | return `${round(bytes / BASE ** 4)} TB`; 61 | } 62 | 63 | return `${round(bytes / BASE ** 5)} PB`; 64 | }; 65 | -------------------------------------------------------------------------------- /utils/stringToArray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} str 3 | * @returns {string[]} 4 | */ 5 | export const stringToArray = (str, delimiter = ',') => { 6 | return str 7 | .trim() 8 | .split(delimiter) 9 | .map((part) => part.trim()) 10 | .filter((part) => part.length > 0); 11 | }; 12 | -------------------------------------------------------------------------------- /utils/stringToArray.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { stringToArray } from './stringToArray.js'; 3 | 4 | describe('stringToArray', () => { 5 | it('should convert string to array', () => { 6 | expect(stringToArray('a,b,c')).to.eql(['a', 'b', 'c']); 7 | }); 8 | 9 | it('should trim surrounding spaces', () => { 10 | expect(stringToArray(' a , b , c ')).to.eql(['a', 'b', 'c']); 11 | }); 12 | 13 | it('should trim empty values', () => { 14 | expect(stringToArray(',,,a,b,c')).to.eql(['a', 'b', 'c']); 15 | }); 16 | 17 | it('should accept custom delimiter', () => { 18 | expect(stringToArray('a b c', ' ')).to.eql(['a', 'b', 'c']); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /utils/template-utils.js: -------------------------------------------------------------------------------- 1 | /** @typedef {{ [key: String]: String | Number | Boolean | InputData }} InputData */ 2 | 3 | const DEFAULT_TRANSFORMER = (value) => value; 4 | const OPEN_TOKEN = '{{'; 5 | const CLOSE_TOKEN = '}}'; 6 | const PLURAL_PREFIX = 'plural:'; 7 | 8 | /** 9 | * @typedef {Object} Options 10 | * @property {String} [openToken='{{'] Default is `'{{'` 11 | * @property {String} [closeToken='}}'] Default is `'}}'` 12 | * @property {(value: String) => String} [transform=DEFAULT_TRANSFORMER] Default is `DEFAULT_TRANSFORMER` 13 | */ 14 | 15 | /** 16 | * @param {String} template 17 | * @param {InputData} [data={}] Default is `{}` 18 | * @param {Options} [options={}] Default is `{}` 19 | * @returns {String} 20 | */ 21 | export function applyTemplateData(template, data, options = {}) { 22 | let { openToken = OPEN_TOKEN, closeToken = CLOSE_TOKEN, transform = DEFAULT_TRANSFORMER } = options; 23 | 24 | for (let key in data) { 25 | let value = data[key]?.toString(); 26 | template = template.replaceAll(openToken + key + closeToken, typeof value === 'string' ? transform(value) : value); 27 | } 28 | return template; 29 | } 30 | 31 | /** 32 | * @param {String} template 33 | * @returns {{ variable: string; pluralKey: string; countVariable: string }[]} 34 | */ 35 | export function getPluralObjects(template) { 36 | const pluralObjects = []; 37 | let open = template.indexOf(OPEN_TOKEN); 38 | while (open !== -1) { 39 | const close = template.indexOf(CLOSE_TOKEN, open); 40 | const variable = template.substring(open + 2, close); 41 | if (variable.startsWith(PLURAL_PREFIX)) { 42 | const keyValue = template.substring(open + 2, close).replace(PLURAL_PREFIX, ''); 43 | const key = keyValue.substring(0, keyValue.indexOf('(')); 44 | const count = keyValue.substring(keyValue.indexOf('(') + 1, keyValue.indexOf(')')); 45 | pluralObjects.push({ variable, pluralKey: key, countVariable: count }); 46 | } 47 | open = template.indexOf(OPEN_TOKEN, close); 48 | } 49 | return pluralObjects; 50 | } 51 | -------------------------------------------------------------------------------- /utils/template-utils.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { applyTemplateData, getPluralObjects } from './template-utils.js'; 3 | 4 | describe('template-utils', () => { 5 | describe('applyTemplateData', () => { 6 | it('should return the same string if no variables passed', () => { 7 | let result = applyTemplateData('Hello world!'); 8 | expect(result).to.equal('Hello world!'); 9 | }); 10 | 11 | it('should replace variables', () => { 12 | let result = applyTemplateData("Hello world! My name is {{name}}. I'm {{age}} years old.", { 13 | name: 'John Doe', 14 | age: 12, 15 | }); 16 | expect(result).to.equal("Hello world! My name is John Doe. I'm 12 years old."); 17 | }); 18 | 19 | it('should work with variables at start/end', () => { 20 | const result = applyTemplateData("{{name}} my name is. I'm {{age}}", { name: 'John Doe', age: 12 }); 21 | expect(result).to.equal("John Doe my name is. I'm 12"); 22 | }); 23 | 24 | it('should work with single variable', () => { 25 | const result = applyTemplateData('{{name}}', { name: 'John Doe' }); 26 | expect(result).to.equal('John Doe'); 27 | }); 28 | 29 | it('should not replace non-defined variabled', () => { 30 | let result = applyTemplateData('My name is {{name}}'); 31 | expect(result).to.equal('My name is {{name}}'); 32 | }); 33 | 34 | it('should accept `transform` option', () => { 35 | let result = applyTemplateData( 36 | 'My name is {{name}}', 37 | { name: 'John Doe' }, 38 | { transform: (value) => value.toUpperCase() }, 39 | ); 40 | expect(result).to.equal('My name is JOHN DOE'); 41 | }); 42 | }); 43 | 44 | describe('getPluralObjects', () => { 45 | it('should return array of plural objects', () => { 46 | expect( 47 | getPluralObjects( 48 | 'Uploading {{filesCount}} {{plural:file(filesCount)}} with {{errorsCount}} {{plural:error(errorsCount)}}', 49 | ), 50 | ).to.deep.equal([ 51 | { variable: 'plural:file(filesCount)', pluralKey: 'file', countVariable: 'filesCount' }, 52 | { variable: 'plural:error(errorsCount)', pluralKey: 'error', countVariable: 'errorsCount' }, 53 | ]); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /utils/toKebabCase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template {string} T 3 | * @typedef {T extends `${infer Head} ${infer Tail}` ? `${Lowercase}-${KebabCase}` : Lowercase} KebabCase 4 | */ 5 | 6 | /** 7 | * @template {string} T 8 | * @param {T} str 9 | * @returns {KebabCase} 10 | */ 11 | export const toKebabCase = (str) => 12 | str 13 | .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) 14 | ?.map((x) => x.toLowerCase()) 15 | .join('-'); 16 | -------------------------------------------------------------------------------- /utils/toKebabCase.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { toKebabCase } from './toKebabCase'; 3 | 4 | describe('toKebabCase', () => { 5 | it('should convert camel string to kebab', () => { 6 | expect(toKebabCase('foo')).to.be.equal('foo'); 7 | expect(toKebabCase('foo1')).to.be.equal('foo1'); 8 | expect(toKebabCase('fooBar')).to.be.equal('foo-bar'); 9 | expect(toKebabCase('fooBar1')).to.be.equal('foo-bar1'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /utils/transparentPixelSrc.js: -------------------------------------------------------------------------------- 1 | export const TRANSPARENT_PIXEL_SRC = 2 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; 3 | -------------------------------------------------------------------------------- /utils/uniqueArray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template T 3 | * @param {T[]} arr 4 | * @returns {T[]} 5 | */ 6 | export const uniqueArray = (arr) => { 7 | return [...new Set(arr)]; 8 | }; 9 | -------------------------------------------------------------------------------- /utils/uniqueArray.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { uniqueArray } from './uniqueArray'; 3 | 4 | describe('uniqueArray', () => { 5 | it('should return deduplicated array', () => { 6 | expect(uniqueArray([1, 2, 3])).to.eql([1, 2, 3]); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /utils/validators/collection/index.js: -------------------------------------------------------------------------------- 1 | export { validateCollectionUploadError } from './validateCollectionUploadError.js'; 2 | export { validateMultiple } from './validateMultiple.js'; 3 | -------------------------------------------------------------------------------- /utils/validators/collection/validateCollectionUploadError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('../../../abstract/ValidationManager.js').FuncCollectionValidator} */ 4 | export const validateCollectionUploadError = (collection, api) => { 5 | if (collection.failedCount > 0) { 6 | return { 7 | type: 'SOME_FILES_HAS_ERRORS', 8 | message: api.l10n('some-files-were-not-uploaded'), 9 | }; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /utils/validators/collection/validateMultiple.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | /** @type {import('../../../abstract/ValidationManager.js').FuncCollectionValidator} */ 4 | export const validateMultiple = (collection, api) => { 5 | const total = collection.totalCount; 6 | const multipleMin = api.cfg.multiple ? api.cfg.multipleMin : 0; 7 | const multipleMax = api.cfg.multiple ? api.cfg.multipleMax : 1; 8 | 9 | if (multipleMin && total < multipleMin) { 10 | const message = api.l10n('files-count-limit-error-too-few', { 11 | min: multipleMin, 12 | max: multipleMax, 13 | total, 14 | }); 15 | 16 | return { 17 | type: 'TOO_FEW_FILES', 18 | message, 19 | payload: { 20 | total, 21 | min: multipleMin, 22 | max: multipleMax, 23 | }, 24 | }; 25 | } 26 | 27 | if (multipleMax && total > multipleMax) { 28 | const message = api.l10n('files-count-limit-error-too-many', { 29 | min: multipleMin, 30 | max: multipleMax, 31 | total, 32 | }); 33 | return { 34 | type: 'TOO_MANY_FILES', 35 | message, 36 | payload: { 37 | total, 38 | min: multipleMin, 39 | max: multipleMax, 40 | }, 41 | }; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /utils/validators/file/index.js: -------------------------------------------------------------------------------- 1 | export { validateIsImage } from './validateIsImage.js'; 2 | export { validateFileType } from './validateFileType.js'; 3 | export { validateMaxSizeLimit } from './validateMaxSizeLimit.js'; 4 | export { validateUploadError } from './validateUploadError.js'; 5 | -------------------------------------------------------------------------------- /utils/validators/file/validateFileType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { IMAGE_ACCEPT_LIST, matchExtension, matchMimeType, mergeFileTypes } from '../../fileTypes.js'; 3 | 4 | /** @type {import('../../../abstract/ValidationManager.js').FuncFileValidator} */ 5 | export const validateFileType = (outputEntry, api) => { 6 | const imagesOnly = api.cfg.imgOnly; 7 | const accept = api.cfg.accept; 8 | const allowedFileTypes = mergeFileTypes([...(imagesOnly ? IMAGE_ACCEPT_LIST : []), accept]); 9 | if (!allowedFileTypes.length) return; 10 | 11 | const mimeType = outputEntry.mimeType; 12 | const fileName = outputEntry.name; 13 | 14 | if (!mimeType || !fileName) { 15 | // Skip client validation if mime type or file name are not available for some reasons 16 | return; 17 | } 18 | 19 | const mimeOk = matchMimeType(mimeType, allowedFileTypes); 20 | const extOk = matchExtension(fileName, allowedFileTypes); 21 | 22 | if (!mimeOk && !extOk) { 23 | // Assume file type is not allowed if both mime and ext checks fail 24 | return { 25 | type: 'FORBIDDEN_FILE_TYPE', 26 | message: api.l10n('file-type-not-allowed'), 27 | payload: { entry: outputEntry }, 28 | }; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /utils/validators/file/validateIsImage.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('../../../abstract/ValidationManager.js').FuncFileValidator} */ 4 | export const validateIsImage = (outputEntry, api) => { 5 | const imagesOnly = api.cfg.imgOnly; 6 | const isImage = outputEntry.isImage; 7 | 8 | if (!imagesOnly || isImage) { 9 | return; 10 | } 11 | if (!outputEntry.fileInfo && outputEntry.externalUrl) { 12 | // skip validation for not uploaded files with external url, cause we don't know if they're images or not 13 | return; 14 | } 15 | if (!outputEntry.fileInfo && !outputEntry.mimeType) { 16 | // skip validation for not uploaded files without mime-type, cause we don't know if they're images or not 17 | return; 18 | } 19 | 20 | return { 21 | type: 'NOT_AN_IMAGE', 22 | message: api.l10n('images-only-accepted'), 23 | payload: { entry: outputEntry }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /utils/validators/file/validateMaxSizeLimit.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { prettyBytes } from '../../prettyBytes.js'; 3 | 4 | /** @type {import('../../../abstract/ValidationManager.js').FuncFileValidator} */ 5 | export const validateMaxSizeLimit = (outputEntry, api) => { 6 | const maxFileSize = api.cfg.maxLocalFileSizeBytes; 7 | const fileSize = outputEntry.size; 8 | if (maxFileSize && fileSize && fileSize > maxFileSize) { 9 | return { 10 | type: 'FILE_SIZE_EXCEEDED', 11 | message: api.l10n('files-max-size-limit-error', { maxFileSize: prettyBytes(maxFileSize) }), 12 | payload: { entry: outputEntry }, 13 | }; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /utils/validators/file/validateUploadError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { NetworkError, UploadError } from '@uploadcare/upload-client'; 3 | 4 | /** @type {import('../../../abstract/ValidationManager.js').FuncFileValidator} */ 5 | export const validateUploadError = (outputEntry, api) => { 6 | const { internalId } = outputEntry; 7 | 8 | // @ts-expect-error Use private API that is not exposed in the types 9 | const internalEntry = api._uploadCollection.read(internalId); 10 | 11 | /** @type {unknown} */ 12 | const cause = internalEntry?.getValue('uploadError'); 13 | if (!cause) { 14 | return; 15 | } 16 | 17 | if (cause instanceof UploadError) { 18 | return { 19 | type: 'UPLOAD_ERROR', 20 | message: cause.message, 21 | payload: { 22 | entry: outputEntry, 23 | error: cause, 24 | }, 25 | }; 26 | } 27 | 28 | if (cause instanceof NetworkError) { 29 | return { 30 | type: 'NETWORK_ERROR', 31 | message: cause.message, 32 | payload: { 33 | entry: outputEntry, 34 | error: cause, 35 | }, 36 | }; 37 | } 38 | 39 | const error = cause instanceof Error ? cause : new Error('Unknown error', { cause }); 40 | return { 41 | type: 'UNKNOWN_ERROR', 42 | message: error.message, 43 | payload: { 44 | entry: outputEntry, 45 | error, 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /utils/waitForAttribute.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {{ 5 | * element: HTMLElement; 6 | * attribute: string; 7 | * onSuccess: (value: string) => void; 8 | * onTimeout: () => void; 9 | * timeout?: number; 10 | * }} options 11 | */ 12 | export const waitForAttribute = ({ element, attribute, onSuccess, onTimeout, timeout = 300 }) => { 13 | const currentAttrValue = element.getAttribute(attribute); 14 | if (currentAttrValue !== null) { 15 | onSuccess(currentAttrValue); 16 | return; 17 | } 18 | 19 | const observer = new MutationObserver((mutations) => { 20 | const mutation = mutations[mutations.length - 1]; 21 | handleMutation(mutation); 22 | }); 23 | 24 | observer.observe(element, { 25 | attributes: true, 26 | attributeFilter: [attribute], 27 | }); 28 | 29 | const timeoutId = setTimeout(() => { 30 | observer.disconnect(); 31 | onTimeout(); 32 | }, timeout); 33 | 34 | /** @param {MutationRecord} mutation */ 35 | const handleMutation = (mutation) => { 36 | const attrValue = element.getAttribute(attribute); 37 | if (mutation.type === 'attributes' && mutation.attributeName === attribute && attrValue !== null) { 38 | clearTimeout(timeoutId); 39 | observer.disconnect(); 40 | onSuccess(attrValue); 41 | } 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /utils/warnOnce.js: -------------------------------------------------------------------------------- 1 | const warnings = new Set(); 2 | 3 | /** @param {string} message */ 4 | export function warnOnce(message) { 5 | if (warnings.has(message)) { 6 | return; 7 | } 8 | 9 | warnings.add(message); 10 | console.warn(message); 11 | } 12 | -------------------------------------------------------------------------------- /utils/wildcardRegexp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} str 3 | * @returns {string} 4 | */ 5 | const escapeRegExp = function (str) { 6 | return str.replace(/[\\-\\[]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 7 | }; 8 | 9 | /** 10 | * @param {string} str 11 | * @param {string} flags 12 | * @returns {RegExp} 13 | */ 14 | export const wildcardRegexp = function (str, flags = 'i') { 15 | const parts = str.split('*').map(escapeRegExp); 16 | return new RegExp('^' + parts.join('.+') + '$', flags); 17 | }; 18 | -------------------------------------------------------------------------------- /utils/wildcardRegexp.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import { wildcardRegexp } from './wildcardRegexp'; 3 | 4 | describe('wildcardRegexp', () => { 5 | it('should return regexp to match wildcard', () => { 6 | const regexp = wildcardRegexp('*.jpg'); 7 | expect(regexp).to.be.instanceOf(RegExp); 8 | }); 9 | 10 | it('should work for mime types', () => { 11 | expect(wildcardRegexp('*.jpg').test('test.jpg')).to.be.true; 12 | expect(wildcardRegexp('image/*').test('image/jpeg')).to.be.true; 13 | expect( 14 | wildcardRegexp('application/vnd.openxmlformats-officedocument.*').test( 15 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 16 | ), 17 | ).to.be.true; 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { defineConfig } from 'vite'; 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)); 6 | 7 | export default defineConfig(({ command }) => { 8 | if (command === 'serve') { 9 | return { 10 | build: { 11 | target: 'es2019', 12 | }, 13 | test: { 14 | coverage: { 15 | provider: 'v8', 16 | reporter: ['text', 'html'], 17 | reportsDirectory: './tests/__coverage__', 18 | }, 19 | }, 20 | resolve: { 21 | alias: { 22 | '@': __dirname, 23 | }, 24 | }, 25 | }; 26 | } 27 | throw new Error('Not implemented'); 28 | }); 29 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config'; 2 | import { commands } from './tests/utils/commands'; 3 | 4 | export default defineWorkspace([ 5 | () => { 6 | return { 7 | extends: 'vite.config.js', 8 | test: { 9 | include: ['./**/*.e2e.test.ts', './**/*.e2e.test.tsx'], 10 | browser: { 11 | enabled: true, 12 | provider: 'playwright', 13 | instances: [ 14 | { 15 | browser: 'chromium', 16 | launch: { 17 | args: [ 18 | '--disable-web-security', 19 | '--use-fake-ui-for-media-stream', 20 | '--use-fake-device-for-media-stream', 21 | ], 22 | }, 23 | }, 24 | ], 25 | commands: { 26 | ...commands, 27 | }, 28 | }, 29 | }, 30 | }; 31 | }, 32 | ]); 33 | --------------------------------------------------------------------------------